You have unlimited access as a PRO member
You are receiving a free preview of 3 lessons
Your free preview as expired - please upgrade to PRO
Contents
Recent Posts
- Object Oriented Programming With TypeScript
- Angular Elements Advanced Techniques
- TypeScript - the Basics
- The Real State of JavaScript 2018
- Cloud Scheduler for Firebase Functions
- Testing Firestore Security Rules With the Emulator
- How to Use Git and Github
- Infinite Virtual Scroll With the Angular CDK
- Build a Group Chat With Firestore
- Async Await Pro Tips
Angular Elements Advanced Techniques
Episode 151 written by Jeff DelaneyHealth Check: This lesson was last reviewed on and tested with these packages:
- Angular v7.1
- RxJS v6.3
Find an issue? Let's fix it
Source code for Angular Elements Advanced Techniques on Github
It is exciting to finally see the adoption of web components going mainstream. There are many tools available for building them, but none can match the power and stability of Angular Elements. The following lesson will provide a handful of useful techniques aimed at building complex production-ready custom elements.
In addition to this post, check out these resources:
- Manfred Steyer’s Angular Elements Series
- Sam Julian’s Getting Started with Angular Elements
- Nrwl’s Five Reasons to Use Angular Elements
- Angular Elements QuickStart
Rendering without Zone.JS
After much experimentation, I’ve come to the conclusion that Zone.JS is not the the ideal way to handle change detection with Angular Elements. Automatic change detection is awesome when working in the context of an Angular app, but when you decouple your Angular components as custom elements you can run into hard-to-debug issues that only seem to happen in production. The Angular team has several open issues to address these bugs, but I think moving away from zones is a smart move in general. You will need to manually tell Angular when to re-render your components (see next sections), but this actually makes your code more explicit and easier to understand.
First, let’s turn off zones globally in the main.ts file.
// omitted ... |
You can also turn zones off at the component level by setting the OnPush
strategy.
@Component({ |
Component State
Now that zones are switched off, we need Angular to render the component when its internal data changes The general idea here is not novel and is similar conceptually in React, Flutter, and Stencil - i.e one way data flow. We have a state
object that when changed with the setState(key, value)
method tells the component to render - simple.
import { Component, ChangeDetectorRef } from '@angular/core'; |
Keep in mind, there are many ways you could implement this code. The only secret sauce is the call to this.cd.detectChanges()
.
Shared Global State
One of the ways Angular Elements stands out is its ability to share data and functionality between components via dependency injection.
ng g service shared |
To run change detection in a shared service, we reference the entire application, then call tick whenever an shared value changes.
import { Injectable, ApplicationRef } from '@angular/core'; |
Page Load Performance
One of the main criticisms of Angular Elements has been the bundle size, which is around 60Kb for a gzipped hello world. (1) The bundle size will decrease significantly when Ivy lands in the near future. (2) You’re getting the full power of Angular in that bundle, and (3) it does not have a significant impact on perf when you defer the script.
When you defer a script tag <script defer src="elements.js">
it tells the browser to render the HTML first, then load the script - i.e no render blocking. This is crucial for static websites the use components because your top priority is getting the main content painted. After the first meaningful paint, your web components can kick in to add interactivity.
The tests below were run with a bundle containing both Angular and Firebase at a weight of 350Kb.
Notice how we’re getting a page load of 600ms ⚡ - 4x faster - and a near perfect performance score. That is awesome considering how much horsepower we have under the hood.
Register Multiple Custom Elements
In most projects, you will have more than one component and it is cumbersome to register them one-by-one. I prefer to create an array of arrays (or tuples if you will) with the config for each element, then register them in a loop.
export class AppModule { |
Exposing Public Methods
A beautiful thing about web components is that we can interact with them using vanilla JS (or within other frameworks). By default, your internal code will not be accessible to the outside world, for example:
document.querySelector('my-element').cool(); |
But it can be useful to allow non-angular code to control your elements. Methods and properties can be exposed using the @Input
decorator. One caveat is that in order to give your element the proper this
context, you need make public methods a function property like so:
@Input() |
Exposing Public Events
You might also want to listen to the the custom events emitted by your component, for example:
document.querySelector('my-element').addEventListener('my-custom-event', (e) => doSomething) |
We can make this happen by dispatching a CustomEvent in the browser alongside an Angular Output/EventEmitter combo.
@Component(...) |
Content Projection and Shadow DOM
One cannot just embed HTML inside a custom element. Currently, if you try to transclude some markup in your web component it will be removed.
<my-element> |
You can save your end users the hassle of adding their own custom markup by projecting content into slots. This feature requires the Shadow DOM to be enabled in your component.
@Component({ |
Now you can add slots to your component markup.
Default Slot
If you only have one place for the user to project markup, you can simply add a default slot.
<!-- Component HTML --> |
Now HTML can be included inside the component and will be rendered below the image.
<!-- Usage --> |
Named Slots
<!-- Component HTML --> |
Now the end user of the web component can just pass a span that references the slot name and it will be rendered in the matching location.
<!-- Usage --> |