Practical Stimulus: Building a Counter Component

Practical Stimulus: Building a Counter Component

August 23, 2023

In this article, we will build a counter component using the Stimulus JavaScript library. This simple example will demonstrate a bunch of useful features of Stimulus such as managing state, handling events, and targeting DOM elements.

This is the third article in the Practical Stimulus series, where we're learning various practical use-cases of Stimulus.js that you'd typically use either plain JavaScript or a single-page application framework like React or Vue.

In this post, we'll build a simple "counter" component to demonstrate the basics of state management, event listening, and DOM targeting, which are bread and butter tasks of front-end web development and the absolute core features of all JavaScript frameworks.

If instead of Stimulus, you'd like to learn how to progressively build a counter using Turbo Drive, Turbo Frames and Turbo Streams, check out the following post: Progressive Application Development with Hotwire

Start with HTML

Instead of JavaScript, HTML takes the center stage in Stimulus, by representing the state, classes, targets, and event handlers.

<div data-controller="counter" data-counter-count-value="10">
<div>
<button data-action="click->counter#increment">Increment</button>
<span data-counter-target="count">10</span>
</div>
</div>

We'll learn what each attribute means later. To keep it clean, I've skipped the CSS classes on the HTML.

Make it Dynamic with a Controller

Create a Stimulus controller named counter_controller.js.

💡 If you're using Rails, just run the rails generate stimulus counter command which will create one for you.

// counter_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static values = { count: Number }
static targets = [ "count" ]

increment() {
this.countValue++
this.countTarget.textContent = this.countValue
}
}

That's it, we're done. The counter should be working as expected. Click on the button and watch the number getting incremented.

Working Counter

Let's walk through what's happening here.

Declaring Data

Use the data-{controller}-{name}-value attribute to add state.

<div data-controller="counter" data-counter-count-value="10">

Here, we connect our <div> to the counter_controller.js controller. We also define the initial count value to 10. You can access this data in the Stimulus controller using Stimulus Values.

export default class extends Controller {
static values = { count: Number }

connect() {
console.log(this.countValue) // 10
}
}

So that's how you manage state, by declaring it in the HTML markup and connecting it to the values on the controllers.

Listening For Events

Use the data-action attribute to listen for any event on an element.

<button data-action="click->counter#increment">Increment</button>

The attribute value follows the event->controller#action format. In this case, we want to listen for a click event on this button, and then call the increment method on the counter controller. So, we'll use click->counter#increment.

As you'd imagine, you can listen for other events. For example, listening for a mouseenter event would look like this: mouseenter->counter#update. To learn more, check out Stimulus Actions.

When a click event happens, Stimulus will call the associated controller method,. In this case, it's the increment method on counter controller. As you can see, we have direct access to the countValue property declared above.

increment() {
this.countValue++
}

On the action attribute, you'll often see click is skipped as Stimulus infers it by default. For clarity, I prefer to leave it there.

Reacting to Changes

Use the data-{controller}-target attribute to mark the target of interest.

<span data-counter-target="count"></span>

Here, we want to update the contents of this div, so we mark it with data target attribute.

You can access this div element in the controller using Stimulus Targets:

export default class extends Controller {
static values = { count: Number }
static targets = [ "count" ]

connect() {
console.log(this.countTarget)
}
}

The only thing remaining is to update the contents of this target when the user clicks the button.

increment() {
this.countValue++
this.countTarget.textContent = this.countValue
}

And that's how you can build a simple counter component in Stimulus.

Although the resulting HTML might look intimidating, after working with Stimulus for almost two years I've come to realize that it results in a highly readable, and as a result, maintainable code. Reading the HTML practically tells you what's going on with a set of conventions and you rarely have to refer to the underlying JavaScript to understand the component.

Sign up for my newsletter

Let's learn to become better developers.

Comments (1)

K
Kris

Thank you for sharing!

Sign in to leave a comment.