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