Angular - ViewChild of dynamic created Component is undefined

215 views Asked by At

I want to open a component from a service without having to add that component to every other component which has the service.

In the service I have the following:

@Injectable({
  providedIn: 'root',
})
export class MyService {

  constructor(private appRef: ApplicationRef) {}

  createComponent(): void {
    const comp = createComponent(MyComponent, {environmentInjector: this.appRef.injector});
    this.appRef.attachView(comp.hostView);
    comp.call();
  }
}

The MyComponent looks something like this:

@Component({
  selector: 'app-my-dialog',
  standalone: true,
  imports: ...,
  template: '... <another-component /> ...',
})
export class MyComponent {
  @ViewChild(AnotherComponent) comp!: AnotherComponent;

  call(): void {
    this.comp.doSomething();
  }
}

Now my problem is, that the viewChild is undefined. I can't figure out, why it's not loaded correctly.

I tried to use the ViewContainerRef, but with that I can't create the component like that, because it needs an EnvironmentInjector, that only the ApplicationRef has.

I know that I could add MyComponent to the HTML of the calling Component, but I'd rather have only the service.

I use Angular 16.2.10, if that helps.

3

There are 3 answers

0
B34v0n On

I solved the issue now, with using setTimeout.

@Injectable({
  providedIn: 'root',
})
export class MyService {

  constructor(private appRef: ApplicationRef) {}

  createComponent(): void {
    const comp = createComponent(MyComponent, {environmentInjector: this.appRef.injector});
    this.appRef.attachView(comp.hostView);
    // Timeout here!
    setTimeout(() => {
      comp.call();
    }
  }
}
6
Debabrata909 On

In Angular, ViewChild queries are resolved after the ngAfterViewInit lifecycle hook. In your case, when you call comp.call() immediately after attaching the view, the ngAfterViewInit hook of MyComponent may not have been called yet, leading to this.comp being undefined.

To address this, you can wait for the ngAfterViewInit lifecycle hook to be triggered before calling methods on MyComponent. You can achieve this by using the AfterViewInit lifecycle hook in your service. Here's an updated version of your code:

   @Injectable({
  providedIn: 'root',
})
export class MyService implements AfterViewInit {

  private compRef: ComponentRef<MyComponent>;

  constructor(private appRef: ApplicationRef, private componentFactoryResolver: ComponentFactoryResolver) {}

  ngAfterViewInit(): void {
    // This will be called after the view of the service is initialized.
    // Now you can safely call methods on MyComponent.
    this.comp.call();
  }

  createComponent(): void {
    const compFactory = this.componentFactoryResolver.resolveComponentFactory(MyComponent);
    this.compRef = compFactory.create(this.appRef.injector);
    this.appRef.attachView(this.compRef.hostView);
    // You may need to append the component's host view to the DOM if needed.
    // document.body.appendChild(this.compRef.location.nativeElement);
  }

  get comp(): MyComponent | undefined {
    return this.compRef ? this.compRef.instance : undefined;
  }
}

Now, you can use myService.createComponent() to create the component and access it later through myService.comp. The ngAfterViewInit hook in the service will be triggered after the view of MyComponent is initialized, ensuring that this.comp is defined when you call this.comp.call().

Also, note that I've added AfterViewInit to the service class, and I've stored the reference to the component (this.compRef) so that you can access it later

0
AT82 On

You should trigger change detection on the created component, it can be by just adding the single line before calling your function:

comp.changeDetectorRef.detectChanges();

complete code:

createComponent(): void {
  const comp = createComponent(MyComponent, {
    environmentInjector: this.appRef.injector,
  })
  comp.changeDetectorRef.detectChanges();
  this.appRef.attachView(comp.hostView)
  comp.instance.call();
}

Your forked STACKBLITZ