# Angular Elements - RFC
## Goals
- **Evolve** the existing Angular Elements API based on developer feedback, feature requests, and commonly encountered challenges.
- Provide **tooling, packaging and deployment support** for Angular Element development via Angular CLI.
- Introduce a **minimal** set of **extensible** primitives allowing developers to begin exploring and solving a wide variety of use-cases today.
- Define an **incremental** roadmap for further evolution of Angular Components and Elements alongside the Web Platform.
## Non-Goals
- Proposing Web Components as a replacement for every Angular Component.
- Changes to how Angular applications are built today.
## Background
Angular Elements was first released with version 6.0 of the Angular framework in May 2018, under the `@angular/elements` package name. The package is an opt-in, not included in Angular projects by default, and has grown to [over 100,000 downloads per month.](https://npmcharts.com/compare/@angular/elements?interval=30)
The `@angular/elements` package currently exports a `createCustomElement(Component, options)` function that wraps the given Angular Component in a "Web Component", allowing it to be used in any web page or web application written in any framework (including Angular) **without writing glue code or special knowledge of Angular APIs**.
Due to release schedule constraints and the potential of upcoming Ivy engine, the initial API was designed around solving a narrow set of use cases that could be enabled with [minimal changes to the Angular framework](https://github.com/angular/angular/pull/24718) and CLI.
Angular's NgModule architecture is well suited to building Angular *applications*, so the current Angular Elements API builds on top of this architecture. Developers can use their existing knowledge to expose Angular Components authored in an Angular application to the outside world, and use them declaratively in HTML not controlled by Angular.
Angular Elements have been by Google and open-source developers in a wide variety of scenarios to good effect, and feedback from developers building solutions within the supported scope has been positive. They've been proven reliable at scale - thousands of Angular developers use them every day on the Angular documentation site. Enterprise customers like Capital One used Elements to [succesfully upgrade their applications from AngularJS to Angular](https://medium.com/capital-one-tech/capital-one-is-using-angular-elements-to-upgrade-from-angularjs-to-angular-42f38ef7f5fd).
Despite our best intentions to limit the initial usage of Elements to these application-first scenarios, many Angular developers immediately found a huge variety of potential use-cases for Angular Elements. In many cases, while these use-cases are technically possible with the existing API, solving them today requires advanced knowledge of Angular's APIs, internals, and toolchain.
The Angular community has stepped in where possible to help out - Manfred Steyer's [ngx-builds-plus](https://github.com/manfredsteyer/ngx-build-plus) project was initially created to allow for more flexible Elements deployment, and led to his recent upstreaming of those improvements to Angular CLI and joining the team as a contributor.
Many of the *technical* challenges early adopters of Elements have faced stem from the fact that while Angular has traditionally optimized our APIs and tooling for **Single Page Applications**, most of the compelling use cases for Angular Elements involve using Angular Components **outside** of a traditional Angular application.
The Ivy architecture enables us to rethink a lot of these early design decisions, but it's important to acknowledge that millions of developers build applications successfully today with Angular, and fundamentally shifting how those **applications** are built is not a goal of this RFC.
The reality of modern web development is that it's really hard, and there's a lot to learn. A significant portion of developers recently surveyed at Google were in some stage of "migration hell" from one framework or API to another.
This is not a unique challenge to the Angular ecosystem, and Angular has made it a priority to provide a choice that lessens some of that decision fatigue for building great applications. The Angular team has put in an amazing amount of hard work in ensuring backwards compatibility with the Ivy runtime and compiler.
What has yet to be developed is a strategy for unlocking the full potential of Ivy for a wide variety of use cases beyond these applications.
The overwhelming reaction from developers using Angular Elements, once they have gotten over the initial setup, is that it *feels right*. Rather than attempting to *replace* the framework with Web Components, Elements augments the framework's strengths with new declarative capabilities.
These result makes using Angular Elements feel like an *extension* of HTML, and as they *are* HTML Elements, they work exactly like you expect them to. Angular Components *already* work like Custom Elements inside of an Angular template: the syntax for binding to a Custom Element is the same as an Angular Component:
```typescript=
import {HelloWorld} from './hello-world'
import {createCustomElement} from '@angular/elements'
customElements.define('hello-world', createCustomElement(HelloWorld, {}));
```
```html=
<h1>Hello {{name}}</h1>
<hello-world [name]="World" (nameChange)="doSomething($event)"></hello-world>
```
By registering the element with the browser, we make this behavior consistent *outside* of an Angular template (and in any other context):
```typescript=
//now its just HTML
const helloWorld = document.createElement('hello-world');
helloWorld.name = 'HTML'
document.body.appendChild(helloWorld);
```
Once registered, developers who want to use the `<hello-world>` element can use it just like any other HTML element, because it *is* an HTMLElement. This opens up a wide range of possibilities.
## Overview
This RFC proposes a strategy for the future development of Angular Elements, as well as defining the potential upstream changes to the Angular Framework and Angular CLI teams.
The potentially unlimited use cases for Angular Elements, combined with the current committments of the framework and tooling teams, mean that an incremental strategy must be adopted - developers should be able to extend, remix and augment the APIs this strategy proposes, without waiting for the Angular team to implement every specific use case.
As such, this document proposes a **high-level** API that improves on the existing implementation of `createCustomElement`, as well as exposing the set of new *low-level APIs* that *enable* the high level API to work.
The intention of this document is NOT to define the *implementation* of these APIs - rather to propose the developer/user facing APIs that would need to be implemented.
## `@angular/elements` API Proposal:
### `createCustomElement` improvements
The current `createCustomElement(Component, config)` API exists in Elements today and accepts two parameters:
- A `Component` class that will be wrapped in a Custom Element
- A `config` parameter that currently includes a required `Injector` prop and an optional `strategyFactory` prop.
#### API Change config.injector => config?.injector?
The internals of this API are generally straightforward - in View Engine applications, developers pass the Component and an Injector. The injector is used to
1. retrieve the `ComponentFactoryResolver` associated with the Component's NgModule
2. retrieve the Component's `ComponentFactory`, which is used to configure the Custom Element wrapper, and wire up props/inputs and events/outputs.
3. Connect the component instance to a parent DI context, allowing developers to inject services into their Angular Elements.
A typical setup for an Angular Element in View Engine must provide the Injector before defining the Element, and in practice that means developers must bootstrap an Angular *application* before they can use an Angular Element:
```typescript=
//View Engine impl
import {platformBrowser} from '@angular/platform-browser';
import {createCustomElement} from '@angular/elements'
import {HelloWorld, HelloWorldModule} from './hello-world.module';
import {HelloWorldModuleNgFactory} from './hello-world.module.ngfactory';
platformBrowser()
.bootstrapModuleFactory(HelloWorldModuleNgFactory)
.then(ref => {
const HelloWorldElement = createCustomElement(HelloWorld, { injector: ref.injector })
});
```
Beyond the obvious complexity of this approach, more dynamic scenarios become challenging, as the `PlatformRef` / `ApplicationRef` APIs are built to be bootstrapped once in a page's lifecycle.
Because the Ivy `NgComponentDef` is available directly on the Component class, the `ComponentFactory` and `ComponentFactoryResolver` are unnecessary. Additionally, Ivy no longer requires a `PlatformRef` to function.
The proposed API would no longer *require* an Injector, but will still allow it for it if a developer wishes to use DI in their Angular Element. In effect, `createCustomElement` allows developers to replace the Platform APIs with a lightweight, Elements-based "platform".
This makes standalone elements simple to define and register:
```typescript=
//proposed API
import {createCustomElement} from '@angular/elements';
import {HelloWorld} from './hello-world';
customElements.define('hello-world', createCustomElement(HelloWorld));
```
And allows for more complex usage with a parent injector if a developer requires this functionality:
```typescript=
//proposed API
import {createCustomElement} from '@angular/elements';
import {HelloWorld} from './hello-world';
const injector = getAParentInjectorFromSomewhere();
customElements.define('hello-world', createCustomElement(HelloWorld, {injector}));
```
This approach continues backwards compatibility for existing users of Angular Elements, and dramatically simplifies the approach for new use cases.
## New APIs: `NgHostElement` + Decorators
By definition, developers must extend from the `HTMLElement` constructor when defining a new Custom Element. Internally, Angular Elements implements this requirement as a class expression, and uses a private `HTMLElement` subclass, known internally as `NgElement`
This class is not currently public, and to allow developers some level of flexibility, the `strategyFactory` parameter allows developers to provide a custom "delegate" to mediate between the Custom Element and the Angular Component.
The functionality provided by the `StrategyFactory` API is hard to extend, and doesn't give advanced developers full access if their use case is not covered.
Instead, this RFC *deprecates* the `StrategyFactory` API and introduces a public base class - `NgHostElement`.
The `NgHostElement` serves as the bridge between the properties, attributes, events, and lifecycle of the "Host Element" and allows developers full access to these capabilities.
The `NgHostElement` itself has no concept of "rendering", in the same way Custom Elements do no provide a "render" API. Instead, it provides a set of lifecycle hooks:
```typescript=
import {NgHostElement, NgOnUpgrade, NgOnChanges, ...} from '@angular/elements'
export class MyHostElement extends NgHostElement implements NgOnUpgrade {
//lifecycle hooks
ngOnUpgrade(){
this.instance = renderComponent(MyComponent, {host: this});
}
ngOnChanges(){
detectChanges(this.instance);
}
//protected methods available for implementers.
ngDoBootstrap(){}
ngDetectChanges(){}
ngMarkForCheck(){}
}
customElements.define('demo-element', MyHostElement);
```
This base class should provide the basic lifecycle for Angular Elements, and enable specific behaviors to be mixed in. Using the base class by itself, without Angular Components is supported, and allows for more flexible use cases.
Similar to Angular Components, decorators can be used to add declarative functionality via runtime decoration or eventual compiler transformation.
## Decorators / Defs.
A minimal set of decorators is proposed in the initial design, which can be augmented over time as use cases demand and resources allow.
### `@NgElement({selector, props})`
- Associates a Custom Element class with a selector
- Allows for props to be declared inline
### `@Prop(propMeta)`
- Defines a property on the class with a setter/getter
- Declaratively describes how to transform and reflect properties/attributes.
- Triggers or marks for later CD
A developer authoring a "custom" NgElement with Angular Elements can leverage these decorators to define a "Host Element" that works in any application context.
These decorators define an `NgElementDef`, similar to an `NgComponentDef`, that the implementation can read to configure an element.
## Use Cases
The ability to re-use Angular Components seamlessly in any web application has potentially unlimited applications.
### Declarative Entry Components
Nearly every declarative web framework in the world shares one imperative API - Angular refers to this as "bootstrap" - associating some instance of a "Component" with a DOM node.
even with Ivy, this limitation remains- the simplest Hello World using Angular's declarative component model must be *imperatively* bootstrapped.
```typescript=
//ivy experimental api
import {renderComponent, Component} from '@angular/core'
@Component({
selector: 'hello-world',
template: 'Hello {{name}}!'
})
export class HelloWorld {
name = 'World'
}
renderComponent(HelloWorld);
```
Assuming `index.html` has a single `<hello-world>` tag, Angular emulates the native browser behavior of Custom Elements by querying the document for a `hello-world` selector and automatically bootstrapping a HelloWorld component onto it.
This emulation works for the vast majority of Single Page Applications, but must be adapted for more complex use cases, by passing a specific host node to the render function:
```htmlmixed=
<body>
<hello-world id="node1"></hello-world>
<hello-world id="node2"></hello-world>
</body>
```
```typescript=
//index.ts
import {renderComponent} from '@angular/core'
import {HelloWorld} from './hello-world'
const rootNodes = document.querySelectorAll('hello-world');
const instances = [...rootNodes]
.map(node => renderComponent(HelloWorld, { host: node }));
```
For cases more complex than this, Angular Elements provide a huge ergonomic improvment - developers define their Angular Element once:
```typescript=
import {createCustomElement} from '@angular/elements'
import {HelloWorld} from './hello-world'
customElements.define('hello-world'createCustomElement(HelloWorld));
```
Once registered, this Element is indistinguishable from any other DOM element, and requires no specialist knowledge to use.
### Legacy Migrations / NgUpgrade
Allowing Angular Components to be consumed trivially inside of any web application means that instead of big bang upgrades, or the need for framework-specific glue code, migrations from legacy tech stacks can be accomplished incrementally, enabling developers to begin to embed modern Angular functionality into non-Angular applications.
This approach does not give the full fidelity possible with a custom solution (like NgUpgrade), but this turns out to be a benefit in many cases - developers already understand the constraints of HTML, and constraining Angular to the same capabilities dramatically simplifies the mental model.
### Design Systems
Many developers have expressed interest in building Custom Elements with Angular to be define a Design System to be used across many sites and applications. Custom Elements are well suited for this case, but currently Angular's architecture is challenging to make work. Enabling lightweight Angular Elements will not *solve* these problems, but provides the first step towards this use case.
## Framework APIs
Angular Elements is well set up for the upcoming Ivy transition - retaining backwards compatibility is straightforward from a *technical* perspective, but it has become clear that the ability to author **Standalone** Components (and Elements) is a desired feature for many developers.
Changing this approach is theoretically enabled by Ivy, but is unlikey to occur before Ivy compatibility is complete. Standalone components are a non-blocker to progressing with Elements, but necessary to provide a first-class experience.
A full-fledged design will need to be developed with support from the framework team, but a hypothetical implementation that retains backward compatibility with NgModules.
## Future Work
A number of challenges and API designs can be incrementally designed and implemented once a foundation is put in place.
#### Styling APIs
- /ng-deep/ replacement via ::part and ::theme APIs
- Constructible Stylesheet Support
- Shadow DOM improvements
#### Lazy-loading
- Angular.io uses a custom solution to trigger loading of elements lazily when needed.
- Should be baked into platform.
## FAQ
//questions to answer here.