Client

jhs-syntax

  • mmo.client
  • agent skill
  • mmoframework-skill://mmo.client/mmo-client/references/jhs-syntax.md
# JHS template syntax Source of truth: `src/jhs/{Node,Nodes,Loop,Test,Expression,Variable,Func,View,Printer}.php` (the compiler) and `src/classes/tNodes.php` (tag→class mapping). Everything below compiles to chained calls on a `MMO.elements.View` (`$view` in generated JS — 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)). ## Expressions: `{path}` and `{=js}` Anywhere in text content or an attribute value: - `{some/path}` — reactive one-way bind to `this.view.get('some/path')`. Re-renders automatically when that path changes in the view's `Map`. Compiles to `.$html('some/path')` (text content) or `.$attr('name','some/path')` (attribute). **In text content the value is inserted as HTML, not escaped**: `$html` → `linkHtml` → `Map.getHTML()` → `MMO.core.Printer.Shared().render(value, {html: true})` (through the key's schema first, when it has one), which has no `'string'` renderer and returns a string as-is, then the view marks it with `MMO.core.dom.html()` (`appendChildren` alone inserts a string as text) and `addHTML` sets it with `innerHTML`. An `<img src=x onerror=…>` in the data runs; a plain `a <b> c` loses its `<b>`. - `{=<js>}` — inline JavaScript. Any `{path}` *inside* the JS is extracted as a function parameter (not string-substituted), so `{= {firstname} + ' ' + {lastname} }` compiles to a function `(firstname, lastname) => firstname + ' ' + lastname` that re-runs whenever either path changes, wired via `.$htmlFn(...)` (text content — the return value goes to `innerHTML` the same way, without even the Printer) / `.$attrFn(...)` (attribute). Paths containing `/` (not usable as JS identifiers) get an internal `_v0_`, `_v1_`... alias — don't rely on a specific literal path string being usable as a variable name inside `{=...}` when it contains `/`. - If your JS has no `return`, the whole expression is treated as the return value (`{= {value} + 5 }` ≈ `return value + 5`). - If it has statements, end with an explicit `return`: `{= let x = {value}; return x + 5 }`. - Both land in the HTML sink because `compileText()` in `src/jhs/Printer.php` picks `$opts['html']??true` and nothing in the compiler passes `false`. While that default stands, **untrusted (user/API) text must not go through `{path}` or `{=...}` in text content** — `docs/SECURITY_REVIEW.md` §1–§2 records this, with the proposed compiler fix. What does keep a value as text, checked against the compiled output and `View.js`/`Template.js`: - `bind-text="path"` on the element that should hold it → `.$text('path', true)` → a text node, set through `nodeValue`. Strings only: `Map.getText()` renders a number as the Printer's `<span>`, which shows up as `[object HTMLSpanElement]`. It also writes the text back to `path` on `blur`, which only fires on a focusable/editable element. - `set-textContent="{path}"` → `.$attr('set-textContent','path')` → `node.textContent = value` (the `set-` branch of `Template.setAttribute`). One-way on a plain element, any type — but it replaces everything the element holds, so give it an element of its own. - `$view.$text('path')` from a `<fn>`; `.text(string)` or `textContent` from a `<script>` method. - `MMO.core.dom.createElement(tag, attrs, ...children)` / `appendChildren(node, ...children)` / `$view.add(tag, attrs, ...children)`: a string child is a text node; markup you wrote goes through `MMO.core.dom.html('...')`. `MMO.app`'s messages (`notify`, `alert`, `confirm`, `ask`, `error`, `showModal`, `showNotification`, `showPanel`) take the same children. - `{path}`/`{=...}` in an attribute value → `.$attr()`/`.$attrFn()` → `setAttribute()` (the `value` property for `value=`): never parsed as HTML. Except that an `on*` attribute compiles its bound *value* into a listener with `new Function` (data becomes code), and nothing checks the scheme of a URL attribute (`href`, `src`…), so `javascript:` goes through. - Escape a literal `{`/`}` you don't want treated as an expression with `\{`/`\}`. - `{$$}` / `{$#}` as the *last* path segment (e.g. `path/to/list/$$`) mean "the current key" / "the current numeric index" of that map entry, instead of its value — used inside `<for>` to read the loop key/index reactively. ## Root tag = the element/view definition ```html <mmo-foo attr1 attr2="default"> ...markup... <script>...</script> <style>...</style> </mmo-foo> ``` or ```html <view id="my-view"> ...markup... <script>...</script> <style>...</style> </view> ``` `<script>` and `<style>` are stripped out of the markup before compiling the rest — they can appear anywhere in the file but conventionally go last. See [custom-elements.md](https://mcp.mmoframework.fr/search/read?uri=mmoframework-skill%3A%2F%2Fmmo.client%2Fmmo-client%2Freferences%2Fcustom-elements.md) for what's expected inside `<script>` and [styling.md](https://mcp.mmoframework.fr/search/read?uri=mmoframework-skill%3A%2F%2Fmmo.client%2Fmmo-client%2Freferences%2Fstyling.md) for `<style>`. ## `<for>` — loop over a Map/array ```html <for path="data/{=key}" key-var="$key" view-var="$view" oninsert="/* js, scope=this=view */" ondelete="/* js */"> <dt class="{$key}">{firstname}</dt> </for> ``` - **`path`** (required): a Map path to an array/object of objects. Items **must be objects/arrays**, not primitives — `MMO.core.Map` values. Supports `{=...}` expressions. - **`key-var`** (default `$key`) / **`view-var`** (default `$view`): names bound inside the loop body — `view-var` gives you the per-item `View` (useful for nested loops or calling `$view.getScope()`). - **`oninsert`** / **`ondelete`**: raw JS run when an item view is created/removed; scope (`this`/`$this`) is the item's view. - Inside the loop body, `this` is the item's view (`const $this = $view.getScope(); this...`), so `{path}` expressions resolve relative to that item. - Runtime: `MMO.elements.View#loop()` — diffs the Map's entries against existing sub-views, keeping DOM stable across insert/delete/reorder rather than re-rendering the whole list. Note: the compiler only reads **`path=`** (`Loop::defineNames()`, `Printer::exportLoop()`), as `docs/JHS.md` and every `.jhs` under `src/customElements/` write it. `value=` survives only in `src/jhs/Loop.php`'s own docblock and in the uncompiled drafts under `.mmo/` — don't copy it from there. ## `<if>` / `<elseif>` / `<else>` — conditional rendering ```html <if test="{= {value} === 'marc' }">Hello marc!</if> <elseif test="{= {value} === 'laura' }">Hi Laura!</elseif> <else>Hi anonymous!</else> ``` - `test` is a `{=...}` (or bare `{path}`) expression; truthiness picks the first matching branch. - Each branch gets its own sub-view; only the active branch is rendered/attached (others are `detach()`ed, not destroyed — cheap to toggle back). - `<else>` is optional. `<elseif>` requires a preceding `<if>`. ## `<view>` — define or embed a reusable view ```html <!-- define (root tag) --> <view id="my-view"> ... </view> <!-- embed inside another element/view's markup --> <view src="my-view" path="user" args="{= {someExpr} }"></view> ``` - `id`: declares a view (only valid as the **root** tag of a `.jhs` file). - `src` (+ optional `path`, `args`): embeds an already-defined view at this point. `path` is the Map sub-path the embedded view should be scoped to (empty = same data as parent). `id` and `src` are mutually exclusive. - Compiles to `.$view("name", "path", args)`, i.e. `MMO.elements.View#$view()` — 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). ## `<fn>` — drop into raw view-builder JS ```html <fn>this.text('computed at build time: ' + Date.now());</fn> ``` Runs arbitrary JS with `this` bound to the current `$view` (a `MMO.core.Template`/`View`) — an escape hatch when the declarative markup can't express something. Prefer `{=...}` or a `<script>` method first; `<fn>` bypasses the reactive machinery, so nothing inside re-runs on its own. ## Special attributes | Attribute | Meaning | |---|---| | `value="{path}"` | Two-way bind. On `input`/`select`/`textarea` (and radio/checkbox groups, `date`/`datetime-local`/`number` inputs get type-aware get/set), the DOM value both reflects and writes back to the Map path. This is the **only** attribute that's two-way by default. | | `set-xxx="{path}"` | Binds DOM **property** `xxx` (not the HTML attribute) to `path`. One-way (`element.xxx = value`) on plain elements; **two-way** if the target is itself an `mmo-*` custom element (`node.view.watch('xxx', ...)` pushes the property's own changes back into `path`). | | `bind-text="path"` | Binds the element's text to `path` as a text node (`.$text('path', true)` — never parsed as HTML, unlike `{path}` in text content), and also writes the text back to `path` on `blur` (editable text). | | `bind-html="path"` | Like `bind-text` but for `innerHTML` (`.$html('path', true)`: the sink `{path}` in text content already uses, plus the write-back) — **XSS risk**, see [SKILL.md](https://mcp.mmoframework.fr/search/read?uri=mmoframework-skill%3A%2F%2Fmmo.client%2Fmmo-client%2FSKILL.md) point 4. Only bind this to content you trust or have sanitized. | | `bind-xxx` (anything else) | Falls back to a plain one-way `$attr('xxx', 'path')`. | | `onclick="js"` / any `on*="js"` | Compiled into a real event listener via `new Function('event','$this','$view', js)`. `$this` = `view.getScope()` (normally the custom element instance), `$view` = the current view/template. This runs static, developer-authored template source — never let *runtime* data flow into an `on*` attribute's string value. | | `jhs-compile="html"` / `="text"` | Escape hatch for large static blocks: renders this node's markup **once**, at compile time, as a plain string (`.html('...')` / `.text('...')`) instead of the normal reactive `.open()/.attr()/.close()` chain. No `{path}`/`{=...}` bindings inside are reactive anymore — use only for genuinely static chunks (perf optimization), not for content containing bindings. | | `is="div"` (root tag only) | Extends a native element instead of `HTMLElement` (a "customized built-in element"): `<mmo-foo is="tr">`. Must be a tag from the `HTMLElements` map in `src/JHSCompiler.php` (standard HTML tags only). | | bare attribute (no `=`) on root tag | Declares an observed attribute with a boolean-ish default (parses as PHP `true` → emitted as `null` default) — presence/absence semantics, pair with a `get`/`set` in `<script>` to coerce. | ## Characters to watch for - Inside `<script>`/`<style>`/`<textarea>`/`<pre>`/`<fn>`, `{...}` is **not** treated as an expression (these tags are in the compiler's ignore-list) — write real JS/CSS there freely. - In attribute values, quote as normal HTML: `'` → `&#39;`, `"` → `&quot;` when needed (per `docs/JHS.md`).