Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Imperative View & Template Composition APIs #43120

Open
IgorMinar opened this issue Aug 11, 2021 · 55 comments
Open

Imperative View & Template Composition APIs #43120

IgorMinar opened this issue Aug 11, 2021 · 55 comments
Labels
action: discuss area: core Issues related to the framework runtime canonical This issue represents a canonical design issue in Angular. core: component extension and customization needs: discussion Indicates than an issue is an open discussion
Milestone

Comments

@IgorMinar
Copy link
Contributor

IgorMinar commented Aug 11, 2021

Note 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:

  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 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).

@IgorMinar IgorMinar added action: discuss area: core Issues related to the framework runtime needs: discussion Indicates than an issue is an open discussion feature Issue that requests a new feature labels Aug 11, 2021
@ngbot ngbot bot modified the milestone: Backlog Aug 11, 2021
@SanderElias
Copy link
Contributor

Question: Isn't it possible to create a small public layer on top of the Ivy instruction set?
This will create a low-level API that still allows evolving the rendering stack, while also providing a flexible basis to build higher-level abstractions.

@SanderElias
Copy link
Contributor

Another question: We can already mix-and-match more dynamic templates by using Angular Elements(web components generated by angular).
The current downside of using those is that you can't tell the language server you have those, so you need to turn off most of the syntax checking for templates.
Would fixing that, not already bring a lot of what this RFC is bringing?

@SanderElias
Copy link
Contributor

SanderElias commented Aug 12, 2021

@IgorMinar Thanks for this proposal! It really looks good.
What I'm missing in the createView array, is an option to add a portion using normal template syntax, something like:

  // this is where the magic happens
  return createView([
    html("<div>some static html, that will be automatically serialized by the `createTemplate` call</div>"),
    tempateText(`
          <app-user [userId]="userId"><app-user>
           <ul>
              <li *nfFor="let pref of preferences>{{pref.name} - {{pref.value}}</li>
           </ul>`)
     } ) ; 
    } ]) 
  ] ) ; 

Where it uses the same context as the myContext in your sample for the template.

Adding support for "normal" templates will name the use of this API a lot simpler. I know using it will need to ship the full Ivy compiler to the client, but I would like to be able to make that trade-off.

@alxhub
Copy link
Member

alxhub commented Aug 12, 2021

Question: Isn't it possible to create a small public layer on top of the Ivy instruction set?

I think that's exactly what this is - it's an API to imperatively construct an Ivy template function, but which doesn't require you to have specific knowledge about how to call the actual template instructions.

We can already mix-and-match more dynamic templates by using Angular Elements(web components generated by angular).
The current downside of using those is that you can't tell the language server you have those, so you need to turn off most of the syntax checking for templates.
Would fixing that, not already bring a lot of what this RFC is bringing?

I don't think so, because such components still need to be written statically, even if their instantiation is more dynamic. This proposal allows for very dynamic constructions of views, based on runtime data structures.

What I'm missing in the createView array, is an option to add a portion using normal template syntax,

This is a cool idea, too. One of the concerns here is that it would require some notion of a template scope (what @NgModule provides today for statically compiled components) and that might get very tricky or messy to specify. But it could be done.

@SanderElias
Copy link
Contributor

@alxhub Thanks for the quick answers! It is good to have this cleared out of the way.
I do like your clarification on how this is a different dynamic as using Angular elements.

This is a cool idea, too. One of the concerns here is that it would require some notion of a template scope (what @NgModule provides today for statically compiled components) and that might get very tricky or messy to specify. But it could be done.

This would be the same thing as it is for stand-alone(module-less) components, or am I looking from the wrong side? Those problems need to be solved anyway for truly dynamic components as far as I can see.

@alxhub
Copy link
Member

alxhub commented Aug 12, 2021

@alxhub Thanks for the quick answers! It is good to have this cleared out of the way.
I do like your clarification on how this is a different dynamic as using Angular elements.

This is a cool idea, too. One of the concerns here is that it would require some notion of a template scope (what @NgModule provides today for statically compiled components) and that might get very tricky or messy to specify. But it could be done.

This would be the same thing as it is for stand-alone(module-less) components, or am I looking from the wrong side? Those problems need to be solved anyway for truly dynamic components as far as I can see.

