PocketPages Datastar Backend SDK
Implements the Datastar SDK ADR for PocketPages, providing realtime DOM updates and signal management.
Installation
npm install pocketpages-plugin-datastar
Configuration
Add to your +config.js
:
module.exports = {
plugins: ['pocketpages-plugin-datastar', 'pocketpages-plugin-realtime'],
}
Add to your +layout.ejs
or similar:
<head>
<%- datastar.scripts() %>
</head>
Important: The datastar.scripts()
call must be included in the <head>
section of your HTML. This injects the Datastar loader script which is required for all Datastar functionality to work.
Script Options
The datastar.scripts()
function accepts optional configuration:
<head>
<%- datastar.scripts(
{
spa: {
scope: 'body', // CSS selector for SPA scope
selector: 'app' // Optional selector for content updates
},
realtime: true // Enable realtime functionality
}
) %>
</head>
Options:
spa
(object, optional) - Enables Single Page Application modescope
(string) - CSS selector for the scope where<a>
tags will be converted to SPA navigationselector
(string, optional) - CSS selector for content updates. When specified, theDatastar-Selector
header is sent with requests and the returned content is patched into the matching DOM element using the Datastarselector
directive. This remedies cases where parent/container divs are lost because they were in+layout.*
files and layouts are disabled whenDatastar-Request: true
is detected.
realtime
(boolean, optional) - Enables realtime functionality for broadcasting updates to all connected clients
SPA Mode
When SPA mode is enabled, the plugin automatically converts all <a>
tags within the specified scope to use client-side navigation instead of full page reloads. This creates a smooth single-page application experience.
How SPA Mode Works
- Link Interception: All
<a>
tags within the specified scope are automatically modified to prevent default navigation - History Management: Clicking links updates the browser history using
pushState()
without page reloads - Content Updates: The target page content is fetched and injected into the DOM
- Back/Forward Support: Browser back/forward buttons work correctly with SPA navigation
SPA Configuration Examples
<!-- Basic SPA with body scope -->
<head>
<%- datastar.scripts({ spa: { scope: 'body' } }) %>
</head>
<!-- SPA with specific selector for content updates -->
<head>
<%- datastar.scripts({ spa: { scope: 'nav', selector: 'main-content' } }) %>
</head>
<!-- SPA with realtime enabled -->
<head>
<%- datastar.scripts({ spa: { scope: 'body' }, realtime: true }) %>
</head>
SPA Behavior
- Scope: Only
<a>
tags within the specified CSS selector are converted to SPA navigation - Selector: If
selector
is provided, theDatastar-Selector
header is sent with requests and returned content is patched into the matching DOM element using the Datastarselector
directive. This remedies cases where parent/container divs are lost because they were in+layout.*
files and layouts are disabled whenDatastar-Request: true
is detected - History: Browser history is properly managed for back/forward navigation
- Headers: The
Datastar-Request: true
header is automatically sent with requests to disable layouts
SPA Example
<!DOCTYPE html>
<html>
<head>
<%- datastar.scripts({ spa: { scope: 'nav', selector: 'content' } }) %>
</head>
<body>
<nav>
<a href="/dashboard">Dashboard</a>
<a href="/profile">Profile</a>
<a href="/settings">Settings</a>
</nav>
<div id="content">
<!-- Page content will be updated here -->
</div>
</body>
</html>
In this example, clicking navigation links will update only the #content
div without full page reloads.
Layout Behavior with SPA
When SPA mode is enabled, the Datastar-Request: true
header is automatically sent with requests. This header disables layout rendering (+layout.*
files) to prevent duplicate HTML structure from being returned.
Why this matters:
- Layout files typically contain the page structure (navigation, footer, etc.)
- When layouts are disabled, only the page-specific content is returned
- This can cause issues if your page content expects to be wrapped in a specific container div that was defined in the layout
The selector
option solves this by:
- Sending the
Datastar-Selector
header with requests - Using the Datastar
selector
directive to patch content into the matching DOM element - Providing the missing container that would normally come from the layout
- Ensuring proper DOM structure for content updates
Example scenario:
<!-- +layout.ejs -->
<div id="main-content"><%- content %></div>
<!-- page.ejs -->
<h1>Page Title</h1>
<p>Page content...</p>
Without the selector
option, SPA requests would return just <h1>Page Title</h1><p>Page content...</p>
and patch it into the entire page. With selector: 'main-content'
, the content is patched into the #main-content
element using the Datastar selector
directive, ensuring it goes into the correct container.
API Reference
Core Methods
datastar.patchElements(elements, options?)
Updates DOM elements with new HTML content.
// Basic usage
datastar.patchElements('<div>New content</div>')
// With options
datastar.patchElements('<div>New content</div>', {
selector: '#target',
mode: 'inner',
useViewTransition: true,
eventId: 'unique-id',
retryDuration: 1000,
})
Options:
selector
- CSS selector for target elementmode
- Patch mode:outer
,inner
,remove
,replace
,prepend
,append
,before
,after
useViewTransition
- Enable ViewTransition APIeventId
- Unique event identifierretryDuration
- SSE retry duration in milliseconds
datastar.patchSignals(signals, options?)
Updates client-side signals with new data.
// Basic usage
datastar.patchSignals(stringify({ count: 42 }))
// With options
datastar.patchSignals(stringify({ count: 42 }), {
onlyIfMissing: true,
eventId: 'unique-id',
retryDuration: 1000,
})
Options:
onlyIfMissing
- Only patch if signals don't existeventId
- Unique event identifierretryDuration
- SSE retry duration in milliseconds
datastar.readSignals(request, target)
Reads signals from request and merges into target object.
// Read signals into a new object
const data = datastar.readSignals(request, {})
// Read into existing object
const form = datastar.readSignals(request, { name: '', email: '' })
Utility Methods
datastar.executeScript(script, options?)
Executes JavaScript on the client.
datastar.executeScript('console.log("Hello from server")')
// With options
datastar.executeScript('alert("Hello")', {
autoRemove: true,
attributes: ['type="module"'],
eventId: 'script-1',
retryDuration: 1000,
})
datastar.consoleLog(message, options?)
Logs a message to client console.
datastar.consoleLog('Server message')
datastar.consoleError(error, options?)
Logs an error to client console.
datastar.consoleError('Something went wrong')
datastar.consoleError(new Error('Server error'))
datastar.redirect(url, options?)
Redirects the client to a new URL.
datastar.redirect('/dashboard')
datastar.dispatchCustomEvent(eventName, detail, options?)
Dispatches a custom event on the client.
datastar.dispatchCustomEvent('user-updated', { id: 123 })
// With options
datastar.dispatchCustomEvent(
'custom-event',
{ data: 'value' },
{
selector: '.target',
bubbles: true,
cancelable: true,
composed: true,
}
)
datastar.replaceURL(url, options?)
Updates the browser URL without navigation.
datastar.replaceURL('/new-path')
datastar.prefetch(urls, options?)
Prefetches URLs using speculation rules.
datastar.prefetch(['/page1', '/page2'])
Realtime Methods
datastar.realtime.patchElements(elements, patchOptions?, realtimeOptions?)
Broadcasts element updates to all connected clients.
// Basic usage
datastar.realtime.patchElements('<div>Broadcast message</div>')
// With patch options
datastar.realtime.patchElements('<div>Broadcast message</div>', {
selector: '#target',
mode: 'inner',
useViewTransition: true,
})
// With realtime options for custom filtering
datastar.realtime.patchElements(
'<div>Broadcast message</div>',
{},
{
filter: (clientId, client, topic, message) => {
// Only send to authenticated clients
return client.get('auth')?.id
},
}
)
Parameters:
elements
(string) - HTML content to broadcastpatchOptions
(object, optional) - Element patch configurationrealtimeOptions
(object, optional) - Realtime delivery optionsfilter
(function, optional) - Custom filter function to target specific clients
datastar.realtime.patchSignals(signals, options?)
Broadcasts signal updates to all connected clients.
datastar.realtime.patchSignals(stringify({ globalCount: 100 }))
Examples
Note: All examples assume you have included <%- datastar.scripts() %>
in your HTML <head>
section as shown in the Configuration section above.
Chat Application
// Save message and broadcast to all clients
const messages = store('messages') || []
const { from, message } = request.url.query.datastar
messages.push({ from, message })
store('messages', messages)
// Broadcast updated chat box
datastar.realtime.patchElements(include('chat-box.ejs', { messages }))
// Clear input
datastar.patchSignals(stringify({ message: '' }))
Counter with Realtime Updates
// Increment counter
$app.runInTransaction(() => {
store('count', (store('count') || 1) + 1)
})
// Return updated counter
<%- include('count.ejs') %>
Form Handling
// Read form data
const formData = datastar.readSignals(request, { name: '', email: '' })
// Process and respond
if (formData.name && formData.email) {
datastar.patchElements('<div>Success!</div>')
} else {
datastar.consoleError('Please fill all fields')
}
Client-Side Integration
The plugin automatically injects the Datastar loader script. Use Datastar attributes in your HTML:
<button data-on-click="@get('/api/increment')">
Count: <span data-bind-count><%= store('count') %></span>
</button>
<form data-on-submit="@post('/api/submit')">
<input name="name" data-bind-name />
<button type="submit">Submit</button>
</form>
Client-Side Helper Functions
patchSignals(signals)
A client-side helper function that dispatches a datastar-fetch
event to patch signals. This function is automatically available in the global scope when the datastar plugin is loaded.
// Patch signals from client-side JavaScript
patchSignals({ count: 42, message: 'Hello' })
// Patch signals with complex data
patchSignals({
user: { id: 123, name: 'John' },
settings: { theme: 'dark' },
})
This function is particularly useful for:
- Updating signals from client-side event handlers
- Syncing state between different parts of your application
- Triggering signal updates from custom JavaScript code
$clientId
Signal
The $clientId
signal is automatically set when the realtime connection is established. This signal contains the unique client identifier assigned by PocketBase's realtime system.
<!-- Display the client ID -->
<div data-text="$clientId"></div>
<!-- Use in conditional rendering -->
<div data-if="$clientId">
Connected with ID: <span data-text="$clientId"></span>
</div>
<!-- Use in data attributes -->
<button data-on-click="@get('/api/action')" data-client-id="$clientId">
Perform Action
</button>
The $clientId
is useful for:
- Identifying the current client in realtime communications
- Debugging connection issues
- Creating client-specific functionality
- Tracking user sessions
Event Types
datastar-patch-elements
- DOM element updatesdatastar-patch-signals
- Signal updates
Default Values
DefaultSseRetryDuration
: 1000msDefaultElementsUseViewTransitions
: falseDefaultPatchSignalsOnlyIfMissing
: falseDefaultElementPatchMode
: 'outer'