Refactor Stimulus.js Controllers to Use Change Callbacks

Rails Designer - Sep 3 - - Dev Community

This articles was originally published on Rails Designer


Stimulus' values API has a great change callback function. This allows you to respond whenever a value changes. 🦉

For a quick refresher: the values API allows you read (and write!) data attributes to the controller's element.

You define them like so in the HTML: <div data-controller="counter" data-counter-time-value="10">10</div>.

Then in the controller define them in the static values:

export default class extends Controller {
  static values = { time: Number }
}
Enter fullscreen mode Exit fullscreen mode

With above controller you might expect a connect function to start the timer.

// app/javascripts/controllers/counter_controller.js
export default class extends Controller {
  static values = { time: Number }

  connect() {
    this.timer = setInterval(() => {
      this.timeValue--;

      if (this.timeValue > 0) {
        this.element.textContent = this.timeValue;
      } else {
        this.element.textContent = "Time's up!"

        clearInterval(this.timer);
      }
    }, 1000);
  }
}
Enter fullscreen mode Exit fullscreen mode

And above might look just fine to you. But I like to keep my functions smaller in scope, and I am pretty sure the next requirement is to add a stop and pause feature (and I need a bridge to show how the changes callbacks can be used).

So let's do a little refactor using Change Callbacks.

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

  connect() {
    this.timer = null;

    this.#start();
  }

  // private

  timeValueChanged() {
    if (this.timeValue > 0) {
      this.element.textContent = this.timeValue;
    } else {
      this.element.textContent = "Time's up!";

      clearInterval(this.timer);
    }
  }

  #start() {
    this.timer = setInterval(() => {
      this.timeValue--;
    }, 1000);
  }
}
Enter fullscreen mode Exit fullscreen mode

timevalueChanged() is called whenever the this.timeValue is changed (which is done in #start().

Is this better? “The class has gotten bigger!” But I'd argue it is better. Yes, significantly more lines of code, but to me it's easier to follow. The #start function is only concerned with starting the timer. You can imagine there being a public function, alongside a stop() and pause() function.

  • show a maximum allowed character count;
  • fetch API endpoint when urlValue changes;
  • show the selected option from a select-element;
  • disable a button on "loading" value.

It is one of those, somewhat, hidden helper functions in Stimulus that if you know it, you see ways to use it in many situations.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player