This plugin is in early development and its API may change.

Spark is a free plugin for Craft CMS that allows you to create real-time 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 run logic all within your Twig templates.

With Spark, you have the ability to define state and manipulate the DOM in real-time on the front-end, and modify parts of the DOM with templates rendered on the back-end. Some use-cases for Spark are:

  • Live search
  • 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

Spark is lightweight, performant and unopinionated about how you build your front-ends, and can be used for simple to complex use-cases.

Read The Case for Spark and join in the discussion.

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 Spark” in the Craft Plugin Store, or install manually using composer.

composer require putyourlightson/craft-spark

Usage #

Data Attributes #

Spark provides data-* attributes (powered by Datastar) that allow you to define state and manipulate the DOM in real-time.

data-store #

A global data store” (similar to x-data in Alpine.js, but global) allows you to define and store values for one or more key-value pairs. Values in the store can be used in multiple parts of the DOM, and are kept in sync wherever used.

Store values can be set by placing the data-store attribute on any element in the DOM. The store is a global singleton that can be accessed and modified from anywhere in the page.

<div data-store="{ title: '' }"></div>

If you place the data-store attribute on multiple elements, their values will be merged (values defined later will override those defined earlier in the DOM tree).

<div data-store="{ title: '' }"></div>
    <div data-store="{ title: 'Test' }"></div>
</div>

This will result in title store value being set to Test.

Spark provides some helper functions for creating the store. The sparkStore() function accepts an array of values and converts them into a JSON encoded string. This is convenient when you want to add Twig variables to the store.

<div data-store="{{ sparkStore({ title: title }) }}"></div>

The sparkStoreFromClass() function accepts a fully qualified class name and returns the class’s public properties as a JSON encoded string. 

<div data-store="{{ sparkStoreFromClass('attributes\\stores\\AutocompleteStore') }}"></div>

Using sparkStoreFromClass() can be helpful for when you use a PHP attribute” class for getting autocompletion in PhpStorm. This way, there is one source of truth” when modifying store value names.

namespace attributes\stores;

use Attribute;
use putyourlightson\spark\models\StoreModel;

 #[Attribute]
class AutocompleteStore extends StoreModel
{
    public string $title = '';
}

data-show #

The data-show attribute can be used to show or hide an element based on whether a JavaScript expression evaluates to true or false.

<input type="submit" data-show="$title != ''" value="Save">

This will result in the submit button being visible only when the title is not an empty string. The $ indicates that $title is a store value.

data-text #

The data-text attribute can be used to set the text content of an element to a JavaScript expression.

<div data-store="{ title: 'Hello' }">
    <h1 data-text="$title"></h1>
</div>

This will result in the h1 displaying the text Hello. The attribute value is an evaluated expression that can be expanded upon further.

<div data-store="{ title: 'Hello' }">
    <h1 data-text="$title + ' world'"></h1>
</div>

This will result in Hello world being displayed in the h1 element.

data-model #

The data-model attribute can be used to create two-way data binding on any form fields (input, textarea, select, checkbox and radio elements). The attribute value must be the name of an existing store value, and the field value will be kept in sync with the store value.

<div data-store="{ title: 'Hello' }">
    <input data-model="title">
</div>

This will result in the input field displaying the text Hello. Typing into the input field will modify the value of the store, as well as anywhere the store value is used.

<div data-store="{ title: '' }">
    <h1 data-text="$title"></h1>
    <input type="text" data-model="title">
</div>

This will result in the h1 element displaying whatever is typed into the input field.

data-bind-* #

The data-bind-* attribute can be used to bind a JavaScript expression to any valid HTML attribute.

<input type="submit" data-bind-disabled="$title == ''" value="Save">

This will result in the submit input field being assigned the attribute disabled="true", and therefore being disabled only when the title store value is an empty string.

data-on-* #

The data-on-* attribute can be used to execute a JavaScript expression whenever an event is triggered on an element.

<button data-on-click="$title = 'New title'">
    Reset
</button>

This will result in the title store value being set to New title when the button element is clicked. If the title store value is used elsewhere, its value will be automatically updated.

<div data-store="{ title: '' }">
    <h1 data-text="$title"></h1>
    <button data-on-click="$title = 'New title'">
        Reset
    </button>
</div>

Clicking the button element will output New title in the h1 element.

Any DOM event can be used, in addition to some special events.

  • data-on-load is triggered whenever the element is loaded into the DOM. Especially useful for refreshing dynamic fragments of statically cached pages.
  • data-on-store-change is triggered whenever the store changes.
  • data-on-raf is triggered on every request animation frame event.

Real-time Template Rendering #

Spark also allows you to modify parts of the DOM with real-time rendered Twig templates, using the spark() function.

<button data-on-click="{{ spark('_spark/save-user.twig') }}">
    Submit
</button>

This will send an AJAX request to the web server telling Spark to render the _spark/save-user.twig template, whenever the button is clicked.

If you prefer to use Datastar’s action plugins explicitly, you can do so using the sparkUrl() function (note the single quotes around the Twig tag!).

