Выходы: межконтроллерное взаимодействие

В Stimulus выходы (outputs) — это механизм для передачи событий от одного контроллера к другому, позволяющий организовать взаимодействие между контроллерами без жёсткой зависимости. Выходы работают как объявленные события, которые контроллер может испускать, а другой контроллер — прослушивать и реагировать на них.

Выходы повышают модульность и переиспользуемость кода, поскольку контроллеры могут общаться через декларативные события, не зная друг о друге напрямую.


Объявление выходов

Выход объявляется в статическом свойстве static outputs контроллера. Формат:

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static outputs = ["selected"]

  connect() {
    console.log("Контроллер подключен")
  }
}

Здесь контроллер объявляет выход selected. Это значит, что он может испускать событие selected, которое может быть перехвачено другим контроллером.


Испускание событий

Чтобы испустить событие, используется метод this.dispatch(outputName, options):

this.dispatch("selected", { detail: { id: 42 } })

Пояснения:

  • "selected" — имя выхода, должно соответствовать объявленному в static outputs.
  • detail — объект с любыми данными, которые необходимо передать.
  • По умолчанию событие всплывает и является кастомным событием DOM.

Обработка выходов в другом контроллере

Контроллер, который должен реагировать на выход, может использовать декларативное связывание через data-атрибуты. Пример:

Контроллер item может иметь метод select, который вызывает this.dispatch("selected"). Контроллер list слушает этот выход через атрибут data-list-selected-output="item" и автоматически вызывает метод item с данными.

Прямое связывание через метод в контроллере:

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["outputContainer"]

  connect() {
    this.element.addEventListener("item:selected", this.handleItemSelected.bind(this))
  }

  handleItemSelected(event) {
    console.log("Получены данные:", event.detail)
  }
}

Ключевой момент: имя события формируется как :, что позволяет точно идентифицировать источник события.


Передача данных через выходы

Выходы удобны для передачи структурированных данных между контроллерами:

this.dispatch("selected", { detail: { id: 123, name: "Тестовый элемент" } })

В контроллере-получателе можно обработать эти данные напрямую:

handleItemSelected(event) {
  const { id, name } = event.detail
  console.log(`Выбран элемент ${id}: ${name}`)
}

Это позволяет строить сложные интерфейсы с динамическим реагированием на действия пользователя без жёсткой связи между компонентами.


Комплексные сценарии межконтроллерного взаимодействия

  1. Событие из дочернего контроллера для родителя Например, контроллер карточки товара отправляет событие selected, родительский контроллер списка получает его и обновляет панель с информацией.

  2. Событие между соседними контроллерами Выход всплывает по DOM, и любой контроллер выше в иерархии может перехватить его, что устраняет необходимость в прямых ссылках на соседей.

  3. Обработка нескольких событий Можно объявлять несколько выходов в одном контроллере:

    static outputs = ["selected", "deleted", "updated"]

    Каждый выход можно слушать отдельно и выполнять разные действия.


Рекомендации по использованию

  • Выходы должны быть логически значимыми событиями интерфейса (например, выбор, удаление, обновление), а не внутренними триггерами.
  • Названия выходов лучше писать в единичном числе и в понятной форме, чтобы было очевидно, что произошло.
  • Для сложных интерфейсов не связывать контроллеры напрямую через методы, использовать выходы и события — это делает код более поддерживаемым.

Пример практического применения

// product_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static outputs = ["addToCart"]

  addToCart() {
    const product = { id: 1, name: "Кофе", price: 150 }
    this.dispatch("addToCart", { detail: product })
  }
}

// cart_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    this.element.addEventListener("product:addToCart", this.handleAddToCart.bind(this))
  }

  handleAddToCart(event) {
    console.log("Добавлен продукт:", event.detail)
  }
}

В этом примере контроллер продукта не знает о контроллере корзины, а корзина реагирует на событие addToCart, обеспечивая чистое межконтроллерное взаимодействие.