К содержанию

Навигация

Жизненный цикл

Глава перечисляет этапы жизни компонента, методы, которые можно переопределить, и правила освобождения ресурсов. Это нужно, чтобы компонент корректно переживал удаление и повторное добавление на страницу.

Этапы

  1. Тег появляется на странице; Loader подключает файлы компонента, а с первым Smart-элементом страницы — и общую среду Smart-компонентов (см. Подключение к странице).
  2. Тег регистрируется (тег библиотечного компонента — когда пришла общая среда); браузер создаёт экземпляр.
  3. Подключение: базовый класс забирает содержимое слотов из детей тега, затем вызывает onConnected() и отправляет sf-connected.
  4. Отрисовка: beforeRender() и sf-before-render, затем шаблон (внешний шаблон сначала загружается вместе со своим index.css), затем ожидание стилей, затем afterRender() и sf-after-render.
  5. Обновление при смене атрибутов, содержимого слотов или языка страницы: отрисовка заново, затем afterUpdate(), sf-updated, sf-props-change. Обновление откладывается до ближайшей микрозадачи и объединяет несколько изменений подряд: сразу после setAttribute() разметка ещё прежняя. Новую разметку читайте после await element.whenRendered(null, { next: true }).
  6. Отключение: onDisconnected(), освобождение внешнего шаблона, sf-disconnected.

Общие события этапов — sf-connected, sf-disconnected, sf-before-render, sf-after-render, sf-updated и sf-props-change. Собственные события конкретного компонента — на его странице в справочнике. События всплывают, поэтому в составном компоненте проверяйте event.target: события вложенных элементов тоже доходят до родителя.

Методы для переопределения

Метод Когда вызывается
onConnected() После каждого подключения к странице, в том числе после повторной вставки удалённого элемента
beforeRender(changed) Перед каждой отрисовкой
afterRender(changed) После отрисовки и загрузки стилей
afterUpdate(changed, mode) После обновления
onDisconnected() При удалении со страницы

Публичные методы для кода проекта:

Метод Что делает
refresh(reason) Перерисовывает компонент, например после изменения данных, которые он читает не из атрибутов
whenRendered(callback?, { next? }) Обещание, которое выполнится после отрисовки; если отрисовка уже была, выполняется сразу. Необязательная функция callback вызывается в тот же момент и получает сведения об отрисовке (component, tagName, template, changedAttributes); обещание выполняется с теми же сведениями. { next: true } ждёт следующую отрисовку, даже если первая уже была; без функции первым аргументом передают null: whenRendered(null, { next: true }) (см. Ожидание готовности)

Правила:

  • connectedCallback() и disconnectedCallback() целиком не переопределяют: в них базовый класс забирает слоты, подписывается на язык страницы и освобождает внешний шаблон.

  • Вызов super в этих методах не обязателен. Базовые onConnected(), beforeRender() и afterUpdate() пусты, afterRender(changed) ничего не делает. Базовый onDisconnected() освобождает внешний шаблон, и базовый класс делает это и сам после вашего onDisconnected(), поэтому super.onDisconnected() — безопасная привычка, а не условие.

  • Поля класса и собственный конструктор допустимы: конструктор первым делом вызывает super(), поле вроде handleRelease = () => { … } хранит функцию экземпляра для обработчиков. Поле не называют именем объявленного свойства: пока атрибута нет, getProp() возвращает значение поля, а не значение по умолчанию.

  • Базовый класс уже определяет свойство value (чтение и запись) и state с методом getState(). Не перекрывайте value одним геттером: присваивание element.value = … станет ошибкой. Переопределяйте его парой get/set или назовите своё свойство иначе. Вход value в static props допустим: базовый value читает и пишет атрибут value.

  • Переопределяют только методы из таблицы выше, template() и templateContext(). Остальные имена базового класса подкласс не занимает ни полем, ни методом: value (кроме пары get/set), state, style, isRendered, getState, setState, patchState, set, refresh, setHidden, isHidden, getProp, getPropValue, getPropsContext, createPropsTemplateContext, createTemplateContext, setAttributes, removeAttributes, getAttributeValue, setAttributeValue, setSlot, clearSlot, hasSlotContent, getSlotContent, whenRendered, whenChildrenDefined, requestComponentUpdate, emitComponentEvent, createRef, addRootClass, removeRootClass, toggleRootClass, setRootClass, setRootStyle, getRootClass, getRootStyle, onPropChange, onUpdate, onAfterRender. Имена свойств вроде name, size, disabled, label, min, max и step свободны.

  • Базовый класс не связан с формой: элемент не участвует в отправке и сбросе <form> сам. Чтобы значение ушло с формой, шаблон рисует обычный <input name="…"> (скрытый или видимый) и пишет в него значение: элемент рисует в своём DOM, без теневого корня, поэтому такое поле принадлежит форме. Сброс формы восстанавливает поле, а состояние компонента — обработчик reset формы:

    JavaScript
    template() {
      return html`…<input type="hidden" name=${this.getProp('name')} .value=${String(this.count)}>`;
    }
    

