Menu

Parts JavaScript API

Parts work without writing any JavaScript — but when you want to script them, the runtime exposes a small, capable API: refresh any Part from your own code, read its state, and subscribe to its lifecycle. This is how Parts talk to the rest of your front end: websockets, keyboard shortcuts, third-party widgets, analytics.

The runtime is injected automatically on any page that renders a <Part/>. There is nothing to include; window.Parts is just there.

Give Your Parts Ids

Everything on this page addresses Parts by element id:

<Part src={@./results.part} id="results"/>

A Part without an id still works normally — it just can't be scripted or targeted, and it fires no events.

Parts.refresh(id, props, options)

Fetch a view and update the Part — the programmatic equivalent of a part-click, but with merging: props are merged on top of the Part's current props, and the merged set becomes the Part's new state.

// Re-fetch the current view with current props
Parts.refresh("results");

// Merge in new props
Parts.refresh("results", { q: "basil" });

// Switch view, debounce, or POST
Parts.refresh("results", { q: "basil" }, {
    view: "search",     // default: the Part's current view
    debounce: 300,      // ms — collapse rapid calls into one (per Part)
    method: "POST"      // default: "GET"
});
Option Default Description
view current view Which view function to fetch
debounce none Wait this many ms after the last call before fetching
method "GET" "GET" sends props as query params, "POST" as a form body

Debouncing is per-Part: rapid calls to the same id collapse into one request. That makes live search a one-liner:

<input oninput="Parts.refresh('results', {q: this.value}, {view: 'search', debounce: 300})"
       placeholder="Search…"/>

<Part src={@./results.part} id="results" view="search" q=""/>

Because the input lives outside the Part, it never reloads — focus and cursor position survive while the results update beneath it. That's the pattern to reach for whenever some of the UI must persist (a search box, a filter bar, a video player) while the rest refreshes: master-detail lists, dashboard filters, live markdown previews.

If the id doesn't exist (or isn't a Part), refresh logs a console warning and does nothing.

Note that refresh returns immediately — it doesn't return a promise, so there's nothing to await. To run code once the update has landed, listen for afterRefresh.

Parts.get(id)

Read a Part's current state. Returns null if the id isn't a Part.

const part = Parts.get("results");
// {
//     id: "results",
//     view: "search",         // current view name
//     props: { q: "basil" },  // current props
//     element: <div>,          // the wrapper DOM element
//     loading: false           // true while a fetch is in flight
// }

Handy for guards (if (!Parts.get("editor").loading) …) and for building on top of a Part's state without duplicating it.

Parts.on(id, event, callback)

Subscribe to a Part's lifecycle. Returns an unsubscribe function.

const off = Parts.on("results", "afterRefresh", (detail) => {
    console.log(`results now showing ${detail.view}`, detail.props);
});

off();  // unsubscribe

Pass "*" as the id to hear from every Part on the page:

Parts.on("*", "error", (detail) => {
    toast(`Something went wrong updating ${detail.id}`);
});

Listeners are registered by id, not by element — they survive refreshes, even though the Part's inner HTML is replaced each time.

Events

Event Fires Detail
beforeRefresh Just before a fetch starts { id, view, props }
afterRefresh After new HTML is in the DOM { id, view, props }
error When a fetch or view fails (old content is kept) { id, view, props, error }

All updates fire events, whatever triggered them: clicks, submits, auto-refresh, lazy loads, part-target, or Parts.refresh. Events only fire for Parts that have an id.

afterRefresh is the hook for work that needs the new DOM — syntax highlighting, focusing a field, re-attaching a third-party widget:

Parts.on("editor", "afterRefresh", () => {
    document.querySelector("#editor input[autofocus]")?.focus();
});

And events make Parts coordinate without knowing about each other:

// When the cart Part updates, refresh the little badge in the header
Parts.on("cart", "afterRefresh", () => Parts.refresh("cart-badge"));

Parts.off(id)

Remove all listeners for a Part in one go:

Parts.off("results");

For removing a single listener, use the function returned by Parts.on.

Declarative Targeting: part-target

You often don't need JavaScript at all — part-target lets any element on the page drive a Part by id, straight from HTML. See Controlling a Part from Outside in the Parts Guide:

<button part-target="editor" part-click="edit" part-id={person.id}>"Edit"</button>

Rule of thumb: reach for part-target first, and for window.Parts when you need timing (debounce), conditions, or a non-click trigger (websocket message, timer, keyboard shortcut):

const socket = new WebSocket("wss://example.com/updates");
socket.onmessage = () => Parts.refresh("notifications");

Under the Hood

Useful when debugging, or if you're curious what the runtime actually does.

The wrapper element

Every <Part/> renders as a <div> that carries the Part's state in data attributes:

<div id="results"
     data-part-src="/parts/results.part"
     data-part-view="search"
     data-part-props='{"q":"basil"}'>
    …current HTML…
</div>
Attribute Meaning
data-part-src URL the runtime fetches views from
data-part-view The view currently displayed
data-part-props The current props, as JSON
data-part-refresh Auto-refresh interval (ms), if set
data-part-load View to fetch on page load, if set
data-part-lazy / data-part-lazy-threshold Lazy-load view and distance, if set

After every update the runtime writes the new view name and props back to these attributes, so the DOM always tells the truth about a Part's state.

The wire format

A view request is an ordinary HTTP request:

GET  /parts/results.part?_view=search&q=basil     ← clicks, refreshes
POST /parts/results.part?_view=save               ← form submits
     body: id=7&text=Hello

_view names the view function; everything else becomes props, coerced back to types server-side (numbers, booleans, signed complex values). Requests without _view get a 404 — Parts can't be fetched directly. During every fetch the wrapper has a part-loading class; on failure the old content is kept and an error event fires.

Reserved attribute names

part-click, part-submit, part-load, part-lazy, part-lazy-threshold, part-refresh, part-target, and part-form are runtime instructions — every other part-* attribute becomes a prop. (part-form is reserved for a future release; it doesn't do anything yet.)

See Also

  • Parts — the essentials
  • The Parts Guide — patterns: nesting, lazy loading, loading states, error handling