Sort of. The problem is that this scope has to be defined at runtime instead of compile time.

One of the challenges here is that Angular actually erases scope information for NgModules during compilation - this prevents all components/directives/pipes in an application from being referenced by NgModules and thus ineligible for tree-shaking. Having a dynamic, runtime way to specify a compilation scope would require retaining this information across the board, since we can't know ahead of time which NgModules would be referenced from such a dynamic scope.

@IgorMinar
Copy link
Contributor Author

IgorMinar commented Aug 12, 2021

@SanderElias thanks for all the questions. I think @alxhub fielded all/most of them already.

I just want to chime in on:

    tempateText(`
          <app-user [userId]="userId"><app-user>
           <ul>
              <li *nfFor="let pref of preferences>{{pref.name} - {{pref.value}}</li>
           </ul>`)
     } ) ; 

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.

@IgorMinar
Copy link
Contributor Author

IgorMinar commented Aug 12, 2021

I added a new section to the issue above describing the trade offs of this proposal.

@IgorMinar
Copy link
Contributor Author

@petebacondarwin kindly pointed out that my example TS code was not syntactically valid due to typos, misaligned braces, and most problematically the use of if within an array literal. I updated the code snippet to fix these. Thanks Pete!

@jnizet
Copy link
Contributor

jnizet commented Aug 12, 2021

@IgorMinar the first snippet has a const named myCustomView, created by calling the function createView. But the second snippet doesn't define a createView function. Instead it defines a myCustomView function calling another (framework-provided) createView function. I was a bit troubled when trying to understand it. Some renaming would make the code corrrect, and thus easier to understand IMHO. I guess some await and asyncare missing too

  protected async renderDynamically() {
    const myCustomView = await createMyCustomView('./myBlue.component',
      {userName: 'Kahless', myRoutePath: ['my-route'], showDetails: true});

    // appends the view
    viewContainerRef.appendEmbeddedView(myCustomView);
  }
  
  async function createMyCustomView(blueComponentESModulePath: string, myContext: {[key: string]: any}) {
    const blueComponent = (await import(blueComponentESModulePath)).default;

    // this is where the magic happens
    return createView(...);
  }

@IgorMinar
Copy link
Contributor Author

IgorMinar commented Aug 12, 2021

@jnizet thanks you so much for pointing this out! I think I updated the code with all of your suggestions.

I expected the code to have some minor syntax flaws when I was typing it out in the github issue editor, but I see that the next time I do something like this I should instead create a stackblitz that compiles but doesn't run.

@sg-gdarnell
Copy link

sg-gdarnell commented Aug 12, 2021

I love this, it's basically what I always hoped Ivy would enable.

However, I'm confused about how updates will work. I realize the syntax is nowhere close to final, but I'll use it as a reference for discussion.

In the example, if you used plain JavaScript expressions (if, switch, for, etc.) in your createView() call instead of NgIf, NgSwitch, NgFor, etc., wouldn't you have to destroy and recreate your view each time your context changed (expensive)? If you limited yourself to using the built-in structural directives instead then you could simply pass a new context to your embedded view, but I can imagine that devs familiar with React might assume that plain expressions would "just work" as they do in React, which might be a pain point.

If we do need to limit ourselves to using structural directives over plain expressions then it may be appropriate to provide some helper functions for the built-in stuff, so maybe instead of this:

template([
       text("some details")
       ], {directives: [
         directive(NgIf, {binding: () => myContext.showDetails})
       ]})

You could do:

ngIfTemplate(
  [text("some details")],
  () => myContext.showDetails
)

Of course, the community could build helper functions like that themselves. My main concern is that the way expressions restrict the ability to update a view may trip people up.

Thanks for sharing this openly! I'm excited for all the things a more dynamic Angular could bring.

@IgorMinar
Copy link
Contributor Author

IgorMinar commented Aug 12, 2021

@sg-gdarnell great points, thanks for bringing them up!

The important thing to note is that the current design doesn't rely on reevaluating the createMyCustomView function on each change detection tick. Instead, this function executes just ones. If you need the view to have conditional parts that respond to model changes and change-detection after the view has been created and attached, then you'd have to use NgIf, NgForOf, etc.

