Stimulus строится вокруг контроллеров — небольших классов, отвечающих за поведение конкретного участка DOM. Контроллер не владеет разметкой и не управляет глобальным состоянием, а реагирует на события и изменения данных, уже присутствующих в HTML.
Контроллер связывается с DOM-элементом через атрибут
data-controller:
<div data-controller="menu">
<button data-action="click->menu#toggle">Toggle</button>
<nav data-menu-target="panel"></nav>
</div>
// menu_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
toggle() {
this.panelTarget.classList.toggle("hidden")
}
}
Контроллер всегда ограничен своим корневым элементом
(this.element). Это обеспечивает локальность поведения и
предсказуемость взаимодействий.
Контроллер проходит строго определённые этапы существования:
initialize() {
this.state = "closed"
}
connect() {
this.observe()
}
disconnect() {
this.cleanup()
}
Жизненный цикл особенно важен в SPA-навигации, где элементы динамически добавляются и удаляются без полной перезагрузки страницы.
Targets описывают значимые элементы внутри контроллера. Они объявляются статически:
static targets = ["panel", "item"]
Stimulus автоматически создаёт свойства:
this.panelTargetthis.itemTargetsthis.hasPanelTargetЭто позволяет избежать ручного поиска элементов через
querySelector и поддерживает согласованность структуры.
Values позволяют хранить состояние контроллера, синхронизированное с HTML-атрибутами:
static values = {
open: Boolean,
index: Number
}
<div data-controller="tabs" data-tabs-open-value="true"></div>
Изменение значения автоматически отражается в DOM:
this.openValue = false
Values полезны при навигации между состояниями SPA, так как они сериализуются и легко восстанавливаются.
Actions связывают DOM-события с методами контроллера:
<a href="/posts" data-action="click->navigation#visit"></a>
visit(event) {
event.preventDefault()
this.navigate(event.currentTarget.href)
}
Actions могут реагировать на любые события, включая кастомные:
<div data-action="modal:open->modal#show"></div>
Это позволяет выстраивать навигацию между компонентами без прямых зависимостей.
Stimulus не включает встроенный роутер. Навигация реализуется через комбинацию:
Пример базового навигационного контроллера:
export default class extends Controller {
navigate(url) {
fetch(url, { headers: { "Accept": "text/html" } })
.then(r => r.text())
.then(html => {
this.replaceContent(html)
history.pushState({}, "", url)
})
}
replaceContent(html) {
this.element.innerHTML = html
}
}
Контроллер отвечает только за переход и обновление области, не зная о структуре страницы целиком.
Для корректной SPA-навигации требуется обработка
popstate:
connect() {
window.addEventListener("popstate", this.restore)
}
disconnect() {
window.removeEventListener("popstate", this.restore)
}
restore = () => {
this.navigate(location.pathname)
}
Таким образом поддерживаются переходы вперёд и назад без перезагрузки.
Один элемент может иметь несколько контроллеров:
<div data-controller="navigation loader analytics"></div>
Каждый контроллер решает свою задачу:
navigation — загрузка контентаloader — индикаторы состоянияanalytics — отслеживание переходовСвязь между контроллерами осуществляется через события, а не прямые вызовы методов.
При смене представлений важно:
Stimulus автоматически вызывает disconnect, что
позволяет освобождать ресурсы и отписываться от подписок.
В реальных SPA Stimulus часто используется вместе с Turbo:
Контроллеры остаются неизменными при переходах, если элементы сохраняются Turbo. Это снижает количество повторных инициализаций.
Навигационные контроллеры часто управляют состояниями загрузки:
static classes = ["loading"]
navigate(url) {
this.element.classList.add(this.loadingClass)
fetch(url)
.then(...)
.finally(() => {
this.element.classList.remove(this.loadingClass)
})
}
Такое поведение локализовано и не требует глобальных менеджеров состояния.
При SPA-навигации важно обновлять:
document.titleКонтроллер навигации может централизованно управлять этими аспектами после каждого перехода.
Контроллеры легко тестируются из-за отсутствия скрытых зависимостей:
Навигация тестируется через подмену fetch и
history.
Stimulus поощряет:
SPA-навигация, построенная на контроллерах, масштабируется за счёт композиции, а не усложнения архитектуры.
this.elementdisconnect при навигацииКорректная работа со SPA в Stimulus достигается строгим соблюдением локальности и событийной модели.