Menu

🥊 moxi.js: inline scripting

Overview

moxi.js is the client-side scripting component of the fixi project, helping you write client-side enhancements to your fixi-based applications. Like fixi.js, moxi focuses on using attributes to add behaviors to HTML elements.

moxi.js can be used independently from fixi.js, but the two dovetail nicely.

Attributes

There are two types of moxi attributes:

attribute description
on-<event> Executes JavaScript when the given <event> occurs on this element.
live JavaScript that is re-evaluated when the DOM or form state changes.

Modifiers

on-<event> handlers can chain modifiers, separated by dots. For example, on-click.prevent.stop="..." calls preventDefault() and stopPropagation() before the handler body runs.

modifier description
.prevent calls event.preventDefault() before the handler runs
.stop calls event.stopPropagation() before the handler runs
.halt shorthand for .prevent.stop
.once removes the listener after the first successful fire
.self skips bubbled events from children (event.target !== this)
.capture passes {capture: true} to addEventListener
.passive passes {passive: true} to addEventListener
.outside attaches the listener to document; fires only when the event happened outside the element
.cc camel-cases the event name (on-my-event.cc listens for myEvent); useful for camelCase events from web components

Scripting Helpers

To make scripting more pleasant, moxi exposes a few global helpers:

helper description
q(x) A query mechanism. x can be a CSS selector, an Element, or any iterable of elements.
wait(x) returns a Promise that resolves after x milliseconds (when x is a number) or when an event named x fires (when x is a string).
transition(fn) wraps fn in a view transition if available.

q() also supports relative selectors like next div and li in this.

The q() Proxy

q() returns a proxy object with useful features for working with sets of elements:

method description
q(...).trigger(name, detail, bubbles?) dispatch a CustomEvent from every match
q(...).take(cls, from) take CSS class cls from elements matching from
q(...).insert(pos, html) parse and insert HTML at every match
q(...).count the count of matches
q(...).arr() converts the proxy to an array

In addition, you can iterate naturally over results from q() and, like jQuery, properties and methods can be invoked on all elements in the collection.

Handler-Scope Helpers

Inside on-* and live handler bodies, two extra functions are in scope:

helper description
trigger(name, detail, bubbles?) dispatches a CustomEvent from this. To dispatch from another element use q(elt).trigger(...).
debounce(ms) debounces the current handler; if it is invoked again within ms it will not execute.

Note that q() directionals (next, prev, closest, in this) and wait("event") are context-aware: in a handler they resolve relative to this; called globally they resolve relative to document.documentElement.

Note also that all fields on the event.detail object will be unpacked into top level scope. This is particularly useful when working with fixi’s events.

Examples

Below are some moxi examples, many showing how well it composes with fixi.js.

A Simple Greeting

Here is a moxi-powered input that updates an output as you type, with a button that clears it:

<input id="name" placeholder="type your name">
<output live="this.innerText = q('#name').value ? 'hello ' + q('#name').value : ''"></output>
<button on-click="q('#name').value = ''">clear</button>

The live expression on the <output> re-runs every time the input fires input or change, so the greeting tracks what you type.

The button’s on-click handler clears the input via the same q() helper.

Disable Submit Until Valid

This is the signup form from the fixi page, with the submit button now staying disabled until every required field passes HTML validation:

<form fx-action="/signup" fx-method="POST"
      fx-target="#status" fx-swap="innerHTML">
    <input name="email" type="email" required placeholder="email">
    <input name="password" type="password" required minlength="8" placeholder="password">
    <button type="submit" live="this.disabled = !q('closest form').checkValidity()">
        Sign up
    </button>
</form>
<output id="status"></output>

The live expression re-runs whenever any input or change fires on the page, so the button updates in real time without manual event wiring.

q('closest form') walks up from the button to its containing form, so the same handler works in any form.

Master Checkbox

A master checkbox that mirrors a group of subordinate checkboxes: checked when every match is checked, indeterminate when some are, unchecked when none are. Clicking the master propagates its state to every subordinate.

<input type="checkbox" id="select-all" checked
       live="let b=q('.pick').arr();
             let n=b.filter(x=>x.checked).length;
             this.checked=n===b.length;
             this.indeterminate=n>0&&n<b.length"
       on-click="for (let x of q('.pick')) x.checked=this.checked">

The live expression re-runs on every input/change anywhere on the page, so toggling any single .pick keeps the master in sync. The on-click handler walks the subordinates and sets each one’s .checked to the master’s, giving select-all / deselect-all behavior in one line.

Confirm Before Delete

moxi handlers can be used to listen for fixi’s lifecycle events.

Setting cfg.confirm on an fx:config event tells fixi to pause the request long enough to call the function, and abort if it returns false:


<button fx-action="/things/42" fx-method="DELETE"
        on-fx:config="cfg.confirm = () => confirm('Delete?')">
    delete
</button>

Here you can see the cfg object is exposed directly because moxi unpacks event.detail keys into scope.

Active Tab

q().take(cls, from) moves a CSS class from one element to another in one line, which is exactly what an active-tab indicator wants:


<nav>
    <button class="tab active" on-click="q(this).take('active', '.tab')">One</button>
    <button class="tab" on-click="q(this).take('active', '.tab')">Two</button>
    <button class="tab" on-click="q(this).take('active', '.tab')">Three</button>
</nav>

Each button takes the active class away from its siblings and onto itself.

Click-Outside-to-Dismiss

The .outside modifier attaches the listener to document and fires only when the event target is outside the element, which is exactly the behavior a dismissable menu or popover wants:

<button on-click.stop="q('#menu').hidden = false">open menu</button>
<div id="menu" hidden on-click.outside="this.hidden = true">menu items...</div>

The .stop on the opener keeps the click that opened the menu from immediately re-dismissing it.

Triggering A fixi Request

A moxi handler can fire a custom event that a fixi element is listening for, which is how you wire one element’s interaction to another element’s request:

<button on-click="q('#feed').trigger('refresh')">refresh</button>
<div id="feed" fx-action="/feed" fx-trigger="refresh"></div>

The #feed div’s fx-trigger="refresh" makes it listen for the refresh event; the button dispatches that event from the div and fixi issues the request.

A search input that fires a fixi request only after the user pauses typing. moxi’s debounce helper waits for a quiet moment, then trigger('search') dispatches the same event the input is itself listening for via fx-trigger:

<input id="q" name="q" placeholder="search"
       fx-action="/search"
       fx-trigger="search"
       fx-target="#results"
       on-input="await debounce(250); trigger('search')">
<div id="results"></div>

Each keystroke schedules a search event 250ms in the future; subsequent keystrokes supersede the previous timer, so only the final one actually fires. fixi then issues GET /search?q=... (the input’s own name=q is picked up automatically) and swaps the response into #results.

Polling

moxi’s on-init runs once when the element is wired up, and because handlers are async, an infinite loop with await wait(...) gives you a polling timer in two lines:


<div id="status"
     fx-action="/status"
     fx-trigger="poll"
     fx-swap="innerHTML"
     on-init="while (this.isConnected) { trigger('poll'); await wait(2000); }">
    loading...
</div>

trigger('poll') dispatches the poll event on the div (in a handler trigger resolves to this), which fixi’s fx-trigger="poll" picks up to issue the request.

Gating the loop on this.isConnected lets it bail cleanly when the element is replaced or removed, so the timer doesn’t leak past the element’s lifetime.

Reference

For the full attribute and modifier reference, the proxy method table, and lifecycle events, see the moxi README on GitHub.

Next up is paxi.js, which adds lossless DOM swaps to fixi.