Skip to main content

Tagged “stimulus”

Adapting Stimulus usage for better Progressive Enhancement

A while back, Jake Archibald tweeted:

Don't render buttons on the server that require JS to work.

The idea is that user interface elements which depend on JavaScript (such as buttons) should be rendered on the client-side, i.e. with JavaScript.

In the context of a progressive enhancement mindset, this makes perfect sense. Our minimum viable experience should work without JavaScript due to the fragility of a JavaScript-dependent approach so should not include script-triggering buttons which might not work. The JavaScript which applies the enhancements should not only listen for and act upon button events, but should also be responsible for actually rendering the button.

This is how I used to build JavaScript interactions as standard, however sadly due to time constraints and framework conventions I don’t always follow this best practice on all projects.

At work, we use Stimulus. Stimulus has a pretty appealing philosophy:

Stimulus is designed to enhance static or server-rendered HTML—the “HTML you already have”

However in their examples they always render buttons on the server; they always assume the JavaScript-powered experience is the baseline experience. I’ve been pondering whether that could easily be adapted toward better progressive enhancement and it seems it can.

My hunch was that I should use the connect() lifecycle method to render a button into the component (and introduce any other script-dependent markup adjustments) at the earliest opportunity. I wasn’t sure whether creating new DOM elements at this point and fitting them with Stimulus-related attributes such as action and target would make them available via the standard Stimulus APIs like server-rendered elements but was keen to try. I started by checking if anyone was doing anything similar and found a thread where Stimulus contributor Javan suggested that DIY target creation is fine.

I then gave that a try and it worked! Check out my pen Stimulus with true progressive enhancement. It’s a pretty trivial example for now, but proves the concept.

Testing Stimulus Controllers

Stimulus JS is great but doesn’t provide any documentation for testing controllers, so here’s some of my own that I’ve picked up.

Required 3rd-party libraries

Basic Test

// hello_controller.test.js
import { Application as StimulusApp } from "stimulus";
import HelloController from "path/to/js/hello_controller";

describe("HelloController", () => {
beforeEach(() => {
// Insert the HTML and register the controller
document.body.innerHTML = `
<div data-controller="hello">
<input data-target="hello.name" type="text">
<button data-action="click->hello#greet">
Greet
</button>
<span data-target="hello.output">
</span>
</div>
`
;
StimulusApp.start().register('hello', HelloController);
})

it("inserts a greeting using the name given", () => {
const helloOutput = document.querySelector("[data-target='hello.output']");
const nameInput = document.querySelector("[data-target='hello.name']");
const greetButton = document.querySelector("button");
// Change the input value and click the greet button
nameInput.value = "Laurence";
greetButton.click();
// Check we have the correct greeting
expect(helloOutput).toHaveTextContent("Hello, Laurence!");
})
})

Modest JS Works

Pascal Laliberté has written a short, free, web-based book which advocates a modest and layered approach to using JavaScript.

I make the case for The JS Gradient, a principle whereby your app can have multiple coexisting modern JS approaches, starting from the global sprinkles to spot view-models to, yes, an SPA if that’s really necessary. At each point in the gradient, you’ll see when it’s a good idea to go a step further toward heavier JavaScript, or not.

