Repository: angular
Issue: Imperative View & Template Composition APIs
Labels: action: discuss,area: core,core: component extension and customization,needs: discussion,canonical

*Note 1: This proposal has its origin in [RFC: Exploration of use-cases for Angular JIT compilation mode](https://github.com/angular/angular/issues/43133) 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](https://youtu.be/FbJLC7GxAAs?t=1891) @ [angularnation.net](https://www.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:

1. 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.

2. Via [ViewContainerRef#createComponent](https://angular.io/api/core/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...


```ts
@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:

```ts
// 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](https://github.com/angular/angular/discussions/43784), 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"](https://angular.io/guide/creating-libraries#transitioning-libraries-to-partial-ivy-format) 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).