Czym jest Angular Elements? Jest to biblioteka, dzięki której możemy wykorzystać komponenty poza kodem Angular. W takim przypadku team frontend React może dodać feature do swojej aplikacji napisany w Angular.
Tworząc elementy korzystamy z technologii Web Components. Pozwala ona stworzyć niestandardowy tag HTML, który możemy wykorzystać wielokrotnie w kodzie. Logika w nim zawarta nie jest związana z resztą kodu.
Stworzenie elementu za pomocą API Angular
Na początek stwórzmy nasz projekt, za pomocą Angular CLI.
1 |
ng new angular-elements-example |
Dodajmy Angular Elements.
1 |
ng add @angular/elements |
Oraz utwórzmy przykładowy komponent.
1 |
ng g c test-component |
Gdy mamy już gotowy projekt możemy dodać nasz element.
Elementy tworzymy za pomocą metody createCustomElement(). Aby jej użyć, należy ją dodać w głównym module aplikacji.
1 2 3 4 5 6 |
export class AppModule { constructor(injector: Injector) { const el = createCustomElement(TestComponent, { injector: injector }); customElements.define('test-component', el); } } |
W powyższym przykładzie użyliśmy komponentu TestComponent. Następnie zdefiniowaliśmy go jako web element <test-component>.
Komponent stał się dynamiczny, dlatego możemy go użyć w kodzie za pomocą prostego kodu JS.
1 2 3 |
ngOnInit() { document.querySelector('#container').innerHTML = '<test-component></test-component>' } |
Prawdopodobnie kod może nam nie zadziałać. I w konsoli otrzymamy błąd.
Failed to construct ‘HTMLElement’: Please use the ‘new’ operator, this DOM object constructor cannot be called as a function.
W takim przypadku potrzebujemy wsparcia dla przeglądarki. Należy doinstalować paczkę @webcomponents/webcomponentsjs
1 |
npm i @webcomponents/webcomponentsjs |
A następnie zaimportować ją w pliku polyfills.ts
1 |
import '@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js' |
Teraz wszystko powinno działać.
Komunikacja z komponentem za pomocą Input() i Output()
Stwórzmy komponent, który będzie nam wyświetlał listę User. Poprzez Input() przekażemy tablicę User a poprzez Output() wyemitujemy wybór konkretnego User.
Dobrą praktyką jest nazywanie Input() i Output() małymi literami. Lepiej nazwać userlist zamiast userList. W przypadku dodawania elementu do strony HTML nie będziemy mieli problemu, natomiast gdy dodamy do aplikacji React, może nam nie zadziałać.
Kolejną rzeczą jest potraktowanie Input() jako string. Gdy będziemy dodawać elementy do innych aplikacji dane takie jak obiekt lub array mogą być niepoprawnie obsłużone. Aplikacje zamiast serializować dane będą używały funkcji toString(). Dlatego dla userslist ustawiony jest setter, który zamienia JSON lub przypisuje array User do property list. Następnie property list jest listowane w widoku.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import { Component, OnInit, EventEmitter, Input, Output } from '@angular/core'; export interface User { name: string; } @Component({ selector: 'app-users-list', templateUrl: './users-list.component.html', styleUrls: ['./users-list.component.sass'] }) export class UsersListComponent { list: User[]; @Input() set userslist(userslist: User[]|string) { if (typeof userslist === 'string') { try { this.list = JSON.parse(userslist); } catch {} } else if (Array.isArray(userslist)) { this.list = userslist; } } @Output() userselect = new EventEmitter(); } <p *ngFor="let user of list" (click)="userselect.emit(user)"> {{ user.name }} </p> |
Za pomocą kodu JS możemy dodać Input() do naszego elementu. Należy odwołać się do elementu HTML, a następnie do atrybutu userslist.
1 2 3 |
const el = document.createElement('users-list') as any; el.userslist = [{name: 'John'}, {name: 'IronMan'}]; document.querySelector('#users-container').appendChild(el); |
Aby poprawnie obsłużyć Output() dla elementu, należy dodać EventListener.
1 |
el.addEventListener('userselect', e => console.log(e.detail)); |
Build elementu do wykorzystania w innych aplikacjach
Aplikacja tworząca build nie będzie posiadała AppComponent ani bootstrap z NgModule. Są one niepotrzebne. W tym momencie nie mamy żadnego komponentu odpowiadającego za root aplikacji oraz nasza aplikacja nie potrzebuje żadnego komponentu do wystartowania.
1 2 3 4 5 6 7 8 9 10 11 12 |
@NgModule({ imports: [ BrowserModule, FormsModule ], declarations: [ AppComponent, UsersListComponent ], }) export class AppModule { constructor(injector: Injector) { const el = createCustomElement(UsersListComponent, { injector: injector }); customElements.define('users-list', el); } ngDoBootstrap() {} } |
W powyższym przykładnie dodałem funkcję ngDoBootstrap(). Jako że nie mamy zdefiniowanego żadnego elementu do startu aplikacji, ta funkcja informuje, żeby aplikacja wystartowała.
Tworzymy produkcyjny build bez trybu cache.
1 |
ng build --prod --output-hashing=none |
Komenda w folderze dist/angular-elements-example stworzyła nam 3 pliki: main.js, polyfills.js oraz runtime.js. Należy je połączyć w jeden w celu prostszego dodania na stronę. Stwórzmy folder demo i za pomocą polecenia połączmy 3 pliki w 1.
1 |
cat dist/angular-elements-example/runtime.js dist/angular-elements-example/polyfills.js dist/angular-elements-example/main.js > demo/angular-elements-example.js |
Posiadamy teraz gotowy plik angular-elements-example.js który możemy dodać na stronę.
Przykład z wykonaniem build umieściłem na GitHub.
Dodanie elementu do strony HTML
Taki element możemy z łatwością zaimportować na prostą stronę html. Do pliku index.html należy dodać skrypt angular-elements-example.js
Tutaj mamy 2 możliwości implementacji. Możemy dodać nasz element dynamicznie lub statycznie. W obydwu przypadkach możliwa jest obsługa Input() i Output().
W przypadku dodawania statycznego przekazuje JSON array.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<html> <body> <script src="./angular-elements-example.js"></script> <h1>Static</h1> <users-list userslist='[{"name":"John"},{"name":"IronMan"}]'></users-list> <script> document.querySelector('users-list').addEventListener('userselect', e => alert(`output static ${JSON.stringify(e.detail)}`)); </script> <h1>Dynamic</h1> <script> const el = document.createElement('users-list'); el.userslist = [{name: 'John 2'}, {name: 'IronMan 2'}]; el.addEventListener('userselect', e => alert(`output dynamic ${JSON.stringify(e.detail)}`)); document.querySelector('body').appendChild(el); </script> </body> </html> |
Podany przykład z użyciem strony HTML możemy zobaczyć na stackblitz.
Dodanie elementu do aplikacji React
Podobnie jak przy dodawaniu do prostej strony HTML, należy dodać plik JS z naszym elementem. W tym celu w projekcie React dodajemy go do katalogu src i implementujemy go w pliku index.js.
Dane przekazywane w Input() zamienimy na JSON. Tak jak wspomniałem wyżej React zamiast serializować dane używa metody toString().
Obsługa Output() elementu odbywa się za pomocą addEventListener. W komponencie React po jego usunięciu należy usunąć EventListener.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import React, { Component } from 'react'; import './App.css'; class App extends Component { users = JSON.stringify([{name: 'John Wick'}, {name: 'IronMan'}]); componentDidMount() { this.component.addEventListener('userselect', this.onUserSelect); } componentWillUnmount() { this.component.removeEventListener('userselect', this.onUserSelect); } onUserSelect(event) { console.log(event.detail); } handleRef = component => { this.component = component; }; render() { return ( <users-list userslist={this.users} ref={this.handleRef}></users-list> ); } } export default App; |
Podany przykład z użyciem React możemy zobaczyć na stackblitz.
Dodanie elementu do aplikacji Vue
Przy dodawaniu elementu do komponentu w Vue posłużymy się takimi samymi zasadami jak w przypadku React. Tablicę User zamienimy na JSON.
W przypadku obsługi Output() funkcja zwróci nam CustomEvent. W celu uzyskania informacji należy odwołać się do detail.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<template> <users-list :userslist="users" @userselect="userselect"></users-list>; </template> <script> export default { name: 'App', data() { return { users: JSON.stringify([{ name: "John Wick" }, { name: "IronMan" }]) } }, methods: { userselect(event) { alert(JSON.stringify(event.detail)) } } } </script> |
Podany przykład z użyciem Vue możemy zobaczyć na stackblitz.
Podsumowanie
Angular Elements jest świetnym narzędziem, jeśli chcemy udostępnić kawałki kodu Angular. Możemy je wykorzystać w mikroserwisach czy też komponentach UI. Uważam, że warto zapoznać się z tą technologią. Jak tam wasze wrażenia z Web Components? Używacie w projektach? Dajcie znać w komentarzu 😉
Świetny artykuł jasno, czytelnie i precyzyjnie.
Zabrakło mi jedynie wzmianki co do wagi takiej paczki? To pierwsze co nasunęło mi się na myśl ponieważ angular jest dość ciężki. A jak ma się to do takowej paczki?
Wszystkie paczki, które użyliśmy muszą zostać dodane do build’a. Jeśli będziemy dodatkowo chcieli skorzystać z reactive forms wielkość pliku nam wzrośnie.
Pingback: Ciemna strona server side renderingu cz.2 - Angular.love
Dokładnie to samo pomyślałem jeszcze zanim zobaczyłem sekcję komentarzy…
‘Ile ta paczka waży?’- jest jakiś usecase w którym to się faktycznie sprawdza? Trochę zalatuje sztuką dla sztuki
Zaletą elementami może być tez lazy loading takiego komponentu w samej aplikacji angulara
Przykład do którego podano link https://angular-elements-example-html.stackblitz.io/ nie działa poprawnie, a w konsoli generuje błąd:
DOMException: Failed to execute ‘define’ on ‘CustomElementRegistry’: the name “users-list” has already been used with this registry
(Chrome 89, Linux Mint 19.2 Cinnamon)
Mam taki problem. Istnieje duża aplikacja napisana w jquery i js. No i jest konieczność przepisania jednej trochę większej funkcjonalności . Zespół najchętniej by to zrobił w Angularze. Pytanie czy Angular Elements nadaje się do większych komponentów czy lepiej użyć coś w stylu single-spa.