# Writing the `<script>` class
Source of truth: `src/JHSCompiler.php` (`compileText`, the regex that finds your class and splices it), `src/js/elements/{CustomElement,CustomShadowElement,Widget,CustomAPIElement}.js`.
## What the compiler does with your `<script>`
Given:
```html
<mmo-progress value>
...markup...
<script>
class Progress {
get value() {return this.view.content.get('value');}
set value(value) {/* ... */}
}
</script>
</mmo-progress>
```
the compiler:
1. Finds `class Progress` (or `class Progress extends SomeBase`) by regex against the tag name in both kebab (`progress`... actually the raw tag minus `mmo-`) and PascalCase form.
2. Rewrites it to `MMO.define('mmo-progress', class MMO_Progress_JHS extends CustomShadowElement(HTMLElement) { ... }` where `CustomShadowElement` is whatever you wrote after `extends` (default: `MMO.elements.CustomShadowElement`) and `HTMLElement` is whatever `is="..."` maps to (default: plain `HTMLElement`).
3. Injects, as the **first** members of the class body: `static defaultAttributes()` (from the root tag's own attributes) and `render($view) { ...compiled markup...; ...compiled <style>...; }`.
4. Splices in the rest of your class body (constructor, getters/setters, methods) verbatim after that.
5. Appends an auto-generated `documentation() { return {...}; }` built from JSDoc comments found in your script (see below) — this feeds the framework's own doc tooling (`mmo-inspector` element, `DocGenerator.php`), so writing `@attr`/`@css`/`@param`/`@return` comments is not optional decoration, it's how this element becomes discoverable/documented at runtime.
**Consequence:** never define a method literally named `render` in your `<script>` — the compiler already generates one and yours would collide (last-definition-wins in JS class bodies, so your logic would silently be dead or override the generated markup, depending on splice order — don't rely on either, just don't name a method `render`).
## Base classes — pick the right one
All are **mixins**: `SomeBase(HTMLElement)`, not `extends SomeBase`. You normally don't write the mixin call yourself — the compiler does it based on your `extends` clause and `is=`.
| Base (in `MMO.elements.*`) | Adds | Use when |
|---|---|---|
| `CustomElement` | Web Component lifecycle wired to `this.view` (a light-DOM `View`): `connectedCallback`→`init()`→`render()`, `attributeChangedCallback` auto-synced to `view.content`, `promise`/`domReady()`/`ready()` promises, `onMutation()` helper. **No shadow DOM.** | You explicitly don't want style/DOM encapsulation (rare — most elements want shadow DOM). |
| `CustomShadowElement` (**default**, what the compiler assumes) | `CustomElement` + `attachShadow({mode:'open'})`, `queryShadow()`/`queryShadowAll()`. This is what `<style>` in your `.jhs` file actually scopes to. | The default for every normal `<mmo-xxx>` element. |
| `Widget` | `CustomShadowElement` + a `url`/`fields`/`data`/`schema` convention for elements that load a *list* of entities from the framework's JSON:API-style backend and want a fields/schema configuration UI (`load()`, `updateFields()`, `reload()`). Marked `@todo remove not used` in source — check if still current before basing new work on it. | A grid/list-style widget bound to a repository collection. |
| `CustomAPIElement` | `CustomShadowElement` + a single-entity API convention: `url` setter accepts either a plain URL string or a `'{"collection":...}'` JSON string, wraps it in `MMO.core.APIRequester`, auto-`reload()`s when visible, exposes `data`/`schema`. | An element bound to one entity/record via the API rather than a data prop passed in. |
## Lifecycle
- **`constructor()`**: runs before the element is in the DOM. `this.view` already exists (empty). If you override it, call `super()` first, as usual. Set up bindings that don't touch the DOM yet.
- **`init()`**: called once, right after `connectedCallback`, before the first `render()`. `CustomShadowElement.init()` just calls `this.render(this.view)`. Override to run setup that needs the (now-existing) shadow DOM/view, calling `super.init()` at the right point — if you need to wait for the full document to be ready first: `init() {this.domReady(() => super.init());}`.
- **attribute → property sync**: for every name in `static get observedAttributes()` (default: `Object.keys(this.constructor.defaultAttributes)`, which the compiler fills from your root tag's attributes), the base constructor defines `get`/`set` on `this` that read/write `this.view.get(key)`/`this.view.set(key, value)` **unless you already defined that getter/setter yourself** (`if (!(key in this))`) — write your own `get value()`/`set value(value)` whenever the raw attribute string needs coercion (number, boolean, JSON) or side effects (updating the DOM directly, dispatching a `change` event), exactly as `mmo-progress.jhs`/`mmo-radio.jhs` do.
- Attribute names are **kebab-case in HTML, camelCase in JS** — `key-var` → `keyVar`, automatically, both in `attributeChangedCallback` and the property-definition loop.
- **`remove()`/`replaceWith()`** are overridden to clean up `this.view` first — don't bypass them by manipulating the DOM node directly if the element owns view state.
## `documentation()` JSDoc conventions (feeds the auto-generated docs)
Inside the class's leading `/** ... */` comment (right above `class Foo {`):
```js
/**
* One-line, then optional longer description.
* @css classname - what this modifier class does
* @css {color} - a templated tag (matches usage like @css success)
* @attr name - what this attribute controls
*/
class Foo { ... }
```
Above a getter:
```js
/**
* Get the value
* @return {number} description of what's returned
*/
get value() { ... }
```
Above a method:
```js
/**
* What this method does
* @param {type} name - description
* @return {type} description
*/
someMethod(name) { ... }
```
Setter JSDoc is parsed but intentionally discarded by `extractJSDoc` in `src/JHSCompiler.php` — document the property on the **getter**, not the setter.
## Attribute declaration shape
Root-tag attributes → `defaultAttributes`. Two forms:
```html
<mmo-radio readonly value required> <!-- bare: boolean-ish, default null -->
<mmo-progress value="0"> <!-- with default -->
```
`static get defaultAttributes() {return {...};}` is generated for you from these — don't hand-write it unless you need attributes that *aren't* on the root tag for some reason (uncommon; if you do, you also then own `static get observedAttributes()`, since the default implementation just mirrors `defaultAttributes`' keys).
## Cross-file dependency errors
The compiler resolves your class body with a plain regex, not a real JS parser — a stray `{` / `}` inside a string or comment in your `<script>` that isn't balanced against `findMatching()`'s tracked contexts (`"`, `'`, `//`, `/* */`) will produce "cannot find class matching brackets" at compile time. Keep braces balanced and prefer template literals over manual string concatenation with embedded `{`/`}` where it matters.
Worse than an error, a mismatch can compile silently: the class then ends early, the rest of the script lands after it, and the bundle fails with `Unexpected token '{'` in the browser (`node --check` on `compileText()`'s output shows it at once). Found writing `mmo-chart-config.jhs`:
- an escaped quote (`'a chart\'s'`) is not understood — the string ends at `\'`. Use the other quote: `"a chart's"`;
- a `"` or `'` inside a **regex literal** opens a string (`/"/g`): write `\x22` / `\x27`, as `mmo-chart.jhs` does;
- `//` inside a regex (`/<\//g`) starts a "comment" to the end of the line — harmless only if that line holds no brace.
The class is also **renamed** (`class Foo` becomes `MMO_Foo_JHS`), so `Foo.STATIC` inside it is a `ReferenceError` at runtime: reach statics through `this.constructor.STATIC` (in a static method, `this.STATIC`).
And every `View` watch — so every `<if>`, `<for>` and `{path}` update — runs on the next animation frame (`MMO.window.run`), not when `set()` returns: a node inside an `<if>` whose test just turned true is not there yet, and a hidden browser tab runs no frame at all. Fill such a node from a `watch()` on the path that shows it.