Sprig: A Reactive Craft CMS Plugin

By Charlie,

July 2021

Web Dev
Reactivity is a means of keeping your HTML updated with new data using Javascript. Vue & React have been dominating the reactivity market in recent years. However Sprig, for Craft CMS, has been developed to bring reactive components to Twig.

What is Sprig?

Ben Croker released Sprig back in May 2020, as a free plugin for Craft, which allows you to create reactive Twig components / PHP classes. Developed with HTMX it allows you to re-render components on triggered events (mouseover, click, changes etc) using AJAX without writing a single line of Javascript.

Why use Sprig?

Having reactive components can help with the UX of a website. Sprig was developed to make components such as Search or Pagination much quicker to navigate.

For example on a search input, rather than sending the user off to a results page, you can display the results in a dropdown. This means the user can find what they are after much quicker than if you’re submitting a form.

To do this you’d only need to add a few lines of HTML/Twig to create the form, and then the results dropdown in which they’d appear.

{# Sets the results to empty if the input is blank #}
{% set results = results ?? '' %}

<input sprig 
    s-trigger="changed" 
    s-replace="#search-results" 
    type="text" 
    name="results" 
    value="{{ results }}">

In the example above we’re using three sprig data attributes:

  1. sprig - This is needed to make sure the Javascript, Sprig provides, knows this a reactive component.

  2. s-trigger - This attribute relates to the HTMX Trigger, which allows you to specify what triggers an AJAX Request. Some components have default triggers, however you can change these using this attribute.

  3. s-replace - with the replace attribute you can specify which element to replace when the AJAX request has completed.

With this you will have created a small reactive component which will populate the results variable set above. A very important thing here is that the name of the component needs to match the variable you want to link to it. For example you can see above the name of the input (results) matches the variable we created beforehand. This makes sure the data is put into the correct place.

The Good

The best part of Sprig is it’s simplicity. Creating reactive components makes the final product slicker and easier to use.

You can also use Sprig to communicate with Craft’s built in actions, which is incredibly useful with Commerce projects. This is done similarly to the above, however with a few more attributes.

<div id="cart-total">
    <form sprig s-method="post" s-action="commerce/cart/update-cart">
        <input type="hidden" name="purchasableId" value="{{ variantId }}">
        <button type="submit">Add to Cart</button>
    </form>

    <div>{{ cart.lineItems | length }}</div>
</div>

{% if success is defined %}
    <script>
        htmx.trigger('#cart-total', 'refresh');
    </script>
{% endif %}

The example above is an Add to Cart button. Without Sprig, you’d add an action to the form and once submitted the page would refresh and the new product would have been added to the cart.

However with Sprig we have defined a method, s-method=”post”, and the commerce action, s-action=”commerce/craft/update-cart”, to the form instead. By doing this, Sprig will send an AJAX post request to the action endpoint with the data inside of the form, and reactively update the cart rather than requiring a refresh.

Underneath we’ve added a small bit of Javascript just to make sure the Cart total, which is shown in the div underneath the form, will update when the AJAX request has been successful.

Sprig also comes with a PlayGround within the admin area of Craft where you can test all your code beforehand, helping you to see your code, and the results, instantly.

Sprig Playground
Sprig Playground

Fallbacks

Coding for non JS users is still something developers need to keep in mind. Without JS Sprig will not run, however you can still make sure your code works.

Using the Add to Cart example above, we can update this to factor in if the JS cannot fire.

<div id="cart-total">
    <form method="post" sprig s-method="post" s-action="commerce/cart/update-cart">
        {{ csrfInput() }}
        {{ actionInput('commerce/cart/update-cart') }}
        {{ hiddenInput('purchasableId', variantId) }}
        
        <button type="submit">Add to Cart</button>
    </form>

    <div>{{ cart.lineItems | length }}</div>
</div>

So including a method, the CSRF input and the action will not change how Sprig works, however it does make sure that if the Javascript doesn’t run, your users can still use this form as normal.

The Bad

The biggest fallback of Sprig seems to be the re-rendering of components. While this is a necessary evil, it wipes out all other JS used on these components, so scripts such as Lazy Load, or Click Events are void after the first re-render.

While you can run this JS again by triggering an event, it becomes a bit more difficult when using a GET Request rather than a POST.

This is because, within Craft, when a POST request has completed, Craft will return a success variable as true. So with this you can use an example, like the below, to rerun your JS.

{% set hasRendered = success ?? '' %}

{% if hasRendered %}
    <script>
        htmx.trigger('#component', 'refresh'));
    </script>
{% endif %}

However with a GET request you don’t receive anything back from Craft to say it’s been successful. So you need to do some additional work there to make sure the event can be triggered.

For example you can run the following:

if (typeof htmx !== 'undefined') {
   htmx.trigger('#component', 'reloaded');
}

This works because HTMX is not defined until Sprig has re-rendered a component. Once that has happened your JS will then trigger a custom event which you can add a listener to within your own JS

The second biggest fallback for Sprig is dependent on the size of your project. Every component used by Sprig must be in it’s own file and imported like so:

{{ sprig('_atoms/_button') }}

If you have a relatively small site, this isn’t a problem as you probably won’t have many files to import. However if your site is for a medium/large business you can find yourself having a number of files where, for something like a button, it feels a bit of an overkill.

The Ugly

Every JS framework has an ugly factor.

For Sprig it is the fact you cannot import arrays or objects into a sprig file. This can be incredibly frustrating for Craft developers as using Arrays/Objects is a part of Craft itself. Sprig requires you to import each item as a string variable rather than using an object itself.

To get around this you have to contain a lot of information inside of one Sprig file, however a variable set by Craft such as Entry or Cart has to be refined inside of the Sprig file.

For example when passing an Entry through to a sprig file, you’ll need to do the following:

{{ sprig('_components/entry, {
    entryID: entry.id,
}) }}
Then within the Sprig file, you’ll need to query the Entry ID to receive the entry Object.
{% set entry = craft.entries()
    .id(entryID)
    .one() %}

This feels quite long winded, as just importing the original Object, which already exists, would be a simpler process.

Final thoughts

It’s great that Craft CMS has a reactive component library just for Twig. Making certain components more slick and introducing a cleaner flow to data is always a plus, and while Sprig may have some bad points, overall it’s great to see a plugin we can use to rival Vue and React!