Императивный API

Haunted — это небольшая библиотека для создания веб-компонентов с использованием функционального подхода, основанного на хуках, похожих на React. Помимо декларативного стиля с хуками useState, useEffect и другими, Haunted предоставляет императивный API, который позволяет управлять жизненным циклом компонентов и их состоянием более детально, напрямую взаимодействуя с DOM и объектами компонентов.


Создание компонентов через component()

Функция component() — основной инструмент для императивного создания Haunted-компонента. Она принимает функцию рендера, возвращающую шаблон (обычно с использованием html из lit-html), и опциональный массив хуков для внедрения функциональности:

import { component, html } from 'haunted';

function MyCounter({ count = 0 }) {
  return html`<p>Счётчик: ${count}</p>`;
}

customElements.define('my-counter', component(MyCounter));
  • component(renderFunction) возвращает класс веб-компонента, который можно зарегистрировать через customElements.define.
  • Аргумент функции рендера может быть объектом props — набором свойств компонента.
  • Можно задать значения по умолчанию для пропсов через деструктуризацию.

Управление состоянием через useState

Хотя это часть хуков, использование useState через императивный API позволяет управлять состоянием без сложной инфраструктуры:

import { component, useState, html } from 'haunted';

function Counter() {
  const [count, setCount] = useState(0);

  return html`
    <div>
      <p>Счёт: ${count}</p>
      <button @click=${() => setCount(count + 1)}>Увеличить</button>
    </div>
  `;
}

customElements.define('counter-element', component(Counter));

Особенности императивного управления:

  • setCount обновляет состояние и вызывает повторный рендер компонента.
  • Можно комбинировать несколько состояний, создавая локальные “переменные состояния” для различных аспектов UI.
  • Обновление состояния не блокирует основной поток выполнения, оно асинхронно.

Жизненный цикл компонентов

Imперативный API позволяет работать с методами жизненного цикла веб-компонентов напрямую:

  • connectedCallback() — вызывается при добавлении элемента в DOM.
  • disconnectedCallback() — при удалении из DOM.
  • adoptedCallback() — при перемещении элемента между документами.
  • attributeChangedCallback(attrName, oldVal, newVal) — при изменении атрибутов.

Haunted позволяет внедрять хуки жизненного цикла:

import { component, useEffect, html } from 'haunted';

function Logger() {
  useEffect(() => {
    console.log('Компонент добавлен в DOM');
    return () => console.log('Компонент удалён из DOM');
  }, []);

  return html`<p>Проверка жизненного цикла</p>`;
}

customElements.define('logger-element', component(Logger));
  • useEffect(callback, deps) используется для наблюдения за изменениями зависимостей.
  • Возвращаемая функция вызывается при размонтировании компонента.
  • Императивный API совместим с обычными колбэками веб-компонентов.

Императивное обновление пропсов

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

const counter = document.createElement('counter-element');
counter.count = 10;
document.body.appendChild(counter);

// Позднее обновление
counter.count = 20;
  • Изменение свойства автоматически инициирует повторный рендер.
  • Для реактивности можно использовать attributeChangedCallback совместно с observedAttributes.

Встраивание стороннего DOM и манипуляции

Императивный подход в Haunted не ограничивается шаблонами:

import { component, html } from 'haunted';

function Wrapper() {
  return html`
    <div id="container"></div>
  `;
}

customElements.define('wrapper-element', component(Wrapper));

const wrapper = document.querySelector('wrapper-element');
const container = wrapper.shadowRoot.getElementById('container');
container.innerHTML = '<p>Императивный контент</p>';
  • Shadow DOM создаётся автоматически при использовании component().
  • Прямое управление DOM возможно через shadowRoot.
  • Позволяет интегрировать библиотеки, не совместимые с шаблонами Haunted.

Расширение компонентов и миксины

Imперативный API поддерживает наследование и миксины, что полезно для создания переиспользуемых компонентов:

class BaseElement extends HTMLElement {
  log(msg) {
    console.log(`[Base] ${msg}`);
  }
}

function enhancedComponent(fn) {
  return class extends BaseElement {
    connectedCallback() {
      super.connectedCallback?.();
      this.attachShadow({ mode: 'open' });
      this.shadowRoot.innerHTML = fn();
    }
  };
}

customElements.define('enhanced-element', enhancedComponent(() => '<p>Расширенный компонент</p>'));
  • Можно комбинировать стандартные веб-компоненты с Haunted-компонентами.
  • Поддерживается вызов методов базового класса.
  • Shadow DOM создаётся вручную, если необходимо полное управление.

Прямое управление рендерингом

Haunted предоставляет функцию render() для императивного обновления DOM без хука состояния:

import { render, html } from 'haunted';

const root = document.getElementById('root');
let counter = 0;

function update() {
  render(html`<p>Счётчик: ${counter}</p>`, root);
}

setInterval(() => {
  counter++;
  update();
}, 1000);
  • render(templateResult, container) повторно рендерит шаблон.
  • Полезно для микро-компонентов, которые не используют component().
  • Можно использовать совместно с императивной логикой состояния и событий.

События и императивный API

Haunted позволяет работать с событиями в стиле нативного DOM, либо через шаблон:

import { component, html } from 'haunted';

function Clicker() {
  const handleClick = () => alert('Клик!');
  return html`<button @click=${handleClick}>Нажми меня</button>`;
}

customElements.define('clicker-element', component(Clicker));
  • События можно навешивать напрямую через свойства элемента (el.oncl ick = ...) или через шаблон (@click).
  • Императивное управление событиями позволяет динамически добавлять и удалять обработчики.

Императивный API в Haunted сочетает гибкость веб-компонентов и удобство хуков, предоставляя полный контроль над состоянием, рендером и жизненным циклом компонентов, позволяя интегрировать функциональность как декларативно, так и напрямую через DOM.