Rocket components (Datastar Pro) — when to reach for them
Everything above is build-stepless HTML: bake the DOM, bind it, inline the
bundle. Rocket is the other mode — Datastar Pro's web-component API
(rocket(tag, {...})). It packages a component as a real custom element with a
typed prop API, instance-scoped state, and a render() that can loop. It is
not a replacement for the recipe above; it is for a specific kind of component.
Use Rocket only when all of these hold (the date picker is the canonical yes):
- There is a public API worth typing — attributes a page author sets
(
mode,value,value-end), decoded via codecs (oneOf,string,date). - The state is genuinely ephemeral and client-only — draft selection, hovered cell, shown month. In a server-owns-state app (land-and-stream), the committed value is server state and flows back in as an attribute; only the throwaway interaction state lives in the component. If the "state" is application data, it does not belong in a client component — render it server-side.
- The internal DOM is repetitive — Rocket's
rendersupports<template data-for="i in [...]">, so the 42 cells are looped, not baked. This is the main ergonomic win over the vanilla recipe. ⚠ The carve-out (gf-44 doctrine): if the server ALREADY renders the markup, Rocket owns interaction ONLY — a behavior wrapper over server truth. A roster needs no rocket at all: it is server markup under the engine's.rostercomponent (gf-98). The app's month calendar is server markup overcal-*; a client-rendered calendar is the standalone-widget case, never the app's. - You reuse it across pages, so a registered element + typed wrapper pays off.
Display-only or server-driven things (card, tag, alert, table, dashboard, a disclosure toggle) are not Rocket candidates — a typed templ/partial wrapper gives the same drop-in reuse without a client runtime. By this test most apps have only a few Rocket components: date/time pickers, combobox, token input.
The shape
import { rocket } from '/static/datastar.js'; // ABSOLUTE path (see gotcha)
// pure date/format helpers on window so rendered expressions can call them
Object.assign(window, { cellDay, cellIso, cellInMonth, cellIsToday, som, monthLabel, fmtPretty });
rocket('event-date-picker', {
mode: 'light', // inherit system.css; shadow DOM walls it off
props: ({ oneOf, string }) => ({
mode: oneOf('single','range').default('single'),
value: string.default(''), // committed start ISO; reflects to attribute
valueEnd: string.default(''),
}),
setup: ({ $$, props, host, action }) => { // destructure `action` — needed below
$$.da = ''; $$.db = ''; $$.hover = ''; // ephemeral draft/UI state
$$.v = props.value ? som(new Date(props.value).getTime(),0) : som(Date.now(),0);
action('commit', () => { // registered action (see gotcha)
host.setAttribute('value', $$.da);
host.dispatchEvent(new CustomEvent('change', { bubbles:true, detail:{ start:$$.da, end:$$.db||$$.da }}));
});
},
render: ({ html }) => html`
<div class="calendar">
<template data-for="i in [0,1,2,/* … */,41]"> <!-- looped, not baked -->
<button data-text="cellDay($$v,i)"
data-attr:data-ui-selected="!!($$da && cellIso($$v,i)===$$da)"
data-on:click="$$clicked=cellIso($$v,i); @pick()"></button>
</template>
<button data-on:click="@commit(); el.closest('[popover]').hidePopover()">Confirm</button>
</div>
`,
});