define() Components
For reusable components, use define(tagName, { $() {...} }) to create custom elements. The $ function receives props and returns children to render.
What define() provides
this - the actual DOM elementthis.$attrs(attrs) - efficiently set multiple attributesthis.$state - component state objectthis.render(...children) - update children
Here's a simple row component:
define('a-row', {
$({ columns, data }) {
let { div } = mx;
return columns.map(col => {
let value = col.fn?.(data[col.key], data) || data[col.key];
return div({ class: 'table-cell' }, value);
});
}
});
// Use it
let columns = [
{ key: 'name', label: 'Name' },
{ key: 'age', label: 'Age' }
];
let user = { name: 'Alice', age: 25 };
document.body.render(
mx.aRow( { columns, data: user })
);Components can compose other components:
define('a-table', {
$({ columns = [], data = [], rowHeight = 48, static = false }) {
let { div } = mx;
let header = div({ class: 'table-header' },
...columns.map(col => div({ class: 'table-cell' }, col.label || col.key))
);
let rows = data.map((item, index) =>
mx.aRow({ columns, data: item, index })
);
if (static) {
return [header].concat(rows);
} else {
return [header, mx.virtualContainer({ height: 400, rowHeight, rows })];
}
}
});
// Use it
document.body.render(
mx.aTable( {
columns: [{ key: 'name' }, { key: 'age' }],
data: [{ name: 'Alice', age: 25 }, { name: 'Bob', age: 30 }]
})
);// 1000-row virtual-scrolling table built from define() components
let columns = [
{ key: 'name', label: 'Name' },
{ key: 'age', label: 'Age' },
{ key: 'city', label: 'City' }
];
let data = [];
for (let i = 0; i < 1000; i++) {
data.push({
name: 'Person ' + (i + 1),
age: 20 + (i % 50),
city: ['New York','Los Angeles','Chicago','Houston','Phoenix'][i % 5]
});
}
document.body.render(
div({ style: 'border:1px solid var(--dim); border-radius:2px; overflow:auto;' },
mx.aTable( { columns, data })
)
);Best Practice
Use define() for reusable components that you'll use across your app. This creates proper custom elements with all the benefits of DOM encapsulation.
Functions (local fragments)
Functions are useful for local, view-specific fragments that you don't need to reuse. For most reusable components, prefer define().
let { div, span } = mx;
// Local helper for this view only
let StatusBadge = (status) => {
let color = status === 'active' ? 'green' : 'gray';
return span({ class: 'badge', style: `color: ${color}` }, status);
};
// Use inline in your view
document.body.render(
div({ class: 'user-card' },
div('Username'),
StatusBadge('active')
)
);When to use functions vs define()
- Functions: Local helpers, view-specific fragments, one-off compositions
- define(): Reusable components, components needing state/methods, shared UI elements
Named Slots (cheap & cheerful)
Pass children by name using a plain object. No Shadow DOM required.
let { div, h3, p, button } = mx;
let Card = (props = {}, kids = []) => {
let slots = Array.isArray(kids) ? { default: kids } : kids;
return div({ class:"card" },
div({ class:"card__header" }, ...(slots.header || [])),
div({ class:"card__body" }, ...(slots.default || []) ),
div({ class:"card__footer" }, ...(slots.footer || []) )
);
};
document.body.render(
Card({}, {
header: [ h3("Title") ],
default: [ p("Body text") ],
footer: [ button("OK") ]
})
);Icon Component (SVG rendering)
Components aren't limited to HTML elements. An icon component that renders SVG paths from a dictionary is a scalable, flexible way to manage icons across your app:
let icons = {
check: 'M5 12l5 5L20 7',
close: 'M6 6l12 12M18 6L6 18',
star: 'M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01z',
upload:'M12 15V3m0 0L7 8m5-5l5 5M4 17v2a2 2 0 002 2h12a2 2 0 002-2v-2'
};
define('an-icon', {
$({ icon, color }) {
if (color) this.style.stroke = color;
return mx.svg({ viewBox: '0 0 24 24' },
mx.path({ d: icons[icon] || '' })
);
}
});
// Use it
document.body.render(
mx.div({ style: 'display:flex; gap:16px' },
mx.anIcon( { icon: 'check' }),
mx.anIcon( { icon: 'star', color: 'gold' }),
mx.anIcon( { icon: 'upload', color: '#4285f4' })
)
);SVG in mx
mx handles SVG elements the same way as HTML. mx.svg(), mx.path(), mx.circle() all work. Attributes like viewBox and stroke-width are set via setAttribute as expected.
Stateful components ($state)
Components can hold persistent state in this.$state. The state object survives across re-renders. Call this.$({ ... }) from inside the component to update state and re-render.
define('a-counter', {
$({ count = 0, step = 1 }) {
let { div, button, span } = mx;
let c = (this.$state.count ??= count);
return div(
button({ onclick: () => this.$({ count: c - step }) }, '−'),
span(c),
button({ onclick: () => this.$({ count: c + step }) }, '+')
);
}
});
document.body.render(
mx.aCounter({ count: 10, step: 5 })
);The ??= pattern
Use this.$state.x ??= defaultValue to initialize state only on the first render. On subsequent re-renders, the stored value is returned instead. This is how components keep state without external stores.
Updating components externally
Use dom() to get a real element reference, then call el.$({ ... }) to push new props into the component at any time:
define('step-indicator', {
$({ steps = [], currentStep = 1 }) {
let { div } = mx;
return steps.map((step, i) => {
let n = i + 1;
let state = n < currentStep ? 'done'
: n === currentStep ? 'active'
: '';
return div({ class: 'step ' + state },
div({ class: 'circle' }, n < currentStep ? '✓' : n),
div({ class: 'label' }, step)
);
});
}
});
// Create with dom() - get a real element
let stepper = dom('step-indicator', {
steps: ['Account', 'Profile', 'Confirm'],
currentStep: 1
});
// Later, advance the step
stepper.$({ currentStep: 2 });
// Even later
stepper.$({ currentStep: 3 });dom() vs mx()
mx() - returns a description (array with ._=true). Use inside render() calls where mx manages the DOM.dom() - returns a real HTMLElement. Use when you need a persistent reference to call .$() on later.