This plugin integrates the Datastar framework with Craft CMS, allowing you to create reactive front-ends driven by Twig templates. It aims to replace the need for front-end frameworks such as React, Vue.js and Alpine.js + htmx, and instead lets you manage state and use logic within your Twig templates.

Use-cases:

  • Live searching elements
  • Loading more elements / Infinite scroll
  • Paginating, ordering and filtering lists of elements
  • Submitting forms and running actions
  • Pretty much anything to do with reactive front-ends

This plugin is in beta and its API may change.

Coming from Spark? Read the Migrating to Datastar guide.

Rocket social

License #

This plugin is licensed for free under the MIT License.

Requirements #

This plugin requires Craft CMS 5.0.0 or later.

Installation #

To install the plugin, search for “Datastar” in the Craft Plugin Store, or install manually using composer.

composer require putyourlightson/craft-datastar:^1.0.0-beta.1

Usage #

Start by reading the Getting Started guide to learn how to use Datastar on the front-end. The Datastar plugin for Craft only handles back-end requests.

The Datastar VSCode extension and IntelliJ plugin have autocomplete for all data-* attributes.

When working with signals, remember that you can convert a Twig object into a JSON object using the json_encode filter.

{% set signals = {foo: 1, bar: 2} %}
<div data-signals="{{ signals|json_encode }}"></div>

Back-end Requests #

The Datastar plugin provides alternative syntax to the back-end sse() action that can be placed in any data-on attribute.

datastar.sse() #

This function returns an sse() action request to render a template path.

<button data-on-click="{{ datastar.sse('_datastar/search.twig') }}">
    Search
</button>

Clicking the button will send an AJAX request to the web server telling the Datastar plugin to render the _datastar/search.twig template. Signals are also sent as part of the request, and are made available in Datastar templates using the signals variable.

Options can be passed into the template using a second argument. All of the sse() action’s options are available, as well as a special variables option. Any variables passed in will become available in the rendered template. Variables are tamper-proof yet visible in the source code in plain text, so you should avoid passing in any sensitive data.

<button data-on-click="{{ datastar.sse('_datastar/search.twig', {variables: {sectionId: 1}}) }}">
    Search
</button>

Only primitive data types can be used as variables: strings, numbers, booleans and arrays. Objects, models and elements cannot be used. If you want to pass an element (or set of elements) into the template then you should pass in an ID (or array of IDs) instead and then fetch the element from within the component.

A method can also be passed in as an option to be used as the HTTP verb. If the method is not get, a CSRF token is generated and sent automatically.

<button data-on-click="{{ datastar.sse('_datastar/save-user.twig', {method: 'post'}) }}">
    Save
</button>

Datastar Templates #

Templates rendered by Datastar can modify DOM fragments, update signals, execute JavaScript and run actions.

Datastar templates should only be called by the functions above, and never included directly. If you find yourself wanting to reuse functionality between Datastar and non-Datastar templates, you should abstract it into a template partial and use the include() function.

Each fragment and signal update results in a server-sent event being streamed to the browser. This makes it possible to send events before performing long-running processes.

Fragments #

The {% fragment %} tag allows you to modify one or more fragments in the DOM.

<div id="results"></div>

<div id="search">
    <button data-on-click="{{ datastar.sse('_datastar/search.twig') }}">
        Search
    </button>
