Stimulus Controller (Actions, Values)
Objective
- Learn Stimulus Values and state management.
- Learn Stimulus Actions.
Controller Instance State
Update frontend/src/controllers/counter_controller.js
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
    connect() {
        // set initial state
        this.count = 0;
        this.element.innerHTML = 'Click me';
        // setup event handler
        this.element.addEventListener('click', () => {
            this.count++;
            this.element.innerHTML = `You clicked ${this.count} times`;
        });
    }
}
Notes:
- Here this.countis something like aprivatevariable of the controller instance.
- We use this.element.addEventListenerto register event handler to the DOM element, if it is clicked, then thethis.countwill increase and theinnderHTMLwill display the value.
Let's update hotwire_django_app/templates/stimulus_basic/counter.html
{% extends "stimulus_basic/base.html" %}
{% block content %}
<div class="w-full max-w-7xl mx-auto px-4">
  <h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Counter</h1>
  <button class="px-4 py-2 bg-blue-500 hover:bg-blue-700 text-white font-semibold rounded-lg" data-controller="counter"></button>
  <h1 data-controller="counter" class="text-4xl sm:text-6xl lg:text-7xl mb-6"></h1>
  <div data-controller="counter" class="text-4xl"></div>
</div>
{% endblock %}
Notes:
- We have button,h1anddivelements on the page, all of them havedata-controller="counter"
- Stimulus will create three controller instances and connect the instances to the respective elements.
Visit http://127.0.0.1:8000/stimulus-basic/counter/

We can click the elements and all of them should work as expected.
Getter, Setter
Update frontend/src/controllers/counter_controller.js
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
    connect() {
        // set initial state
        this.count = 0;
        this.element.innerHTML = 'Click me';
        // setup event handler
        this.element.addEventListener('click', () => {
            this.count++;
            this.element.innerHTML = `You clicked ${this.count} times`;
        });
    }
    get count() {
        return parseInt(this.element.dataset.count);
    }
    set count(value) {
        this.element.dataset.count = value;
    }
}
Notes:
- We add getterandsetterto the stimulus Controller class
- The getterandsettercan help write/read value to/from the DOM element dataset property, you can check https://javascript.info/class#getters-setters to learn more about the syntax.
- If we set value using this.count = 0, the value would be set to thedatasetof the DOM element.
Now if we check the element in the browser devtool, we can see data-count attributes like this
<div data-controller="counter" class="text-4xl" data-count="4">You clicked 4 times</div>
Stimulus Values
It is tedious to write getter and setter for each variable, and we can use Stimulus Values to simplify the code.
Update frontend/src/controllers/counter_controller.js
import {Controller} from '@hotwired/stimulus';
export default class extends Controller {
  static values = {
    count: { type: Number, default: 0 },
  };
  connect() {
    // set initial state
    this.element.innerHTML = 'Click me';
    // setup event handler
    this.element.addEventListener('click', () => {
      this.countValue++;
      this.element.innerHTML = `You clicked ${this.countValue} times`;
    });
  }
  countValueChanged(value, previousValue) {
    console.log(`${previousValue} changed to ${value}`);
  }
}
Notes:
- At the top, we defined static values, which has the value name, value type, and default value.
- We can get or set the value using this.countValue(Valueis the suffix) and Stimulus will help us read, do type conversion, and write DOM dataset property.
- With Stimulus Value, we can remove the annoying getterandsetterand the code is cleaner.
- What is more, we can use countValueChangedas value change callback, this is very useful in some cases.
Now if we check the element in the devtool, we can see data-counter-count-value attribute, the controller name has been added as prefix to avoid potential conflict. (Because one DOM elements can have more than one Stimulus controller in some cases)
In the console tab, we can also see the log message
undefined changed to 0
0 changed to 1
1 changed to 2
2 changed to 3
3 changed to 4
You can check https://stimulus.hotwired.dev/reference/values to learn more.
Actions
Actions are how you handle DOM events in your controllers.
Update frontend/src/controllers/counter_controller.js
import {Controller} from '@hotwired/stimulus';
export default class extends Controller {
  static values = {
    count: { type: Number, default: 0 },
  };
  connect() {
    this.element.innerHTML = 'Click me';
  }
  countValueChanged(value, previousValue) {
    console.log(`${previousValue} changed to ${value}`);
  }
  increment(){
    this.countValue++;
    this.element.innerHTML = `You clicked ${this.countValue} times`;
  }
}
- Code this.element.addEventListenerhas been removed.
- We created incrementmethod to handle the click event of the DOM element.
Update hotwire_django_app/templates/stimulus_basic/counter.html
{% extends "stimulus_basic/base.html" %}
{% block content %}
<div class="w-full max-w-7xl mx-auto px-4">
  <h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Counter</h1>
  <button
    class="px-4 py-2 bg-blue-500 hover:bg-blue-700 text-white font-semibold rounded-lg"
    data-controller="counter"
    data-action="click->counter#increment"
  ></button>       <!-- Update -->
  <h1 data-controller="counter" data-action="click->counter#increment" class="text-4xl sm:text-6xl lg:text-7xl mb-6"></h1>
  <div data-controller="counter" data-action="click->counter#increment" class="text-4xl"></div>
</div>
{% endblock %}
Notes:
- We add data-action="click->counter#increment"to the HTML elements.
- clickis the event we want to listen.
- counteris the controller name.
- incrementis the controller method we want to fire for the event we listen.
- The data-actionvalue means, theclickevent will firecountercontrollerincrementmethod.
Now you can test on http://127.0.0.1:8000/stimulus-basic/counter/ and it should still work.
And the code is easy to understand and maintain.
Notes:
- It seems addEventListeneranddata-actioncan do the same thing here, so which one is recommended?
- When we use addEventListener, we should always remember to runremoveEventListenerin thedisconnectmethod (which run when the controller is disconnected from the DOM tree). If we forget do so, some bugs might happen in some cases. (especially when we use Turbo)
- data-actionis better option in most cases.
You can check https://stimulus.hotwired.dev/reference/actions to learn more.
Conclusion
Let's check again our counter.html, with data-controller and data-action, we attach JS code and event handler to the DOM elements.
We can easily reuse code of counter_controller.js with other DOM elements.
The whole logic is in the JS code, with valina JS and Stimulus, we can write clean and maintainable code.