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
Updating from the beta? View the release notes.
License #
This plugin is licensed for free under the MIT License.
Requirements #
This plugin requires Craft CMS 5.4.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-RC.1
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.
JSON Encoding Signals #
Twig objects can be JSON encoded using the json_encode
filter.
<div data-signals="{{ {foo: 1, bar: 2}|json_encode }}"></div>
Backend Requests #
The Datastar plugin provides two ways of handling backend requests: using a Datastar template, and using a custom controller. Datastar templates are simpler to set up and use, since they are Twig templates, whereas custom controllers may be better suited for when more logic is required, since they are written in PHP.
Datastar makes it possible to modify DOM elements, update signals, execute JavaScript and run controller actions in Craft.
Datastar Templates #
Datastar templates can be called using an 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('_partials/search') }}">
Search
</button>
Clicking the button will send an AJAX request to the web server telling the Datastar plugin to render the _partials/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('_partials/search', {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('_partials/save-user') }}">
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.
Signals #
Signals can be accessed within templates rendered by Datastar using the signals
variable, which is an array of signals received by the request that is automatically injected into the template.
<input data-bind-username>
<button data-on-click="{{ datastar.get('_partials/check-username') }}">
Check
</button>
{# _partials/check-username #}
{# Gets the value of the `username` signal. #}
Username: {{ signals.username }}
Template Tags & Variables #
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 element and signal patch in a server-sent event being streamed to the browser. This makes it possible to send events before performing long-running processes.
Patch Elements #
The {% patchelements %}
tag allows you to patch elements into the DOM.
<div id="results"></div>
<div id="search">
<button data-on-click="{{ datastar.get('_partials/search') }}">
Search
</button>
</div>
{# _partials/search.twig #}
{% patchelements %}
<div id="results">
...
</div>
{% endpatchelements %}
{% patchelements %}
<div id="search">
Search complete!
</div>
{% endpatchelements %}
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, unless a mode is specified (see below).
Element Patch Options #
Elements are patched into the DOM based on element IDs, by default. It’s possible to use other modes and other element patch options using the with
parameter.
{% patchelements with {selector: '#list', mode: 'append'} %}
<li>A new list item</li>
{% endpatchelements %}
Remove Elements #
Elements can be removed from the DOM using the {% removeelements %}
tag, which accepts a CSS selector.
{% removeelements '#list' %}
Patch Signals #
The {% patchsignals %}
tag allows you to patch signals into the frontend signals.
{# Sets the value of the `username` signal. #}
{% patchsignals {username: 'johnny'} %}
{# Sets multiple signal values using an array of key-value pairs. #}
{% patchsignals {username: 'bobby', success: true} %}
{# Removes the `username` signal by setting it to `null`. #}
{% patchsignals {username: null} %}
Signals patches cannot be wrapped in
{% patchelements %}
tags, since each patch creates a server-sent event which will conflict with the element’s contents.
Executing JavaScript #
The {% executescript %}
tag allows you to send JavaScript to the browser to be executed on the front-end.
{% executescript %}
alert('Username is valid');
{% endexecutescript %}
Redirecting #
The {% location %}
tag allows you to redirect the page by updating window.location
on the front-end.
{% location '/guide' %}
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.
{# _partials/save-user #}
{# @var response \craft\web\Response #}
{% 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.
{% patchelements %}
<div id="main">
{% if response.isSuccessful %}
User successfully saved!
{% else %}
Errors: {{ response.data.errors|join() }}
{% endif %}
</div>
{% endpatchelements %}
See a list of available actions in Craft.
Custom Controllers #
You can use your own controller instead of the Datastar controller by using the DatastarEventStream
trait. Return the getStreamedResponse()
method, passing a callable into it that calls zero or more of the methods provided by the DatastarEventStream
trait.
use craft\web\Controller;
use putyourlightson\datastar\DatastarEventStream;
use yii\web\Response;
class MyCustomController extends Controller
{
use DatastarEventStream;
protected array|bool|int $allowAnonymous = true;
public function actionIndex(): ?Response
{
return $this->getStreamedResponse(function() {
$this->patchElements('<div id="results">...</div>');
$this->patchElements('<div id="search">Search complete!</div>');
});
}
}
DatastarEventStream Trait #
patchElements()
#
Patches elements into the DOM.
$this->patchElements('<div id="new">New element</div>');
removeElements()
#
Removes HTML element that match the provided selector from the DOM.
$this->removeElements('#old');
patchSignals()
#
Patches signals into the frontend signals.
$this->patchSignals(['foo' => 1, 'bar' => 2]);
executeScript()
#
Executes JavaScript in the browser.
$this->executeScript('alert("Hello, world!")');
location()
#
Redirects the browser by setting the location to the provided URI.
$this->location('/guide');
renderDatastarTemplate()
#
Renders a Datastar template.
$this->renderDatastarTemplate('_partials/search-results', ['results' => $results]);
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-RC.1"
}
}
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();
}
}
Since the Craft control panel uses data-*
attributes that can clash with Datastar’s, the module loads an aliased version of the framework, meaning that Datastar attributes take the form data-star-*
.
<button data-star-on-click="..."></button>
Config Settings #
A config file is available for modifying configuration settings. To use it, copy the config.php
to your project’s main config
directory as datastar.php
and update any default settings you wish to change. All available settings are listed and documented in the config file.
Security Considerations #
As per the security reference, you should always escape user input (including user generated content) and implement backend validation. Opt for passing variables into templates rather than sending signals, when possible, as variables are tamper-proof.
Always validate that a user inputted value is allowed, if it could lead to revealing sensitive data.
{% set section = signals.section ?? '' %}
{% set allowedSections = ['articles', 'plugins'] %}
{% if section not in allowedSections %}
This section is not allowed.
{% else %}
{% set entries = craft.entries.section(section).all() %}
%}
The code above will only fetch the entries in the provided section if it is either articles
or plugins
. This avoids the possibility that someone could change section
to secrets
and gain access to entries that should not be visible.
Since variables can contain user input, you should be aware of the risk of introducing a Cross-Site Scripting (XSS) vulnerability into your code. Variables output by Twig are automatically escaped, but you should always ensure that your HTML attribute values are wrapped in quotes and that you escape any variables that are output inside of executescript
tags using the JavaScript escape filter.
{% patchelements %}
{# It is unsafe to omit quotes, do NOT do this! #}
<div data-text={{ name }}>
{# It is safe to use either double or single quotes. #}
<div data-text="{{ name }}">
<div data-text='{{ name }}'>
{% endpatchelements %}
{% executescript %}
{# Escape for JavaScript. #}
console.log('{{ name|escape('js') }}');
{% endexecutescript%}
Examples #
The Datastar how-to guides and examples show how many of the data-*
attributes can be used. The following example is more relevant to how you might use Datastar specifically 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('_partials/save-entry') }}" data-show="$entryId == {{ entry.id }}">
Save
</button>
</td>
</tr>
{% endfor %}
</table>
{# _partials/save-entry.twig #}
{% set response = datastar.runAction('entries/save-entry', {
entryId: signals.entryId,
title: signals.title,
}) %}
{% if response.isSuccessful %}
{% patchelements %}
<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>
{% endpatchelements %}
{% patchsignals {entryId: 0} %}
{% else %}
{% patchelements %}
<div id="alert">
{% for error in response.data.errors %}
{{ error|first }}
{% endfor %}
</div>
{% endpatchelements %}
{% endif %}
View the full source code.
Support #
Support for Datastar plugin functionality is provided via GitHub issues only. 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.