API Reference
Complete reference for every function and method exposed by mx.js. The library exposes four globals: mx, dom, define, and components.mx(tag, attrs?, ...children)
Creates a lightweight element description (an array with ._=true). This is not a DOM node - it's a compact representation that render() will reconcile against the live DOM.
mx is a Proxy. You can call it directly or destructure tag helpers from it:
// Direct call
mx('div', { class: 'box' }, 'Hello');
// Proxy destructuring (most common)
let { div, span, button, h1 } = mx;
div({ class: 'box' }, span('Hello'));
// CamelCase auto-converts to kebab-case
mx.myComponent({ prop: 'value' });
// equivalent to: mx('my-component', { prop: 'value' })Signature
mx(tagName: string, attrs?: object, ...children) → Array
If the first argument after the tag is not a plain object (or has a nodeType - i.e. it is a real DOM node), it is treated as a child, not attributes.
dom(tag, attrs?, ...children)
Creates and returns an actual DOM element immediately. Same Proxy/destructuring pattern as mx, but produces live nodes instead of descriptions.
// Direct call - returns a real DOM element
let el = dom('div', { class: 'box' }, 'Hello');
document.body.appendChild(el);
// Proxy destructuring
let { div, input } = dom;
let field = input({ type: 'text', '.value': 'hello' });
document.body.appendChild(field);
// With components - calls $() and returns the element
let btn = dom('a-button', { label: 'Click me' });Signature
dom(tagName: string, attrs?: object, ...children) → HTMLElement
For component elements (registered via define()), dom() calls element.$(attrs) and returns the element. For plain elements, it calls $attrs() and render().
When to use dom() vs mx()
Use mx() inside render() calls for efficient reconciliation. Use dom() when you need a real element immediately (e.g., to store a reference, call methods on it, or pass to third-party code).
define(name, methods)
Registers a component tag name. When mx creates an element with that tag, it copies the methods onto the element instance. The special $ method is the render function.
define('my-counter', {
$({ count = 0 }) {
let { div, button, span } = mx;
return div(
button({ onclick: () => this.$({ count: count + 1 }) }, '+'),
span(count)
);
}
});
// Usage with mx()
document.body.render(
mx.myCounter({ count: 5 })
);
// Usage with dom()
let counter = dom('my-counter', { count: 5 });The $ function
The $ method is the core of every component. It receives a state object (merged from attributes and this.$state) and returns an array of children to render.
define('greeting', {
$({ name = 'World' }) {
// this = the DOM element
// this.$state = persistent state across renders
// Return children (array or single mx array)
return mx.div(
mx.h2('Hello, ' + name),
mx.p('Welcome to mx.js')
);
}
});What $ receives
When called via render(), the state argument is the attributes object from mx('tag', attrs), merged into this.$state via Object.assign.
When called directly as element.$(newState), the argument is merged the same way.
this.$state
A plain object that persists across renders. Every $ call runs Object.assign(this.$state, props) before your function, so $state always reflects the most recently merged props. You rarely reference it directly - destructure from the argument and call this.$({...}) to update:
$({ count = 0 }) {
// count is destructured from $state after the merge.
// Default (= 0) applies on first render when nothing has been set.
return mx.button({
onclick: () => this.$({ count: count + 1 })
}, count);
}For the rare case of seeding a value that can't come as a prop (stable IDs, component-owned Sets) and needs to be externally readable, this.$state.x ??= defaultValue is available. For internal-only state (intervals, caches, DOM refs) use this._x instead - see $state vs this._property.
this.$attrs(attrs)
Efficiently sets multiple attributes/properties on the element. Supports the same prefix system as render(). Returns this for chaining.
let el = dom.input();
el.$attrs({
type: 'text',
'.value': 'hello',
class: 'form-input',
placeholder: 'Type here...'
});this.render(...children)
Available on every element (added to Element.prototype). Inside components, the wrapper calls render() with whatever $ returns.
Element.prototype.render(...nodes)
The core reconciliation method. Accepts any mix of mx arrays (._=true), DOM nodes, strings, and numbers. Falsy values (null, undefined, false) are skipped.
let { div, span } = mx;
let container = document.getElementById('app');
// Initial render
container.render(
div({ class: 'box' }, span('Hello')),
div({ class: 'box' }, span('World'))
);
// Update - mx diffs and patches minimally
container.render(
div({ class: 'box active' }, span('Hello!')),
div({ class: 'box' }, span('World'))
);Behavior
- Walks children left-to-right, reusing existing DOM nodes by tag name.
- If a child is a component (has
$), delegates rendering to that component. - Text nodes are updated in place when content changes.
- Excess old children are removed. New children are appended.
- Returns
this for chaining.
Signature
Element.prototype.render(...nodes: Array<MxArray | Node | string | number | null>) → this
Element.prototype.$attrs(attrs)
Sets attributes and properties on an element using the same prefix system as render() - but a plain key is always setAttribute; the checked/selected/value property writes belong to render() alone. dom() applies its attrs through $attrs, so the same holds for dom.select({ value }). Returns this for chaining.
el.$attrs({
class: 'active', // setAttribute('class', 'active')
'.value': 'hello', // el.value = 'hello'
'.checked': true, // el.checked = true
onclick: fn, // el.onclick = fn (functions are assigned as properties)
disabled: null, // el.removeAttribute('disabled')
hidden: false // el.removeAttribute('hidden')
});Differences from render()
$attrs is a thin attribute setter, not a full reconciler. Three asymmetries with render():
- Event handlers can't be cleared via $attrs.
$attrs({ onclick: null }) calls removeAttribute('onclick') - a no-op for handler properties. The previously assigned function stays attached. To remove a handler imperatively, do el.onclick = null directly. - $attrs never writes the value/checked/selected properties. A plain
$attrs({ checked: true }) only calls setAttribute. On a fresh <input> that looks right - the attribute still reflects into the property - but once the user has touched the control the attribute no longer drives it, so re-asserting the same value does nothing: input.$attrs({ checked: true }) after a click leaves input.checked === false, and $attrs({ value: 'b' }) after typing leaves the property on the typed text while the attribute reads "b". <select> and <textarea> have no value attribute at all, so dom.select({ value: 'b' }) and dom.textarea({ value: 'hi' }) select and fill nothing even when fresh - the same goes for $attrs on them. Use the .-prefix for the property path ('.checked': state, '.value': v), or use render(), which dual-tracks both. - $attrs doesn't clear value/checked/selected properties either.
el.$attrs({ checked: null }) removes the attribute but leaves el.checked === true. render() handles this via the element's attribute cache; $attrs does not.
If you need full cleanup semantics, call render() with fresh descriptions instead of mutating attrs directly.
Attribute Prefix Reference
The attribute system recognizes these patterns when setting attributes in both render() and $attrs() (and therefore dom(), which goes through $attrs). The dual-tracking of checked/selected/value noted below is render() only - $attrs and dom() do setAttribute/removeAttribute and nothing else for a plain key - see Differences from render():
| Syntax | Type | Behavior | Example |
|---|
name | Standard attribute | setAttribute(name, value). In render() only, checked/selected/value also set the property. | class: 'box' |
.name | Property | Directly sets el[name] = value. Bypasses attributes entirely. | '.value': 'text' |
name: null | Removal | removeAttribute(name). In render() only, checked/selected/value also clear the property to ''. | disabled: null |
name: false | Removal | Same as null - removes the attribute. | hidden: false |
name: true | Boolean attribute | setAttribute(name, 'true') - sets the attribute to the string "true" (consistent across mx, dom, and $attrs; correct for enumerated attrs like aria-*). | disabled: true |
onname: fn | Event handler | Functions are assigned as properties: el[name] = fn. Cleaned up automatically when the handler changes or is removed. | onclick: fn |
Event handler cleanup
mx tracks event handlers in the element's attribute cache. When a handler function changes between renders, the old one is replaced. When a handler is no longer present in attrs, it is set to null automatically.
Special attributes
Three attributes - checked, selected, and value - are dual-tracked by render(): setting one writes the attribute and the DOM property, and dropping one clears the property to ''. $attrs() does neither - it only calls setAttribute/removeAttribute.
On a pristine <input> the difference is invisible, because the attribute still reflects into the property. It appears the moment the control is dirty or the attribute doesn't reflect at all: after the user types, input.$attrs({ value: 'b' }) leaves the property on the typed text; select.$attrs({ value: 'b' }) does nothing to select.value at all, because <select> has no reflecting value attribute (nor does <textarea>), and that is true on a fresh element too. Use the .property prefix ('.value': v) whenever you mean the property - it works identically in both.
components
A plain object that serves as the global component registry. When you call define(name, methods), the methods are stored in components[name]. You can check if a component is registered:
if (!components['my-widget']) {
define('my-widget', { $() { ... } });
}Global exposure
components is a real global (window) property, not module-scoped. Alongside mx, dom, and define, loading mx adds these four names to window. mx also augments Element.prototype with render, $attrs, and R (a short alias for removeAttribute). Component elements additionally get $ and $state.
Quick Reference
| API | Returns | Purpose |
|---|
mx(tag, attrs?, ...children) | Array | Create element description for render() |
mx.tag(attrs?, ...children) | Array | Shorthand via Proxy destructuring |
dom(tag, attrs?, ...children) | HTMLElement | Create a real DOM element immediately |
define(name, { $, ... }) | void | Register a component tag name |
components | object | Global component registry |
el.render(...nodes) | this | Reconcile children into element |
el.$attrs(attrs) | this | Batch-set attributes/properties |
el.$(state) | this | Update component state and re-render |
el.$state | object | Component state (persistent across renders) |