Sitelet https://web.archive.org/web/20220131125103/https://github.com/angular/angular/issues/35328
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

Support for dynamic Angular Elements / Webcomponents (with Renderers) #35328

Open
tomastrajan opened this issue Feb 11, 2020 · 17 comments
Open

Support for dynamic Angular Elements / Webcomponents (with Renderers) #35328

tomastrajan opened this issue Feb 11, 2020 · 17 comments

Comments

@tomastrajan
Copy link
Contributor

@tomastrajan tomastrajan commented Feb 11, 2020 •

🚀 feature request

Relevant Package

The functionality relates to the @angular/platform-browser package as this is the package which implements DomRendererFactory2 and DefaultDomRenderer2

Description

Angular Elements and Webcomponents in general represent great solution for various real world use cases like...

  • sub-applications - ability to deploy library independently which will update all consumers without need to rebuild these consumer SPAs because lib is only referenced by url (as Angular Element bundle)
  • microfrontends ( runtime-configurable )

Usual approach to consuming Angular Elements and Web components leaves us with 2 options:

  1. eager loading + standard Angular template binding <some-element [prop]="value">
  2. lazy loading + imperative component creation and props / event binding document.createElement('some-element'); ...

The library @angular-extensions/elements solves this by enabling both lazy loading and standard Angular template binding <some-element *axLazyElement="bundle.js" [prop]="value"> and this works with both ViewEngine and IVY...

Dynamic element use case

In previous section we're committing to element being <some-element> by hard-coding it into template of the consumer Angular component. What if we wanted to support loading element configuration at runtime (eg from backend) to enable fully dynamic microfrontends?

As it turns out this works too (at least for the ViewEngine) so it is possible to write <ax-lazy-element *axLazyElementDynamic="'some-element'; url: 'bundle.js'" [prop]="value"></ax-lazy-element>.
The bundle will be lazy loaded, and then the element will be rendered as <some-element>.
The only thing needed to do that is to override value of the tagName in the TemplateRef of the *axLazyElemendDynamic directive and Angular will render desired element supporting our use case.

Now this does NOT work with IVY since in IVY template is an function.
Rob @robwormald gave me hint that it should be possible to override Renderer (or use custom) to hook into rendering and override element there.

The Issue

It is possible to achieve that with render BUT:

  • currently both DomRendererFactory2 and DefaultDomRenderer2 are private so it is NOT possible to extend them and hence I would need to re-implement the whole renderer which sounds pretty excessive and hard to maintain
  • PLUS as far as I am aware the render API changed older NRWL article so there is no more RootRenderer ?

Current solution

Play around with live demo (which serves both lib and demo and rebuilds on changes to any of them) using this one liner git clone https://github.com/angular-extensions/elements.git elements && cd elements && npm ci && npm start once it runs, please navigate to Examples > Dynamic to see it in action.

@Directive({
  selector: '[axLazyElementDynamic]'
})
export class LazyElementDynamicDirective implements OnInit {
  @Input('axLazyElementDynamic') tag: string;
  @Input('axLazyElementDynamicUrl') url: string;

  constructor(
    @Inject(DOCUMENT) private document: Document,
    private renderer: Renderer2,
    private cdr: ChangeDetectorRef,
    private template: TemplateRef<any>,
    private elementsLoaderService: LazyElementsLoaderService
  ) {}

  ngOnInit() {
    this.elementsLoaderService
      .loadElement(this.url)
      .then(() => {
        this.vcr.clear();
        const originalCreateElement = this.renderer.createElement;
        this.renderer.createElement = (name: string, namespace: string) => { // temporary override 
          if (name === 'ax-lazy-element') {
            name = this.tag;
          }
          return this.document.createElement(name);
        };
        this.vcr.createEmbeddedView(this.template);
        this.renderer.createElement = originalCreateElement;
        this.cdr.markForCheck();
      })
  }
}

Describe the solution you'd like

Probably being able to easily extend default renderer and being able to register it like we're used to with interceptors or control value accessors.

I would like to give people option (or do it for them behind the scenes) to do

@NgModule({
   import: [LazyElementsModule],
  providers: [{ provide: Renderer, useClass: LazyElementsRenderer }]
})
export class AppModule {}

export class LazyElementsRenderer  extends DefaultDomRenderer2  {
  
