Web Components with Angular Elements

Gordon Chen
Apr 26 · 7 min read

The Setup

ng new elements
ng add @angular/elements
ng g component dropdown
@NgModule({
declarations: [
DropdownComponent
],
imports: [
BrowserModule
],
entryComponents: [
DropdownComponent
]

})
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);
}
}
}

Component Creation

@Component({
selector: 'app-dropdown',
templateUrl: './dropdown.component.html',
styleUrls: ['./dropdown.component.css'],
encapsulation: ViewEncapsulation.ShadowDom,
changeDetection: ChangeDetectionStrategy.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);
}
}
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);
}

}

The Shadow Dom

<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>
<my-accordion>
<h2 slot="title" class="title">Comics</h2>
<span slot="icon" class="fas fa-caret-down"></span>
</my-accordion>
<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>
el.addEventListener('selectedItem', (event) => {
console.log(event);
})
.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));
}
my-dropdown {
--my-max-width: 400px;
--header-color: green;
--hover-header-color: lightblue;
}

Packaging

npm install @webcomponents/custom-elements
import '@webcomponents/custom-elements/custom-elements.min';
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')
})()
npm run build:elements
<link rel="stylesheet" href="./elements/styles.css">
<script src="./elements/elements.js"></script>

Final Thoughts

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade