Search code examples
angularangular2-changedetectionangular-dynamic-componentschange-detector-ref

Angular - change detection in dynamically loaded module/component without using `ChangeDetectorRef`


I compile an Angular module (to load the module dynamically) with compiler's compileModuleAsync and want to insert a component of the module into view.

I tried to insert the component into ViewContainer but the component doesn't detect changes automatically. I should call changeDetectorRef.detectChanges each time when I update a component's property.

Is there any way to achieve this without using the changeDetectorRef?

Angular version is 10.0.4.

Example code that I load the component:


The Component where I load another component:

<ng-template #dynamic></ng-template>
@ViewChild('dynamic', { read: ViewContainerRef })
dynamic: ViewContainerRef;

constructor(
    private compiler: Compiler,
    private injector: Injector
) {}

async ngAfterViewInit() {
    // Load a module dynamically
    const exampleModule = await import('../example/example.module').then(m => m.ExampleModule);
    const moduleFactory = await this.compiler.compileModuleAsync(exampleModule);
    const moduleRef = moduleFactory.create(this.injector);
    const componentFactory = moduleRef.instance.resolveComponent();
    const ref = container.createComponent(componentFactory, null, moduleRef.injector);
}

ExampleModule:

@NgModule({
    declarations: [
        ExampleComponent
    ],
    imports: [...]
})
export class ExampleModule {
    constructor(private componentFactoryResolver: ComponentFactoryResolver) { }

    public resolveComponent(): ComponentFactory<ExampleComponent> {
        return this.componentFactoryResolver.resolveComponentFactory(ExampleComponent);
    }
}

An example case of calling detectChanges:

ExampleComponent

<button (click)="toggle()">Show/Hide</button>
<span *ngIf="show">Show</span>
public toggle() {
  this.show = !this.show;
  this.cdr.detectChanges(); // <- I want to not use this.
}

Solution

  • I use this service to create component an set inputs:

    @Directive({
      selector: '[formField]'
    })
    export class FormFieldDirective implements Field, OnChanges, OnInit, AfterViewInit {
      @Input() config: FieldConfig;
      @Input() lang: string;
      @Input() group: FormGroup;
    
      @Input('class') classList: string = 'col-12';
    
      component: ComponentRef<Field>;
    
      constructor(
        private resolver: ComponentFactoryResolver,
        private container: ViewContainerRef,
        private renderer: Renderer2
      ) { }
    
      ngOnChanges() {
        if (this.component) {
          this.component.instance.config = this.config;
          this.component.instance.group = this.group;
        }
      }
    
      ngOnInit() {
        if (!components[this.config.type]) {
          const supportedTypes = Object.keys(components).join(', ');
          throw new Error(
            `Trying to use an unsupported type (${this.config.type}).
            Supported types: ${supportedTypes}`
          );
        }
        const component = this.resolver.resolveComponentFactory<Field>(components[this.config.type]);
        this.component = this.container.createComponent(component);
        this.renderer.addClass(this.component.location.nativeElement, this.config.widthClass ? this.config.widthClass : 'col-12');
        this.component.instance.config = this.config;
        this.component.instance.group = this.group;
        this.component.instance.lang = this.lang;
        if (this.config.type === ModelType.group) {
          this.component.instance.innerForm = this.group.get(this.config.name) as FormGroup;
        }
    
      }
    
      ngAfterViewInit() {
        this.config.label += 'cica';
      }
    }
    

    Usage:

    <div [ngClass]="formClass" [formGroup]="form">
        <ng-container *ngFor="let field of config;" formField [config]="field" [lang]="lang" [group]="form"> </ng-container>
        <ng-content></ng-content>
    </div>