This brings me to the second part of your suggestion, it absolutely makes sense to add native support for NgIf and friends using an API like ngIfTemplate. I'll think about it a bit more, and update the proposal to include this suggestion.

@manfredsteyer
Copy link
Contributor

Awesome proposal! Thanks! (+ thanks for the hidden Star Trek reference)

@IgorMinar
Copy link
Contributor Author

(+ thanks for the hidden Star Trek reference)

@manfredsteyer wow, that was fast! I thought that it would take longer for someone to connect the dots. #highFive err... #elbowBump

@SanderElias
Copy link
Contributor

@IgorMinar Thanks for the additions and the answers.
It is good to know that template compilation is out of the scope of this proposal. Makes a lot of sense.

After having seen enough examples of how compile$ was used, I was always glad it wasn't available in Angular 😉 (I'm not afraid to say, that a few of those were of my own hand tho 😁)
And I will not shed any tears on seeing the JIT compiler go when there is an alternative like this is in place.

Now a new question arises:

      // you can use javascript expressions instead of `NgIf` to conditionally add view "nodes":
      (Math.random() > 0.5) && text(" and followed by another component"),

That will put false into the array literal. No problem there. But it brings me to the question.
What will be rendered into the view? Will the createView function just filter out anything that's not a known-thing™? (Or a function that returns a known-thing™?)
Or will it just ignore falsy values, and render the rest as text (I hope this is not going to happen!)

@IgorMinar
Copy link
Contributor Author

IgorMinar commented Aug 13, 2021

@SanderElias this API is really just a placeholder for something more solid and refined. But a solution could be to just ignore unknown entities.

I used the nested array API design because I wanted to avoid:

  • fluent/chaining API (like RxJS 5) because they don't tree-shake, or
  • typical AST API (like babel or typescript AST) because they are hard to read, and are unnecessarily complex

A lightweight and declarative-ish API design is preferable, because we don't need ability to modify or rearrange nodes, we just need an api to declare a node hierarchy and nothing else.

We could experiment with "pipe-able" design like RxJS 6+, but that might be an overkill as well. All of this is still TBD.

@ha100790tag
Copy link

Very interesting proposal! Would be greate to have an example of using pipes in bindings and conditions as it's also a very important part of building templates.

@IgorMinar
Copy link
Contributor Author

@ha100790tag good point. I'll add a todo.

@ha100790tag
Copy link

Some more points:

  1. example with *ngIf: the text function should render html string provided (correct me please if I got something wrong). The text provided to the function does not contain any tags. So instead of <p *ngIf='condition' > I thing it should render something like <ng-container *ngIf='condition' > as we have pure text node.

  2. I thought about potential type checking for such approach and can't find a proper solution. So the bindings should return the values for directive inputs... Have no idea how to make typescript understand that some object property was decorated as input and check it's type.

Not so long ago angular got template type check update that helps avoiding a lot of errors. In case we will skip type check here the idea might be miss understood as just a way to pass by type checking by beginners.

*this messages was written from phone late at night but I hope it's clear)

@chaosmonster
Copy link

chaosmonster commented Aug 17, 2021

A great feature for dynamic createView and as an solution to @SanderElias question would be a tool which can consume an angular component as input and return the above createView array.

This could be a visual tool or part of the cli

Could the angular compiler do that @IgorMinar?
Could this feature be considered in #43165

@bcarson
Copy link
Contributor

bcarson commented Aug 19, 2021

We had a great discussion about this RFC on Angular Nation yesterday, thanks to @IgorMinar, @alxhub @SanderElias, @chaosmonster, @kroeder, @bampakoa and everyone else who joined us! Here's the link for anyone who would like to watch: https://www.youtube.com/watch?v=FbJLC7GxAAs

@Wintermoose
Copy link

I like the proposal, though it seems the usage won't be easy - then again such code should be quite localized to special dynamic components, not spread around the whole application, so it should be manageable (and I would echo the call for helper (offline) compiler that can convert string template into sample code: in a vaguely analogous situation, writing C# expression trees, being able to inspect regular compiler output saved me many hours)

You are asking about the need to support dynamic TemplateRef generation - it's probably not absolutely essential, but I would really like it. In most use cases I have in mind we would want to drive our UI with data - table definitions, charts, tooltips etc. Being able to generate a template and integrate it with the existing code would be excellent.