<button data-on-click="$$get('{{ sparkUrl('_spark/save-user.twig') }}')">
    Submit
</button>

Templates rendered by Spark can contain one or more top-level elements, each with an ID that determines which element in the DOM should be swapped. This is handled by Idiomorph.

<div id="username"></div>

<div id="main">
    <button data-on-click="{{ spark('_spark/save-user.twig') }}">
        Submit
    </button>
</div>
{# _spark/save-user.twig #}

<div id="username">
    Username: {{ store.username }}
</div>

<div id="main">
    User successfully saved!
</div>

Elements with the IDs username and main must already exist in the DOM, otherwise the script will not work and an error will appear in the browser console.

Store values can be accessed and modified within templates rendered by Spark. The store variable is populated with the store values and injected into the template.

<div data-store="{ username: 'bob' }">
    <button data-on-click="{{ spark('_spark/save-user.twig') }}">
        Submit
    </button>
</div>
{# _spark/save-user.twig #}

Username: {{ store.username }}

{# Modifies the value of `username` in the store. #}
{% do store.username('bobby') %}

{# Modifies multiple values in the store using a fluid model syntax. #}
{% do store.username('bobby').success(true) %}

{# Modifies multiple values in the store using an array of key-value pairs. #}
{% do store.setValues({ username: 'bobby', success: true }) %}

Variables can be passed into the template using a second argument. Variables are tamper-proof yet visible in the source code in plain text, so you should avoid passing through any sensitive data.

<button data-on-click="{{ spark('_spark/save-user.twig', { userId: 1 }) }}">
    Submit
</button>

Only primitive data types can be used as values: 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.

Actions can be run within templates rendered by Spark using craft.app.runAction(), which accepts a controller action route and a set of params (optional), and returns the action’s response. Note that since the store values are sent as query params, they will be processed by the controller action as well.

{# _spark/save-user.twig #}

{% set response = craft.app.runAction('users/save-user', { userId: userId }) %}

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

Most actions require a POST request which requires that a CSRF token is sent along with the request. You can set the method type in the third parameter and if it is a non-GET request, Spark will automatically add a CSRF token for you.

<button data-on-click="{{ spark('_spark/save-user.twig', { userId: 1 }, 'post') }}">
    Submit
</button>

If you prefer, you can also use a named argument for the method, which may be desirable when not passing variables into the template.

<button data-on-click="{{ spark('_spark/save-user.twig', method: 'post') }}">
    Submit
</button>

Spark Functions #

The following functions are available in templates rendered by Spark.

spark.remove #

Removes all elements that match the selector from the DOM.

{% do spark.remove('#alert') %}

{% do spark.remove('.alerts .warnings') %}

spark.redirect #

Redirects the page to the provided URI

{% do spark.redirect('/new-page') %}

spark.console #

Returns a console variable for logging messages to the browser console.

{% do spark.console.log('This message will appear in the browser console.') %}

{# Outputs to the console using other modes. #}
{% do spark.console.debug(message) %}
{% do spark.console.warn(message) %}
{% do spark.console.error(message) %}

Examples #

Inline Entry Editing #

This example uses the store 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 store value of entryId to 0. If there are errors, it outputs them in the alert.

{# inline-entry-editing.twig #}

<table data-store="{ entryId: 0, title: '' }">
    {% 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 type="text" data-model="title">
                </div>
            </td>
            <td>
                <button id="edit-{{ store.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="{{ spark('_spark/save-entry.twig', method: 'post') }}" data-show="$entryId == {{ entry.id }}">
                    Save
                </button>
            </td>
        </tr>
    {% endfor %}
</table>
{# _spark/save-entry.twig #}

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

Adding a confirmation message before sending the Spark request can be achieved using JavaScript.

<button data-on-click="confirm('Are you sure?') && {{ spark('_spark/save-entry.twig', method: 'post') }}" data-show="$entryId == {{ entry.id }}">

Or as follows, using the more explicit syntax.

<button data-on-click="confirm('Are you sure?') && $$(post('{{ sparkUrl('_spark/save-entry.twig', method: 'post') }}')" data-show="$entryId == {{ entry.id }}">

We could take the same action whenever the Enter key is pressed on the input field as follows.

<input type="text" data-on-keydown.key_enter="confirm('Are you sure?') && $$(post('{{ sparkUrl('_spark/save-entry.twig', method: 'post') }}')" data-model="title">

Since we’ve duplicated the code used on the click event of the save button, we could instead add a reference to the button using data-ref and then trigger a click event on it from the input field.

<button data-ref="save" data-on-click="confirm('Are you sure?') && $$(post('{{ sparkUrl('_spark/save-entry.twig', method: 'post') }}')" data-show="$entryId == {{ entry.id }}">

<input type="text" data-on-keydown.key_enter="(~save).click()" data-model="title">

Note that the parentheses around the reference ~save are required to ensure it is properly evaluated before calling .click().

Watch the demo video [needs updating] and view the full source code.

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