To checkout demo you can

git clone
cd stimulus-starter
yarn install
yarn start
gnome-open http://localhost:9000/

To add to existing Rails app

rails webpacker:install:stimulus

Magic keywords


  • data-controller='hello' controller that is defined in hello_controller.js. Each dash corresponds to underscore (or dash) in file name, two dashes corresponds to subfolder users--list-item -> users/list_item_controller.js
  • data-action='click->hello#greet' event name is click, controller hello method greet. If element is <a>, <button>, <input type="submit"> than you do not need to write click->. <input>, <select> and <textarea> has change as default event. <form> has submit default event. If you want to event.preventDefault() (for example click on link) than pass parameter greet(event) {}. To see who invoke the click you can use e.currentTarget
  • data-target='' creates nameTarget property in a controller so we can use it to access element and set value. We need to declare it also inside controller static targets = [ 'name' ]. Beside this.nameTarget you can check if there are more targets like this this.nameTargets or if exists at all this.hasNameTarget (return true or false)
  • to access current element on which whole controller is connected you can use this.element. To access element on which action is triggered you can pass the event to the action hello(greed) { and use event.currentTarget
  • data-hello-index='1' used to pass data to CONTROLLER which you can get on initialize instead of this.element.getAttribute('data-hello-index')) you can use stimulus shorthand'index'). Only for data on controller. But since controller can be initialized on parent of the action element, better is to use event.currentTarget.getAttribute('data-hello-index') Also'index') to check if data existis and'index', 2) to set data so controller. Do not need to store any data in js, just use those setter and getter to store data in DOM.

  • communicating between controllers is best to use event dispatch

    for addListener event listener you can use closures

    let controller = this
    some.addListener('some_event', function() {

    but better is to use events

    window.dispatchWindowEvent = function(event_name, ...detail) {
    const event = new CustomEvent(event_name, { detail: detail })
      autocomplete.addListener('place_changed', function() {
        let place = autocomplete.getPlace();
        window.dispatchWindowEvent('google-maps-place-changed', marker, place)
             [email protected]>google-map#placeChanged
    placeChanged(event) {
      let marker, place
      [marker, place] = event.detail

    for google initMap callback you can dispatch event from window

    window.dispatchMapEvent = function(...args) {
    const event = new CustomEvent('google-maps-callback', { args: args })


// src/controllers/hello_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  // triggered when controller is connected to the document
  // you can add classes to element
  connect() {
  // anytime when controller is disconnected from the dom
  disconnect() {}
  // initialize is once when controller is first instantiated
  initialize() {

  showCurrentSlide() {
    this.slideTargets.forEach((el, i) => {
      el.classList.toggle("slide--current", this.index == i)

  # getter
  get index() {
    return parseInt('index'))

  # setter
  set index(value) {'index', value)

Example of adding Rxjs to stimulus so user on slow connections get latest results, do not load if they clicked on the same link and show loader