Spec-Zone.ru › Lit 3

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

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

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

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

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

Директивы бывают двух типов:

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

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

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

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

  • Напрямую обращаться к отрисованному DOM (например, добавлять, удалять или менять порядок отрисованных узлов DOM).
  • Сохранять состояние между отрисовками.
  • Асинхронно обновлять DOM вне вызова рендеринга.
  • Освобождать ресурсы, когда директива отключается от 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/templates/custom-directives/

Spec-Zone.ru

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