Stimulus Controller

Practical Stimulus: Building a Counter Component

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.

4 min read

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
This is the day when you fall in love with Hotwire. We’re going to build a simple counter. But we’re not going to build it once and be done with it. Instead, we’ll build and progressively enhance it with all three frameworks in Hotwire: Turbo Drive, Turbo Frames, and Turbo Streams.

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


That's a wrap. I hope you liked this article and you learned something new. If you're new to the blog, check out the start here page for a guided tour or browse the full archive to see all the posts I've written so far.

As always, if you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I look forward to hearing from you.

If you'd like to receive future articles directly in your email, please subscribe to my blog. If you're already a subscriber, thank you.