@IgorMinar
Copy link
Contributor Author

BTW, I believe an array structure isn't the simplest way to create those views. Views are trees by definition.
Something along those lines: (far from complete or even correct, it is about the idea!)

@SanderElias yeah. I'm sure that there is a better API to use than an array of arrays. I just used that as a place holder because it's easy to understand, but obviously it's not the most type-safe design. All of this is TBD once we actually dedicate time to design this stuff. For now I'm just trying to see if this feature is interesting to the community at all.

@IgorMinar
Copy link
Contributor Author

IgorMinar commented Aug 24, 2021

However, I am wondering how this approach could work with events.

@profanis @chaosmonster I think the approach in the comment above is roughly right, except that we'd most likely have helpers for directly creating elements that supports adding event bindings (e.g. element('button', {events: {click: () => { ... }}})).

One thing we do need to avoid is a pattern where a string translates to code or function name, like for example on-click='clickHandler' that was mentioned earlier – this string "execution" is how XSS happens, and that's what we are trying to avoid with this proposal.

@IgorMinar
Copy link
Contributor Author

We made a small library called tshtml that helps with defining complex page templates. ... it can be seen on the projects page: https://github.com/xorets/tshtml

Interesting! Thanks for sharing @xorets. This is an interesting design direction to explore further once we are actually designing the API.

@LayZeeDK
Copy link
Contributor

Is it possible to create this API in a way that allows tree-shaking away the entire API or its individual parts?

@IgorMinar
Copy link
Contributor Author

@LayZeeDK the composition API should be designed as tree-shakable API. The API sketch in the description has that property. If however you want to build an API on top of this API that creates views from either JSON or strings then you only have three options:

  • load all composition API functions up front
  • need AOT compiler (which kind of defeats the purpose)
  • make all composition API functions always available for your app, but lazy-load them as needed

@LayZeeDK
Copy link
Contributor

Those are interesting perspectives to keep in mind, thanks @IgorMinar!

@chihab
Copy link
Contributor

chihab commented Aug 24, 2021

It might be interesting to have a playground that generates the imperative instructions for a provided template.

One would put this template in the playground:

<div>some static html, that will be automatically serialized by the `createTemplate` call</div>
<my-yellow>
  html string followed by a binding {{ userName }} and followed by another component
 <my-red [routeLink]="myRoutePath"></my-red>
 <p *ngIf="showDetails">Some details</p>
</my-yellow>

and get the imperative code above as a result (except the Random part obviously).

@fkolar
Copy link

fkolar commented Aug 25, 2021

BTW, I believe an array structure isn't the simplest way to create those views. Views are trees by definition.
Something along those lines: (far from complete or even correct, it is about the idea!)

type Pipe = (...args: any[]) => any;
type Binding = {
  (context:Object): any;
  pipes: Pipe[]
};

interface Attribute {
  name: string;
  value: string | Binding;
}

interface Directives {
  class: Object; // the className of the directive.
  value: string|Binding;
}

interface ViewNode {
  type: 'text' | 'html' | 'comment' | 'fragment' |'domElement'| 'component'|'viewNode';
  content?: string | Binding;
  context?: Object;
  children?: viewNode[];
  directives?: Directives[];
  attributes?: Attribute[];
}

All of the helpers would return a ViewNode every type would have its own restrictions of course.
This would make composing a view more natural to someone that is used to work with DOM

I believe the array structure its it a bit hard to use for really dynamically assembled layers, which you for example receive from some REST-End point. I agree with Ellias, it needs to be really an API, that contains also some objects to carry on the tree.

const button =   api.createComponent(MyButton, context,  bindingsMap)
const label =   api.createComponent(MyLabel, context,  bindingsMap)

button.applyDirective(MyStateColorDirective);
button.addContent(label)

I think it needs to be composable by Object (Tree) instead of array, otherwise it will be hard to work with this in dynamic manner.

Just like DOM API:

  • You create element
  • You can add this element to the view
  • Element has other API method to add children or set attributes, style, classes, ...etc..

To make it composable by array, I am not really use about this idea and working with strings.

@fkolar
Copy link

fkolar commented Aug 25, 2021

