Unleash the Power of Hotwire(Part 2): Getting Started with Stimulus

by Satya Swaroop Mohapatra, Senior System Analyst

Stimulus

In Part 1, we explored how Turbo Drive, Turbo Frames, and Turbo Streams help us create seamless server-side updates. Now, let's learn about Stimulus - a simple yet powerful JavaScript framework that helps us add client-side interactivity when we need it.

In this part, we'll start with the basics and build our understanding through simple examples:

  1. Understanding Stimulus Controllers and Lifecycle Methods
  2. Working with Targets to interact with DOM elements
  3. Handling Events with Actions
  4. Using Values to manage and sync state

Getting Started with Stimulus

Let's start by creating a simple counter example to understand the basics. First, generate a Stimulus controller:

rails g stimulus counter

This creates a new file app/javascript/controllers/counter_controller.js:

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    console.log("Counter controller connected!")
  }
}

Understanding Lifecycle Methods

Stimulus controllers have three main lifecycle methods that help us manage our code:

export default class extends Controller {
  initialize() {
    // Called once when the controller is first instantiated
    console.log("Counter initialized!")
  }

  connect() {
    // Called when the controller is connected to the DOM
    console.log("Counter connected!")
  }

  disconnect() {
    // Called when the controller is disconnected from the DOM
    console.log("Counter disconnected!")
  }
}

Let's create a simple view to use our counter:

<div data-controller="counter">
  <h1>Simple Counter</h1>
  <p>Open your browser console to see the lifecycle methods in action!</p>
</div>

When you load this page, you'll see the lifecycle messages in your console. This helps us understand when our controller is active.

Working with Targets

Now let's make our counter functional. We'll need to reference some DOM elements - this is where targets come in. They're like querySelector but much simpler to use.

Update the counter controller:

export default class extends Controller {
  static targets = ["output"]

  connect() {
    this.count = 0
    this.updateOutput()
  }

  increment() {
    this.count++
    this.updateOutput()
  }

  updateOutput() {
    this.outputTarget.textContent = this.count
  }
}

And update our view:

<div data-controller="counter">
  <h1>Simple Counter</h1>
  <p>Count: <span data-counter-target="output">0</span></p>
  <button data-action="click->counter#increment">Increment</button>
</div>

Let's break down what's happening:

  1. We define targets using static targets = ["output"]
  2. We reference a target using this.outputTarget
  3. The view connects to targets using data-counter-target="output"

Handling Events with Actions

You might have noticed data-action="click->counter#increment" in our button. This is how Stimulus handles events:

  • click is the event type
  • counter is our controller name
  • increment is the method to call

Let's add a few more actions to make our counter more interesting:

export default class extends Controller {
  static targets = ["output"]

  connect() {
    this.count = 0
    this.updateOutput()
  }

  increment() {
    this.count++
    this.updateOutput()
  }

  decrement() {
    this.count--
    this.updateOutput()
  }

  reset() {
    this.count = 0
    this.updateOutput()
  }

  updateOutput() {
    this.outputTarget.textContent = this.count
  }
}

And update our view:

<div data-controller="counter" class="p-4">
  <h1 class="text-xl mb-4">Simple Counter</h1>
  <p class="mb-4">Count: <span data-counter-target="output" class="font-bold">0</span></p>
  
  <div class="space-x-2">
    <button data-action="click->counter#decrement" 
            class="px-4 py-2 bg-red-500 text-white rounded">
      Decrease
    </button>
    
    <button data-action="click->counter#increment" 
            class="px-4 py-2 bg-green-500 text-white rounded">
      Increase
    </button>
    
    <button data-action="click->counter#reset" 
            class="px-4 py-2 bg-gray-500 text-white rounded">
      Reset
    </button>
  </div>
</div>

Understanding Stimulus Values

In our counter example, we're storing the count in a controller property (this.count). While this works, Stimulus provides a better way to handle state: Values. Values let us declare our controller's state and automatically sync it with data attributes in the DOM.

Let's improve our counter using Values:

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

  connect() {
    this.updateOutput()
  }

  increment() {
    this.countValue++
    this.updateOutput()
  }

  decrement() {
    this.countValue--
    this.updateOutput()
  }

  reset() {
    this.countValue = 0
    this.updateOutput()
  }

  updateOutput() {
    this.outputTarget.textContent = this.countValue
  }
}

And our updated view:

<div data-controller="counter" 
     data-counter-count-value="0" 
     class="p-4">
  <h1 class="text-xl mb-4">Simple counter</h1>
  <p class="mb-4">Count: <span data-counter-target="output" class="font-bold">0</span></p>
  
  <div class="space-x-2">
    <button data-action="click->counter#decrement" 
            class="px-4 py-2 bg-red-500 text-white rounded">
      Decrease
    </button>
    
    <button data-action="click->counter#increment" 
            class="px-4 py-2 bg-green-500 text-white rounded">
      Increase
    </button>
    
    <button data-action="click->counter#reset" 
            class="px-4 py-2 bg-gray-500 text-white rounded">
      Reset
    </button>
  </div>
</div>

Benefits of using Values:

  1. State is declared explicitly in the controller
  2. Changes are reflected automatically in the DOM
  3. Type checking is built-in
  4. Values can be observed via change callbacks

Key Takeaways

Through these examples, we've learned the core concepts of Stimulus:

  1. Controllers and Lifecycle Methods - How controllers connect to the DOM
  2. Targets - How to reference DOM elements
  3. Actions - How to handle user interactions
  4. Values - How to manage and sync state

Remember, Stimulus is designed to augment your HTML, not take it over. By understanding these concepts, you can add just the right amount of JavaScript behavior to your Rails applications while keeping them maintainable and scalable.

References

  1. Stimulus Handbook
  2. Stimulus Values

More articles

Auth0 actions

This blog explains how to configure Auth0 actions, specifically focusing on using the Post-Login action to restrict access based on the user's email domain during social logins, such as with Google.

Read more

Understanding AI Orchestrators, Agents, Tools, and Workflow Automation

Discover how AI orchestrators, agents, and tools work together to automate workflows, enabling AI to interact with real-world systems efficiently. Learn how this structured approach enhances productivity and reduces manual effort.

Read more

Ready to Build Something Amazing?

Codemancers can bring your vision to life and help you achieve your goals