Таргеты в сложных DOM-структурах

Stimulus использует концепцию таргетов для прямого связывания элементов DOM с контроллером. Таргеты представляют собой заранее определённые HTML-элементы, доступ к которым осуществляется через свойства контроллера. Каждый таргет объявляется с помощью атрибута data-<controller>-target, где <controller> соответствует имени контроллера, а значение — имени таргета.

Пример базового объявления:

<div data-controller="example">
  <input data-example-target="input">
  <button data-action="click->example#submit">Отправить</button>
</div>

В контроллере example_controller.js доступ к таргету осуществляется через свойство this.<name>Target:

import { Controller } from "@hotwired/stimulus"

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

  submit() {
    console.log(this.inputTarget.value)
  }
}

Использование нескольких таргетов одного типа

Если на странице присутствует несколько элементов одного типа, можно объявить таргет как массив, используя this.<name>Targets. Stimulus автоматически собирает все соответствующие элементы в массив.

<div data-controller="list">
  <li data-list-target="item">Элемент 1</li>
  <li data-list-target="item">Элемент 2</li>
  <li data-list-target="item">Элемент 3</li>
</div>

Контроллер:

import { Controller } from "@hotwired/stimulus"

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

  highlightAll() {
    this.itemTargets.forEach(el => el.classList.add("highlight"))
  }
}

Метод highlightAll применяет класс ко всем таргетам сразу.

Комплексные DOM-структуры и вложенные таргеты

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

<div data-controller="gallery">
  <div class="album" data-gallery-target="album">
    <img src="photo1.jpg" data-gallery-target="photo">
    <img src="photo2.jpg" data-gallery-target="photo">
  </div>
  <div class="album" data-gallery-target="album">
    <img src="photo3.jpg" data-gallery-target="photo">
    <img src="photo4.jpg" data-gallery-target="photo">
  </div>
</div>

Контроллер:

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["album", "photo"]

  logStructure() {
    console.log(this.albumTargets.length) // 2
    console.log(this.photoTargets.length) // 4
  }

  highlightAlbum(index) {
    this.albumTargets[index].classList.add("active")
  }

  highlightPhotosInAlbum(albumIndex) {
    const photos = this.albumTargets[albumIndex].querySelectorAll("[data-gallery-target='photo']")
    photos.forEach(photo => photo.classList.add("highlight"))
  }
}

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

Динамическое добавление таргетов

Stimulus автоматически не отслеживает новые элементы, добавленные после инициализации контроллера. Для работы с динамически созданными элементами используется обновление ссылок на таргеты через метод this.initialize() или отдельный кастомный метод.

addPhoto(newPhotoElement) {
  this.element.querySelector(".album").appendChild(newPhotoElement)
  this.photoTargets.push(newPhotoElement)
}

Альтернативно, можно использовать MutationObserver для автоматического отслеживания изменений DOM:

connect() {
  this.observer = new MutationObserver(() => this.refreshTargets())
  this.observer.observe(this.element, { childList: true, subtree: true })
}

refreshTargets() {
  this.photoTargets = Array.from(this.element.querySelectorAll("[data-gallery-target='photo']"))
}

Группировка таргетов по логике

Для упрощения работы со сложными интерфейсами часто применяют логические группы таргетов через множественные имена:

<div data-controller="form">
  <input data-form-target="field username">
  <input data-form-target="field email">
</div>

В контроллере можно обращаться по каждому имени или использовать общий массив для всех полей:

static targets = ["field"]

validateAll() {
  this.fieldTargets.forEach(input => {
    if (!input.value) input.classList.add("error")
  })
}

Рекомендации по работе с таргетами

  • Ясные имена таргетов: использовать имена, отражающие роль элемента, а не его HTML-тип.
  • Минимизация глубоких цепочек: избегать сложного вложенного поиска через querySelector там, где можно объявить таргет на нужном уровне.
  • Использование массивов: при работе с множественными элементами всегда использовать Targets, чтобы не создавать вручную массивы через DOM-методы.
  • Динамические элементы: заранее продумывать обновление таргетов для элементов, которые добавляются после инициализации контроллера.

Эти подходы позволяют управлять сложными DOM-структурами эффективно и предсказуемо, сохраняя контроль над состоянием интерфейса и снижая вероятность ошибок при взаимодействии с множественными элементами на странице.