Pascal’s philosophy starts with the following ideals:

  • prefer server-generated HTML over JavaScript-generated HTML. If we need to add more complex JavaScript layers we may deviate from that ideal, but this should be the starting point;
  • we should be able to swap and replace the HTML on a page on a whim. We can then support techniques like pjax (replacing the whole body of a page with new HTML such as with Turbolinks) and ahah (asynchronous HTML over HTTP: replacing parts of a page with new HTML, so as to make our app feel really fast while still favouring server-generated HTML;
  • favour native Browser APIs over proprietary libraries. Use the tools the browser gives us (History API, Custom Event handlers, native form elements, CSS and the cascade) and polyfill older browsers.

He argues that a single application can combine the options along the JS Gradient, but also that we need only move to a new level if and when we reach the current level’s threshold.

He defines the levels as follows:

  • Global Sprinkles: general app-level enhancements that occur on most pages, achieved by adding event listeners at document level to catch user interactions and respond with small updates. Such updates might include dropdowns, fetching and inserting HTML fragments, and Ajax form submission. This might be achieved via a single, DIY script (or something like Trimmings) that is available globally and provides reusable utilities via data- attributes;
  • Component Sprinkles: specific page component behaviour defined in individual .js files, where event listeners are still ideally set on the document;
  • Stimulus components: where each component’s HTML holds its state and defines its behaviour, with a companion controller .js file which wires up event handlers to elements;
  • Spot View-Models: using a framework such as Vue or React only in specific spots, for situations where our needs are more complex and generating the HTML on the server would be impractical. Rather than taking over the whole page, this just augments a specific page section with a data-reactive view-model.
  • A single-page application (SPA): typically an all-JavaScript affair, where whole pages are handled by Reactive View-Models like Vue and React and the browser’s handling of clicks and the back button are overriden to serve different JavaScript-generated views to the user. This is the least modest approach but there are times when it is necessary.

One point to which Pascal regularly returns is that it’s better to add event listeners to the document (with a check to ensure the event occurred on the relevant element) rather than to the element itself. I already knew that Event Delegation is better for browser performance however Pascal’s point is that in the context of wanting to support swapping and replacing HTML on a whim, if event listeners are directly on the element but that element is replaced (or a duplicate added) then we would need to keep adding more event listeners. By contrast, this is not necessary when the event listener is added to the document.

Note: Stimulus applies event handlers to elements rather than the document, however one of its USPs is that it’s set up so that as elements appear or disappear from the DOM, event handlers are automatically added and removed. This lets you swap and replace HTML as you need without having to manually define and redefine event handlers. He calls this Automated Behaviour Orchestration and notes that while adding event listeners to the document is the ideal approach, the Stimulus approach is the next best thing.

Also of particular interest to me was his Stimulus-based Shopping Cart page demo where he employs some nice techniques including:

  • multiple controllers within the same block of HTML;
  • multiple Stimulus actions on a single element;
  • controller action methods which use document.dispatchEvent to dispatch Custom Events as a means of communicating changes up to other components;
  • an element with an action which listens for the above custom event occurring on the document (as opposed to an event on the element itself).

I’ve written about Stimulus before and noted a few potential cons when considering complex interfaces, however Pascal’s demo has opened my eyes to additional possibilities.

Loading and Templating JSON Responses in Stimulus.js (by John Beatty)

Just because Stimulus.js is designed to work primarily with existing HTML doesn’t mean it can’t use JSON APIs when the need arises.

Here, John pimps up his Stimulus controller to get and use JSON from a remote API endpoint.

He triggers a fetch() from the connect() lifecycle callback, then iterates the returned JSON creating each HTML list item using a JavaScript template literal before finally rendering them into an existing <ul> via a Stimulus target.

Easy when you know how!

Progressively Enhanced JavaScript with Stimulus

I’m dipping my toes into Stimulus, the JavaScript micro-framework from Basecamp. Here are my initial thoughts.

I immediately like the ethos of Stimulus.

The creators’ take is that in many cases, using one of the popular contemporary JavaScript frameworks is overkill.

We don’t always need a nuclear solution that:

  • takes over our whole front end;
  • renders entire, otherwise empty pages from JSON data;
  • manages state in JavaScript objects or Redux; or
  • requires a proprietary templating language.

Instead, Simulus suggests a more “modest” solution – using an existing server-rendered HTML document as its basis (either from the initial HTTP response or from an AJAX call), and then progressively enhancing.

It advocates readable markup – being able to read a fragment of HTML which includes sprinkles of Stimulus and easily understand what’s going on.

And interestingly, Stimulus proposes storing state in the HTML/DOM.

How it works

Stimulus’ technical purpose is to automatically connect DOM elements to JavaScript objects which are implemented via ES6 classes. The connection is made by data– attributes (rather than id or class attributes).

data-controller values connect and disconnect Stimulus controllers.

The key elements are:

  • Controllers
  • Actions (essentially event handlers) which trigger controller methods
  • Targets (elements which we want to read or write to, mapped to controller properties)

Some nice touches

I like the way you can use the connect() method – a lifecycle callback invoked whenever a given controller is connected to the DOM - as a place to test browser support for a given feature before applying a JS-based enhancement.

Stimulus also readily supports the ability to have multiple instances of a controller on the page.

Furthermore, actions and targets can be added to any type of element without the controller JavaScript needing to know or care about the specific element, promoting loose coupling between HTML and JavaScript.

Managing State in Stimulus

Initial state can be read in from our DOM element via a data- attribute, e.g. data-slideshow-index.

Then in our controller object we have access to a this.data API with has(), get(), and set() methods. We can use those methods to set new values back into our DOM attribute, so that state lives entirely in the DOM without the need for a JavaScript state object.

Possible Limitations

Stimulus feels a little restrictive if dealing with less simple elements – say, for example, a data table with lots of rows and columns, each differing in multiple ways.

And if, like in our data table example, that element has lots of child elements, it feels like there might be more of a performance hit to update each one individually rather than replace the contents with new innerHTML in one fell swoop.

Summing Up

I love Stimulus’s modest and progressive enhancement friendly approach. I can see me adopting it as a means of writing modern, modular JavaScript which fits well in a webpack context in situations where the interactive elements are relatively simple and not composed of complex, multidimensional data.

See all tags.

External Link Bookmark Note Entry Search