В последнее время популярность веб-компонентов сильно выросла. С библиотеками для создания веб-компонентов, таких как Stencil.js, Skate.js, Polymer; Теперь библиотеки javascript, такие как Vue.js, React и Angular, уже вошли в состав веб-компонентов.

В этом посте мы рассмотрим, как начать использовать Angular Elements для создания веб-компонента. Самое замечательное, что это очень похоже на то, как мы обычно пишем Angular, что позволяет любому легко преобразовать существующие компоненты Angular в веб-компоненты.



Установка

Для начала создадим новое приложение из Angular CLI.

ng new elements

Далее мы добавляем пакет Angular elements

ng add @angular/elements

Мы собираемся добавить компонент, над которым будем работать, с помощью следующей команды

ng g component dropdown

Кроме того, нам не нужны какие-либо сгенерированные файлы приложений, кроме app.module.ts. Итак, у нас должно получиться что-то вроде этого:

Затем давайте обновим файл app.module.ts. По сути, мы будем делать следующее

  • Добавление нашего компонента в entryComponents для определения наших пользовательских элементов
@NgModule({
  declarations: [
    DropdownComponent
  ],
  imports: [
    BrowserModule
  ],
  entryComponents: [
    DropdownComponent
  ]
})
  • Используйте ngDoBootstrap для начальной загрузки наших пользовательских элементов
  • Позвоните в createCustomElement, который преобразует наш код Angular для работы в API DOM.
export class AppModule {
  constructor(private injector: Injector) {}
ngDoBootstrap() {
    // array of tuples containing component and html name to be used in createCustomElement
    const elements: any[] = [
      [DropdownComponent, 'my-dropdown']
    ];
for(const [component, name] of elements) {
      const el = createCustomElement(component, { injector:  this.injector});
      customElements.define(name, el);
    }
  }
}

В ngDoBootstrap вы заметите, что у нас есть const элементы имени, которые содержат кортеж компонента и имя настраиваемого тега, которое будет использоваться. Мы перебираем этот кортеж и вызываем createCustomElement для каждого значения в нашем массиве.

Создание компонентов

Прежде всего, давайте обновим наш компонент, чтобы использовать новую версию Shadow Dom v1 и обнаружение изменений OnPush.

@Component({
  selector: 'app-dropdown',
  templateUrl: './dropdown.component.html',
  styleUrls: ['./dropdown.component.css'],
  encapsulation: ViewEncapsulation.ShadowDom,
  changeDetection: ChangeDetectionStrategy.OnPush
})

Shadow Dom позволяет полностью инкапсулировать стили наших компонентов только в нашем единственном компоненте. Кроме того, это также дает нам доступ к использованию слотов. Это дает нам возможность проецирования контента, о чем мы поговорим позже в этом блоге. Причина использования обнаружения изменений onPush заключается в улучшении производительности и некоторых странностях, которые возникают, когда мы объединяем это как веб-компонент (по крайней мере, по моему опыту).

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

export class DropdownComponent {
  state = {
    hideList: true,
    showHeader: true,
    selectedItem: null, 
    listItems: []
  };
  constructor(private cd: ChangeDetectorRef) {}
  private setState(key, value) {
    this.state = { ...this.state, [key]: value };
    this.cd.detectChanges();
  }
  toggleList() {
    this.setState('hideList', !this.state.hideList);
  }
}

В приведенном выше примере у нас есть простое состояние, которое содержит несколько свойств для нашего класса. Мы создаем простую функцию, которая позволяет нам изменять состояние. Поскольку мы знаем, когда изменяется состояние, мы можем выполнить detectChanges, чтобы вызвать обновление пользовательского интерфейса, что позволяет нам использовать OnPush обнаружение изменений.

Мы также хотим добавить @Input, чтобы принимать значения в наш компонент, и @Output, чтобы передавать значение любым слушателям на нашем выходе.

export class DropdownComponent {
  @Input() set listItems(value: any[]) {
    this.setState('listItems', value);
  }
  get listItems(): any[] {
    return this.state.listItems;
  }
  @Output() selectedItem = new EventEmitter<any>();
  state = {
    hideList: true,
    showHeader: true,
    selectedItem: null, 
    listItems: []
  };
  constructor(private cd: ChangeDetectorRef) {}
  private setState(key, value) {
    this.state = { ...this.state, [key]: value };
    this.cd.detectChanges();
  }
  toggleList() {
    this.setState('hideList', !this.state.hideList);
  }
  clickedItem(item) {
    this.setState('selectedItem', item);
    this.toggleList();
    this.selectedItem.emit(item);
  }
}

Мы объявили наш ввод, вывод и добавили функцию под названием clickedItem , которая изменяет state и emits это событие через наш selectedItem eventemitter. В целом код нашего компонента - это очень стандартный код Angular.

Дом теней

С Angular мы можем использовать Shadow Dom v1, который широко используется в браузерах и содержит инструменты, необходимые для создания изолированных компонентов.

  • Изолированный Dom, dom нашего компонента недоступен через селекторы javascript, такие как document.querySelector
  • CSS с ограниченной областью видимости, наш CSS не проникает в родительский или другие компоненты.
  • Композиция (проекция содержимого) позволяет нам отображать разметку, вводимую в наш компонент через slots.. Мы можем увидеть это ниже:
