Imperative View & Template Composition APIs
See original GitHub issueNote 1: This proposal has its origin in RFC: Exploration of use-cases for Angular JIT compilation mode and aims to address the concerns around AOT compiler not being flexible enough for very dynamic, or heavily data-driven UIs.
Note 2: 📺 Check out the video recording of a community discussion about this proposal @ angularnation.net
Description
While declarative templating is great for most cases, developers often need more flexibility to create UIs dynamically, using components and directives (often) not known at the time of writing the component that generates this UI. Common use cases include data driven UIs (including forms), A/B experiments, UIs generated via plugin systems, etc.
Angular currently enables dynamic view or template creation only with major drawbacks in two ways:
-
Via the JIT compiler, which can take any string and compile it on the fly within the browser. This approach compromises security of the application (there is a high risk of introducing XSS and compromising the overall security model of Angular) and also comes with a major performance penalty due to the runtime cost of the compilation, and payload size of the overhead of the compiler.
-
Via ViewContainerRef#createComponent and similar APIs, which don’t offer enough flexibility to dynamically compose views out of a mixture of static HTML, HTML with dynamically created bindings, components, and directives.
Proposed solution
With Ivy rendering/compilation pipeline being the default and ViewEngine being removed in v13, we can finally start taking advantage of flexibility of Ivy without having to worry about compatibility with ViewEngine, this opens up new possibilities including the follow…
We could introduce a new set of APIs that are built specifically for dynamic view composition. A proper research needs to be conducted to determine the shape of the API, but we can use the following PLACEHOLDER APIs to demonstrate the functionality and serve as a foundation for discussion, brainstorming, and actual API design research…
@Component({
...
template: '<button (click)="renderDynamically()">render</button>'
...
})
class DemoComponent {
constructor(private viewContainerRef: ViewContainerRef) {}
protected async renderDynamically() {
const myCustomView = await createMyCustomView('./myBlue.component',
{userName: 'Kahless', myRoutePath: ['my-route'], showDetails: true});
// appends the view
viewContainerRef.appendEmbeddedView(myCustomView);
}
}
Where the templateRef
is generated dynamically as follows:
// this doesn't exist yet + API design is to be determined
import {createView, html, component, directive, text, binding, template} from "@angular/core/compose";
import {NgIf} from "@angular/core";
import {RouterLink} from "@angular/router";
import {MyRedComponent} "./myRed.component";
import {MyYellowComponent} "./myYellow.component";
async function createMyCustomView(blueComponentESModulePath, myContext) {
const blueComponent = (await import(blueComponentESModulePath)).default;
// this is where the magic happens
return createView([
html("<div>some static html, that will be automatically serialized by the `createTemplate` call</div>"),
component(MyYellowComponent, { projection: [
text("html string followed by a binding"),
// {{ userName }}
binding(() => myContext.userName),
// you can use javascript expressions instead of `NgIf` to conditionally add view "nodes":
(Math.random() > 0.5) && text(" and followed by another component"),
// <my-red [routeLink]="myRoutePath"></my-red>
component(MyRedComponent, { directives: [
directive(RouterLink , {binding: () => myContext.myRoutePath})
]}),
// but NgIf works as well:
// <p *ngIf="showDetails">Some details</p>
template([
text("some details")
], {directives: [
directive(NgIf, {binding: () => myContext.showDetails})
]})
// NgForOf, NgSwitch, and any custom regular or structural directives would work as well
// TODO: update the proposal with a built-in support for structural directives in `@angular/common`,
// e.g. `ngIfTemplate` - see suggestion from @sg-gdarnell in the comments below.
// TODO: update the proposal with an example of how to use pipes in bindings as requested by @ha100790tag
]})
]);
}
How would this be implemented?
Functions template
, component
, directive
, text
would be just higher level wrappers around the private Ivy instruction APIs.
Risks & unresolve stuff
There is a lot that needs to be researched in this area to determine the complexity of the implementation and overall viability. The following is an incomplete list of unresolved questions / issues:
- The existing
ViewContainerRef
APIs accept only TemplateRefs or Components and not views directly. Is this a problem? - Do we need to support dynamic template composition (outputting TemplateRef) as well? Or is view composition sufficient?
- Are current Ivy instruction APIs flexible enough to accomodate this feature or do we need to tweak them?
- Can we keep all existing security guarantees to prevent XSS?
- How to attach the new view to the correct injector in the parent?
- Is the existing $localize API sufficient to support localization?
- Should we limit this API just to upcoming “standalone” components, directives, and pipes, or can we make this API work with entities that are part of an NgModule without a major hassle?
- Could this composition API eventually replace the “partial compilation” used to publish Angular libraries? Maybe, it’s a bit of a stretch, but it’s worth a further exploration…
- and many more…
As a result of these, you can expect major revisions of the proposed solution. The goal of this effort and use-cases this functionality would support should however remain the same.
Are there any trade offs?
Of course, there are always some trade offs.
The main ones that come to my mind:
- We don’t plan on supporting templates as strings. Only static html or text (without bindings, Angular components or directives) could be provided as string) — such feature would require the availability of the entire JIT compiler at runtime, which would go against the very core goal of this proposal.
- Performance of creation of dynamically composed views will likely not be as good as that of AOT compiled code. The update performance would be less impacted. There are ways we could improve perf of dynamically created views (e.g. by supporting dynamic template creation), but more research is needed.
- The API is more verbose than the declarative template syntax. Since the goal here is to enable dynamic view creation using imperative JavaScript/TypeScript code, we are trading off some verbosity for flexibility of the imperative coding.
- Limited tree-shaking impact — the higher level API might need to retain a few instructions that would otherwise be tree-shaken away if the template was created using AOT compilation, but the impact should be very limited, especially if each higher level API is a standalone tree-shakable function (as proposed in the example above).
What’s the goal of this issue?
The goal is to write down some of the ideas that have been made possible thanks to Ivy and start collecting early feedback that could be used as an input for future RFC.
Alternatives considered
Expose the Ivy instructions as a public API. This has a major drawback that these APIs are low level (not developer friendly) and not stable (it would be hard to evolve the rendering stack if these APIs were public).
Issue Analytics
- State:
- Created 2 years ago
- Reactions:210
- Comments:53 (33 by maintainers)
Top GitHub Comments
@SanderElias thanks for all the questions. I think @alxhub fielded all/most of them already.
I just want to chime in on:
Dynamic template creation out of templates is something that I’d really like to avoid - it would basically be just a glorified way of using the current JIT compiler in production, which has lots and lots of problems (critical security, performance, and maintenance issues). I have an RFC coming out today, exploring the possibility of eventually removing JIT compilation mode entirely from Angular. I should post it within the next few hours — please stay tuned.
The goal of this proposal is to enable dynamic composition of views in a way that is performant and secure. Many of us would like to have the good old
$compile
back from the AngularJS days, and this proposal tries to strike the balance between the flexibility of$compile
and security and performance of AOT compilation.View Composition is something that we are waiting since 2016 so this is a great news. But (always there is a “but” we use to say), the flat structure is absolutely not enough for us. I try to explain our project.
We created a UI Framework Platform, based on Angular, that allow Application Developers to create pages using an interactive designer at runtime without the need to code the corresponding components and compile it.
Simplifying a lot to understand the concept:
When a user access to this page…