Абстрактный контроллер в контексте Stimulus — это базовый класс,
расширяющий Controller, который не предназначен для прямого
подключения к DOM через data-controller. Его задача —
инкапсулировать повторяющееся поведение, общую логику, соглашения по
работе с target’ами, value и lifecycle-хуками, чтобы конкретные
контроллеры могли наследовать эту функциональность.
Абстрактные контроллеры позволяют:
// controllers/base_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
this.log("connected")
}
disconnect() {
this.log("disconnected")
}
log(message) {
console.debug(`[${this.identifier}] ${message}`)
}
}
Этот контроллер:
Controller;log.Он не должен регистрироваться в HTML. Его использование происходит исключительно через наследование.
// controllers/modal_controller.js
import BaseController from "./base_controller"
export default class extends BaseController {
open() {
this.log("modal opened")
}
}
Stimulus корректно объединяет статические свойства при наследовании.
// controllers/form_base_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["input"]
static values = {
disabled: Boolean
}
disable() {
this.disabledValue = true
this.inputTargets.forEach(el => el.disabled = true)
}
}
// controllers/login_form_controller.js
import FormBaseController from "./form_base_controller"
export default class extends FormBaseController {
static targets = ["submit"]
submit() {
this.disable()
}
}
Результат:
input и submit доступны одновременно;disabledValue корректно работает в дочернем
контроллере;targets и
values.Важно учитывать, что переопределение static targets или
static values полностью заменяет родительское определение.
Для расширения требуется повторно указать родительские значения.
Абстрактные контроллеры часто содержат логику в connect,
disconnect, initialize.
export default class extends Controller {
initialize() {
this.startedAt = Date.now()
}
connect() {
this.bindEvents()
}
disconnect() {
this.unbindEvents()
}
bindEvents() {}
unbindEvents() {}
}
Дочерний контроллер реализует только нужные части:
export default class extends BaseController {
bindEvents() {
window.addEventListener("resize", this.onResize)
}
unbindEvents() {
window.removeEventListener("resize", this.onResize)
}
onRes ize = () => {
this.log("resized")
}
}
Такой подход:
Частая практика — создание контроллеров, отвечающих за технические аспекты приложения.
export default class extends Controller {
async request(url, options = {}) {
const response = await fetch(url, {
headers: {
"X-Requested-With": "XMLHttpRequest",
...options.headers
},
...options
})
if (!response.ok) {
this.handleError(response)
throw new Error(response.statusText)
}
return response
}
handleError(response) {
console.error("Request failed", response)
}
}
Дочерние контроллеры используют готовый API:
export default class extends FetchBaseController {
async submit() {
const response = await this.request("/login", {
method: "POST",
body: new FormData(this.element)
})
}
}
Абстрактный контроллер может задавать строгие соглашения по структуре DOM.
export default class extends Controller {
static targets = ["container"]
connect() {
if (!this.hasContainerTarget) {
throw new Error("Container target is required")
}
}
}
Это превращает контроллер в контракт:
В JavaScript отсутствуют настоящие protected-методы, но соглашения именования решают задачу.
export default class extends Controller {
_normalizeValue(value) {
return value.trim().toLowerCase()
}
}
export default class extends BaseController {
process(input) {
const value = this._normalizeValue(input)
}
}
Метод:
Наследование в Stimulus удобно, но не всегда достаточно. Абстрактный контроллер может использоваться как миксин через вспомогательные функции.
export const WithDebounce = (Base) => class extends Base {
debounce(fn, delay = 300) {
clearTimeout(this._debounceTimer)
this._debounceTimer = setTimeout(fn, delay)
}
}
import BaseController from "./base_controller"
import { WithDebounce } from "./with_debounce"
export default class extends WithDebounce(BaseController) {
search() {
this.debounce(() => {
this.log("search executed")
})
}
}
Такой подход:
application.register.static-свойств требует явного
объединения.Типичная организация:
controllers/
base/
base_controller.js
form_controller.js
fetch_controller.js
modal_controller.js
dropdown_controller.js
login_form_controller.js
Базовые контроллеры:
Абстрактные контроллеры превращают Stimulus из набора изолированных DOM-скриптов в структурированный слой поведения. Они позволяют формировать единый стиль кода, переиспользовать логику и удерживать сложность под контролем без отказа от декларативной природы фреймворка.