Parts
Parts are interactive components that update themselves without a full page reload — counters, inline editors, live clocks, forms that validate as you go. No JavaScript framework: you write view functions in Parsley, and Basil swaps the HTML over the wire.
Basil-only. Parts require the Basil server to serve view updates.
Quick Example
A Part file (counter.part) exports one function per view:
export default = fn(props) {
<div>
"Count: " + props.count
<button part-click="increment" part-count={props.count}>"+"</button>
</div>
}
export increment = fn(props) {
let newCount = props.count + 1
<div>
"Count: " + newCount
<button part-click="increment" part-count={newCount}>"+"</button>
</div>
}
Use it from any handler:
<Part src={@./counter.part} view="default" count={0}/>
Click the button; the counter updates in place.
How It Works
.partfiles are view modules. They may only export functions. Each export is a view — a state the component can be in. (Imports and locallets are fine; they just can't be exported.)- The server renders the initial view into the page, wrapped in a
<div>that records the Part's source, current view, and props. - Interactions fetch a new view.
part-clickandpart-submitask the server to render a different view function, and the Part's HTML is replaced with the result. - A small JavaScript runtime is injected automatically — only on pages that contain a
<Part/>. There is nothing to install or import.
Because the initial view is rendered server-side, Parts are SEO-friendly and show real content before any JavaScript runs.
The <Part/> Component
<Part src={@./clock.part} id="clock" part-refresh={1000}/>
| Attribute | Description |
|---|---|
src |
Path to the .part file (required) |
view |
Initial view to render (default: "default") |
id |
Element id — needed for part-target and the JavaScript API |
part-refresh |
Auto-refresh interval in milliseconds (minimum 100) |
part-load |
View to fetch immediately after page load (show a placeholder first) |
part-lazy |
View to fetch when the Part scrolls into the viewport |
part-lazy-threshold |
Start lazy-loading this many pixels before the Part is visible |
| anything else | Passed to the view function as props |
Triggering Views
Inside a Part's HTML, two attributes trigger view changes:
part-click="name" on any element calls the name view on click. It sends only the part-* props written on that element — view switches start fresh, so pass along any state you want to keep:
<button part-click="edit" part-id={props.id} part-name={props.name}>"Edit"</button>
part-submit="name" on a <form> calls the view on submit. Form fields are merged on top of the Part's current props, so existing state carries through automatically:
<form part-submit="save">
<input name="name" value={props.name}/>
<button>"Save"</button>
</form>
part-target="id" lets an element outside a Part update it. Give the Part an id, then trigger it from anywhere with part-click or part-submit. When the trigger isn't inside a form, add part-form="formId" to send a named form's fields:
<button part-click="save" part-target="editor" part-form="editorForm">"Save"</button>
A display/edit/save cycle is three views:
export default = fn(props) {
<div>props.name <button part-click="edit" part-name={props.name}>"Edit"</button></div>
}
export edit = fn(props) {
<form part-submit="save">
<input name="name" value={props.name}/>
<button>"Save"</button>
</form>
}
export save = fn(props) {
// props.name comes from the form field
<div>props.name <button part-click="edit" part-name={props.name}>"Edit"</button></div>
}
Props
Props travel as query parameters (clicks) or form data (submits), and Basil coerces them back to sensible types before your view function sees them:
| Sent value | Received as |
|---|---|
42 |
Integer |
3.14 |
Float |
true / false |
Boolean |
on / off |
Boolean (checkbox values) |
| anything else | String |
Complex values work too. Records, datetimes, paths, and URLs passed to a <Part/> — including inside dictionaries and arrays — are serialized automatically and HMAC-signed with the session secret, so they arrive in your view function as their original types and can't be tampered with in the browser:
<Part src={@./profile.part} user={user} lastLogin={@now}/>
// profile.part
export default = fn(props) {
<div>
<h1>props.user.name</h1> // still a Record
<p>"Last login: " + props.lastLogin.time</p> // still a datetime
</div>
}
Functions, database connections, and file handles can't be passed as props.
Routing Parts
The .part file must be reachable by URL so the browser can request view updates. In site mode, put it anywhere under your site directory. In routes mode, add a route:
routes:
- path: /
handler: ./handlers/index.pars
- path: /counter.part
handler: ./handlers/counter.part
Parts can't be fetched directly: a request for a .part URL without a view returns 404. Views are only served through the runtime's requests (or your own, via the JavaScript API).
There's More
This page covers the essentials. Parts can also:
- Nest — Parts inside Parts, each updating independently
- Lazy-load — render a placeholder, fetch the real view when scrolled into sight
- Auto-refresh — live clocks and dashboards with
part-refresh - Show loading states — a
part-loadingCSS class during every fetch - Be controlled from outside — buttons elsewhere on the page can drive a Part with
part-target, and scripts can usewindow.Parts
See Also
- The Parts Guide — patterns and deep dives: nesting, lazy loading, loading states, live data, and error handling
- Parts JavaScript API —
window.Parts, events, and cross-Part targeting - Routing — how Part URLs resolve