angular
April 13

Использование web-компонентов в Angular

Веб-компоненты позволяют нам создавать переиспользуемые, настраиваемые элементы. Основное преимущество веб-компонентов - это их интероперабельность: поскольку они поддерживаются нативно в браузерах, веб-компоненты могут использоваться в любой HTML-среде, с любым фреймворком или вообще без фреймворка.

Одним из основных аспектов веб-компонентов является инкапсуляция. Вы можете разделять и скрывать структуру разметки, стиль и поведение, чтобы различные части страницы не конфликтовали между собой.

Поэтому веб-компоненты идеально подходят для разработки дизайн-систем, компонентов, которые можно использовать повторно, а так же встраиваемых виджетов. Давайте посмотрим, как использовать нативный веб-компонент с лучшими библиотеками в отрасли:

Создание веб-компонентов с Lit

Создание веб-компонента с нуля на чистом JavaScript может быстро превратиться в легаси, который сложно масштабировать и поддерживать. К счастью, мы можем создавать быстрые, легкие веб-компоненты с помощью Lit.

В основе Lit лежит базовый класс компонентов, который убивает необходимость в шаблонах, предоставляя реактивное состояние, стили с областью видимости и декларативную систему шаблонов.

Хотя мы будем использовать Lit, я рекомендую изучить, как разрабатывать веб-компоненты с использованием нативного JavaScript.

Для начала реализуем простейший Select, который будет работать через нативный API. Вопросы сборщиков здесь обсуждаться не будут, можно использовать любой.

Начнем с элемента select:

import { css, html, LitElement } from 'lit';

export class SelectElement<T extends Record<string, unknown>> extends LitElement {
  static styles = css`
    button[active] {
      background-color: honeydew
    }
  `
  @property({ type: Array }) 
  data: T[] = [];
  
  @property({ attribute: 'id-key' }) 
  idKey: keyof T = 'id' as keyof T;
  
  @property({ attribute: 'val-key' }) 
  valKey: keyof T = 'label' as keyof T;
  
  @property({ state: true }) 
  private activeItem: T | null = null;

  render() {    
    return html``
  }
}

customElements.define('ui-select', SelectElement);

Мы создаем SelectElement и определяем три входных параметра — data, idKey и valKey. Мы также определяем состояние activeItem для отслеживания текущего выбранного элемента. Добавим шаблон:

import { css, html, LitElement } from 'lit';
import { repeat } from 'lit/directives/repeat.js';

export class SelectElement extends LitElement {
  ...

  selectItem(item: T) {
    this.activeItem = item;

    const event = new CustomEvent<T>('select-item', {
      detail: item,
      bubbles: true,
      composed: true
    });

    this.dispatchEvent(event);
  }

  render() {    
    return html`
      <p>Active: ${this.activeItem ? this.activeItem[this.valKey] : 'None' }</p>
      
      ${repeat(
         this.data, 
         current => current[this.idKey], 
         current => html`
          <button ?active=${this.activeItem?.[this.idKey] === current[this.idKey]} 
                  @click=${() => this.selectItem(current)}>
            ${current[this.valKey]}
          </button>
      `)}
      `
  }
}

customElements.define('ui-select', SelectElement);


Мы используем директиву repeat для эффективного рендеринга наших элементов. Она принимает три параметра - collection, keyFunction, которая принимает элемент в качестве аргумента и возвращает уникальный ключ для него, и itemTemplate, которая принимает элемент и его текущий индекс в качестве аргументов и возвращает TemplateResult.

При щелчке на элементе он помечается как активный и отправляется пользовательское событие, на которое родитель может подписаться.

Мы используем хорошую возможность из Lit, называемую выражениями для атрибутов boolean. Атрибут active будет добавлен или удален в зависимости от результата выражения. Мы используем его для стилизации активного элемента.

Если вы ищете генератор lit-файлов, то может пригодиться эта библиотека.

Использование веб-компонентов в Angular

Сначала нужно использовать схему CUSTOM_ELEMENTS_SCHEMA. Angular игнорирует пользовательские элементы (названные с дефисами), которые он не распознает, вместо того чтобы генерировать ошибку.

import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { TodosComponent } from './todos.component';

@NgModule({
  declarations: [TodosComponent],
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class TodosModule {}

Теперь можно использовать веб-компонент ui-select внутри компонента TodosComponent:

import '@org/ui/lib/select';
import { Component } from '@angular/core';
import { Todo, randTodo } from '@ngneat/falso';

@Component({
  selector: 'app-todos',
  template: `
    <button (click)="replace()">Replace</button>
    <ui-select [data]="todos" 
               (select-item)="onSelect($event)" 
               val-key="title">
    </ui-select>   
  `
})
export class TodosComponent {
  todos: Todo[] = randTodo({ length: 3 });

  replace() {
    this.todos = randTodo({ length: 3 });
  }

  onSelect(e: CustomEvent<Todo>) {
    console.log(e.detail);
  }
}

Как показывает пример, интеграция Angular с веб-компонентами работает из коробки. Angular связывает свойство компонента todos со свойством данных в элементе ui-select. Привязка событий слушает событие select-item и вызывает метод onSelectItem() компонента всякий раз, когда оно срабатывает.

Это вольный перевод материала