Spec-Zone.ru › Lit 2

Пользовательские директивы

Директивы — это функции, которые расширяют Lit, настраивая способ отображения выражения шаблона. Директивы полезны и обладают широкими возможностями, поскольку они могут хранить состояние, получать доступ к DOM, получать уведомления об отключении и повторном подключении шаблонов, а также независимо обновлять выражения вне вызова render.

Использовать директиву в шаблоне так же просто, как вызвать функцию в выражении шаблона:

html`<div>
       ${fancyDirective('some text')}
     </div>`

Lit поставляется с несколькими встроенными директивами, такими как repeat() и cache(). Пользователи также могут создавать собственные директивы.

Существует два вида директив:

  • Простые функции
  • Директивы на основе классов

Простая функция возвращает значение для отображения. Она может принимать любое количество аргументов или не принимать аргументов вовсе.

export noVowels = (str) => str.replaceAll(/[aeiou]/ig,'x');

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

  • Получать прямой доступ к отрисованному DOM (например, добавлять, удалять или менять порядок отрисованных узлов DOM).
  • Сохранять состояние между отрисовками.
  • Асинхронно обновлять DOM вне вызова render.
  • Освобождать ресурсы, когда директива отключается от DOM.

На оставшейся части этой страницы описаны директивы на основе классов.

Создание директив на основе классов

Чтобы создать директиву на основе класса:

  • Реализуйте директиву как класс, расширяющий класс Directive.
  • Передайте свой класс в фабрику directive(), чтобы создать функцию директивы, которую можно использовать в выражениях шаблонов Lit.
import {Directive, directive} from 'lit/directive.js';

// Define directive
class HelloDirective extends Directive {
  render() {
    return `Hello!`;
  }
}
// Create the directive function
const hello = directive(HelloDirective);

// Use directive
const template = html`<div>${hello()}</div>`;

При вычислении этого шаблона функция директивы (hello()) возвращает объект DirectiveResult, который указывает Lit создать или обновить экземпляр класса директивы (HelloDirective). Затем Lit вызывает методы экземпляра директивы, чтобы выполнить логику обновления.

Некоторым директивам необходимо асинхронно обновлять DOM вне обычного цикла обновления. Чтобы создать асинхронную директиву, расширьте базовый класс AsyncDirective вместо Directive. Подробнее см. в разделе Асинхронные директивы.

Жизненный цикл директивы на основе класса

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

  • Конструктор класса для однократной инициализации.
  • render() для декларативной отрисовки.
  • update() для императивного доступа к DOM.

Для всех директив необходимо реализовать обратный вызов render(). Реализация update() необязательна. Реализация update() по умолчанию вызывает render() и возвращает его значение.

Асинхронные директивы, которые могут обновлять DOM вне обычного цикла обновления, используют несколько дополнительных обратных вызовов жизненного цикла. Подробнее см. в разделе Асинхронные директивы.

Однократная настройка: constructor()

Когда Lit впервые встречает DirectiveResult в выражении, он создает экземпляр соответствующего класса директивы (выполняя конструктор директивы и все инициализаторы полей класса):

class MyDirective extends Directive {
  // Class fields will be initialized once and can be used to persist
  // state between renders
  value = 0;
  // Constructor is only run the first time a given directive is used
  // in an expression
  constructor(partInfo: PartInfo) {
    super(partInfo);
    console.log('MyDirective created');
  }
  ...
}
class MyDirective extends Directive {
  // Class fields will be initialized once and can be used to persist
  // state between renders
  value = 0;
  // Constructor is only run the first time a given directive is used
  // in an expression
  constructor(partInfo) {
    super(partInfo);
    console.log('MyDirective created');
  }
  ...
}

Пока при каждой отрисовке в том же выражении используется одна и та же функция директивы, предыдущий экземпляр используется повторно, поэтому его состояние сохраняется между отрисовками.

Конструктор получает один объект PartInfo, который содержит метаданные о выражении, в котором использовалась директива. Это может быть полезно для проверки ошибок, если директива предназначена для использования только в выражениях определенных типов (см. раздел Ограничение директивы одним типом выражений).

Декларативная отрисовка: render()

Метод render() должен возвращать значение для отображения в DOM. Он может возвращать любое отображаемое значение, включая другой объект DirectiveResult.

