Disclaimer
This is my personal summary of the sessions from ngconf. While I summarize the things with my own words, the material used such as images, graphs, source code examples are not my own. Most of them are from the Youtube videos or slide of the respective speakers of the various sessions.
Other sessions?
This article is cross-posted from my blog. If you wanna read the original one, also covering other sessions, head over to the original article ».
Not Every App is a SPA
Rob Wormald
Rob targets the graph mentioned by Igor about the current field Angular apps are being adopted.
Going forward the team's goal is to target the two missing edges in this graph.
Small-medium Apps, Demos, Edu Apps
To target this left side of the graph, where small to medium apps reside, the answer is definitely Angular Elements.
If this sounds new to you, check out my related article.
Mixed environments are also a good example where Angular Elements fits in nicely:
- Lots of different frameworks
- Not everyone can start from greenfield
- Google has this problem too (Angular, AngularJS, Dart, GWT, Polymer,...)
- Mini-apps running on 3rd party sites
- NgUpgrade
In the context of Angular Elements, the registration process for bundling a single component as an Angular Element is currently (< Angular v7) still quite verbose:
@NgModule({
imports: [BrowserModule, CommonModule],
declarations: [HelloWorld],
entryComponents: [HelloWorld]
})
class HelloWorldModule {}
And then it needs to be registered as an Angular Element:
platformBrowser()
.bootstrapModule(HelloWorldModule)
.then(({injector}) => {
const HelloWorldElement = createCustomElement(HelloWorld, {injector});
customElements.define('hello-world', HelloWorldElement);
});
How is that going to change with Ivy?
The simplest way of rendering a component in Ivy is the following:
import { Component, Input, Output, renderComponent } from '@angular/core';
@Component({
selector: 'hello-world',
template: `...`
})
class HelloWorld {
@Input() name: string;
@Output() nameChange = new EventEmitter();
changeName = () => this.nameChange.emit(this.name);
}
renderComponent(HelloWorld);
So how can we make this an Angular Element in Ivy? Rob shows on stage how that will look like.
import { renderComponent } from '@angular/core';
import { HelloWorld } from './hello-world.component';
// manually define the host rather than let Angular look for it
// then pass it as a 2nd argument to the renderComponent
const host = document.querySelector('hello-world');
renderComponent(HelloWorld, { host });
// create a custom element using the native browser API
class HelloWorldElement extends HTMLElement {}
This is the first step. Next, we can create a Custom Element using the native browser API and invoke the renderComponent
from there.
import { renderComponent } from '@angular/core';
import { HelloWorld } from './hello-world.component';
// create a custom element using the native browser API
class HelloWorldElement extends HTMLElement {
component: HelloWorld;
constructor() {
super();
// associate "this" as the host element
this.component = renderComponent(HelloWorld, { host: this })
}
}
Note how we pass this
(which is the Custom Element instance as the host to the render function). We can also add properties which we simply wrap.
import { renderComponent, detectChanges } from '@angular/core';
import { HelloWorld } from './hello-world.component';
// create a custom element using the native browser API
class HelloWorldElement extends HTMLElement {
component: HelloWorld;
constructor() {
super();
// associate "this" as the host element
this.component = renderComponent(HelloWorld, { host: this })
}
set name(value) {
this.component.name = value;
detectChangs(this.component);
}
get name() {
return this.component.name;
}
}
detectChanges
can just be imported from Angular. It's just a function 💪 (no DI necessarily needed to inject the ChangeDetectorRef
etc..)!
To have attributes, we just continue using the native browser APIs.
import { renderComponent, detectChanges } from '@angular/core';
import { HelloWorld } from './hello-world.component';
// create a custom element using the native browser API
class HelloWorldElement extends HTMLElement {
static observedAttributes = ['name'];
component: HelloWorld;
constructor() {
super();
// associate "this" as the host element
this.component = renderComponent(HelloWorld, { host: this })
}
attributeChangedCallback(attr, oldValue, newValue) {
this.name = newValue;
}
set name(value) {...}
get name() {...}
}
Now this just to show how easy it is to build it by yourself with Ivy. You don't have to do this every time. Most likely this will look like this with Ivy:
import { withNgComponent } from '@angular/elements';
import { HelloWorld } from './hello-world.component';
// create a Custom Element that wraps the Angular Component
const HelloWorldElement = withNgComponent(HelloWorld);
// register it
customElements.define('hello-world', HelloWorldElement);
No platforms, no modules 🎉 🎉 You can of course still use the Injector if you want:
...
// create a Custom Element that wraps the Angular Component
const HelloWorldElement = withNgComponent(HelloWorld, {injector});
...
In many cases you already have an Angular Component that you want to turn into an Element. But what if you don't want to have an Angular Component, but just an Angular Element? 🤔 Basically you just want the benefit the Angular templating system gives you. The "problem" right now is that we have the NgModule
which tells the compiler which dependencies are needed and helps it optimize the final outcome. Technically Ivy doesn't need a NgModule
, but still, we need to have a way to tell the component what other directives/components live in its template. One proposal (<< this is an EARLY PROPOSAL the team wants feedback on) is to allow to register the dependencies directly in the @Component
tag, very much like you already can with providers
and what was already there in Angular RC4
(yes I remember 😅). Something like this:
@Component({
selector: 'hello-world',
template: `...`,
providers: [SomeService],
deps: [SomeDirective, SomePipe]
})
class HelloWorld {}
This is definitely more verbose, but also more direct and "simpler" if you want. To achieve the final goal of just having an Ng Element (without an Angular Component) could look something like this (based on what we've discussed before):
import { NgElement, withElement } from '@angular/elements';
...
@NgElement({
selector: 'hello-world',
template: `...`,
providers: [SomeService],
deps: [SomeDirective, SomePipe]
})
class HelloWorld extends withNgElement {}
This gives you an Angular Element without an Angular Component. Something that might make sense in some scenarios, like when building a design system.
Scaling Up - or What is project "Angular Photon"?
To the other side of the chart: scaling up.
In this context (during the keynote - see further up), the name **Angular Photon" came up. Imporant:
It's a research project for experimenting and "deciding how to build the right tools for the next gen of Angular Developers". It's a project in collaboration with
- Google Shopping Express (build with Angular)
- Wiz
Loading components as they are needed is a big part. As a sneak peek this is what it might look like
import { withLazyNgComponent } from '@angular/elements';
// create a Custom Element that wraps the Angular Component
const HelloWorldElement = withLazyNgComponent(() => import('./hellow-world.component'));
// register it
customElements.define('hello-world', HelloWorldElement);
Note the withLazyNgComponent
that fetches the necessary scripts only when really needed.