angular-extensions / elements

Lazy load Angular Elements (or any other web components / custom elements ) with ease!
https://angular-extensions.github.io/elements/
MIT License
317 stars 41 forks source link

getting reference to the new lazy loaded element #70

Open TheILPlace opened 3 years ago

TheILPlace commented 3 years ago

hi, i am using *axLazyElementDynamic to lazy load dynamic component. very similar to the dynamic demo in the docs. the configuration is an Array retrieved from the server.

the problem i am facing: i need to grab a hold on the created element in my class, in order to set some attributed, and register to event emitters.

i've tried using ViewChildren, but cannot get a reference to the element. i've tried various lifecycle hooks (oninit, aftercontentini, afterviewinit) but the viewchildren variable is always undefined.

you're help is appriciated.

tomastrajan commented 3 years ago

Hi @TheILPlace !

Why not just use [someData] and (someEvent) bindings in the template?

If not possible (maybe they are always different) then you can always fall back to querySelector('my-tag')

TheILPlace commented 3 years ago

thanks for your reply. the idea was to have a complete dynamic solution. the tag-name, source url, and also the input attributes...

even if resolving to querySelector, the question is... in which lifecycle hook to catch it ?

maybe you should consider adding an event/hook that will fire after the new dynamic component is created , so we could use that ! i've read about the load hooks, but am not sure that we resolve this issue

tomastrajan commented 3 years ago

@TheILPlace that's a good point, emitting custom axElementLoaded event would make sense, would you fancy to submit a PR for this? 😉

TheILPlace commented 3 years ago

@tomastrajan working on it... i thought of emitting the instance of the created component in the event emitter... to make life easier for the application using this feature.

tomastrajan commented 3 years ago

@TheILPlace sounds good! Looking forward to the PR and wish you happy holidays (if applicable 😉)

TheILPlace commented 3 years ago

@tomastrajan well, i've spend a couple of hours on this issue. it seems that event emitters are not supported for structural directives :(

so we have 2 options:

  1. the user will create a Subject, and pass it as an input to the lazyelemendDirective. and inside the directive we will "next" the created element. the user will subscribe to his own subject. one problem with this - if you want to reach the created elements attributes, you need to wait for the changeDetection cycle to finish .. with delay(0) or setTimeout this.elementCreated.pipe(delay(0), take(1)).subscribe((s) => { }

  2. add a subject to the lazy-element-loader-service, and let the users subscribe to a function that will return the subject with already a pipe with delay(0) and take(1). the user will have to inject the service and subscribe. problem: doesnt support a scenario of more than one lazycompoment on the same component. in order to support more than one, we need to enhance the subject to emit an interface with the selector name, and the created element (or maybe get the selector from the element itself)

i've tested scenario 1 and it works quite well.

and as opposed to what i've suggested above, i cannot return a reference to the Component , but rather the html element by using: this.viewRef.rootNodes[0]

what do you think ?

tomastrajan commented 3 years ago

Hi @TheILPlace !

Thank you for all the effort and research. It's really unfortunate that there seems to be no really nice way of doing this on the library side.

I would rather not add some pretty sub-optimal ways of doing this in the lib itself.

One thing that comes to mind is then elements providing loaded event themselves.

eg <my-org-webcomponent *axLazyElement="url" (loaded)="onLoadedHandler($event)"> but this will only work for the elements you have full control of so you can add this yourself.

What about, instead of using subject if we passed in callback, eg <my-org-webcomponent *axLazyElement="url;onLoaded:myOnLoadedHandler"> would that work better than subject in terms of change detection, also element loads (or fails) only once such callback can have standard node-like interface with cb(err, element), thoughts?

TheILPlace commented 3 years ago

@tomastrajan hi ! the way of having a subject as an input was found after some research and found that as a solution for structural directives. anyways, having a callback as an input should work the same way, i presume. so i will give it a try later, and post my results (ease of use, and importantly, the change detection issue)

TheILPlace commented 3 years ago

@tomastrajan ok, i've tested this. passed a callback function to the *axLazyElementDynamic . we don't need anything to be passed from the axLazyElementDynamic . just calling us after the element was created.

there is a still problem with the change detection cycle

<ax-lazy-element *axLazyElementDynamic="customElement.selector, url: customElement.url; module: false; loadingTemplate: loading; loaded: elementCreated " [data] = "myData"

setting a breakpoint in the "elementCreated" function, i get a reference to the created element by using: this.dynamicComponent = this.elementRef.nativeElement.querySelector(this.customElement.selector);

      and checking: this.dynamicComponent.data  , the property is undefined.

putting this in a setTimeout with then show the actual value of the data property :(

tomastrajan commented 3 years ago

@TheILPlace thank you for trying this out!

Now when I think about it, it seems reasonable that the binding was NOT executed yet when the element was loaded as that is really the first thing that happens and Angular runs after.

Maybe we could specify some other flag if the onLoaded callback should be timed out or not? eg onLoadedWaitForBindings: boolean or just write it in the docs that this executes immediately when the webcomponent was loaded and hence developer needs to wait for bindings themselves, thoughts?

TheILPlace commented 3 years ago

@tomastrajan i've tried to have the setTimeout call inside the library and use that to wrapp the onLoaded emitter, but it doesnt work. you have to write it yourself in your application code that loads the element.

about documentation - from experience, people first will try it, then find bindings are not present, then create an issue, and they you will point them to the docs :)

what about my suggestion to have a subject in the loader-service ? expose it via a function like: elementLoaded(selector: string) what will have a pipe(delay(0), take(1)) . so even no need to unsubscribe to it

TheILPlace commented 3 years ago

@tomastrajan What do you think ? having a callback being sent to the loaded input of the element (the user is responsible for using setTimeout , or expose a function in the loader-service (we will take care of the 'delay(0)' part ?

tomastrajan commented 3 years ago

@TheILPlace service sounds good, but wasn't there some issue with multiple elements / tags or smthing?

problem: doesnt support a scenario of more than one lazycompoment on the same component.
in order to support more than one, we need to enhance the subject to emit an interface with the selector name, and the created element (or maybe get the selector from the element itself)

But if this can be resolved then it would be really nice!