API
| API | Returns | Purpose |
|---|
mx(tag, attrs?, ...children) | Array | Element description for render() |
mx.tag(attrs?, ...children) | Array | Shorthand via Proxy |
dom(tag, attrs?, ...children) | HTMLElement | Create real DOM element |
dom.tag(attrs?, ...children) | HTMLElement | Shorthand via Proxy |
define(name, { $, ... }) | void | Register a component |
components | object | Component registry |
el.render(...nodes) | this | Reconcile children into element |
el.$attrs(attrs) | this | Batch-set attributes/properties |
el.$(state) | this | Update component state + re-render |
el.$state | object | Persistent state across renders |
Attributes
| Syntax | Behavior | Example |
|---|
name: 'value' | setAttribute | class: 'active' |
'.name': value | el[name] = value | '.value': text |
name: null | removeAttribute | disabled: null |
name: false | removeAttribute | hidden: false |
name: true | setAttribute(name, '') | disabled: true |
onname: fn | el[name] = fn | onclick: handler |
value / checked | dual-track (attr + prop) | value: text |
render() Rules
| Input | Behavior |
|---|
mx.tag(...) | Create/reuse by tag name, update attrs, recurse children |
| Real DOM node | Identity match by reference - same = no-op, different = move |
| String / Number | Create/update text node (numbers auto-cast) |
null / false | Skipped - 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)));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
| Rule | Do | Don't |
|---|
| Variables | let | const / var |
| Unused param | _ => | () => |
| Defaults | size = 20 | size || 20 |
| Events | onclick | onClick |
| Numbers | mx.span(count) | mx.span(String(count)) |
| Clearing | el.textContent = '' | el.innerHTML = '' |
| Callbacks | onchange?.(v) | onchange && onchange(v) |
| CSS units | px | rem |
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 })