Помимо обращения к состоянию экземпляра директивы, метод render() также может принимать произвольные аргументы, переданные функции директивы:

const template = html`<div>${myDirective(name, rank)}</div>`

Параметры, определенные для метода render(), задают сигнатуру функции директивы:

class MaxDirective extends Directive {
  maxValue = Number.MIN_VALUE;
  // Define a render method, which may accept arguments:
  render(value: number, minValue = Number.MIN_VALUE) {
    this.maxValue = Math.max(value, this.maxValue, minValue);
    return this.maxValue;
  }
}
const max = directive(MaxDirective);

// Call the directive with `value` and `minValue` arguments defined for `render()`:
const template = html`<div>${max(someNumber, 0)}</div>`;
class MaxDirective extends Directive {
  maxValue = Number.MIN_VALUE;
  // Define a render method, which may accept arguments:
  render(value, minValue = Number.MIN_VALUE) {
    this.maxValue = Math.max(value, this.maxValue, minValue);
    return this.maxValue;
  }
}
const max = directive(MaxDirective);

// Call the directive with `value` and `minValue` arguments defined for `render()`:
const template = html`<div>${max(someNumber, 0)}</div>`;

Императивный доступ к DOM: update()

В более сложных сценариях директиве может понадобиться доступ к базовому DOM для чтения данных или его изменения императивным способом. Это можно сделать, переопределив обратный вызов update().

Обратный вызов update() принимает два аргумента:

  • Объект Part с API для непосредственного управления DOM, связанным с выражением.
  • Массив, содержащий аргументы render().

Метод update() должен возвращать значение, которое Lit может отобразить, или специальное значение noChange, если повторная отрисовка не требуется. Обратный вызов update() достаточно гибок, но обычно используется для следующих целей:

  • Чтение данных из DOM и их использование для создания отображаемого значения.
  • Императивное обновление DOM с помощью ссылки element или parentNode в объекте Part. В этом случае update() обычно возвращает noChange, указывая, что Lit не нужно предпринимать никаких дополнительных действий для отображения директивы.

Части

Для каждой позиции выражения существует собственный объект Part:

  • ChildPart для выражений в позиции дочернего элемента HTML.
  • AttributePart для выражений в значении атрибута HTML.
  • BooleanAttributePart для выражений в значении логического атрибута (имя начинается с ?).
  • EventPart для выражений в позиции обработчика событий (имя начинается с @).
  • PropertyPart для выражений в значении свойства (имя начинается с .).
  • ElementPart для выражений в теге элемента.

Помимо метаданных для конкретной части, содержащихся в PartInfo, все типы Part предоставляют доступ к DOM element, связанному с выражением (или к parentNode в случае ChildPart), к которому можно напрямую обратиться в update(). Например:

// Renders attribute names of parent element to textContent
class AttributeLogger extends Directive {
  attributeNames = '';
  update(part: ChildPart) {
    this.attributeNames = (part.parentNode as Element).getAttributeNames?.().join(' ');
    return this.render();
  }
  render() {
    return this.attributeNames;
  }
}
const attributeLogger = directive(AttributeLogger);

const template = html`<div a b>${attributeLogger()}</div>`;
// Renders: `<div a b>a b</div>`
// Renders attribute names of parent element to textContent
class AttributeLogger extends Directive {
  attributeNames = '';
  update(part) {
    this.attributeNames = part.parentNode.getAttributeNames?.().join(' ');
    return this.render();
  }
  render() {
    return this.attributeNames;
  }
}
const attributeLogger = directive(AttributeLogger);

const template = html`<div a b>${attributeLogger()}</div>`;
// Renders: `<div a b>a b</div>`

Кроме того, модуль directive-helpers.js содержит несколько вспомогательных функций, которые работают с объектами Part и могут использоваться для динамического создания, вставки и перемещения частей внутри ChildPart директивы.

Вызов render() из update()

Реализация update() по умолчанию просто вызывает render() и возвращает его значение. Если вы переопределили update() и хотите по-прежнему вызывать render() для создания значения, необходимо явно вызвать render().

Аргументы render() передаются в update() в виде массива. Передать аргументы в render() можно следующим образом:

