8

Using Angular 10

There are many questions on SO that are similar to this, but I have yet to find one that answers my situation.

I'm hoping someone can guide me.

I'm using a third party library to display 360° photos. This third party library has a built-in API to display hotspots in the scene. Simply give the library the element you want to be the hotspot, and it takes care of the rest.

I have most of it working as expected, but there are a couple pieces that are not.

So far, I'm dynamically generating my components like so:

this._hotspotFactory = this.resolver.resolveComponentFactory(HotspotComponent);
const component = this._hotspotFactory.create(this.injector);

//Hydrate component with  bunch of data
component.instance.id = data.id;
...

// Create the Hotspot with Third Party
// Calling this third party method injects the native element into the DOM. 
// Passing the nativeElement in. Looks great at first glance. 
const hotspot = this._scene.createHotspot(data, component.location.nativeElement);

this.appRef.attachView(component.hostView);
component.hostView.detectChanges();

if(component.instance.over.observers.length) {
  hotspot.on('over', (evt) => {
    this.zone.run(() => {
      component.instance.over.emit(evt);
    });
  });
}

if(component.instance.out.observers.length) {
  hotspot.on('out', (evt) => {
    this.zone.run(() => {
      component.instance.out.emit(evt);
    });
  });
}

if(component.instance.navigate.observers.length) {
  hotspot.on('click', (evt) => {
    this.zone.run(() => {
      component.instance.navigate.emit(evt);
    })
  });
}

No errors are thrown and I successfully see the hotspot where it should be in the scene. Even Data interpolation in the HotspotComponent template occurs as expected.

BUT, [ngStyle] bindings never result in dynamic styling in HotspotComponent.

I'm 99% sure this is because change detection is not taking place in the component.

I am manually attaching the view with this.appRef.attachView(component.hostView) because the third party is responsible for injecting the element into the DOM, not Angular. Thus Angular needs to know about it so it will perform change detection.

Even with manually calling attachView, I still think Angular doesn't know about this component in the view because the Angular Chrome Extension debugger doesn't register it in its dev tools as a known component in the view....despite be seeing it on screen and in the DOM.

What am I missing?

helvete
  • 2,455
  • 13
  • 33
  • 37
calbear47
  • 1,060
  • 2
  • 18
  • 38
  • The issue is that the events triggered by your non-angular lib do not link into the angular life cycle and vive-versa... you might have to look into ngZone as this service provides the hooks in and out of angular scopes. – The Fabio Dec 17 '21 at 06:22
  • @TheFabio I've updated my answer. I'm listening to events from the third party and then firing those events in angular. Are you saying I also need to listen to component lifecycle events and then act upon them? – calbear47 Dec 17 '21 at 06:32
  • hard to say without a deeper-dive into your source.. but my gut feeling is that when you call `this._hotspotFactory.create` it is creating the component instance in a different scope from the angular one. Do you have to create this component dynamically? – The Fabio Dec 17 '21 at 07:10
  • Could you create the `hotspot` variable inside the angular component instead? – The Fabio Dec 17 '21 at 07:12
  • @TheFabio The code you see above takes place in a service. In my first attempt, I created the `hotspot` variable inside the Angular component. That resulted in an Angular component in the view which had no purpose, and the element the third party injected into the DOM...thus there was duplication. The I was baby sitting both...trying to keep them in sync. My hope was that by creating he components dynamically, I would eliminate duplication. I'll keep digging around. – calbear47 Dec 17 '21 at 07:18
  • could you create a STACKBLITZ reproduction of the issue? Which third party library are you using? – Andriy Dec 29 '21 at 07:27
  • I've injected components that use third party code by wrapping the dynamic component creation in a component, not in a service. I believe services are not meant to change the DOM, they are meant to do the business logic heavy lifting. – perepm Jan 09 '22 at 12:15
  • I did [this](https://angular.io/guide/dynamic-component-loader) on the parent component to display the dynamic component as a child. This is different from your approach in which the component is dynamically created in a service which I'm not sure works well when it comes to updating the DOM. – perepm Jan 09 '22 at 12:18

3 Answers3

0

What change detection strategy does the component have? When a component is added to a view, it's life cycle hooks will be triggered by angular(ngOninit, ngAfterContentInit etc). Log something in these and see if theme life cycle hooks are being called. Irrespective of the change detection strategy one change detection cycle should happen on the component after it is added to view.

If the life cycle hook invoking is not happening, then it would mean that angular is not involved in adding the element to DOM.

saivishnu tammineni
  • 1,092
  • 7
  • 14
-1

It seems angular has a lifecycle hook precisely for your use-case 'ngDoBootstrap'.

As we can not debug your full source code, from the information you have mentioned it seems the dynamic component you are trying to attach to the view is not available to Angular in NgModule. Every component that angular bootstraps must be in NgModule.

you can although bootstrap it dynamically using 'ngDoBootstrap'.

It is used in the following manner:

ngDoBootstrap(appRef: ApplicationRef) {

      this.fetchDataFromApi().then((componentName: string) => {
        if (componentName === 'ComponentOne') {
          appRef.bootstrap(ComponentOne);
        } else {
          appRef.bootstrap(ComponentTwo);
        }
      });
    }

In your case, you can do it before attaching the component to the view.

...
appRef.bootstrap(component);
this.appRef.attachView(component.hostView);
component.hostView.detectChanges();
...

Please check the documentation here: https://angular.io/api/core/ApplicationRef

-1

We use resolveComponentFactory method given by ComponentFactoryResolver class present in angular which is used for component level lazy loading. For the confirmation that your component is really breaked in chunks, do ng build --prod or you con and you will see the generated .js chunk for SoftwareListComponent.

app.component.html

  <button (click)="loadSoftwareListDynamically()> Load </button>
  <div #softwareListContainer></div>

app.component.ts

   constructor(
    private componentFactoryResolver: ComponentFactoryResolver
  ) {}

  @ViewChild('softwareListContainer', { read: ViewContainerRef })
  softwareListContainer: ViewContainerRef;

  loadSoftwareListDynamically() {
    import('../common-features/software-list/software-list.component').then(
      ({ SoftwareListComponent }) => {
        const componentFactory =
          this.componentFactoryResolver.resolveComponentFactory(
            SoftwareListComponent
          );
        this.softwareListContainer.createComponent(componentFactory);
      }
    );
  }

software-list.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

@NgModule({
  declarations: [
   SoftwareListComponent
  ],
  imports: [
    CommonModule,
    RouterModule,
    SwiperModule    <- External Library
  ],
})
export class SoftwareListModule {}

For more info, you can go over to my complete discussion for Lazy loading of components created using ComponentFactoryResolver. You will get more info here -> StackOverflow Discussion

Stackblitz Link Here

  • While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - [From Review](/review/low-quality-posts/30764539) – camille Jan 08 '22 at 00:07
  • @camille Updated as per your suggestions – Ashwini Kumar Jan 08 '22 at 18:39