Client

examples

  • mmo.client
  • agent skill
  • mmoframework-skill://mmo.client/mmo-client/references/examples.md
# Worked examples Three real files from this repo, annotated. Copy the closest one and adapt rather than starting from a blank file. ## 1. Minimal attribute-driven custom element — `src/customElements/elements/mmo-progress.jhs` Single observed attribute (`value`), no loops/conditionals, a getter/setter that coerces + validates the raw attribute and updates the shadow DOM directly for a part that isn't worth making fully reactive (a `style.width`/label text). ```html <mmo-progress value> <div id="content"> <div id="bar"></div> <div id="text"><slot></slot></div> </div> <div id="percent"></div> <script> /** * Progress element to display a progress bar * @attr value - the value of the progress */ class Progress { init() { super.init(); } /** @return {number} The value of the progress */ get value() { return this.view.content.get('value'); } /** @param {number} value - The value of the progress */ set value(value) { if (isNaN(value)) {value = 1;} else if ((value > 1) || (value < 0)) {throw new RangeError('value must be between 0 and 1.');} this.view.content.set('value', value); const p = Math.floor(value * 100) + '%', div = this.queryShadow('#percent'); this.queryShadow('#bar').style.width = p; if (value == 1) {div.style.display = 'none';} else { div.style.display = ''; div.style.left = p; div.textContent = p; } } } </script> <style> :host { position: relative; display: flex; flex-direction: row; align-items: center; gap: $space.xs; } #content { @use lib::surface($radius.sm); position: relative; min-width: 300px; flex: 1; padding: $space.xs; box-shadow: $shadow.sm; } /* the filled part, which grows from the left */ #bar { position: absolute; top: 0; left: 0; height: 100%; background: $color.brand; color: $color.text-on-brand; border-radius: $radius.xs 0 0 $radius.xs; transition: width 0.5s ease-in; } #text { mix-blend-mode: luminosity; text-align: center; font-size: 90%; font-family: $font.mono; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; } #percent { font-weight: $weight.bold; font-size: $text.sm; transition: left 0.5s ease-in; z-index: $z.base; width: 32px; } </style> </mmo-progress> ``` **Notes:** `<slot></slot>` inside `#text` lets callers write `<mmo-progress value="0.5">Uploading…</mmo-progress>` and have the light-DOM text land there. `<div id="content">`/`<div id="percent">` are plain static markup (no bindings needed — the setter drives them imperatively via `queryShadow`), which is a legitimate pattern for anything that needs more control than declarative `{path}` binding gives you. The `<style>` is plain Simple CSS with no `lang`: `@use lib::surface($radius.sm)` gives `#content` the same ground, hairline and radius as every card of the design system, and every value is a token (`$space.xs`, `$color.brand`, `$z.base`), so the bar follows the dark theme and a client's reskin with no code of its own. ## 2. Element with `<for>` + `<if>/<else>` — `src/customElements/inputs/mmo-radio.jhs` (excerpt) ```html <mmo-radio readonly value required> <fieldset> <legend>{config/label}</legend> <div id="inputs"> <for path="config/values"> <label onkeydown="$this.onKeyDown(event)" tabindex="0" title="{label}"> <input type="radio" name="input-radio" value="{=$key}" /> <if test="{icon}"> <mmo-icon name="{icon}" class="{class}">{label}</mmo-icon> </if> <else> <span class="{class}">{label}</span> </else> </label> </for> </div> </fieldset> <script> class Radio { constructor() { super(); this.onInputChange = this.onInputChange.bind(this); } static get ViewDefault() { return {required: false}; } init() { super.init(); this.view.watch('config', () => { this.updateInputs(); this.value = this.view.get('value'); }); } get value() { return this.view.get('value'); } set value(value) { if (this.view.set('value', value)) {MMO.core.dom.dispatchEvent(this, 'change', value);} // ...imperative DOM sync for `.active`/`checked` state... } // ... } </script> </mmo-radio> ``` **Notes:** - `<for path="config/values">` iterates a Map of objects; each item's `{label}`/`{icon}`/`{class}` are relative to that item. - `{=$key}` — `$key` is the default loop key variable, wrapped in `{=...}` because it's a JS variable reference, not a Map path. - `onkeydown="$this.onKeyDown(event)"` — `$this` resolves to `view.getScope()`, i.e. the `Radio` element instance, so this calls the instance method. - `<if test="{icon}">...<else>...</else>` chooses between an `<mmo-icon>` and a plain `<span>` per item, driven by whether that item has an `icon` field. - `static get ViewDefault() {return {required: false};}` — sets an initial value in `this.view.content` *before* `defaultAttributes` processing runs, distinct from `defaultAttributes` (which is compiler-generated from the root tag and drives which attributes are *observed*). - Two-way `value`: the root tag declares `value` bare (so it's observed); the class's `get/set value` fully owns coercion and DOM sync — nothing here relies on the framework's default `view.get(key)`/`view.set(key,value)` passthrough because a custom getter/setter was defined. ## 3. Standalone view (no shadow DOM, no custom tag) — `src/customElements/views/Issue.jhs` ```html <view id="Issue"> <fieldset class="vflex gap"> <label class="inline"> <span class="label">Url</span> <span class="input"><input type="text" value="{url}" /></span> </label> <label class="inline"> <span class="label">Title</span> <span class="input"><input type="text" value="{title}" /></span> </label> <div class="hflex gap"> <figure> <figcaption>Screenshot</figcaption> <img src="{image}" /> </figure> <label class="flex-1 vflex stretch"> <span class="label">Description</span> <span class="input flex-1"><textarea value="{description}" style="height: 100%"></textarea></span> </label> </div> </fieldset> </view> ``` **Notes:** - No `<script>`/`<style>` at all here — a view doesn't require either; without `<style>`, it relies entirely on the app's **global** `styles.scs` classes (`vflex`, `gap`, `hflex`, `flex-1`, `inline`, `label`, `input`), since it renders into light DOM, not a shadow root — which is also why a view rarely needs a `<style>`: one applies to the whole document, so every selector in it hangs off a class of the view's own root (see [styling.md](https://mcp.mmoframework.fr/search/read?uri=mmoframework-skill%3A%2F%2Fmmo.client%2Fmmo-client%2Freferences%2Fstyling.md#host-and-shadow-dom)). - `value="{url}"` on plain `<input>`/`<textarea>` is the two-way binding — editing the field updates the underlying Map at `url`/`title`/`description` immediately (no `bind-text`/blur needed, that special case is only for **text content**, not form control `value`). - Called from `mmo-app.jhs`'s `addIssue()` via `MMO.elements.View.RenderView('Issue', node, {url, title, image, description})` (see [views-and-data.md](https://mcp.mmoframework.fr/search/read?uri=mmoframework-skill%3A%2F%2Fmmo.client%2Fmmo-client%2Freferences%2Fviews-and-data.md)) — the data object's keys must match the view's `{path}` names exactly. ## Checklist before you consider a new `.jhs` file done 1. Root tag name: `mmo-` + hyphen for elements, or `id=` for a view. 2. `<script>` class name matches the tag (PascalCase, `mmo-` stripped) or explicit `extends`. 3. No method literally named `render`. 4. Every attribute on the root tag has a matching getter/setter *if* the raw string needs coercion (bool/number/JSON) — otherwise the default pass-through is fine. 5. Class-level JSDoc with `@attr`/`@css` for every attribute/modifier class a consumer would need to know about; `@param`/`@return` on public methods and getters. 6. `<style>` is plain Simple CSS (no `lang`, no sass, no `theme.*`) that uses the tokens (`$color.*`, `$space.*`, `$radius.*` …) and the `lib::`/`controls::` recipes, not raw hex colors or magic-number pixel values, and (`:host`/`:host(...)`) for element-level styling — in a view, every selector under a class of the view's root. 7. Whatever reaches an HTML sink — `{path}`/`{=...}` in text content (they compile to `$html`/`$htmlFn`), `bind-html`, `jhs-compile="html"`, or a `<script>`-driven `$html`/`.html()` call — is content you control or explicitly sanitized, never raw user/API text; that goes through `bind-text`, `set-textContent="{path}"` or `textContent` instead (see SKILL.md point 4 and `docs/SECURITY_REVIEW.md`).