We can also look at how we can use dynamic imports to load some components from the 3thparty, cuz not all the components are known and you just know that you want to use import {MatCardModule} from '@angular/material/card', which you receive from DB, and you want to import it and use as part of this Dynamic API.

@ashley-hunter
Copy link
Contributor

Proposal looks great! I can see this being very useful and open up lots of new possibilities! I realise the API above is not representative of the what it would be like if implemented, but one thing that we take advantage of is component attribute selectors and multiple selectors.

I'm curious to how this would be handled with the composition api, for example:

@Component({
    selector: '[uxa-button]'
})
export class UxaButtonComponent {}

With the following composition api:

component(UxaButtonComponent, { 
  projection: [
    text("projected content"),
  ]
});

Ideally we would want this to be applied to a button element (or perhaps an a element in this case) but there is no way to indicate this in the composition api or derive it from the component selector (I realise the selector in this specific case could be updated to button[uxa-button] but there are many cases where the element tag is not so limited). So perhaps we would need a way to define that in such cases:

component(UxaButtonComponent, { 
  element: 'button',
  projection: [
    text("projected content"),
  ]
});

And if not provided in such cases would it default to a div or throw an error?

The other related scenario is when there are multiple selectors:

@Component({
    selector: '[uxa-button], [uxa-icon-button]'
})
export class UxaButtonComponent {}

