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

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():

SyntaxTypeBehaviorExample
nameStandard attributesetAttribute(name, value). In render() only, checked/selected/value also set the property.class: 'box'
.namePropertyDirectly sets el[name] = value. Bypasses attributes entirely.'.value': 'text'
name: nullRemovalremoveAttribute(name). In render() only, checked/selected/value also clear the property to ''.disabled: null
name: falseRemovalSame as null - removes the attribute.hidden: false
name: trueBoolean attributesetAttribute(name, 'true') - sets the attribute to the string "true" (consistent across mx, dom, and $attrs; correct for enumerated attrs like aria-*).disabled: true
onname: fnEvent handlerFunctions 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

APIReturnsPurpose
mx(tag, attrs?, ...children)ArrayCreate element description for render()
mx.tag(attrs?, ...children)ArrayShorthand via Proxy destructuring
dom(tag, attrs?, ...children)HTMLElementCreate a real DOM element immediately
define(name, { $, ... })voidRegister a component tag name
componentsobjectGlobal component registry
el.render(...nodes)thisReconcile children into element
el.$attrs(attrs)thisBatch-set attributes/properties
el.$(state)thisUpdate component state and re-render
el.$stateobjectComponent state (persistent across renders)