system.cssthe engine GitHub

Rocket components

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 render supports <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 .roster component (gf-98). The app's month calendar is server markup over cal-*; 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

javascript
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>
  `,
});