What a Stimulus controller connects

Free preview 2 min to read

The gap between plain HTML and a full JS framework

A dropdown that opens and closes, a password field with a strength meter, a dialog that closes when you click outside it: none of these needs a client-side framework rendering views in the browser. Each needs plain, server-rendered HTML with a little behavior attached to specific elements. Stimulus is built for that gap, and this app has 30 controllers doing it:

app/javascript/controllers/
  application.js
  nested_form_controller.js
  tabs_controller.js
  upload_progress_controller.js
  welcome_modal_controller.js
  auth/password_strength_controller.js
  auth/password_visibility_controller.js
  auth/recovery_codes_controller.js
  auth/time_zone_controller.js
  catalog/mermaid_controller.js
  catalog/roadmap_path_controller.js
  execution/editor_prefetch_controller.js
  execution/hints_controller.js
  execution/irb_controller.js
  notes/filter_controller.js
  ui/chart_controller.js
  ui/clipboard_controller.js
  ui/command_palette_controller.js
  ui/confirmable_controller.js
  ui/confirmation_controller.js
  ui/country_picker_controller.js
  ui/dialog_controller.js
  ui/flash_controller.js
  ui/inquiry_tier_controller.js
  ui/locale_switch_controller.js
  ui/mobile_sidebar_controller.js
  ui/navbar_controller.js
  ui/no_uploads_controller.js
  ui/print_controller.js
  ui/request_error_controller.js
  ui/toggle_controller.js

How they all get loaded, with no manual import lines

app/javascript/controllers/index.js, in full:

import { application } from "controllers/application"
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"

const isAdmin = document.body.dataset.admin === "true"

eagerLoadControllersFrom("controllers", application)

if (isAdmin) {
    eagerLoadControllersFrom("admin_controllers", application)
}

eagerLoadControllersFrom("controllers", application) registers every controller file in that directory tree, so nobody writes an import FlashController from "..." line by hand. document.body.dataset.admin reads an attribute you'll meet again: both layouts set data-admin="<%= admin? %>" on <body>, so a second directory of controllers, admin_controllers, loads only in an admin session.

Every controller has the same shape

Every file above starts with the same two lines, as in app/javascript/controllers/ui/flash_controller.js:

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  ...
}

What goes inside that class body is this course's subject. Targets name the elements a controller needs to reach (lesson 3). Actions wire a DOM event to a method on the controller (lesson 4). Values read typed configuration from data attributes (lesson 5). Lesson 2 covers something that comes first: how a file's path becomes the identifier (ui--flash, auth--password-strength) that HTML uses to connect to it.

What this course covers

Each lesson checks its claims against this app's controllers and the views that wire them up. Lesson 2 shows how a controller's file path becomes its data-controller identifier. Lesson 3 covers targets, with a static targets declaration and its generated accessors. Lesson 4 covers actions, both the explicit event->controller#method syntax and the shorthand that infers the event. Lesson 5 covers values, including a typed value rendered from a Ruby constant into HTML. Lesson 6 shows two independent controllers sharing one element tree. Lesson 7 puts it all together in one example, with a small coding challenge.

Lesson 1 of 7 · free

Keep going with lesson 2

Naming: how a file becomes an identifier

Explains the fixed rule that turns a controller's file path into an identifier like auth--password-strength or tabs.

A free account also gives you the quizzes, your progress and notes, and the discussion.

Type to start searching...