Освобождение ресурсов

Компонент завершает свою работу при отключении: снимает обработчики на window и document, останавливает таймеры, отменяет незавершённые запросы. Пример показывает снятие обработчика через AbortController:

JavaScript
onConnected() {
  this.abort = new AbortController();
  window.addEventListener('resize', this.handleResize, { signal: this.abort.signal });
}
onDisconnected() {
  this.abort?.abort();
  super.onDisconnected();
}

handleResize — сохранённая функция экземпляра, а не новая стрелочная функция при каждом подключении. При повторном подключении создаётся новый AbortController: отменённый контроллер повторно не используют. Ответ на запрос, пришедший после удаления или после нового запроса, не применяется.

Проверка: добавить элемент, выполнить действие, удалить, добавить снова. Одно действие — одно событие.

Пример готового элемента

Счётчик проходит все этапы: регистрация тега, подключение, отрисовка и обновление после нажатия «Добавить».

HTML
<project-counter count="1"></project-counter>
<p id="counter-log" role="status">Нажмите кнопку: ниже появятся данные события.</p>
JavaScript
async function start() {
  if (typeof window.SF?.loadSmartBase !== 'function') {
    await new Promise((resolve, reject) => {
      const ready = () => { clearTimeout(timeout); resolve(); };
      const timeout = setTimeout(() => { window.removeEventListener('sf-loader-init', ready); reject(new Error('Loader не запустился')); }, 15000);
      window.addEventListener('sf-loader-init', ready, { once: true });
    });
  }
  await SF.loadSmartBase();
  const { SfBaseElement, html } = SF.smart;
  // The component's own words: English in `en`, Russian in `ru`.
  const TEXT = {
    en: { count: 'Count', add: 'Add' },
    ru: { count: 'Количество', add: 'Добавить' },
  };
  class ProjectCounter extends SfBaseElement {
    static get props() {
      // A label prop defaults to empty: the English word lives in the dictionary.
      return { count: { type: Number, default: 0 }, addLabel: { default: '' } };
    }
    templateContext() {
      // Keeps component, rootClass, rootStyle and every declared prop.
      return this.createPropsTemplateContext({
        countText: SF.pageText(TEXT, 'count', this),
        addText: this.getProp('addLabel') || SF.pageText(TEXT, 'add', this),
      });
    }
    increment() {
      const count = this.getProp('count') + 1;
      this.setAttribute('count', String(count));
      this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true, detail: { value: count } }));
    }
    template() {
      const { count, countText, addText, rootClass } = this.templateContext();
      return html`<div class="flex gap-2 items-cross-center ${rootClass}">
        <output aria-live="polite">${countText}: ${count}</output>
        <button type="button"
                class="sf-button sf-button--outline sf-button--on-surface sf-button--size-1/2"
                @click=${() => this.increment()}>
          <span class="sf-button-text-container">${addText}</span>
        </button>
      </div>`;
    }
  }
  ProjectCounter.define('project-counter');
  document.querySelector('project-counter').addEventListener('change', event => {
    document.querySelector('#counter-log').textContent = JSON.stringify({ value: event.detail.value });
  });
}
start().catch(error => { document.querySelector('#counter-log').textContent = error.message; });

Смотрите также

Поиск по документации

Введите минимум 2 символа

    Настройки

    Тема
    Размер шрифта
    Ширина страницы