</div>
{# _datastar/search.twig #}

{% fragment %}
    <div id="results">
        ...
    </div>
{% endfragment %}

{% fragment %}
    <div id="search">
        Search complete!
    </div>
{% endfragment %}

This will swap the elements with the IDs results and search into the DOM. Note that elements with those IDs must already exist in the DOM.

Fragment Options #

Fragments are swapped into the DOM based on element IDs, by default. It’s possible to use other merge modes and other merge fragment options using the with parameter.

{% fragment with {selector: '#list', mergeMode: 'append'} %}
    <li>A new list item</li>
{% endfragment %}
Removing Fragments #

Fragments can be removed from the DOM using the remove parameter, which accepts a CSS selector.

{% fragment remove '#list' %}

Signals #

Signals can be accessed within templates rendered by Datastar using the signals variable, which is injected into the template.

<input data-bind-username>
<button data-on-click="{{ datastar.sse('_datastar/check-username.twig') }}">
    Check
</button>
{# _datastar/check-username.twig #}

{# Gets the value of the `username` signal. #}
Username: {{ signals.username }}

{# Gets the value of the `username` signal using the `get()` function. #}
Username: {{ signals.get('username') }}

{# Gets the value of a nested signal using dot notation. #}
Username: {{ signals.get('user.username') }}
Updating Signals #

Signals can be updated within Datastar templates using one of the following approaches.

{# Sets the value of the `username` signal. #}
{% do signals.username('bobby') %}

{# Sets the value of the `username` signal using the `set()` function. #}
{% do signals.set('username', 'bobby') %}

{# Sets the value of a nested signal using dot notation. #}
{% do signals.set('user.username', 'bobby') %}

{# Sets multiple signal values using an array of key-value pairs. #}
{% do signals.setValues({username: 'bobby', success: true}) %}
Removing Signals #

Signals can be removed within Datastar templates using the following approach.

{# Removes the `username` signal. #}
{% do signals.remove('username') %}

{# Removes a nested signal using dot notation. #}
{% do signals.remove('user.username') %}

Signals updates cannot be wrapped in {% fragment %} tags, since each update creates a server-sent event which will conflict with the fragment’s contents.

Executing JavaScript #

The {% executescript %} tag allows you to send JavaScript to the browser to be executed on the front-end.

{# _datastar/check-username.twig #}

{% executescript %}
    alert('Username is valid');
{% endexecutescript %}
Execute Script Options #

It’s possible to pass execute script options using the with parameter.

{% executescript with {autoRemove: false, attributes: {defer: true}} %}
    alert('Username is valid');
{% endexecutescript %}

Running Actions #

Actions can be run within Datastar templates using the datastar.runAction() function, which accepts a controller action route and a set of optional key-value pairs that are passed through to the controller action as params.

{# _datastar/save-user.twig #}

{% set response = datastar.runAction('users/save-user', { 
    userId: userId,
    username: signals.username, 
}) %}

The function returns a Response object, on which you can check the success status and output any errors.

{% fragment %}
    <div id="main">
        {% if response.isSuccessful %}
            User successfully saved!
        {% else %}
            Errors: {{ response.data.errors|join() }}
        {% endif %}
    </div>
{% endfragment %}

See a list of available actions in Craft.

Control Panel Usage #

The Datastar module provides the core functionality for the Datastar plugin. If you are developing a Craft plugin/module and would like to use Datastar in the control panel, you can require this package to give you its functionality, without requiring that the Datastar plugin is installed.

Advanced JavaScript Usage #

Datastar’s data-* attributes and the {% executescript %} tag provide you with everything you need to build almost anything. Any additional JavaScript you require should ideally be extracted out into external scripts or web components.

External Scripts #

When using external scripts, it’s good practice to send data to functions via arguments, and listen for custom events dispatched from them.

<div data-signals="{input: '', output: ''}"
     data-on-load="myfunction(input.value)"
     data-on-mycustomevent__window="output.value = evt.detail.value"
     data-text="output.value"
>
</div>

See the two-way binding external script example.

Web Components #

Web components are reusable, encapsulated custom elements, built with HTML and JavaScript. They are native to the web and require no external libraries or frameworks. Web components unlock custom elements – HTML tags with custom behaviour and styling.

When using web components with Datastar, it is good practice to pass data via attributes down to web components, and listen for events that bubble up from web components. In this way, we are following the props down, events up pattern.

See the two-way binding web component example.

Examples #

The Datastar examples show how many of the data-* attributes can be used. The following examples are more relevant to how you might use Datastar and Craft.

Inline Entry Editing #

This example uses signals for most of its interactivity, making it extremely snappy. Only on saving an entry is a POST request made to the server, which saves the entry, responds with an element containing the new title and an alert containing a success message, and resets the entryId signal to 0. If there are errors, it outputs them in the alert.

{# inline-entry-editing.twig #}

<div id="alert"></div>
<table data-signals="{entryId: 0}">
    {% for entry in craft.entries.all() %}
        <tr>
            <td>{{ entry.id }}</td>
            <td>
                <div data-show="entryId.value != {{ entry.id }}">
                    <div id="title-{{ entry.id }}">
                        {{ entry.title }}
                    </div>
                </div>
                <div data-show="entryId.value == {{ entry.id }}">
                    <input data-bind-title type="text">
                </div>
            </td>
            <td>
                <button id="edit-{{ signals.entryId }}" data-on-click="entryId.value = {{ entry.id }}; title.value = '{{ entry.title }}'" data-show="entryId.value != {{ entry.id }}">
                    Edit
                </button>
                <button data-on-click="entryId.value = 0" data-show="entryId.value == {{ entry.id }}">
                    Cancel
                </button>
                <button data-on-click="{{ datastar.sse('_datastar/save-entry.twig', {method: 'post'}) }}" data-show="entryId.value == {{ entry.id }}">
                    Save
                </button>
            </td>
        </tr>
    {% endfor %}
</table>
{# _datastar/save-entry.twig #}

{% set response = datastar.runAction('entries/save-entry', {
    entryId: signals.entryId,
    title: signals.title,
}) %}
{% if response.isSuccessful %}
    <div id="alert">
        Entry successfully saved!
    </div>
    <div id="title-{{ signals.entryId }}">
        {{ signals.title }}
    </div>
    <button id="edit-{{ signals.entryId }}" data-on-click="entryId.value = {{ signals.entryId }}; title.value = '{{ signals.title }}'" data-show="entryId.value != {{ signals.entryId }}">
        Edit
    </button>
    {% do signals.set('entryId', 0) %}
{% else %}
    <div id="alert">
        {% for error in response.data.errors %}
            {{ error|first }}
        {% endfor %}
    </div>
{% endif %}

View the full source code.

Craft Pokemon Demo #

This Craft Pokemon Demo was made for a Dot One Toronto 2024 conference talk on using the Craft Datastar plugin to create a reactive UI. You can spin it up locally or in a browser via Github Codespaces.

Two-way Binding #

Two-way data binding between templates and external scripts or web components is possible by passing data into to them, and listening for events dispatched from them.

External Script #

This is an example of two-way binding between a template and a function in an external script that reverses a string.

{# two-way-binding-function.twig #}

<div data-signals="{reversed: ''}"
     data-on-reverse__window="reversed.value = evt.detail.value"
>
    <input data-bind-name data-on-input="reverse(name.value)">
    <span data-text="reversed.value"></span>
</div>

The input field is bound to the name signal, and on each input, the reverse() function is called. An event listener on the window element modifies the reversed signal sent in the reverse event.

The function takes a string as input, reverses it and dispatches a reverse event containing the resulting value.

function reverse(input) {
    const value = input ? input.split('').reverse().join('') : '';
    window.dispatchEvent(new CustomEvent('reverse', {detail: {value}}));
}

View the full source code.

Web Component #

This is an example of two-way binding between a template and a web component that reverses a string. Normally, the web component would output the reversed value, but in this example, all it does is perform the logic and dispatch an event containing the result, which is then displayed in the template.

{# two-way-binding-web-component.twig #}

<div data-signals="{reversed: ''}">
    <input data-bind-name>
    <span data-text="reversed.value"></span>
    <reverse-component
        data-attributes-name="name.value"
        data-on-reverse="reversed.value = evt.detail.value"
    ></reverse-component>
</div>

The name attribute is bound to the name signal, and an event listener modifies the reversed signal sent in the reverse event.

The web component observes changes to the name attribute and responds by reversing the string and dispatching a reverse event containing the resulting value.

class ReverseComponent extends HTMLElement {
    static get observedAttributes() {
        return ['name'];
    }

    attributeChangedCallback(name, oldValue, newValue) {
        const value = newValue.split('').reverse().join('');
        this.dispatchEvent(new CustomEvent('reverse', {detail: {value}}));
    }
}

customElements.define('reverse-component', ReverseComponent);

View the full source code.

Support #

Support for Datastar plugin functionality (only) is provided via GitHub issues. General support for the Datastar framework is provided via the dedicated Discord server.

Have a suggestion to improve the docs? Create an issue with details, and we'll do our best to integrate your ideas.