Stimulus — это легковесный JavaScript-фреймворк, ориентированный на управление поведением элементов DOM с помощью контроллеров. Одним из ключевых аспектов организации сложного интерфейса является композиция контроллеров. Композиция позволяет объединять несколько контроллеров, обеспечивая модульность, повторное использование кода и чистоту архитектуры.
1. Один контроллер — одна ответственность Каждый контроллер должен решать строго определённую задачу: обработка событий, управление состоянием или взаимодействие с API. Сложная логика распределяется между несколькими контроллерами, которые взаимодействуют через DOM или события.
2. Вложенные контроллеры Stimulus поддерживает вложенные контроллеры через DOM-структуру. Контроллер может находиться внутри элемента, управляемого другим контроллером. Взаимодействие реализуется через события или напрямую через публичные методы.
Пример:
<div data-controller="menu">
<ul>
<li data-controller="menu-item">Пункт 1</li>
<li data-controller="menu-item">Пункт 2</li>
</ul>
</div>
Здесь menu управляет общим состоянием, а
menu-item отвечает за отдельный пункт. Контроллеры
взаимодействуют независимо, сохраняя изоляцию логики.
Stimulus активно использует CustomEvent для связи между контроллерами. Вложенный контроллер может оповестить родительский о произошедшем событии:
// menu_item_controller.js
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
SELECT() {
this.element.dispatchEvent(new CustomEvent("menu:item:selected", {
bubbles: true,
detail: { text: this.element.textContent }
}));
}
}
// menu_controller.js
import { Controller } FROM "@hotwired/stimulus";
export default class extends Controller {
connect() {
this.element.addEventListener("menu:item:selected", this.handleSelection.bind(this));
}
handleSelection(event) {
console.log("Выбран пункт:", event.detail.text);
}
}
Ключевой момент: bubbles: true
позволяет событию подняться по DOM, связывая вложенные контроллеры с
родительскими без жесткой зависимости.
Контроллеры могут вызывать методы друг друга напрямую, если известна ссылка на элемент с другим контроллером:
// alert_controller.js
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
show(message) {
this.element.textContent = message;
this.element.style.display = "block";
}
}
// form_controller.js
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static targets = ["alert"]
submit(event) {
event.preventDefault();
this.alertController().show("Форма отправлена!");
}
alertController() {
return this.alertTarget.controller;
}
}
Важно: Такой подход требует явного указания целей
(data-target) и сохраняет строгую типизацию
взаимодействия.
Для уменьшения дублирования кода применяются миксины — функции, расширяющие поведение контроллеров:
export function Toggleable(Base) {
return class extends Base {
toggle() {
this.element.classList.toggle("active");
}
};
}
Использование:
import { Controller } from "@hotwired/stimulus";
import { Toggleable } from "./mixins/toggleable";
export default class extends Toggleable(Controller) {
connect() {
console.log("Контроллер подключён");
}
}
Миксины позволяют сохранять принцип единственной ответственности и использовать общий функционал без наследования.
Композиция упрощает управление состоянием сложного интерфейса:
Пример передачи состояния:
// tab_controller.js
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static targets = ["panel"]
select(index) {
this.panelTargets.forEach((panel, i) => {
panel.hidden = i !== index;
});
this.element.dispatchEvent(new CustomEvent("tab:changed", { bubbles: true, detail: { index } }));
}
}
Композируемые контроллеры обеспечивают модульность интерфейса, масштабируемость и удобство поддержки даже сложных веб-приложений.