Абстрактные контроллеры

Абстрактный контроллер в контексте 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;
  • реализует общие lifecycle-хуки;
  • добавляет вспомогательный метод log.

Он не должен регистрироваться в HTML. Его использование происходит исключительно через наследование.

// controllers/modal_controller.js
import BaseController from "./base_controller"

export default class extends BaseController {
  open() {
    this.log("modal opened")
  }
}

Наследование targets и values

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 корректно работает в дочернем контроллере;
  • Stimulus автоматически объединяет targets и values.

Важно учитывать, что переопределение static targets или static values полностью заменяет родительское определение. Для расширения требуется повторно указать родительские значения.

Переиспользование lifecycle-хуков

Абстрактные контроллеры часто содержат логику в 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")
  }
}

Такой подход:

  • устраняет необходимость помнить про очистку ресурсов;
  • формирует единый шаблон жизненного цикла.

Абстрактные контроллеры как слой инфраструктуры

Частая практика — создание контроллеров, отвечающих за технические аспекты приложения.

Контроллер с автоматической обработкой fetch

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")
    }
  }
}

Это превращает контроллер в контракт:

  • наличие обязательных target’ов;
  • проверка корректности конфигурации;
  • раннее обнаружение ошибок.

Использование protected-подобных методов

В 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")
    })
  }
}

Такой подход:

  • уменьшает глубину иерархий;
  • позволяет комбинировать поведение;
  • сохраняет совместимость со Stimulus.

Ограничения и подводные камни

  • Абстрактный контроллер не должен регистрироваться в application.register.
  • Переопределение static-свойств требует явного объединения.
  • Глубокие иерархии усложняют понимание поведения.
  • Логика, зависящая от DOM, должна быть максимально изолирована.

Структура проекта с абстрактными контроллерами

Типичная организация:

controllers/
  base/
    base_controller.js
    form_controller.js
    fetch_controller.js
  modal_controller.js
  dropdown_controller.js
  login_form_controller.js

Базовые контроллеры:

  • располагаются в отдельной директории;
  • не имеют прямых привязок к HTML;
  • служат строительными блоками для прикладных компонентов.

Роль абстрактных контроллеров в архитектуре Stimulus

Абстрактные контроллеры превращают Stimulus из набора изолированных DOM-скриптов в структурированный слой поведения. Они позволяют формировать единый стиль кода, переиспользовать логику и удерживать сложность под контролем без отказа от декларативной природы фреймворка.