class MyDirective extends Directive {
  update(part: Part, [fish, bananas]: DirectiveParameters<this>) {
    // ...
    return this.render(fish, bananas);
  }
  render(fish: number, bananas: number) { ... }
}
class MyDirective extends Directive {
  update(part, [fish, bananas]) {
    // ...
    return this.render(fish, bananas);
  }
  render(fish, bananas) { ... }
}

Различия между update() и render()

Хотя обратный вызов update() обладает большими возможностями, чем обратный вызов render(), есть важное различие: при использовании пакета @lit-labs/ssr для серверной отрисовки (SSR) на сервере вызывается только метод render(). Для совместимости с SSR директивы должны возвращать значения из render() и использовать update() только для логики, которой необходим доступ к DOM.

Указание на отсутствие изменений

Иногда директиве нечего нового отображать средствами Lit. Чтобы указать на это, верните noChange из метода update() или render(). Это отличается от возврата undefined, который приводит к очистке Part, связанного с директивой. Возврат noChange оставляет ранее отображенное значение без изменений.

Есть несколько распространенных причин возвращать noChange:

  • На основе входных значений отображать нечего нового.
  • Метод update() императивно обновил DOM.
  • В асинхронной директиве вызов update() или render() может вернуть noChange, поскольку отображать пока нечего.

Например, директива может отслеживать переданные ей предыдущие значения и самостоятельно проверять изменения, чтобы определить, нужно ли обновлять результат директивы. Метод update() или render() может вернуть noChange, чтобы указать, что результат директивы не нужно отображать повторно.

import {Directive} from 'lit/directive.js';
import {noChange} from 'lit';
class CalculateDiff extends Directive {
  a?: string;
  b?: string;
  render(a: string, b: string) {
    if (this.a !== a || this.b !== b) {
      this.a = a;
      this.b = b;
      // Expensive & fancy text diffing algorithm
      return calculateDiff(a, b);
    }
    return noChange;
  }
}
import {Directive} from 'lit/directive.js';
import {noChange} from 'lit';
class CalculateDiff extends Directive {
  render(a, b) {
    if (this.a !== a || this.b !== b) {
      this.a = a;
      this.b = b;
      // Expensive & fancy text diffing algorithm
      return calculateDiff(a, b);
    }
    return noChange;
  }
}

Ограничение директивы одним типом выражений

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

Например, директива classMap проверяет, что она используется только в AttributePart и только для атрибута class:

class ClassMap extends Directive {
  constructor(partInfo: PartInfo) {
    super(partInfo);
    if (
      partInfo.type !== PartType.ATTRIBUTE ||
      partInfo.name !== 'class'
    ) {
      throw new Error('The `classMap` directive must be used in the `class` attribute');
    }
  }
  ...
}
class ClassMap extends Directive {
  constructor(partInfo) {
    super(partInfo);
    if (
      partInfo.type !== PartType.ATTRIBUTE ||
      partInfo.name !== 'class'
    ) {
      throw new Error('The `classMap` directive must be used in the `class` attribute');
    }
  }
  ...
}

Асинхронные директивы

Предыдущие примеры директив синхронные: они синхронно возвращают значения из обратных вызовов жизненного цикла render()/update(), поэтому результаты записываются в DOM во время обратного вызова update() компонента.

Иногда директиве нужно асинхронно обновлять DOM — например, если она зависит от асинхронного события, такого как сетевой запрос.

Чтобы асинхронно обновлять результат директивы, она должна расширять базовый класс AsyncDirective, предоставляющий API setValue(). setValue() позволяет директиве «передавать» новое значение выражению шаблона вне обычного цикла update/render шаблона.

Ниже приведен пример простой асинхронной директивы, отображающей значение Promise:

class ResolvePromise extends AsyncDirective {
  render(promise: Promise<unknown>) {
    Promise.resolve(promise).then((resolvedValue) => {
      // Rendered asynchronously:
      this.setValue(resolvedValue);
    });
    // Rendered synchronously:
    return `Waiting for promise to resolve`;
  }
}
export const resolvePromise = directive(ResolvePromise);
class ResolvePromise extends AsyncDirective {
  render(promise) {
    Promise.resolve(promise).then((resolvedValue) => {
      // Rendered asynchronously:
      this.setValue(resolvedValue);
    });
    // Rendered synchronously:
    return `Waiting for promise to resolve`;
  }
}
export const resolvePromise = directive(ResolvePromise);

