Custom Elements интеграция

Stimulus — это фреймворк JavaScript, ориентированный на минимальное вмешательство в существующую разметку HTML, что позволяет управлять поведением страниц без необходимости полного SPA-подхода. Одной из важных возможностей современного фронтенда является использование Custom Elements (пользовательских элементов), стандарта Web Components. Интеграция Stimulus с Custom Elements позволяет создавать высокоинкапсулированные, повторно используемые компоненты, сохраняя декларативный стиль управления поведением через контроллеры.


Основные принципы Custom Elements

Custom Elements позволяют определить собственные HTML-теги с уникальной логикой. Для этого используется стандартная API:

class MyElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = `<p>Привет из Custom Element!</p>`;
  }
}

customElements.define('my-element', MyElement);

Ключевые моменты:

  • connectedCallback() вызывается при добавлении элемента в DOM.
  • attachShadow({ mode: 'open' }) создаёт Shadow DOM для инкапсуляции стилей и структуры.
  • customElements.define() регистрирует тег, который можно использовать в HTML.

Подключение Stimulus к Custom Elements

Stimulus не требует специальных настроек для работы с Custom Elements. Контроллер можно привязать через атрибут data-controller прямо на пользовательском элементе:

<my-element data-controller="example"></my-element>

Контроллер Stimulus будет работать так же, как и на обычных HTML-элементах, обрабатывая события и взаимодействуя с DOM внутри элемента. Однако следует учитывать два нюанса:

  1. Shadow DOM и доступ к элементам Stimulus по умолчанию работает с обычным DOM. Если Custom Element использует Shadow DOM, контроллеру необходимо получать доступ к shadowRoot:

    import { Controller } from "@hotwired/stimulus";
    
    export default class extends Controller {
      connect() {
        const shadow = this.element.shadowRoot;
        const button = shadow.querySelector('button');
        button.addEventListener('click', () => {
          console.log('Кнопка нажата внутри Custom Element');
        });
      }
    }
  2. Инициализация контроллеров после рендеринга Custom Element Если элемент создаётся динамически через JavaScript, контроллеры Stimulus могут не инициализироваться автоматически. В таких случаях используется Application.start() или application.load() после добавления элемента в DOM.


Использование Targets и Actions с Custom Elements

Targets и Actions работают стандартно, но с учётом Shadow DOM потребуется обращаться к внутренним элементам через shadowRoot. Пример:

// HTML
<my-element data-controller="counter">
  <button data-counter-target="increment">+</button>
  <span data-counter-target="value">0</span>
</my-element>

// Controller
import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static targets = ["value", "increment"];
  connect() {
    this.count = 0;
    this.incrementTarget.addEventListener('click', () => this.increment());
  }

  increment() {
    this.count++;
    this.valueTarget.textContent = this.count;
  }
}

Для Shadow DOM элементы data-*-target находятся внутри shadowRoot, поэтому необходимо модифицировать обращение:

this.valueTarget = this.element.shadowRoot.querySelector('[data-counter-target="value"]');
this.incrementTarget = this.element.shadowRoot.querySelector('[data-counter-target="increment"]');

Передача данных через атрибуты

Stimulus поддерживает value properties, что позволяет передавать параметры в контроллер через HTML-атрибуты. В комбинации с Custom Elements это особенно удобно для конфигурации компонента:

<my-element data-controller="greeting" data-greeting-name-value="Мир"></my-element>
import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static values = { name: String };
  connect() {
    console.log(`Привет, ${this.nameValue}!`);
  }
}

Custom Element можно сделать полностью настраиваемым, передавая различные значения через атрибуты, которые будут автоматически использоваться контроллером Stimulus.


Обработка событий внутри Custom Elements

Custom Elements могут испускать свои события (CustomEvent). Stimulus легко подписывается на них с помощью data-action:

// Внутри Custom Element
this.dispatchEvent(new CustomEvent('custom-click', { bubbles: true }));

// В HTML
<my-element data-controller="listener" data-action="custom-click->listener#handleClick"></my-element>

// Controller
import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  handleClick(event) {
    console.log('Событие custom-click получено', event);
  }
}

Особенности:

  • Атрибут bubbles: true необходим, чтобы событие достигло контроллера Stimulus.
  • Stimulus может реагировать на любые события, даже кастомные, с прозрачной привязкой через data-action.

Динамическая интеграция и lazy-loading

Stimulus поддерживает динамическую инициализацию контроллеров, что важно для компонентов, создаваемых на лету. Например, при загрузке Custom Element через fetch:

fetch('/components/widget.html')
  .then(r => r.text())
  .then(html => {
    const container = document.createElement('div');
    container.innerHTML = html;
    document.body.appendChild(container);
    application.load(container); // Инициализация Stimulus контроллеров
  });

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


Практические рекомендации

  • Использовать Shadow DOM только при необходимости, так как он требует дополнительных шагов для доступа к элементам через Stimulus.
  • Передавать конфигурацию через data-атрибуты, чтобы избежать жёсткой привязки к коду.
  • Использовать CustomEvent для взаимодействия между компонентами, сохраняя декларативность через data-action.
  • Инициализация динамически созданных элементов должна выполняться через application.load().

Stimulus и Custom Elements образуют мощное сочетание, позволяя создавать инкапсулированные, реактивные компоненты, при этом сохраняя минимализм и простоту архитектуры страницы.