 constructor(...args) {
     this.super(...args);
  }

  createElement() {
      // override logic and change tag name if necessary
  }
}

Describe alternatives you've considered

I spent quite some time trying to override tag inside of template function without success

@flash-me
Copy link
Contributor

@flash-me flash-me commented Feb 12, 2020

Hi there,

we too make heavy use of the @angular/element package in order to build custom elements. (The term "Web Components" is something created by google. You won't find anything in the W3C / WHATWG specs).

And I can totally ensure that this works. Maybe not with the standard functionalities provided by angular, but still we solved it. Without going to much into detail, you could just simply do following:

  1. Every Custom Element (no matter if wrapped by @angular/element) must register itself to the current browser context by calling customElements.define('my-tag', CustomHtmlElementClass) .

  2. Since the string my-tag needs to be defined by you, you could save the value to a variable const tag = 'my-tag'.

  3. Right after your custom element successfully registered itself to the browser context, you can provide the tagName (or maybe some more information? it's up to you :) ) by making it globally available

window.myCustomElementRegistry = new Set(); // when loading the page
// ....
// customElement registriation
customElements.define(myTag, FooClass); // assuming does not error
window.myCustomElementRegistry.add(myTag);

Not you are able to

  1. Load the customElementbundle.js
  2. Check if the customElement has been defined globally
  3. Get the Tag Name
  4. ???
  5. Profit

You may need to create a link between specific bundle.js and its tagname somehow (this is what i meant with 'more information') e.g. the server could response like this

{
  "elements": [
    { "tag": "my-tag", "file": "my-bundle.js" }
  ]
}

This way you would know how to load the bundle and how to create an instance.

Hope I could help.

Cheers
flash

@tomastrajan
Copy link
Contributor Author

@tomastrajan tomastrajan commented Feb 12, 2020

@flash-me thanks for the reply but you are addressing something completely else than the original issue so I would rather remove it to not confuse the readers, thank you

@flash-me
Copy link
Contributor

@flash-me flash-me commented Feb 12, 2020

I'm pretty sure I'm not.

@tomastrajan
Copy link
Contributor Author

@tomastrajan tomastrajan commented Feb 12, 2020 •

@flash-me you speak about registering custom elements...

The issue itself focuses on being able to NOT hard-code tags in templates of Angular components and being able to override them ALSO in IVY using Angular renderers...

For example

Angular component template: <some-placeholder [data]="data" [tag]='comesFromBackendCOnfig'></some-placeholder>

Which should then be rendered by Angular as <tag-from-backend-config [data]="data"></tag-from-backend-config>

SO as you can see its NOT about where the tag is coming from BUT about the last step where we want to render other tag that was provided in the component template WHILE being able to still use standard template bindings like [data]="data".

So again please move that in other issue...

edit: plus it already works in ViewEngine and IVY, the only problem being that the IVY solution is fragile and I would like to improve it with more robust approach to overriding used renderer...

@flash-me
Copy link
Contributor

@flash-me flash-me commented Feb 12, 2020 •

No, I won't.

Ok, your problem is different. But The way you initially described it was not that clear.

Qutestion: Why not using TemplateRefs?

@tomastrajan
Copy link
Contributor Author

@tomastrajan tomastrajan commented Feb 12, 2020 •

@flash-me library already uses template refs and in ViewEngine all we need to do is to set new tag to template data structure like

(this.template as any)._def.element.template.nodes[0].element.name = this.tag;

and this works and renders the element with provided tag, BUT in IVY the template is a function not a data structure that's why we have to use renderers to override the tag as the last step before rendering, again please keep this issue focused...

EDIT:

Again this was already discused in original issue

As it turns out this works too (at least for the ViewEngine) so it is possible to write <ax-lazy-element *axLazyElementDynamic="'some-element'; url: 'bundle.js'" [prop]="value"></ax-lazy-element>.
The bundle will be lazy loaded, and then the element will be rendered as <some-element>.
The only thing needed to do that is to override value of the tagName in the TemplateRef of the *axLazyElemendDynamic directive and Angular will render desired element supporting our use case.

@tomastrajan
Copy link
Contributor Author

@tomastrajan tomastrajan commented Feb 12, 2020

@flash-me so I would suggest we delete this whole thread and leave the original issue as it describes other problem and it should focus on that

@flash-me
Copy link
Contributor

@flash-me flash-me commented Feb 12, 2020

Sorry mate to disappoint you, but I'm not going to delete anything except I assaulted someone. if that's the case, just tell me the exact sentence and i will apologize.

for your surprise, we also make heavy usage of the micro Frontends approach with angular. And really performant by the way.

Even if you will find a solution for your problem, your intented use case is very questionable.

What if we wanted to support loading element configuration at runtime (eg from backend) to enable fully dynamic microfrontends

Pretty simple. Thats what Inputs in Angular Components and Attributes/Properties in DOM Elements are meant for.

You want to completely change the logic for the same tag which has been already defined on the browser context. That means, between two instances of the same tag I could get completely different things loaded by a backend?

  1. This sounds very dirty to me
  2. I would not allow something like that in production because of security concerns.
    It's the same like injecting HTML/JS sent by a backend, but the angular way.

If you want to inject additional content in your page without refresh, then there are better ways, as I already mentioned previously.

Cheers

@tomastrajan
Copy link
Contributor Author

@tomastrajan tomastrajan commented Feb 12, 2020

@flash-me again you write about stuff which does NOT relate to the issue so please open a other issue where you can solve what you want or need and let this focused, I opened it after chat with Rob who told me to do it. I believe your concerns are valid just that they DO NOT relate to this use case, so please move to new place where it can be discussed appropriatelly...

@angular-robot
Copy link

@angular-robot angular-robot bot commented Jun 4, 2021

Just a heads up that we kicked off a community voting process for your feature request. There are 20 days until the voting process ends.

Find more details about Angular's feature request process in our documentation.

@tomastrajan
Copy link
Contributor Author

@tomastrajan tomastrajan commented Jun 4, 2021

For reference, here is the current impl which allows dynamically override rendered tag with IVY

        this.vcr.clear();

        const originalCreateElement = this.renderer.createElement;

        this.renderer.createElement = (name: string, namespace: string) => {
          if (name === 'ax-lazy-element') {
            name = this.tag;
          }
          return this.document.createElement(name);
        };

        this.viewRef = this.vcr.createEmbeddedView(this.template);
        this.renderer.createElement = originalCreateElement;
        this.cdr.markForCheck();

@angular-robot
Copy link

@angular-robot angular-robot bot commented Jun 25, 2021

Thank you for submitting your feature request! Looks like during the polling process it didn't collect a sufficient number of votes to move to the next stage.

We want to keep Angular rich and ergonomic and at the same time be mindful about its scope and learning journey. If you think your request could live outside Angular's scope, we'd encourage you to collaborate with the community on publishing it as an open source package.

You can find more details about the feature request process in our documentation.

@jpzwarte
Copy link

@jpzwarte jpzwarte commented Nov 8, 2021 •

@tomastrajan Can't you provide your own RendererFactory2 based on DomRendererFactory2 (which is exported):

@Injectable()
export class DomRendererFactory2 implements RendererFactory2 {

And then overwrite createRenderer, call super and then modify createElement on the DefaultDomRenderer2 instance super returned?

@tomastrajan
Copy link
Contributor Author

@tomastrajan tomastrajan commented Nov 8, 2021

@jpzwarte maybe, but the way I understand it is that all this is outdated as Renderer 2 is for VE which was now removed and Angular 13 is IVY only which might use something else ?

@jpzwarte
Copy link

@jpzwarte jpzwarte commented Nov 8, 2021 •

The comment here implies that Ivy also uses this?

  createElement(name: string, namespace?: string): any {
    if (namespace) {
      // In cases where Ivy (not ViewEngine) is giving us the actual namespace, the look up by key
      // will result in undefined, so we just return the namespace here.
      return document.createElementNS(NAMESPACE_URIS[namespace] || namespace, name);
    }

    return document.createElement(name);
  }

But I don't know exactly how Ivy renders to the DOM. :/

@jpzwarte
Copy link

@jpzwarte jpzwarte commented Nov 8, 2021

Code is from master btw (so angular 13).

@jpzwarte
Copy link

@jpzwarte jpzwarte commented Nov 8, 2021

I think Renderer3 is nothing more than document:

export const domRendererFactory3: RendererFactory3 = {
  createRenderer: (hostElement: RElement|null, rendererType: RendererType2|null): Renderer3 => {
    return getDocument();
  }
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
5 participants