API

APIReturnsPurpose
mx(tag, attrs?, ...children)ArrayElement description for render()
mx.tag(attrs?, ...children)ArrayShorthand via Proxy
dom(tag, attrs?, ...children)HTMLElementCreate real DOM element
dom.tag(attrs?, ...children)HTMLElementShorthand via Proxy
define(name, { $, ... })voidRegister a component
componentsobjectComponent registry
el.render(...nodes)thisReconcile children into element
el.$attrs(attrs)thisBatch-set attributes/properties
el.$(state)thisUpdate component state + re-render
el.$stateobjectPersistent state across renders

Attributes

SyntaxBehaviorExample
name: 'value'setAttributeclass: 'active'
'.name': valueel[name] = value'.value': text
name: nullremoveAttributedisabled: null
name: falseremoveAttributehidden: false
name: truesetAttribute(name, '')disabled: true
onname: fnel[name] = fnonclick: handler
value / checkeddual-track (attr + prop)value: text

render() Rules

InputBehavior
mx.tag(...)Create/reuse by tag name, update attrs, recurse children
Real DOM nodeIdentity match by reference - same = no-op, different = move
String / NumberCreate/update text node (numbers auto-cast)
null / falseSkipped - use for conditional rendering

Excess old children removed. Returns this for chaining.

Component

define('my-thing', {
    $({ label = '', count = 0, onclick }) {
        return [
            mx.span(label),
            mx.span(count),
            mx.button({ onclick }, '+')
        ];
    }
});

// Use
container.render(mx('my-thing', { label: 'Clicks', onclick: _ => ... }));

// Update
el.$({ count: 5 });

State

// Props merge into $state automatically
$({ count = 0 }) {
    // this.$state === the argument object
    // Parent: el.$({ count: 5 })  →  count = 5
    // Self:   this.$({ count: count + 1 })  →  re-renders

    // Private cache (not in $state)
    this._rowMap ||= new Map;
}

Events

// On children - property in attrs
mx.button({ onclick: _ => this.$({ count: count + 1 }) }, '+')

// On component root - direct assignment
this.onclick = _ => navigation.navigate('/page');

// Callback props
$({ onchange }) {
    mx.input({ oninput: e => onchange?.(e.target.value) })
}

// Event bus
events.send('asset:select', id);
events.on('asset:select', id => updateAll());

Conditional Rendering

// Short-circuit (preferred)
isOpen && mx.div('Content')
hasItems && mx.ul(...items.map(i => mx.li(i.name)))
showBtn && mx.button('Click')

// Ternary (when you need an else branch)
isLoading ? mx('spinner') : mx.div('Loaded')

// In arrays - falsy values skipped
return [
    mx.h1('Title'),
    showBtn && mx.button('Click'),
    mx.p('Footer')
];

Lists

// Simple - fresh descriptions each render
mx.ul(...items.map(item => mx.li(item.name)))

// Keyed - persistent DOM for stateful rows
this._rowMap ||= new Map;
for (let d of data) {
    let el = this._rowMap.get(d.id) ?? this._rowMap.set(d.id, dom('my-row')).get(d.id);
    el.$({ data: d });
}
container.render(...data.map(d => this._rowMap.get(d.id)));

Forms

// Input binding
mx.input({
    value: name,
    oninput: e => this.$({ name: e.target.value })
})

// Checkbox
mx.input({
    type: 'checkbox',
    checked: isActive,
    onchange: e => this.$({ isActive: e.target.checked })
})

// Form submit
mx.form({ onsubmit: e => {
    e.preventDefault();
    // handle submission
}}, ...fields)

SVG

// Auto-detects SVG namespace
mx.svg({ viewBox: '0 0 24 24', width: 20, height: 20 },
    mx.path({ d: 'M12 2L2 7l10 5 10-5z', fill: 'none',
        stroke: 'currentColor', 'stroke-width': '2' })
)

// Sparkline
let pts = data.map((v, i) =>
    (i / (data.length - 1) * w).toFixed(1) + ',' +
    (h - (v - min) / range * h).toFixed(1)
).join(' ');
mx.svg({ viewBox: '0 0 ' + w + ' ' + h },
    mx.polyline({ points: pts, fill: 'none', stroke: color })
)

Timers & Intervals

$({ isPlaying = false }) {
    clearInterval(this._interval);  // always clear first

    if (isPlaying) {
        this._interval = setInterval(_ => {
            this.$({ progress: this.$state.progress + 1 });
        }, 1000);
    }
}

Class Binding

// Ternary concat
class: 'tab' + (active ? ' active' : '')
class: 'col ' + (pct >= 0 ? 'positive' : 'negative')

// Array join - cleaner for multiple conditionals
class: ['btn', selected && 'selected', disabled && 'disabled'].filter(Boolean).join(' ')

Dynamic Styles

// Inline string
style: 'width:' + pct + '%;background:' + color

// On component root
this.style.height = height + 'px';

// css() helper - object to style string (not part of mx, just handy)
let css = o => Object.entries(o).map(([k, v]) => k + ':' + v).join(';');
style: css({ width: pct + '%', background: color, opacity: 0.8 })

Routing

navigation.addEventListener('navigate', e => {
    let url = new URL(e.destination.url);
    if (url.origin !== location.origin) return;
    e.intercept({ handler: _ => navigate(url.pathname) });
});

// SPA link
mx.a({ href: path, onclick: e => {
    e.preventDefault();
    navigation.navigate(path);
}}, label)

render() vs append()

// render() - declarative: reconciles, reuses, handles conditionals, removes excess
container.render(
    mx.h1('Title'),
    showSub && mx.p('Subtitle'),  // false → skipped
    mx.div('Content')
);

// append() - imperative DOM: just adds nodes, no reconciliation
container.append(element);

// Prefer render() everywhere - even where append() would work.
// render() gives you declarative control, conditionals, and cleanup for free.

Conventions

RuleDoDon't
Variablesletconst / var
Unused param_ =>() =>
Defaultssize = 20size || 20
EventsonclickonClick
Numbersmx.span(count)mx.span(String(count))
Clearingel.textContent = ''el.innerHTML = ''
Callbacksonchange?.(v)onchange && onchange(v)
CSS unitspxrem

Common Mistakes

// ✗ Mutates parent array
assets.sort(compareFn);
// ✓ Copy first
let sorted = [...assets].sort(compareFn);

// ✗ Stacks listeners every render
$() { this.addEventListener('click', _ => doStuff()); }
// ✓ Property assignment
$() { this.onclick = _ => doStuff(); }

// ✗ React event names
mx.button({ onClick: fn })
// ✓ Lowercase
mx.button({ onclick: fn })