This plugin integrates the Datastar framework with Craft CMS, allowing you to create reactive frontends driven by Twig templates. It aims to replace the need for frontend 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 frontends

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.3

Usage #

Start by reading the Getting Started guide to learn how to use Datastar on the frontend. The Datastar plugin for Craft only handles backend requests.

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

Template Variables #

The Datastar plugin provides convenience functions for generating signals.

datastar.signals() #

This function returns a JSON encoded array of signals from a Twig object.

<div data-signals="{{ datastar.signals({foo: 1, bar: 2}) }}"></div>

datastar.signalsFromClass() #

This function returns a JSON encoded array of signals from the public properties of a class. This is useful for generating signals from an attribute class or a model.

<div data-signals="{{ datastar.signalsFromClass('attributes\\signals\\AutocompleteSignals') }}"></div>

A second argument containing a Twig object can be passed in. The values will be assigned to the class properties before converting to signals.

<div data-signals="{{ datastar.signalsFromClass('attributes\\signals\\AutocompleteSignals', {foo: 1, bar: 2}) }}"></div>

Backend Requests #

The Datastar plugin provides alternative syntax to the backend plugin actions that can be placed in any data-on attribute.

datastar.get() #

This function returns a @get() action request to render a template path.

<button data-on-click="{{ datastar.get('_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.

Variables can be passed into the template using a second argument. 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.

Options can be passed into the @get() action using a third argument.

<button data-on-click="{{ datastar.get('_datastar/search.twig', {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.

datastar.post() #

Works the same as datastar.get() but returns a @post() action request to render a template path. A CSRF token is automatically generated and sent along with the request.

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

datastar.put() #

Works the same as datastar.post() but returns a @put() action request.

datastar.patch() #

Works the same as datastar.post() but returns a @patch() action request.

datastar.delete() #

Works the same as datastar.post() but returns a @delete() action request.

Datastar Templates #

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

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.get('_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.get('_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 namespaced 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 namespaced 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 namespaced 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.

First require the package in your plugin/module’s composer.json file.

{
  "require": {
    "putyourlightson/craft-datastar-module": "^1.0.0-beta.4"
  }
}

Then bootstrap the module from within your plugin/module’s init method.

use craft\base\Plugin;
use putyourlightson\datastar\Datastar;

class MyPlugin extends Plugin
{
    public function init()
    {
        parent::init();

        Datastar::bootstrap();
    }
}

JavaScript API #

For edge-cases in which you find yourself having to change the DOM without involving Datastar, you can import Datastar and apply it to any element and its children.

The Datastar module can expose the Datastar object in the global scope so that importing is not necessary.

use craft\base\Plugin;
use putyourlightson\datastar\Datastar;

class MyPlugin extends Plugin
{
    public function init()
    {
        parent::init();

        Datastar::bootstrap();
        Datastar::getInstance()->expose();
    }
}

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 with 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 != {{ entry.id }}">
                    <div id="title-{{ entry.id }}">
                        {{ entry.title }}
                    </div>
                </div>
                <div data-show="$entryId == {{ entry.id }}">
                    <input data-bind-title type="text">
                </div>
            </td>
            <td>
                <button id="edit-{{ signals.entryId }}" data-on-click="$entryId = {{ entry.id }}; $title = '{{ entry.title }}'" data-show="$entryId != {{ entry.id }}">
                    Edit
                </button>
                <button data-on-click="$entryId = 0" data-show="$entryId == {{ entry.id }}">
                    Cancel
                </button>
                <button data-on-click="{{ datastar.post('_datastar/save-entry.twig') }}" data-show="$entryId == {{ 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 = {{ signals.entryId }}; $title = '{{ signals.title }}'" data-show="$entryId != {{ 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.

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.