Hono: what MX compiles to
hono/jsx has no control-flow components, so every structural tag becomes a plain JSX expression: what you would have written by hand.
| Written | Emitted |
|---|---|
text, ${expr} |
text, {expr} |
$!{expr} as the sole child |
dangerouslySetInnerHTML={{ __html: expr }} |
class="a", .a |
class="a" |
class={ a: cond }, class=[…] |
class={mxClass({ a: cond })} |
<label for=…> |
for={…}, unchanged |
style={ color: c } |
style={{ color: c }} |
onClick() { … } |
onClick={() => { … }} |
<if> / <else if> / <else> |
a conditional expression, null for a missing else |
<for|x| of=xs> |
[...xs].map((x) => …) with a key |
<for|v, k| in=obj> |
an Object.entries map, keyed by the property name |
<for|i| from=a to=b> |
a generated range map |
<Comp x=1>children</Comp> |
the component called with x and children |
<@name> |
the prop name; repeated tags become an array |
<@name|p|> |
the prop name as a function of p |
<define/Row|p|> |
a local function, called as Row(p) |
<try> |
ErrorBoundary and Suspense, both from hono/jsx |
Element or component?
MX follows Marko’s rule, not JSX’s capital letter: a tag is a component when a binding in scope or a discovered custom tag has that name, whatever its case. A tags/badge.mx makes <badge/> a component call. The other side of the rule: an import named like an element (import { label } from "./i18n") turns <label> into a call of it. Rename the import.
Keys
Every <for> row carries a key: by="id" keys by a field of the row, by=fn by a function of it, and with no by the key is the row itself. A server render does not need them; they matter when the same component runs under hono/jsx/dom in the browser, where a list of objects or a list with duplicates needs a by.
Events
For components that run in the browser with hono/jsx/dom. MX reads the DOM event name (the text after on, lowercased) and emits the prop Hono’s JSX types declare for it: onClick=f is onClick={f}, onKeydown=f and on-keydown=f are both onKeyDown={f}, and onDblClick=f and on-dblclick=f are both onDoubleClick={f}, which Hono binds to dblclick.
- Only names Hono declares. Hono’s types declare
onDoubleClick, so the authoredonDoubleClickemits it too (MX still warns thatdoubleclickis not a DOM event;onDblClickis the spelling that works on every host). A DOM event Hono’s types have no handler prop for, such ason-toggleoron-search, is a compile error. onChangefires on every keystroke. Hono binds it to theinputevent, as React does. UseonInputto say so plainly.- Custom DOM events (
on-my-event=f) are a compile error on React, Preact and Hono alike. Use arefcallback that callsaddEventListener. - On a component,
onSelect=pickis an ordinary prop.
The shared rules are in Attributes.
<try>: error and loading states
<try>
<Profile id=userId/>
<@placeholder><p>Loading…</p></@placeholder>
<@catch|err|><p.error>${err.message}</p></@catch>
</try>
Hono has both pieces, so MX wraps nothing: <@catch> becomes hono/jsx’s ErrorBoundary with fallbackRender, and <@placeholder> becomes its Suspense. With both present, the placeholder sits inside the boundary. ErrorBoundary resolves asynchronously once a child throws, so await the render (await App(props).toString()) when a tree can throw.
Attribute tags on your own components
Export an Input type from the component and mark the markup props with AttrTag from @mxlang/hono. A renderable is a Hono Child; with params it is a function returning one; AttrTag[] is a real array. With no declaration, MX infers the shape from the call. See AttrTag.
How this is checked
bun run oracle:hono renders the shared fixture set through hono/jsx and compares it with Marko’s own output on every CI run. examples/hono-app serves a live Hono server, and its tests assert the response carries no <script>.