В этом примере в отрисованном шаблоне отображается строка «Ожидание разрешения промиса», а после разрешения промиса — его значение.

Асинхронным директивам часто нужно подписываться на внешние ресурсы. Чтобы предотвратить утечки памяти, асинхронным директивам следует отменять подписку или освобождать ресурсы, когда экземпляр директивы больше не используется. Для этого AsyncDirective предоставляет следующие дополнительные обратные вызовы и API жизненного цикла:

  • disconnected(): вызывается, когда директива больше не используется. Экземпляры директивы отключаются в трех случаях:

    • Когда дерево DOM, в которое входит директива, удаляется из DOM.
    • Когда хост-элемент директивы отключается.
    • Когда выражение, создавшее директиву, больше не разрешается в ту же директиву.

    После вызова обратного вызова disconnected директива должна освободить все ресурсы, на которые она могла подписаться в update или render, чтобы предотвратить утечки памяти.

  • reconnected(): вызывается, когда ранее отключенная директива снова начинает использоваться. Поскольку поддеревья DOM могут временно отключаться, а затем подключаться снова, отключенной директиве может понадобиться реагировать на повторное подключение. Например, DOM может быть удален и сохранен в кеше для последующего использования или перемещение хост-элемента может вызвать отключение и повторное подключение. Обратный вызов reconnected() всегда следует реализовывать вместе с disconnected(), чтобы вернуть отключенную директиву в рабочее состояние.

  • isConnected: отражает текущее состояние подключения директивы.

Обратите внимание: AsyncDirective может продолжать получать обновления, даже будучи отключенным, если его содержащее дерево повторно отрисовывается. Поэтому update и/или render всегда должны проверять флаг this.isConnected перед подпиской на ресурсы с длительным сроком использования, чтобы предотвратить утечки памяти.

Ниже приведен пример директивы, которая подписывается на Observable и корректно обрабатывает отключение и повторное подключение:

class ObserveDirective extends AsyncDirective {
  observable: Observable<unknown> | undefined;
  unsubscribe: (() => void) | undefined;
  // When the observable changes, unsubscribe to the old one and
  // subscribe to the new one
  render(observable: Observable<unknown>) {
    if (this.observable !== observable) {
      this.unsubscribe?.();
      this.observable = observable
      if (this.isConnected)  {
        this.subscribe(observable);
      }
    }
    return noChange;
  }
  // Subscribes to the observable, calling the directive's asynchronous
  // setValue API each time the value changes
  subscribe(observable: Observable<unknown>) {
    this.unsubscribe = observable.subscribe((v: unknown) => {
      this.setValue(v);
    });
  }
  // When the directive is disconnected from the DOM, unsubscribe to ensure
  // the directive instance can be garbage collected
  disconnected() {
    this.unsubscribe!();
  }
  // If the subtree the directive is in was disconnected and subsequently
  // re-connected, re-subscribe to make the directive operable again
  reconnected() {
    this.subscribe(this.observable!);
  }
}
export const observe = directive(ObserveDirective);
class ObserveDirective extends AsyncDirective {
  // When the observable changes, unsubscribe to the old one and
  // subscribe to the new one
  render(observable) {
    if (this.observable !== observable) {
      this.unsubscribe?.();
      this.observable = observable
      if (this.isConnected)  {
        this.subscribe(observable);
      }
    }
    return noChange;
  }
  // Subscribes to the observable, calling the directive's asynchronous
  // setValue API each time the value changes
  subscribe(observable) {
    this.unsubscribe = observable.subscribe((v) => {
      this.setValue(v);
    });
  }
  // When the directive is disconnected from the DOM, unsubscribe to ensure
  // the directive instance can be garbage collected
  disconnected() {
    this.unsubscribe();
  }
  // If the subtree the directive is in was disconneted and subsequently
  // re-connected, re-subscribe to make the directive operable again
  reconnected() {
    this.subscribe(this.observable);
  }
}
export const observe = directive(ObserveDirective);

Изменить эту страницу

© Google LLC
Licensed under the Creative Commons Attribution 3.0 Unported License.
https://lit.dev/docs/v2/templates/custom-directives/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API