Search

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 mode
    • scope (string) - CSS selector for the scope where <a> tags will be converted to SPA navigation
    • selector (string, optional) - CSS selector for content updates. When specified, the Datastar-Selector header is sent with requests and the returned content is patched into the matching DOM element using the Datastar selector directive. This remedies cases where parent/container divs are lost because they were in +layout.* files and layouts are disabled when Datastar-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

  1. Link Interception: All <a> tags within the specified scope are automatically modified to prevent default navigation
  2. History Management: Clicking links updates the browser history using pushState() without page reloads
  3. Content Updates: The target page content is fetched and injected into the DOM
  4. 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, the Datastar-Selector header is sent with requests and returned content is patched into the matching DOM element using the Datastar selector directive. This remedies cases where parent/container divs are lost because they were in +layout.* files and layouts are disabled when Datastar-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 element
  • mode - Patch mode: outer, inner, remove, replace, prepend, append, before, after
  • useViewTransition - Enable ViewTransition API
  • eventId - Unique event identifier
  • retryDuration - 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 exist
  • eventId - Unique event identifier
  • retryDuration - 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 broadcast
  • patchOptions (object, optional) - Element patch configuration
  • realtimeOptions (object, optional) - Realtime delivery options
    • filter (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 updates
  • datastar-patch-signals - Signal updates

Default Values

  • DefaultSseRetryDuration: 1000ms
  • DefaultElementsUseViewTransitions: false
  • DefaultPatchSignalsOnlyIfMissing: false
  • DefaultElementPatchMode: 'outer'