# 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`).