We would potentially need to have a way to determine which selector should be used, for example I believe the material button checks which attributes are present on the host element (https://github.com/angular/components/blob/master/src/material/button/button.ts#L99), so behavior or appearance may differ based on which selector is actually used.

Obviously there is no API actually designed for this yet, so this is mainly just intended to outline some additional use cases to be considered 😊.

Lots of great proposals recently, the future of Angular looks very exciting, thanks for all the hard work!

@quanterion
Copy link

Great proposal! I hope it enables us to reuse projections. For example if I have multiple components all of them have inside them array of loaded via HtppClient. I would like to extract logic for getting list of options into separate function or service.
As far as I can tell this proposal enable us to create a view that contains array of

But I have two questions:

  1. How can i use this view to defined content projection of component declared inside HTML.
<mat-select>
  {{getArrayOfMatOptionsView()}}
</mat-select>

Will it work this way?

  1. I think it is helpful to be able provide Promises and Observables as a view items and handle loading of views at framework level. Will it makes sense?

@vnesterovsky
Copy link

Am I right that current template syntax is not going to be removed in any case in favor of any other template API?

If yes, then having a good intention to enhance existing templates you just add one more API to solve the same task.
Won't it confuse developers more than it is assumed to help?

Besides, in my opinion, current templates are more data driven, which means it's easier to store them, e.g. in database, while composition API is more code driven, which raises questions about how substitutable those two API can be.

@gmaggiodev
Copy link

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:

  • Our UI designer tool will generate this JSON inside the DB:
{
    "type": "grid-layout",
    "items": [
        {
            "type": "card",
            "properties": {
                "cols": 4,
                "title": "Alex Dornick",
                "subtitle": "Front-End Developer",
                "imgSrc": "/assets/images/alex.JPG",
                "imgMode": "avatar"
            },
            "items": [{
                "type": "simple-panel",
                "properties": {
                    "position": "footer"
                },
                "items": [
                    {
                        "type": "label",
                        "properties": {
                            "repeat": "let i of [1,2,3]",
                            "text": "Label {{i}}"
                        }
                    }
                ]
            }]
        }
    ]
}

When a user access to this page...

  • Our UI Engine transform JSON into angular HTML that will be incorporated as view inside a runtime compiled component/module:
<tf-grid-layout id="dfd32969-0bc9-4fa7-b57e-02f7ddd7cfc3">
    <tf-card cols="4" title="Alex Dornick" subtitle="Front-End Developer" imgSrc="/assets/images/alex.JPG" imgMode="avatar" id="cc33f245-dcfc-4d39-b24d-291430a38048">
        <tf-simple-panel footer id="807e08ab-8a93-44e0-8544-3af2f9ff057d">
            <ng-template ngFor let-let i [ngForOf]="[1,2,3]" let-i="index">
                <tf-label text="Label {{i}}" id="4ef3dfb0-e3d8-4a0a-b255-ff173c188ed0"></tf-label>
            </ng-template>
        </tf-simple-panel>
    </tf-card>
</tf-grid-layout>
  • And the component is rendered immediatly and interact with the application

image

All this work, obviously, is based on JIT. A flat imperative view is not enough.
Please, @IgorMinar consider it in your work.

@jeme
Copy link

jeme commented Jan 26, 2022

Hello.

Reading thought this as well as reading through all the related topics is a bit concerning to us. We are in the process of evaluating how feasible it is to finally switch from AngularJS to Angular (13 as of this writing).

And we use $compile in the old solution to enable the use of AngularJS Directives/Components in content provided by the users. And so obviously JIT sounds like the thing we are looking for in Angular to replace this, and then finding this is ofc raises quite a big concern for us. (Security is of less concern to us in regards to this as it's a closed of system, but is something in the back of our minds, and I understand the argument, yet it could then be an option that is opt in).

In our case, content is provided by the users via TinyMCE. So the content is provided as HTML as that is what we get out of TinyMCE. The place where AngularJS directives/components come into the picture is with a custom set of plugins to TinyMCE that allows the user to add certain blocks to the source HTML (what he/she is editing) - Once the user submits the content and it's rendered in the client, these custom blocks are actually AngularJS components/directives which are then activated.

Because of this I am having a very hard time wrapping my head around doing the same thing in Angular with the constraints set forth here without a insurmountable amount of trouble. (Essentially having to build our own sort of compiler that translate into the above instructions? that seems like redoing something that is already in Angular after all)

A sandboxed compiler experience would be welcome where it's possible to limit what components, directives, pipes (essentially modules?) that are recognized. But having to use the above "syntax" seems to be a very tough pill for us to swallow with what we know at this point.

@torsolaso
Copy link

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:

  • Our UI designer tool will generate this JSON inside the DB:
{
    "type": "grid-layout",
    "items": [
        {
            "type": "card",
            "properties": {
                "cols": 4,
                "title": "Alex Dornick",
                "subtitle": "Front-End Developer",
                "imgSrc": "/assets/images/alex.JPG",
                "imgMode": "avatar"
            },
            "items": [{
                "type": "simple-panel",
                "properties": {
                    "position": "footer"
                },
                "items": [
                    {
                        "type": "label",
                        "properties": {
                            "repeat": "let i of [1,2,3]",
                            "text": "Label {{i}}"
                        }
                    }
                ]
            }]
        }
    ]
}

When a user access to this page...

  • Our UI Engine transform JSON into angular HTML that will be incorporated as view inside a runtime compiled component/module:
<tf-grid-layout id="dfd32969-0bc9-4fa7-b57e-02f7ddd7cfc3">
    <tf-card cols="4" title="Alex Dornick" subtitle="Front-End Developer" imgSrc="/assets/images/alex.JPG" imgMode="avatar" id="cc33f245-dcfc-4d39-b24d-291430a38048">
        <tf-simple-panel footer id="807e08ab-8a93-44e0-8544-3af2f9ff057d">
            <ng-template ngFor let-let i [ngForOf]="[1,2,3]" let-i="index">
                <tf-label text="Label {{i}}" id="4ef3dfb0-e3d8-4a0a-b255-ff173c188ed0"></tf-label>
            </ng-template>
        </tf-simple-panel>
    </tf-card>
</tf-grid-layout>
  • And the component is rendered immediatly and interact with the application

image

All this work, obviously, is based on JIT. A flat imperative view is not enough.
Please, @IgorMinar consider it in your work.

I am interested on this behaviour. It's extremely useful to have the opportunity to draw JSON data in the UI using this feature. When changing from JIT to AOT this feature will disappear? There will be an alternative used by a project for using data inside JSONs and give to the user a friendly interface?

@zijianhuang
Copy link

The use case is for MailMerge. The frontend app provides some JSON data object, and merge with a HTML template drafted by a computer literate among end users. The merged HTML result will be used as report or Email message. People with skills in MS Word MailMerge should be able to pick up MailMerge based on Angular HTML template.
Therefore, I would expect the design of the API should make it easier for app developers to develop such User Experience for end users.

@mrjfalk
Copy link

mrjfalk commented Jun 7, 2022

Unfortunately I didn't see the RFC in time (we just started with Angular), but for us the option to compile angular HTML string templates at runtime is very critical to our current application. We have a plugin-like system where we essentially do the following at runtime:

  • Fetches HTML, CSS, and typescript files over a websocket
  • Transpiles the typescript file which contains "Angular components" (but using a custom decorator) to JavaScript
  • Compiles these as angular components, along with the fetched HTML/CSS, using the Angular JIT compiler

For us this works great and allows building really flexible dynamic plugin UI's. We haven't had any issues with the performance yet, the bundle size is obviously large due to including both the TypeScript and Angular JIT compilers, but the application runs on a local network, so that is not an issue.

@MOALANGSVG
Copy link

We have an application that must display SVGs (html) that we must interact with click and ngIFs and binging, the SVGs come from the db as a string, their numbers are variable, and since all these SVGs contain the same function ( CloseOpen(){this.condition = !this.condition ; if(this.condition) {this.stats = 'open'}else{this.stats='close'} ) with *ngIF='condition' for display inside the SVG a tag) according to the variable 'condition: boolean', we have SVGs with images like: EYE, DOOR, WINDOW…ect each svg contains the two states of the same image ( SVG eye: eye closed and open eye, SVG door: closed door and open door….)
Example of the structure of its SVGs:

<svg width="100" height="100" *ngIF="condition" (click)="CloseOpen()">
   <circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
</svg>
<svg width="100" height="100" *ngIF="!condition" (click)="CloseOpen()">
   <circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="black" />
</svg>
<p>{{stats}}</p>

As mentioned above we don't know how many svgs we have but since they all look alike we chose to create a single component which contains the 'CloseOpen()' function and the 'condition: boolean = true;' variables. and 'stats:string=open;' and to associate the svg that will be associated with this component dynamically as we click on link eye or door , or window… the proper SVG will be associated with this component, so we have a component with differentiate html (Template) associated with it dynamically, with angularjs we do $compile which works well, but since we want to switch to angular the most recent possible (currently 13 or 14) we came across the AOT constraint of ivy the SVG is displayed but there is no compilation or interaction with the dynamically generated SVG, and the API you are citing is not understandable if possible examples on its usage would be a blessing.

@vojtechmarek-dev
Copy link

vojtechmarek-dev commented Mar 27, 2024

As others, I unfortunatelly have missed the RFC.
We are currently upgrading our app from Angular 12 and hit a wall since on 13+ the hybrid AOT+JIT options no longer works.

The concept is important for us because we have templates stored on db, where templates can be configured on gui level and then renedered on demand. This however is probably only 20% of our application, whereas the rest are static components.

We are reluctant give up the benefit of having 80% of our app compiled Ahead Of Time so we are considering staying on old version until this proposed API (or an alternative) is implemented.

However I would like to ask what is the state of this proposal? There has not been any update about this for quite some time.

@dennnisk
Copy link

dennnisk commented Apr 9, 2024

We have an ERP software built in Java and AngularJS, which serves as our "BASE," so to speak. It supports dynamic UIs, but through an API, and currently retrieves and compiles HTML/JS.

So, generic business rules belong to the standard project, however, many resources are customized and stored within the client's DB, being loaded into AngularJS (currently) and rendered (HTML, CSS, Javascript, etc). These resources are quite complex objects, sometimes encompassing various components and screens.

The base project was started in 2012/2013, using AngularJS, and we are researching to update the entire frontend structure. While researching how to achieve the same functionality in Angular, I came across this RFC, which, like many, we "missed" because we didn't follow the project so closely.

Anyway, we have something very similar to what mrjfalk has developed.

For us, what JIT allows would be ideal, and since these features will be discontinued in Angular, we are considering 2 hypotheses:

  • Developing custom resources through Web-Components (more or less like micro-frontends) as separate Angular projects possibly compiled on the server side; or;
  • Using another framework that allows us to utilize the feature of rendering on the frontend, or rendering part on the backend and sending "complex components" to the frontend, customizing part of the software.

We understand the security issue involved with the use of JIT, but for our reality it permit the flexibility we need.
Anyway, just sharing another case. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
action: discuss area: core Issues related to the framework runtime canonical This issue represents a canonical design issue in Angular. core: component extension and customization needs: discussion Indicates than an issue is an open discussion
Projects
None yet
Development

No branches or pull requests