Rocket is Datastar's way of making web components, and it comes with Datastar in a bundle of its own. To Tropical, a
Rocket component is a script the islands declare (Scripts the page needs) and an element like any
other: the island renders its props as attributes and takes its events as actions. Nothing in Tropical is Rocket's
own; this guide is how the two meet. Rocket is in beta, and the guide follows the demo's <star-rating>
(dev/demo/star_rating.js), against Datastar 1.0.4 with Rocket beta.2.
The page loads the bundle with Rocket in place of datastar.js, and names it in an import map:
(def datastar-url
"https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js")
[:head
;; Before the first module.
[:script {:type "importmap" :nonce nonce} (h/raw (ui/json {:imports {:datastar datastar-url}}))]
[:script {:type "module" :nonce nonce :src datastar-url}]
(ring/scripts nonce)]
A component imports Rocket from it:
// app/star_rating.js
import {rocket} from 'datastar';
rocket('star-rating', {
props: ({number}) => ({average: number.clamp(0, 5), mine: number.clamp(0, 5)}),
setup: ({$$, action, emit}) => {
$$.hover = 0;
action('rate', (_, n) => emit('rate', {stars: n}));
},
render: ({html, props: {average, mine}}) => html`
<slot></slot>
${[1, 2, 3, 4, 5].map((n) => html`
<button data-class:lit="($$hover || ${mine}) >= ${n}" data-on:mouseenter="$$hover = ${n}"
data-on:mouseleave="$$hover = 0" data-on:click="@rate(${n})">★</button>`)}
<span>${average.toFixed(1)}</span>`,
});
And an island uses it:
(script/defscript star-rating
"`<star-rating>`, a Rocket component."
{:resource "app/star_rating.js"})
(defisland rating
[uid]
(let [ratings (use-watch !ratings)
rate (use-action :rate
(fn [_ {stars "stars"}]
;; Untrusted: a whole number of stars, from 1 to 5, or nothing.
(when (contains? #{1 2 3 4 5} stars)
(swap! !ratings assoc uid stars))
nil)
{:value "evt.detail"})]
(script/use star-rating)
[:star-rating {:average (average ratings) :mine (get ratings uid 0) :data-on:rate rate}
[:span "Rate this."]]))
datastar.js, and a component imports
rocket from the page's copy. A second copy would be a second engine. In CSP mode, it would compile nothing either,
since Datastar takes the page's nonce off <html> as it starts. The import map names the copy once, so the page's
tag and the modules can't disagree on its URL. It goes before the first module, with the nonce.defscript and script/use load the component's module: in the head with the first
frame, or over the stream when an island using it is patched in, once per page. Rocket defines the element as the
module runs, and won't define it again, as with any customElements.define: a page reloads when its stream
connects to a server on which the component has changed, as after a deploy (Scripts).ui/json
writes. Rocket decodes them with the component's codecs, and renders again when a patch changes them.data-preserve-attr naming
the attribute, and the morph leaves it.emit. The island takes it with
data-on:<event> on the element, and the event's detail with use-action's {:value "evt.detail"}, untrusted, as
any value from the browser (Actions). The element's own attributes are in
the page's scope, not the component's.js codec is code. It makes functions of what an attribute holds, so a server value in such a prop is the
injection Security describes for expressions. Values go through the others, such as json and
string.$$name is the component's own signal, under _rocket.<tag>.<id>. Its
key starts with _, so no action or stream sends it, and a patch leaves it. It lasts as long as the element stays
where it is: one that is moved, as the morph moves an element when its list reorders, or replaced, starts over, as
Rocket drops its signals when it leaves the page. What must outlast that is the island's (use-state) or the
browser's (State the browser owns).light. The default mode, open, and closed draw into a shadow root, which Datastar's
morph of the island doesn't reach. light draws into the element's children, which the morph matches to the
island's. An island sent again without changing the element's props, such as in the resync after a reconnect,
removes what the component drew. One that changes them is undone the other way: the component renders again with
the children it took in when it started, in place of those the island just sent.<slot>, and patched as the rest of the island. Rocket rewrites the
expressions in it for the component: $$name is the component's signal, and an @action(...) goes to the
component first, which passes those it doesn't define to Datastar. An island's actions work there, as @post
isn't the component's, but a component action named as one of Datastar's would take its calls.data-rocket-host, which the morph removes, as any attribute the island didn't render. A component's own
children, the island's actions among them, don't need it.Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |