134 lines
11 KiB
Markdown
134 lines
11 KiB
Markdown
# Understanding Datastar: Backend and Frontend Synergy
|
|
|
|
You've got it! Understanding how the Go `datastar` package on the server and `datastar.js` in the browser collaborate is key to grasping the "whole picture." Let's break down this synergistic relationship.
|
|
|
|
## The Big Idea: Server-Driven Interactivity with Client-Side Reactivity
|
|
|
|
Datastar enables you to build dynamic web UIs where the server often dictates changes, sending HTML fragments or instructions over Server-Sent Events (SSE). The client-side datastar.js intelligently applies these updates, manages a local reactive state (called "signals"), and provides declarative ways to bind this state and behavior to your HTML.
|
|
|
|
## Step-by-Step Flow
|
|
|
|
Here's a step-by-step flow and explanation of how they work together:
|
|
|
|
### 1. Initial Page Load & Client-Side Setup (`datastar.js`)
|
|
|
|
* **HTML Served**: Your Go application serves an initial HTML page.
|
|
* **`datastar.js` Execution**: The browser loads and executes `datastar.js`.
|
|
* **DOM Scan & Initialization** (`rt`, `Ie`, `dr` in `datastar.js`):
|
|
* `datastar.js` scans the DOM for special attributes (e.g., `data-datastar-show`, `data-datastar-on-click`, `data-datastar-bind`, `data-datastar-signals`). The prefix (`datastar-`) can be aliased using `Kt` (e.g., to just `data-`).
|
|
* For each recognized attribute, it initializes the corresponding client-side plugin:
|
|
* `data-signals`: Populates the client-side reactive "signal" store with initial values. Signals are like JavaScript variables, but when they change, parts of the UI that depend on them can automatically update.
|
|
* `data-computed`: Defines signals whose values are calculated from other signals.
|
|
* `data-show`, `data-text`, `data-attr`, `data-class`: These attributes take JavaScript-like expressions. `datastar.js` sets up "effects" that watch the signals mentioned in these expressions. When a relevant signal changes, the expression is re-evaluated, and the DOM is updated accordingly (e.g., an element is shown/hidden, its text content changes).
|
|
* `data-bind`: Creates two-way data binding between form input elements and signals. Changes in the input update the signal, and changes to the signal update the input's value.
|
|
* `data-on-{event}` (e.g., `data-on-click`): Attaches event listeners. The attribute's value is an expression that gets executed when the event fires. This expression can call "actions."
|
|
* `data-ref`: Stores a reference to the DOM element itself in a signal.
|
|
* **MutationObserver**: `datastar.js` sets up a `MutationObserver` to automatically initialize any new HTML content that gets added to the page later (e.g., through Datastar's own fragment merging).
|
|
|
|
### 2. Client Interaction & Request to Server (`datastar.js` actions)
|
|
|
|
* **User Action**: A user interacts with an element, for example, clicks a button:
|
|
html
|
|
<button data-datastar-on-click="@post('/api/submit-data', { contentType: 'form' })">Submit</button>
|
|
Action Triggered: The data-on-click directive executes its expression.
|
|
@post Action (cn plugin in datastar.js):
|
|
The @post(...) syntax calls a registered "action" plugin. datastar.js has built-in actions for HTTP methods (@get, @post, @put, @patch, @delete).
|
|
This action initiates an HTTP request to the specified URL (/api/submit-data).
|
|
Sending Data to Server (B function in datastar.js):
|
|
Headers: A Datastar-Request: true header is added to signal to the server that this is a Datastar-initiated request. The Accept header indicates preference for text/event-stream among others.
|
|
Payload:
|
|
If contentType: 'json' (default for POST/PUT/PATCH), datastar.js can automatically collect the current values of its signals, JSON-stringify them, and send them in the request body (unless excludeSignals: true). The Go server can then use datastar.ReadSignals() to parse this.
|
|
If contentType: 'form', it finds the closest <form> (or one specified by a selector option), gathers its FormData, and sends that.
|
|
For @get requests, signals (if included) are typically sent as a datastar query parameter.
|
|
3. Server-Side Processing (Go datastar package)
|
|
|
|
Request Handling: Your Go HTTP handler (e.g., in main.go or routes.go) receives the request.
|
|
Reading Client Data:
|
|
If the client sent signal data, the server can use datastar.ReadSignals(r, &myStruct) to unmarshal it into a Go struct.
|
|
Business Logic: The server performs its logic (database queries, calculations, etc.).
|
|
Preparing SSE Stream (datastar.NewSSE):
|
|
To send updates back, the handler calls sse := datastar.NewSSE(w, r, ...options).
|
|
This function:
|
|
Sets HTTP headers appropriate for SSE (Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive).
|
|
Can apply compression (e.g., gzip, brotli) to the SSE stream if configured with options like datastar.WithCompression(datastar.WithBrotli()).
|
|
Returns a ServerSentEventGenerator (the sse object).
|
|
Sending Updates via SSE (sse.Send and sugar methods): The server now uses the sse object to send various types of events to the client:
|
|
sse.MergeFragments(htmlString, opts...):
|
|
Sends an HTML snippet to the client.
|
|
opts can include:
|
|
datastar.WithSelector("#target-id") or datastar.WithSelectorf(".css-class-%s", "foo"): Specifies where to merge the HTML. If omitted, Datastar expects the fragment itself to have an ID and will target an element with the same ID.
|
|
datastar.WithMergeMode(datastar.FragmentMergeModeInner): How to merge (e.g., morph (default, uses idiomorph-like diffing), inner (innerHTML), outer (replace element), append, prepend, etc.).
|
|
datastar.WithViewTransitions(true): To use the browser's View Transition API for smoother updates.
|
|
This sends an SSE event with event: datastar-merge-fragments and data lines for the selector, merge mode, and the HTML fragments.
|
|
sse.MergeSignals(jsonBytes, opts...) or sse.MarshalAndMergeSignals(goStruct, opts...):
|
|
Sends JSON data to update the client's signal store.
|
|
opts can include datastar.WithOnlyIfMissing(true).
|
|
Sends an SSE event with event: datastar-merge-signals and data lines for the signals JSON.
|
|
sse.ExecuteScript(scriptContent, opts...):
|
|
Sends JavaScript code for the client to execute.
|
|
opts can include datastar.WithExecuteScriptAutoRemove(false) (to keep the script tag) or datastar.WithExecuteScriptAttributes("type module", "defer").
|
|
Sends an SSE event with event: datastar-execute-script and data lines for the script, attributes, and auto-remove flag.
|
|
Convenience methods like sse.ConsoleLog("message"), sse.Redirect("/new-page"), sse.DispatchCustomEvent("my-event", detail) are built on top of ExecuteScript.
|
|
sse.RemoveFragments(selector, opts...):
|
|
Instructs the client to remove DOM elements matching the CSS selector.
|
|
Sends an SSE event with event: datastar-remove-fragments.
|
|
sse.RemoveSignals(path1, path2, ...):
|
|
Instructs the client to remove specified signals from its store.
|
|
Sends an SSE event with event: datastar-remove-signals.
|
|
Templating Integration: Methods like sse.MergeFragmentTempl(myTemplComponent) allow rendering Go templates (like Templ) directly into an HTML fragment string before sending.
|
|
4. Client-Side Reception & DOM/State Update (datastar.js watchers)
|
|
|
|
SSE Connection (Er in datastar.js): datastar.js maintains the SSE connection established by an action (like @post).
|
|
Event Parsing (hr, vr in datastar.js): When an SSE message arrives:
|
|
It's parsed into its components (event type, data lines, id, retry).
|
|
An internal Datastar event is dispatched (e.g., ne(ge, s, F) which corresponds to datastar-merge-fragments).
|
|
Watcher Plugins Handle Events: Specific "watcher" plugins in datastar.js listen for these internal events:
|
|
datastar-merge-fragments (handled by An plugin):
|
|
Parses the selector, mergeMode, fragments, and useViewTransition from the event data.
|
|
Locates the target DOM element(s).
|
|
If useViewTransition is enabled, it wraps the DOM update in document.startViewTransition(...).
|
|
Applies the fragment using the specified mergeMode:
|
|
morph (xr function): This is the most sophisticated mode. It uses an algorithm similar to idiomorph to diff the existing DOM with the new fragment and apply only the necessary changes. This is efficient and helps preserve things like input focus and scroll position. It respects data-datastar-ignore-morph attributes.
|
|
Others (inner, outer, append, etc.): Perform straightforward DOM manipulations.
|
|
Crucially, after merging, it re-scans the modified/new elements (ce(o, Ie)) to initialize any Datastar attributes within them, ensuring new content is also reactive.
|
|
datastar-merge-signals (handled by wn plugin):
|
|
Parses the JSON payload of signals.
|
|
Updates the client-side signal store (ke function). This is batched (Pe function) to optimize updates.
|
|
datastar-execute-script (handled by fn plugin):
|
|
Creates a <script> tag, populates it with the received script content and attributes, appends it to the <head>, and (by default) removes it after execution.
|
|
datastar-remove-fragments (handled by Dn plugin):
|
|
Removes DOM elements matching the provided selector.
|
|
datastar-remove-signals (handled by Cn plugin):
|
|
Removes the specified signals from the client-side store.
|
|
5. Client-Side Reactivity (Automatic UI Updates)
|
|
|
|
Signal Changes: When a signal's value changes (either due to a datastar-merge-signals event from the server, or a data-bind update from user input, or direct manipulation via @setAll or @toggleAll actions):
|
|
The signal system in datastar.js identifies all "effects" and "computed signals" that depend on the changed signal.
|
|
Re-evaluation:
|
|
These dependent expressions (from data-show, data-text, data-bind, etc.) are re-evaluated.
|
|
If the result of an expression changes, the corresponding DOM update is performed automatically by datastar.js. For example:
|
|
If $isLoading (a signal) changes from true to false, an element with data-datastar-show="!$isLoading" will become visible.
|
|
If $user.name changes, an element with data-datastar-text="$user.name" will have its text content updated.
|
|
The Cycle Continues
|
|
|
|
This creates a reactive loop: User Interaction -> Client sends request (with optional state) -> Server processes -> Server sends SSE updates (HTML, data, scripts) -> Client receives, updates DOM & local state -> Client-side reactivity further updates UI based on state changes.
|
|
|
|
In Summary:
|
|
|
|
Go Backend (datastar package):
|
|
Handles HTTP requests.
|
|
Manages business logic.
|
|
Can read client-side state sent with requests.
|
|
Pushes fine-grained UI updates (HTML fragments), data updates (signals), or script executions to the client via Server-Sent Events.
|
|
Provides helpers for common tasks like templating, compression, and generating client-side action attributes.
|
|
JavaScript Frontend (datastar.js):
|
|
Initializes declarative behaviors from HTML attributes.
|
|
Manages a reactive client-side data store ("signals").
|
|
Makes requests to the server, potentially including current signal state.
|
|
Listens for SSE events from the server.
|
|
Intelligently merges HTML fragments into the DOM (with morphing as a default).
|
|
Updates its signal store based on server messages.
|
|
Executes scripts sent by the server.
|
|
Automatically updates the UI when signals change, based on declarative bindings in the HTML.
|
|
This architecture aims to keep much of the rendering logic on the server (sending HTML fragments) while still providing a responsive and interactive experience on the client, with a reactive data layer managed by datastar.js.
|