# The .jhs file
One file holds a component's markup, its javascript class and its styles. The compiler turns it into
javascript; the browser never sees the `.jhs`.
Where they live in this package:
```
src/customElements/elements/ mmo-icon.jhs, mmo-tree.jhs, mmo-code.jhs …
src/customElements/inputs/ form controls
src/customElements/layouts/ mmo-app.jhs, mmo-tabs.jhs, mmo-grid.jhs …
src/customElements/modules/ self-contained features (mmo-markdown, mmo-swagger …)
src/customElements/tools/ documentation and sandbox tooling
src/customElements/views/ standalone `<view>` fragments, PascalCase filenames
```
An element's file is named after its tag (`mmo-tree.jhs` defines `<mmo-tree>`); a view's is named after
what it renders. The reference that follows is this package's own, `libs/client/docs/JHS.md`.
# The JHS format
`JHS` stands for **J**avascript, **Ht**ml and **S**tyle: one file holding the markup of a custom element
or a view, the javascript class that drives it, and the styles that belong to it. `MMO\client\JHSCompiler`
compiles each file into javascript, and concatenates the lot into `bin/{theme}.jhs.js`.
Nothing in the syntax below is standard html. It is a small template language, and the compiler turns it
into chained calls on a `MMO.elements.View`.
## Two kinds of file
- A **custom element** has an `<mmo-xxx>` root tag. It is a real Web Component: shadow dom, observed
attributes, a lifecycle, registered globally. Its `<script>` must declare `class Xxx` — the root tag's
name without `mmo-`, in PascalCase — and the compiler rewrites it into the real class, generating a
`render()` and a `documentation()` for it. Never write a method called `render`: the generated one would
silently replace yours.
- A **view** has a `<view id="...">` root tag. It is a named template fragment bound to a `Map`, with no
shadow dom, no tag of its own and no lifecycle. Use it for markup you want to render into an element you
already have.
## Views
A view is an html template connected to a javascript `Map`. When the data in the map changes, the template
updates itself.
Data is injected with curly brackets: `{xxx/yyy}` is the value at that path, the same thing as
`map.getAt('xxx/yyy')` or `map.get('xxx').get('yyy')`.
`{= … }` injects javascript instead. Every `{path}` inside it becomes a parameter of a function that is
re-run whenever one of those paths changes — the path is not substituted as text:
- `{= {xxx} + 5 }` evaluates as `map.get('xxx') + 5`;
- `{= let x = {xxx}; return x + 5 }` needs the explicit `return`, because the expression has statements. An
expression with no `return` at all is itself the returned value.
Write `\{` and `\}` for a literal brace you do not want read as an expression. As the **last** segment of a
path, `{$$}` means the current key and `{$#}` the current index — useful inside a loop.
### Create a view
1. Create a `.jhs` file.
2. Its root tag is `view`, with an `id` in kebab-case.
3. Its content is the template. `<script>` and `<style>` are taken out of the markup before the rest is
compiled, so they can sit anywhere; by convention they go last. Inside the script, `this` is the view.
Rendered in the document, the style applies to the whole page, so its selectors hang off a class of
the view's own ([Styles](#styles)).
```jhs
<view id="my-view">
<div class="my-view">
<p>Hello, this is {name}</p>
</div>
<script>
this.changeName = function (value) {this.content.set('name', value);}
</script>
<style>
.my-view {
color: $color.text;
&:hover {background: $color.brand; color: $color.text-on-brand;}
}
</style>
</view>
```
with the data:
```json
{"name": "John"}
```
### Render a view
```javascript
MMO.domReady(() => {
MMO.elements.View.RenderView('my-view', document.getElementById('my-container'), {name: 'John'});
});
```
A view can also be embedded in another one's markup:
```jhs
<view id="my-parent-view">
<view src="my-view" path="user"></view>
</view>
```
### Compile a JHS file from PHP
```php
//one file, or several, into one javascript file
\MMO\client\JHSCompiler::Shared()->compile('bin/elements.js', ['src/customElements/elements/my-element.jhs']);
//a string, returning the generated javascript -- with $debug true it comes back indented and readable,
//which is the quickest way to see what a loop or a test actually compiled to
\MMO\client\JHSCompiler::Shared()->compileText($source, sassOpts: [], debug: true);
```
Both take `$sassOpts` and `$scsOpts`; neither needs them for a file that styles itself in `.scs`.
## Styles
A plain `<style>` is **Simple CSS**. That is the compiler's default style language, so an
element says nothing about it, and only a block written in something else declares itself:
```jhs
<style>…</style> <!-- Simple CSS; `lang="scs"`, `simplecss` and `simple-css` say it too -->
<style lang="sass">…</style> <!-- compiled by DartSass, and therefore needs dart on the machine -->
```
Every element in this package styles itself in `.scs` and none of them writes a `lang`; `lang="sass"`
survives for applications that have not moved yet. Anything else raises *unknown style language*.
The default is `JHSCompiler::$styleLanguage` — a package whose elements are still sass can flip it back
rather than annotate every file.
The compiler puts a prelude in front of every `<style>`, so the design tokens (`$color.text`,
`$space.md`, `$radius.sm`), `lib::` and `controls::` need no import. Which prelude depends on where the
css lands:
- A **custom element**'s stylesheet lands in its shadow root, which means the document's own css never
reaches it — only inherited custom properties do. Its prelude, `@import "shadow.scs";`
(`JHSCompiler::$scsElement`), also emits the resets the shadow root needs: `box-sizing: border-box`,
form controls that inherit the font and colour, a flat bare `<button>`.
- A **view** has no shadow root of its own: its `<style>` is added where the view renders — in the
document, it applies to the whole page. Its prelude, `@import "controls.scs";` (`JHSCompiler::$scsView`), brings in
the same three and emits no rule — the element's resets would land after the document stylesheet and
beat it. The view's own rules reach the whole page all the same: scope them under a class of the
view's root, like `.my-view` above.
Tokens and `@use` of a style group are how an element or a view takes part in the theme. See
**the design system**.
## Special tags
### `<view>`
| attribute | |
|---|---|
| `id` | declares a view. Only valid on the root tag, and MUST be unique |
| `src` | embeds an already declared view here. Mutually exclusive with `id` |
| `path` | the map sub-path the embedded view is scoped to. Empty means the parent's own data |
| `args` | arguments passed to the embedded view |
### `<for>`
Iterate an array, or an object of objects.
> **The items MUST be objects**, never primitive values.
| attribute | |
|---|---|
| `path` | a path in the current model to iterate. Accepts a `{= … }` expression |
| `key-var` | the variable holding the current key. Default `$key` |
| `view-var` | the variable holding the current item's view — not the loop's. Default `$view` |
| `oninsert` / `ondelete` | javascript run when an item's view is created or removed, scoped to that view |
Inside the body, the scope is the current item's view, so a `{path}` resolves against the item.
```json
{
"greeting": "Hello ",
"data": [{"firstname": "a", "msg": "welcome 1"}, {"firstname": "b", "msg": "welcome 2"}]
}
```
```jhs
<dl>
<for path="data" key-var="$key" view-var="$view">
<dt class="{=$key}">{= $view.get('/greeting') + {firstname}}</dt><dd>{msg}</dd>
</for>
</dl>
```
renders as:
```html
<dl>
<dt class="0">Hello a</dt><dd>welcome 1</dd>
<dt class="1">Hello b</dt><dd>welcome 2</dd>
</dl>
```
The loop diffs the map's entries against the views it already built, so inserting or removing one item
does not re-render the list.
### `<if>`, `<elseif>` and `<else>`
Test a value and render the matching branch.
| attribute | |
|---|---|
| `test` | an expression to evaluate. The first truthy branch wins |
```json
{"value": "laura"}
```
```jhs
<if test="{= {value} === 'marc' }">Hello marc !</if>
<elseif test="{= {value} === 'laura' }">Salut Laura !</elseif>
<else>Hy anonymous !</else>
```
renders as `Salut Laura !`. `<else>` is optional, `<elseif>` needs an `<if>` before it. Each branch keeps
its own sub-view and the inactive ones are detached rather than destroyed, so toggling back is cheap.
### `<fn>`
Raw view-builder javascript, with `this` bound to the current view:
```jhs
<fn>this.text('built once: ' + Date.now());</fn>
```
It bypasses the reactive machinery — nothing inside re-runs on its own — so reach for `{= … }` or a method
in `<script>` first.
## Special attributes
| attribute | |
|---|---|
| `value` | bound **both ways**. On an `input`, `select`, `textarea` or a custom element, changing the control writes back into the map. It is the only attribute that is two-way by default |
| `set-xxx` | binds the element's **property** `xxx` to the map: `element.xxx = map.get(path)`. If the element is itself an `mmo-*` element, the binding works both ways |
| `bind-text` | like `{path}` for text content, and writes the text back into the map on `blur` |
| `bind-html` | the same for `innerHTML`. **This is an html injection sink** — only ever bind content you produced or sanitized |
| `bind-xxx` | anything else falls back to a one-way attribute binding |
| `on*` | `onclick`, `oninput`… are compiled into real listeners. `$this` is the element, `$view` the current view. The javascript there is template source, never runtime data |
| `jhs-compile` | `"html"` or `"text"`: render this node once, at compile time, as a plain string. Bindings inside stop being reactive. For large static blocks only |
| `view-basepath` | sets the base path of the view, for a custom element rendering someone else's data |
| `is` | on the root tag: extend a native element rather than `HTMLElement`, e.g. `<mmo-row is="tr">` |
| a bare attribute on the root tag | declares an observed attribute. Pair it with a `get`/`set` in `<script>` to decide how the raw string is coerced |
# MMO.elements.View Class Documentation
The `MMO.elements.View` class extends the `MMO.core.Template` class and provides functionality for synchronizing a template with a `Map` of data. This document provides an overview of the methods available in the `MMO.elements.View` class.
## Methods
### Constructor
- **constructor(map, basepath = '', node = null, ref = null)**
- Initializes a new view instance. `map` is the data map, `basepath` is the JSON path in the map, `node` is the DOM element to insert nodes in, and `ref` is the reference element to insert nodes after.
### Static Methods
- **static RenderView(name, node, map, basepath = '', ref = null, args = [], props = {})**
- Renders a view by name into the specified node.
### Instance Methods
- **remove()**
- Removes the view from the DOM and cleans up event bindings.
- **hide()**
- Hides the view and its subviews.
- **show()**
- Shows the view and its subviews.
- **get rootView**
- Returns the root view of the current view.
- **createView(path, node, nodeRef = null, root = null)**
- Creates a new subview.
- **removeView(view)**
- Removes a subview.
- **resolvePath(path)**
- Resolves a JSON path to the root and basepath.
- **renderView(name, path, node, nodeRef, ...args)**
- Renders a subview by name.
- **$view(name, path, ...args)**
- Renders a subview and updates the reference node.
### Content Methods
- **walkTo(path, create = false)**
- Walks through the map using a JSON path.
- **switchMap(oldMap, newMap)**
- Switches event bindings from the old map to the new map.
- **get contentParent**
- Returns the parent map of the view's content.
- **get contentKey**
- Returns the key of the view's content in the parent map.
- **get contentIndex**
- Returns the index of the view's content in the parent map.
- **get content**
- Returns the view's content.
- **set content(value)**
- Sets the view's content.
- **get(path)**
- Gets the value at the specified path.
- **has(path)**
- Checks if the specified path exists in the map.
- **getText(path)**
- Gets the text representation of the value at the specified path.
- **getHTML(path)**
- Gets the HTML representation of the value at the specified path.
- **set(path, value)**
- Sets the value at the specified path.
- **delete(path)**
- Deletes the value at the specified path.
- **push(path, value)**
- Adds a value to the end of the array at the specified path.
- **pop(path)**
- Removes and returns the last value of the array at the specified path.
- **merge(value)**
- Merges values from another object or map into the view's map.
### Event Handling Methods
- **on(path, listener, scope = null, last = true)**
- Attaches a listener to changes on the view's map.
- **off(path, fn, scope = null)**
- Detaches a listener from the view's map.
### Function Methods
- **createFunction(name, fn, ...args)**
- Creates a function with the specified arguments.
- **removeFunction(map)**
- Removes a function from the view.
- **getFunction(name)**
- Gets the index of a function by name.
### Attribute Methods
- **linkAttribute(path, node, name)**
- Links a DOM attribute to a map path.
- **$attr(name, path)**
- Creates a link on the current node's attribute.
- **$attrFn(name, fn, ...args)**
- Creates a link on the current node's attribute using a function.
### Text Methods
- **linkText(path, textNode, bind = false)**
- Links a DOM text node to a map key.
- **$text(path, bind = false)**
- Creates a link on the current node's text.
- **$textFn(fn, ...args)**
- Creates a link on the current node's text using a function.
### HTML Methods
- **htmlUpdater(node, nodeRef)**
- Returns a function to set the HTML at a given node.
- **linkHtml(path, node, nodeRef = null, bind = false)**
- Links a node's HTML to a map key.
- **$html(path, bind = false)**
- Creates a link on the current node's HTML.
- **$htmlFn(fn, ...args)**
- Creates a link on the current node's HTML using a function.
### Complex Methods
- **loopFn(pathFn, args, fn)**
- Creates a loop with an expression in the path.
- **loop(path, fn)**
- Creates a loop in the view.
- **test(...tests)**
- Creates a test in the view.
# Special characters
Inside `<script>`, `<style>`, `<textarea>`, `<pre>` and `<fn>`, `{...}` is **not** read as an expression —
those tags are on the compiler's ignore list, so real javascript and css can be written there freely.
In an attribute value, quote as you would in any html: `'` must be written `'`, `"` must be written
`"`.
- `"`: the double quotation MUST be encoded as `"`
# Writing a custom element
A worked example, `mmo-icon`, from the file that really defines it
(`src/customElements/elements/mmo-icon.jhs`). Read [The .jhs file](frontEnd?guide=10-jhs.md) first for the
template syntax.
## The markup
The root tag is the contract: its name is the element's tag, and every bare attribute on it becomes an
observed attribute.
```jhs
<mmo-icon name>
<svg viewBox="0 0 24 24" preserveAspectRatio="xMidYMid meet">
<use x="0" y="0" width="24" height="24"></use>
</svg>
<span></span>
<slot></slot>
…
</mmo-icon>
```
`<mmo-icon name>` declares one observed attribute, `name`. There is no `observedAttributes` to write.
## The class
The class is named after the tag with `mmo-` removed, in PascalCase — `mmo-icon` needs `class Icon`. The
compiler rewrites it into the real element class, and generates `render()` and `documentation()` for it,
so **never define a method called** `render`: the generated one replaces it silently.
```jhs
<script>
/**
* Icon element to display an icon
* @attr name - the name of the icon
* @css round - make the icon round
* @slot - a label, shown next to the glyph
*/
class Icon {
/**
* Set the name of the icon
* @param {string} name - The name of the icon
*/
set name(name) {
if (!this.view.set('name', name)) {return ;}
const svg = this.queryShadow('svg'),
text = this.queryShadow('span');
…
}
/** @return {string} The name of the icon */
get name() {
return this.view.get('name');
}
}
</script>
```
Three things this shows:
- `this.view` **is the element's data.** `this.view.set(path, value)` returns `false` when the value did
not actually change, which is the idiomatic way to leave a setter early instead of re-rendering.
- **An attribute is reached through a getter/setter pair**, not through `attributeChangedCallback`. The
setter is where the raw attribute string is coerced.
- `queryShadow(selector)` / `queryShadowAll(selector)` **query the element's shadow root.** The document's
own `querySelector` cannot see inside it.
`init()` is where anything that has to happen once goes, and it starts with `super.init()`.
## The styles
```jhs
<style>
:host {display: block; position: relative;}
#content {
@use controls::list;
margin: 0;
}
</style>
```
No `lang` means [Simple CSS](../scs/overview). The stylesheet is scoped to the shadow root, which is also
why the document's stylesheet cannot reach in: an element takes part in the theme through tokens
(`$color.text`, `$space.md`) and `@use` of a shared style group, never by inheriting page rules. See
[Styling](frontEnd?guide=50-styling.md).
## Documenting it
The jsdoc on the class **is** the element's reference: `@attr`, `@css`, `@slot` on the class comment, and
an ordinary comment on each method and getter. The compiler emits a `documentation()` from it and a page
shows it with one tag:
```jhs
<mmo-element-doc element="mmo-icon"></mmo-element-doc>
```
Nothing is written twice, so nothing can drift. See
[How to write an element's documentation](documentationGuideline?guide=20-customElements.md).
# Declaring the site's pages
Pages are not registered in code. A folder of templates, an `index.json` beside them, and the theme builds
the menu, the breadcrumbs and the urls out of the tree — which is how this very site is put together.
# Pages
A site built on `mmo.client` does not register its pages in code. It puts them in a folder, drops an
`index.json` next to them, and `MMO\client\Theme` turns the whole tree into the menu, the breadcrumbs and
the routes that `MMO\client\controllers\PageController` serves.
## Where the pages live
The theme is told which folders to read:
```json
"MMO\\client\\Theme": {
"pages": ["{basepath}/contents/pages"]
}
```
Every `index.json` **anywhere under** one of those folders is loaded, and its own folder — relative to the
pages root — is the url it describes. `contents/pages/guides/index.json` describes `/guides`; the files
beside it are its pages.
## index.json
```json
{
"label": "Application",
"icon": "desktop",
"group": "Framework",
"order": 1,
"default": "admin",
"files": {
"admin": {
"label": "Admin",
"title": "Admin shell",
"icon": "table-cells-large",
"description": "The application shell on real content."
},
"landing": {"label": "Landing", "title": "Landing page", "icon": "bullhorn"}
}
}
```
That is `contents/pages/app/index.json`, and it produces `/app/admin` and `/app/landing`, with `/app`
itself serving `admin`.
### Keys of a folder
| key | |
|---|---|
| `path` | the url segment. Defaults to the folder's own name, and setting it overrides the whole path |
| `label` | what the menu shows. Falls back to the capitalised `path` |
| `title` | the long name, used for the page's `<title>` and its heading |
| `description` | goes into the page's meta description |
| `image` | the image for the Open Graph tag |
| `icon` | an icon name, or a utf-8 character |
| `group` | the menu section this folder is filed under |
| `order` | sorts among siblings. Equal orders keep declaration order |
| `default` | the child served when the folder itself is requested |
| `query` | query parameters the content needs |
| `inMenu` | `false` keeps it out of the menu but still reachable |
| `visible` | `false` hides it entirely |
| `css` | the class of the `<mmo-app>` shell on its pages (`admin`, `landing`, `blog`, ...), instead of the theme's. Its pages inherit it |
| `files` | the pages of this folder |
### Keys of a file
An entry of `files` takes every key above, plus:
| key | |
|---|---|
| `file` | the template to render. **Defaults to** `<key>.phtml`, resolved next to the `index.json` |
| `data` | anything the page itself wants to read |
So `"admin": {...}` renders `admin.phtml` at `/app/admin` unless `file` says otherwise. A key is a url
segment, not a filename: give `file` explicitly when the two differ.
## The landing page
The root (`/`) serves whatever `PageController` is configured to serve — a folder name, or a
`folder/file` pair:
```json
"MMO\\client\\controllers\\PageController": {
"homepage": "pitch"
}
```
## A folder without a page
A folder requested without a `default` is a category: `PageController` renders it with `category.phtml`,
imported once with the folder as `$_VARS['content']`. It looks the template up on the document's import path,
so a `category.phtml` at the root of one of the theme's `pages` folders replaces it; with none, the package's
own `src/templates/category.phtml` is used — the folder's sub-folders as topic cards, its own pages below
them, and a filter bar over the whole tree (styles in `styles/parts/topics.scs`).
A topic card lists 6 children and folds the rest; set `limit` in the vars to change it:
```php
<?php $this->import(\MMO\filename('{MMO\client}/templates/category.phtml'), ['content' => $_VARS['content'], 'limit' => 10]); ?>
```
The template name itself is the controller's `categoryTpl` setting.
## What a page file is
A page is a `.phtml` rendered into the document the theme has already set up: the `<mmo-app>` shell, the
stylesheet, the compiled javascript and the icon sprite are in place before it runs. What it echoes becomes
the content of `<mmo-app>`.
```php
<article>
<h1>My page</h1>
<p>Ordinary html, plus any custom element the client provides.</p>
</article>
```
`PageController` picks what to do from the extension of the entry's `file`:
| extension | what the page becomes |
|---|---|
| `.phtml` | `$document->import($file, $data)` -- php, run once per request |
| `.md` | the file's text inside a `<mmo-markdown>`, rendered in the browser |
| anything else | a link to the file |
## The page file (.phtml)
### How it runs
The file is `require`d by `MMO\core\documents\Document::import()`, inside that method. So a page is plain
php, and two things are in scope:
- `$this` -- the `HtmlDocument` being built (an `MMO\core\documents\HtmlDocument`), not a controller.
- `$_VARS` -- an array of variables. They are **not** extracted: write `$_VARS['route']`, never `$route`.
Whatever the file echoes is captured and added to the document; an exception thrown inside it is logged and
printed in the page's place rather than taking the whole document down.
### `$_VARS`
`$_VARS` is built from two sources, and the first wins:
1. the entry's **`data`** in `index.json` -- the variables given to this `import()`;
2. the variables of the document, which every `import()` gets:
| key | what it is | set by |
|---|---|---|
| `document` | the `HtmlDocument` itself -- the same object as `$this` | `Document::__construct` |
| `app` | the `MMO\core\Application` -- **not** the `<mmo-app>` element | `Response::__construct` |
| `route` | the current `MMO\core\server\Route` | `Response::__construct` |
| `controller` | the route's controller -- here the `PageController` | `Response::__construct` |
```json
"files": {
"pricing": {"label": "Pricing", "data": {"plans": ["solo", "team"], "currency": "€"}}
}
```
```php
<?php foreach ($_VARS['plans'] as $plan): ?>
<section class="card"><?= htmlspecialchars($plan) ?> · <?= $_VARS['currency'] ?></section>
<?php endforeach; ?>
```
A `data` key with one of those four names **shadows** it: `"data": {"route": 1}` leaves the page without its
route. Keep `data` to names of your own.
### `$_VARS['document']`: reaching the shell
The document is an array over its nodes, and the theme names two of them. After `MMO\client\Theme` has
configured the document, `$_VARS['document']['app']` -- and `$_VARS['document']['body']`, which the theme
repoints at the same node -- is the `<mmo-app>` element itself, an `MMO\core\helpers\XmlNode`. A key that
starts with `@` reads or writes one of its attributes.
The shell's template **is** its class (`admin`, `landing` or `blog` -- see `<mmo-app>`), so one line in a
page changes the whole layout of that page:
```php
<?php $_VARS['document']['app']['@class'] = 'blog'; ?>
<article class="prose">
<h1>Release notes</h1>
...
</article>
```
The site is `admin` everywhere (the theme's `rootAttributes`), and this page alone renders with the
editorial shell: no rail, a sticky header, one content column and the footer. `landing` gives the marketing
shell the same way. The class is written in the html the server sends, so there is no flash of the admin
layout before the switch.
The same node takes any other attribute or class:
```php
<?php
$app = $_VARS['document']['app']; //or $this['app']
$app['@class'] = 'landing'; //replaces the whole class: the template
$app->addCSSClass('menu-collapsed'); //adds one, keeps the rest: the rail starts reduced to icons
unset($app['@id']); //removes an attribute (don't: the app is found by its id)
?>
```
To change the layout of a whole site rather than of one page, set it once in the configuration:
```json
"MMO\\client\\Theme": {
"rootAttributes": {"id": "app", "class": "landing"}
}
```
`$_VARS['document']['head']` is the `<head>` node the same way. The shell only exists in a document the
theme configured: in a raw document (an email body) `['app']` is `null`.
### Filling the shell's regions
The page's html lands in the light dom of `<mmo-app>`, so a **top-level** element with a `slot` attribute
goes to that region of the shell, and a region no slot fills removes itself from the layout:
```php
<?php $_VARS['document']['app']['@class'] = 'blog'; ?>
<section slot="hero" class="blog-hero"><h1>The journal</h1></section>
<article class="prose">...</article>
<aside slot="aside">On this page ...</aside>
```
`hero` turns on the hero band (`has-hero`), `aside` the sticky rail of the `blog` template (`has-aside`),
`page-title`/`page-actions` the page header of `admin` (`has-page-head`). A `slot` only counts on a direct
child of `<mmo-app>`: inside the page's own `<article>`, it does nothing.
### What the page can change, and what the controller overwrites
`$this` is the document, so the page can use its whole api:
```php
<?php
$this->addStylesheet(\MMO\url('css/charts.css', '{assets}'));
$this->addScript(\MMO\url('js/charts.js', '{assets}'));
$this->setMeta('og:image', \MMO\url('og/pricing.png', '{assets}'), 'property');
$this->addImportPath(__DIR__ . '/parts', true); //partials of this folder first
?>
```
`PageController` runs the page **before** it sets the title and the description, so these two are written
over: `setTitle()` in a page is lost (the title is `<app title> · <entry title>`), and `setMeta('description')`
survives only for an entry without a `description`. Give both in `index.json` instead.
### The page entry and its tree
The controller knows which entry it is serving, with its parent and children, so a page can build its own
table of contents:
```php
<?php $content = $_VARS['controller']->getMainContent(); ?>
<h1><?= htmlspecialchars($content->getTitle()) ?></h1>
<ul>
<?php foreach ($content->getParent()->getChildren() as $sibling): ?>
<li><a href="<?= $sibling->url() ?>"><?= htmlspecialchars($sibling->getLabel()) ?></a></li>
<?php endforeach; ?>
</ul>
```
### Partials
`import()` looks relative names up on the document's import path -- every `pages` folder of the theme, then
its `imports` -- so partials live next to the pages. Its third argument says where the output goes:
```php
<?php
$this->import('parts/header.phtml'); //appended to the body, where it is
echo $this->import('parts/card.phtml', ['id' => 3], false); //returned instead, and echoed here
?>
```
In a page, use `false` and `echo`. With the default (`'body'`), the partial is added to the body as a node
of its own, **after** the capture of the page that called it -- so it lands below the whole page, not where
the call is. The partial gets its own `$_VARS`: what you pass, plus the document's variables -- not the
caller's `data`. Pass `$_VARS` on to hand it all down:
```php
<?= $this->import('parts/status.phtml', $_VARS, false) ?>
```
A relative name is resolved against the import path, **not** against the calling file: `'../parts/x.phtml'`
or `'solutions/menu.phtml'` mean the same from every page. And the name is literal -- `'card.phtml'`, never
`'card'`.
A partial reads its optional arguments with `??`, and can let the url win over them -- one list template
then serves a dozen one-line pages, and the address bar can still override it:
```php
<?php //entities.phtml
$fields = $_VARS['route']->get('fields', $_VARS['fields'] ?? null);
$class = $_VARS['class'] ?? '';
?>
```
```php
<?php //rh/employees.phtml -- the whole page ?>
<?= $this->import('entities.phtml', ['collection' => 'employees', 'fields' => 'name,firstname', 'class' => 'fullscreen'], false) ?>
```
### One template, several pages
The `file` and `data` of an entry make a `.phtml` reusable without a partial: two entries, one file, and
what differs is in `data`. `file` is relative to the `index.json`, so it can reach a template higher up:
```json
"files": {
"rates": {"label": "Rates", "file": "rates.phtml", "data": {"collection": "rates"}},
"imputed": {"label": "Imputed", "file": "rates.phtml", "data": {"collection": "imputedRates"}},
"documents": {"label": "Documents", "file": "../../documents.phtml", "query": "id={id}", "data": {"collection": "accidents"}}
}
```
An entry whose `query` holds a placeholder (`id={id}`) needs a parameter to make sense, so it stays out of
the menu on its own.
### Reading the request
```php
<?php
$route = $_VARS['route'];
$page = (int)$route->get('page', 1); //?page=2, with a default
$zoom = $route->get('zoom', 'week');
?>
```
**Throwing does not answer an error.** `import()` catches whatever the page throws, logs it and prints it
where the page was: an `e404` thrown in a page is a 200 with an error message in it. Act on the route's
response instead -- `redirect()` replaces it, and `withStatus()` keeps the page but changes its code:
```php
<?php
$route = $_VARS['route'];
if (!$post) {$route->setResponse($route->getResponse()->withStatus(404)); ?>
<p>This post does not exist.</p>
<?php return; }
if ($post->isDraft()) {$route->redirect(\MMO\url('blog')); return;}
?>
```
Neither `redirect()` nor `stop()` throws: the rest of the file still runs, so `return` after them. A
top-level `return` simply ends the page -- what was echoed so far is kept.
`stop()` also lets a page answer something other than html -- an export of the table it shows, a form it
handles itself:
```php
<?php
use MMO\core\server\Response;
$route = $_VARS['route'];
if ($route->get('export') === 'xlsx') {$route->stop(Response::Xlsx(buildSheet($stats))); return;}
if (($route->isPost()) && ($post = $route->getRequestBody())) {
//save $post, then show the page again
}
?>
```
A missing *page* is a 404 already: `PageController` throws it, before any page runs, for a path no entry
matches -- and for an entry marked `"visible": false`, a 401.
### Escaping
A page is php: `<?= $x ?>` prints raw. Use `htmlspecialchars()` for text, and
`\MMO\core\printers\JsonPrinter::Encode()` to hand a value to javascript:
```php
<script>
const plans = <?= \MMO\core\printers\JsonPrinter::Encode($_VARS['plans']) ?>;
MMO.domReady(() => document.getElementById('tree').value = <?= json_encode($selected) ?>);
</script>
<mmo-filter value="<?= htmlspecialchars(json_encode($filter)) ?>"></mmo-filter>
```
JSON is a javascript literal: write it bare, never inside quotes. `JSON.parse('<?= json_encode($x) ?>')` and
`'<?= $name ?>'` break on the first apostrophe, and are an injection when the value comes from a user. In an
attribute, the JSON goes through `htmlspecialchars()` like any other text.
### Pieces every page shares
What belongs to every page -- the user's name in the top bar, the logo, a global `<style>` -- is not repeated
in each file. A listener on `DocumentInit` imports it into every html document before the page runs, and
since it writes into `<mmo-app>` too, its top-level `slot`s fill the shell:
```php
//bootstrap.php of the project
\MMO\core\events\DocumentInit::AddListener(function (\MMO\core\events\DocumentInit $event) {
if (($event->document instanceof \MMO\core\documents\HtmlDocument) && (!$event->document->isRaw())) {
$event->document->import('common.phtml');
}
}, 100);
```
The order (`100`) puts it after the theme's own listener: before that, there is no `<mmo-app>` to write into
and no `pages` folder on the import path.
```php
<?php //common.phtml ?>
<a slot="logo" href="<?= \MMO\url('') ?>"><img src="<?= \MMO\url('logo.svg', '{assets}') ?>" alt="" height="24"/></a>
<div slot="user-info"><?= htmlspecialchars(\MMO\oauth\user()?->getUsername() ?? '') ?></div>
```
Such a file is also the place for the php functions the pages share: it is `require`d once per request, before
them.
### Special templates
A few names are looked up on the import path, so the first `pages` folder that has one overrides it:
| template | rendered for | its `$_VARS` |
|---|---|---|
| `category.phtml` | a folder without `default` (see above) | `content`, `limit` |
| `entity.phtml` | `PageController::routeEntity` (`entity?uid=…`) -- no default, the project writes it | `entity` |
| `error.phtml` | any exception of a route, via `Theme::createErrorResponse` -- without one, the application's plain error page | `status` (the http code), `error` (the exception) |
```php
<?php //error.phtml
$status = $_VARS['status'];
if ($status === 503) {$this->import('maintenance.phtml'); return;}
?>
<div class="panel error">
<h1><?= \MMO\core\server\statusMessage($status) ?></h1>
</div>
```
### Templates outside the page tree: emails
`MMO\core\controllers\MailController` renders its templates with the same `import()`, in a **raw** `HtmlDocument`: the theme
leaves it alone, so there is no `<mmo-app>` (`$this['body']` is the real `<body>`), no stylesheet, and no
`app`/`route`/`controller` in `$_VARS` -- only `document` and the data given to the mail.
A page can still show such a template inside the application, to preview it. `$_VARS` only has to be an
`ArrayAccess`, so an object that answers `{name}` for every missing key prints the template with its holes
visible:
```php
<?php
$vars = new class implements \ArrayAccess {
protected array $data = [];
//only what was really set exists, so import() still adds `document` and the others
public function offsetExists(mixed $k): bool {return array_key_exists($k, $this->data);}
public function offsetGet(mixed $k): mixed {return $this->data[$k] ?? '{' . $k . '}';}
public function offsetSet(mixed $k, mixed $v): void {$this->data[$k] = $v;}
public function offsetUnset(mixed $k): void {unset($this->data[$k]);}
};
$this->addImportPath(dirname(__DIR__) . '/emails', true);
?>
<?= $this->import('welcome.phtml', $vars, false) ?>
```
## References
### See also
- **The JHS format**
- **Styles**
## Tags
`#page`, `#theme`, `#menu`
# The MMO javascript API
Everything the client provides hangs off one global, `MMO`. The sources are in `src/js`, and in dev mode
the theme recompiles them into `demo/www/bin/mmo.client.js` whenever one of them is newer than the bundle
— you save a file and reload. `CTRL+SHIFT+R` if the browser is holding on to the old bundle.
> Compiled javascript is what the page loads, so a syntax error shows up as a missing bundle rather than a
> console error on the line you just wrote. `demo/www/bin/mmo.client.js.map` maps it back to the source.
## How it is laid out
```
src/js/mmo.js the MMO global: urls, fetching, printing, logging
src/js/core/ Map, Template, Window, Storage, Date, Color, HTMLSearch, APIRequester
src/js/core/functions/ the namespaced helpers: MMO.core.dom, .strings, .path, .server, .svg, .markdown
src/js/elements/ View, CustomElement, CustomShadowElement, Widget, Entity — what a .jhs compiles against
src/js/schemas/ the client-side JSON Schema validator
src/js/inputs/ unified pointer/touch/mouse gestures
src/js/i18n/ translations; MMO.i18n.$('Search') is the lookup
```
## The ones worth knowing
### Urls
```javascript
MMO.url('contents/page.html'); // resolved against the site's url
MMO.url('#house', 'icons'); // against a named path: icons, assets, bin, basepath, baseurl
```
The named paths are the ones the server put in the page's configuration, which is why an element can point
at the icon sprite without knowing where it was deployed.
### Fetching
```javascript
const data = await MMO.fetchJSON(url);
const text = await MMO.fetchText(url);
const svg = await MMO.fetchSvg(url);
```
`MMO.fetchAPI()` is the one that understands the framework's own json api envelope, and gives back
`{data, links, schema, logs}` with the entities already built — see [Controllers and routing](applicationLogic?guide=20-controllers.md).
### The DOM helpers
```javascript
MMO.core.dom.$('#my-element'); // querySelector, shadow-root aware
MMO.core.dom.$$('.row', root); // querySelectorAll, and it looks through slots
MMO.core.dom.createElement('li', {class: 'x'}, 'text', childNode);
MMO.core.dom.setAttributes(node, {hidden: true, 'data-x': null}); // a null value sets nothing
MMO.core.dom.dispatchEvent(node, 'change');
```
`$` and `$$` are not "by id": they take any selector. What they add over the native calls is the shadow
dom — `$` falls back to the host's tree when a selector does not match inside a shadow root, and `$$`
also searches the elements assigned to the slots.
`setAttributes` skips a `null` value rather than writing `"null"`, which is how an optional attribute is
left off.
`createElement` and `appendChildren` insert a string child as **text**, never as markup: a file's name or a
user's label shows as it was typed, `<b>` and all, and runs nothing. Markup you wrote yourself is marked with
`MMO.core.dom.html()`:
```javascript
MMO.core.dom.createElement('p', {}, MMO.core.dom.html('<b>Saved</b> in '), folder.name);
```
The shell's messages — `MMO.app.notify()`, `alert()`, `confirm()`, `ask()`, `error()`, `showModal()`,
`showPanel()` — take their content the same way.
### Waiting for the page
```javascript
MMO.domReady(() => { … });
```
Custom elements are defined by then, so this is where a page configures one.
### Rendering a view
```javascript
MMO.elements.View.RenderView('my-view', document.getElementById('container'), {name: 'John'});
```
See [The .jhs file](frontEnd?guide=10-jhs.md) for what a view is.
## Where to write it
Javascript for a component belongs in that component's `.jhs`, in its `<script>`. A page's own javascript
goes in the page's `.phtml`, inside `MMO.domReady()`. Shared code belongs in `src/js` and becomes part of
the bundle.
# Styling
Styling is not documented here. It has sections of its own on this site, because it is a design system
rather than a set of conventions, and every page of it is compiled live from the same source the framework
uses.
| where | what it covers |
|---|---|
| [Simple CSS](../scs/overview) | the `.scs` language: `@let`, `@style`, `@use`, nesting, screens, and the compiler |
| [SCS reference](../scs/reference) | every at-rule and function, with the css each one generates |
| [Styles](../styles/overview) | the design system itself: what exists, and what to reach for |
| [Tokens](../styles/tokens) | the colour, spacing, radius, type and shadow scales — `$color.text`, `$space.md`, `$radius.sm` |
| [Theme configurator](../theme/configurator) | change the palette and see the whole site recompile |
## The two rules worth repeating here
**An element's stylesheet is scoped to its shadow root.** The document's css never reaches inside a custom
element. What crosses the boundary is inherited custom properties — which is exactly what the token layer
is made of, and why an element that styles itself with `$color.text` follows the theme while one that
writes `#333` does not.
**Nothing is written twice.** A shared pattern is a style group, `@use`d by name:
```scs
#content {
@use controls::list;
margin: 0;
}
```
The groups live in `src/styles` (`controls.scs`, `lib.scs`, `core.scs`), and `src/styles/tokens.scs` is
the single file every value comes from.
Sass is gone from this package: all 42 elements style themselves in `.scs`. `MMO\client\DartSass` is kept
only for an application that still ships `<style lang="sass">`.