<div class="dropdown">
  <header class="dropdown__header" (click)="toggleList()">
    <slot name="title" *ngIf="!state.selectedItem; else selectedLabel">
      <div class="dropdown__title">Default Header</div>
    </slot>
    <ng-template #selectedLabel><label>{{state.selectedItem.label}}</label></ng-template>
    <slot name="icon"></slot>
  </header>
  <ul class="dropdown__list" [ngClass]="{'hidden':state.hideList}">
    <li class="dropdown__item" *ngFor="let item of listItems" (click)="clickedItem(item)">
      {{item.label}}
    </li>
  </ul>
</div>

В нашей разметке все практически так же, как если бы это был просто угловой компонент. Новая идея, которую мы здесь имеем, - это идея slots. С помощью slots мы определяем область нашего компонента, где мы позволим потребителю заполнить свою разметку. Если мы хотим получить техническую терминологию, разметка потребителя нашего компонента называется Light DOM, а наша разметка для нашего компонента называется Shadow DOM. В нашем примере выше у нас есть два слота, каждый с атрибутом name. Мы также можем заполнить слот элементами, которые будут служить значениями по умолчанию в случае, если потребитель нашего компонента не предоставляет контент для слота. Итак, если бы мы использовали этот компонент, наша разметка была бы такой:

<my-accordion>
    <h2 slot="title" class="title">Comics</h2>
    <span slot="icon" class="fas fa-caret-down"></span>
</my-accordion>

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

Если бы мы не указали слоты, то получили бы следующее:

Если мы укажем содержимое слота, оно будет заполнено этой разметкой.

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

<script defer>
      const object = [
        {
          label: 'Spiderman',
          publisher: 'Marvel'
        },
        {
          label: 'Batman',
          publisher: 'DC'
        },
        {
          label: 'Saga',
          publisher: 'Image Comics'
        },
        {
          label: 'Hellboy',
          publisher: 'Dark Horse Comics'
        }
      ];
      const el = document.querySelector('my-dropdown');
      el.listItems = object;
</script>

У нас также есть событие вывода, которое мы можем использовать с addEventListener

el.addEventListener('selectedItem', (event) => {
    console.log(event);
})

При создании стилей для нашего компонента мы можем использовать переменные CSS, чтобы сделать доступными параметры стиля для потребителей нашего компонента, чтобы стилизовать приложение по своему усмотрению.

.dropdown {
  max-width: var(--my-max-width, 250px);
}
.dropdown__header {
  border-bottom: 1px solid var(--header-color, var(--primary-shade));
  ...
}
.dropdown__header:hover {
  --header-color: var(--hover-header-color, var(--secondary));
}

Как видите, существует несколько переменных CSS, на которые можно настроить таргетинг при попытке стилизовать компонент следующим образом:

my-dropdown {
  --my-max-width: 400px;
  --header-color: green;
  --hover-header-color: lightblue;
}

На всякий случай, если вы не знакомы с переменными CSS, вот небольшая бессовестная вставка моего блога, посвященного переменным CSS.

Упаковка

Теперь, когда мы создали наш компонент, как нам связать наш веб-компонент для использования вне Angular? Мы собираемся использовать npm install fs-extra concat — save-dev, который позволяет нам объединить наш вывод из ng build.

Если вы хотите включить поддержку IE11, нам потребуется установить следующий пакет:

npm install @webcomponents/custom-elements

В наш файл pollyfill.js нам нужно включить:

import '@webcomponents/custom-elements/custom-elements.min';

Чтобы начать упаковку, мы создаем новый файл, который мы назовем build-script.js.

const fs = require('fs-extra');
const concat = require('concat');
(async function build() {
    const files =[
      './dist/elements/runtime.js',
      './dist/elements/polyfills.js',
      './dist/elements/scripts.js',
      './dist/elements/main.js',
    ]
    await fs.ensureDir('elements')
    await concat(files, 'elements/elements.js')
    await fs.copyFile('./dist/elements/styles.css', 'elements/styles.css')
})()

А чтобы облегчить себе жизнь, мы добавим “build:elements”: “ng build elements — prod && node build-script.js” в свойство package.json scripts. Это позволит нам запустить простую команду, чтобы получить один файл для наших js и CSS.

npm run build:elements

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

<link rel="stylesheet" href="./elements/styles.css">
<script src="./elements/elements.js"></script>

Последние мысли

В производственной среде вы можете учитывать размеры пакетов для одного веб-компонента при попытке использовать Angular Elements. В настоящее время в этом примере мы получаем пакет размером 199 КБ. Это связано с тем, что в настоящее время Angular объединяет полную библиотеку angular с нашим веб-компонентом. Angular в настоящее время работает над этой проблемой, и доступность Ivy Compiler в Angular 8 должна решить некоторые из этих проблем с размером пакета с доступным встряхиванием дерева. Еще одним преимуществом Angular Elements является то, что мы можем использовать внедрение зависимостей, чтобы также обмениваться состоянием между несколькими элементами, что является довольно удобной функцией для веб-компонентов.

В следующем блоге мы рассмотрим Stencil.js и посмотрим, что он принесет с собой.

Если вы хотите посмотреть на интересный пример использования Angular Elements, Capital One использовал Angular Elements, чтобы помочь им перейти с AngularJS