В последнее время популярность веб-компонентов сильно выросла. С библиотеками для создания веб-компонентов, таких как 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