In Progress
Unit 1, Lesson 21
In Progress

StimulusJS

Back when I got started writing Rails applications, if we wanted to make our HTML more dynamic we sprinkled it with some jQuery event handlers. Fast forward a decade or so, and many web frontends are built from the ground up on robust JavaScript frameworks like React and Ember. These frameworks work great for richly interactive client-side applications, supported by a backend API. But what if we just want to take a typical Rails app and spruce it up with a little bit of extra liveness? Today, guest chef and veteran Rails screencaster Chris Oliver joins us to demonstrate the Stimulus JavaScript library. He’ll show you how Stimulus can be applied to the HTML you *already have*, to add a touch of extra client-side richness. Enjoy!

Video transcript & code

Stimulus JS

comparing JavaScript frameworks

Stimulus is a client side Javascript framework for building interactive frontends. It works by using annotations in your HTML. Let's say we're building a form that needs to count the number of characters in a text field.

Our HTML would look something like this before we add Stimulus:

<html>
  <head>
  </head>
  <body>

    <form>
        <textarea></textarea>
        0 Characters
        </form>

  </body>
</html>

We add Stimulus by including the Javascript and calling Stimulus.Application.start

<html>
  <head>
    <script src="https://unpkg.com/stimulus/dist/stimulus.umd.js"></script>
    <script>
      const application = Stimulus.Application.start()
    </script>
  </head>
  <body>

    <form>
        <textarea></textarea>
        0 Characters
        </form>

  </body>
</html>

To connect this form with a Stimulus controller, we can add data-controller="counter" to our form.

<form data-controller="counter">

Stimulus will look for a CounterController and connect it to our form DOM element.

We can define the counter controller by creating a Javascript class that extends from the Stimulus Controller class.

application.register("counter", class extends Stimulus.Controller {
}

This controller doesn't do anything, but if we hook into the connect() event callback, we can see that Stimulus has connected to our form.

application.register("counter", class extends Stimulus.Controller {
  connect() {
    console.log("hello")
  }
}

If we open this page in our browser, we'll see that Stimulus printed hello in the console when it connected to our form.

screenshot with "hello"

To add functionality to our controller, we need to add data-action to one of our elements. This annotation in our HTML sets up event handlers without having to define any of those in our Javascript.

If we add data-action="keyup->counter#update" to our textarea, Stimulus will setup an event handler on the "keyup" event to call the update method the counter controller.

<form data-controller="counter">
  <textarea data-action="keyup->counter#update"></textarea>
  0 Characters
</form>

We then need to define the update method in our controller. Let's have it print out the number of characters in the console.

application.register("counter", class extends Stimulus.Controller {
  update(event) {
    console.log(event.target.value.length)
  }
}

If we type in the text field, we will see the character count is printed out in the console.

"hello world" with character count

Now we need to update the paragraph tag with the character count. Stimulus provides the "data-target" annotation to easily reference other elements in your controller.

We can define a target in our HTML by writing data-target="counter.count" on the paragraph tag.

<form data-controller="counter">
  <textarea data-action="keyup->counter#update"></textarea>
  <p data-target="counter.count">0 Characters</p>
</form>

Our controller also needs to register these targets in an array. We can do that by adding static targets to the top of our controller.

application.register("counter", class extends Stimulus.Controller {
  static targets = ["count"]

  update(event) {
    console.log(event.target.value.length)
  }
}

The targets listed in the controller will define variables that reference the DOM elements. For example, we can simply update the text of our counter paragraph by using the countTarget that we just registered:

application.register("counter", class extends Stimulus.Controller {
  static targets = ["count"]

  update(event) {
    this.countTarget.textContent = `${event.target.value.length} Characters`
  }
}

Now if we reload the page, we'll see that our paragraph is updated every time we type a character.

count to six

If we duplicate the form tag on the page, we can add a second form that operates completely independent.

<form data-controller="counter">
  <textarea data-action="keyup->counter#update"></textarea>
  <p data-target="counter.count">0 Characters</p>
</form>

<form data-controller="counter">
  <textarea data-action="keyup->counter#update"></textarea>
  <p data-target="counter.count">0 Characters</p>
</form>

Stimulus creates a separate instance of our controller for each data-controller tag that it finds on the page. We don't have to write any code for that and our character counts work independently.

two text fields and two character counters

If we insert a form onto the page dynamically, Stimulus will detect the change and connect a new controller instance. It does this by using the browser's Mutation Observer API under the hood.

Let's try that. We're going to use document.body.insertAdjacentHTML to insert a third form dynamically on to the page.

document.body.insertAdjacentHTML('beforeend', '<form data-controller="counter"><textarea data-action="keyup->counter#update"></textarea><p data-target="counter.count">0 Characters</p></form>')

Now we have a third form on the page and Stimulus has connected a new instance of the controller to this form automatically. Stimulus detects changes to the DOM using the browser's Mutation Observer API under the hood.

three text boxes, three counters

If we type into this new form, we'll see the character count also works and is separate from the other forms.

third box and counter are now populated

Because Stimulus annotates the HTML separately from the Javascript, your Javascript and HTML can be nicely decoupled from each other. So far, we have built separate forms, but what if we were building a questionaire that had character counts for several textareas in the same form?

We can simply remove the data-controller from the form and instead place it around the textareas in a wrapper div.

<form>
  <div data-controller="counter">
    <textarea data-action="keyup->counter#update"></textarea>
    <p data-target="counter.count">0 Characters</p>
  </div>

  <div data-controller="counter">
    <textarea data-action="keyup->counter#update"></textarea>
    <p data-target="counter.count">0 Characters</p>
  </div>
</form>

Our counters continue to work and we've had to make no changes to our Javascript. Stimulus only cares that we have defined our data attributes correctly and we could even add other data-actions to trigger the update such as on the paste event.

two boxes and two counters, highlighted and annotated

Stimulus makes it easy to add Javascript functionality to your existing HTML. It's design also makes compatibility with Turbolinks and dynamic HTML work seamlessly without any changes to your code.

Responses