How to load component dynamically using component name in angular2?
Asked Answered
K

5

29

I am currently loading angular components dynamically in my application using following code.

export class WizardTabContentContainer {
  @ViewChild('target', { read: ViewContainerRef }) target: any;
  @Input() TabContent: any | string;
  cmpRef: ComponentRef<any>;
  private isViewInitialized: boolean = false;

  constructor(private componentFactoryResolver: ComponentFactoryResolver,   private compiler: Compiler) {
  }

  updateComponent() {
     if (!this.isViewInitialized) {
       return;
     }
     if (this.cmpRef) {
       this.cmpRef.destroy();
     }
     let factory = this.componentFactoryResolver.resolveComponentFactory(this.TabContent);

     this.cmpRef = this.target.createComponent(factory);
   }
}

Here resolveComponentFactory function accepts component type. My question is, Is there any way I can load component using component name string e.g I have component defined as

export class MyComponent{
}

How can I add above component using component name string "MyComponent" instead of type?

Kragh answered 18/10, 2016 at 18:3 Comment(0)
R
43

Perhaps this will work

import { Type } from '@angular/core';

@Input() comp: string;
...
const factories = Array.from(this.resolver['_factories'].keys());
const factoryClass = <Type<any>>factories.find((x: any) => x.name === this.comp);
const factory = this.resolver.resolveComponentFactory(factoryClass);
const compRef = this.vcRef.createComponent(factory);

where this.comp is a string name of your Component like "MyComponent"

Plunker Example

To do it working with minification see

Ruthenious answered 18/10, 2016 at 18:23 Comment(19)
Its throwing error at x.name. Property 'name' does not exist on type '{}'.Kragh
Thanks. I resolved error in above comment using factories.find(x=>x['name'] === this.comp); however now getting error Argument of type '{}' is not assignable to parameter of type 'Type<{}>'. Property 'apply' is missing in type '{}'. on line const factory = this.resolver.resolveComponentFactory(factoryClass);Kragh
Updated. Don't forget to import TypeRuthenious
I added import { Type } from '@angular/core'; and am still getting 'Argument of type '{}' is not assignable to parameter of type 'Type<{}> Property 'apply' is missing in type '{}'' error. Any other thoughts on this issue please?Soleure
I figured it out. I needed to add var factoryClass = <Type<any>>factories.find((x: any) => x.name === this.comp); The Plunker example is different. Thank you for this approach!!!Soleure
In the documentation, ` _factories` is not mentioned to get all components. I can see in the doc( github.com/angular/angular/blob/… ) it is declared as private. Shall we use it? I mean if it changes in future releases then we will not be able to know about it.Cockoftherock
@HiteshKumar You should know about all your factories. Use object as map. Don't use private apiRuthenious
Here this.resolver['_factories'], the _factories data member is private. Please correct me if I am wrong.Cockoftherock
Thanks for this. So close to working... but for some reason for one of my components- which is declared in ngModule etc, exactly like all the others, throws the error ERROR Error: No provider for name!Blackcock
This doesn't seem to work when using --prod. Any ideas why? All the factory names have been obfuscated and all called "n", hence never finding a matching typeHallucinate
@mattytommo Check out this answer #40529092Ruthenious
I agree with @HiteshKumar in that this is a usage of an Angular internal. I have a similar question/answer here #40223079 But this answer technically works better. Unfortunately as of now I haven't found a way to cleanly do this :(Raynell
@Raynell Did you check this? #40529092Ruthenious
@Ruthenious that's exactly what my linked answer did, just slightly different execution. It's what I was hoping to avoid. :P Also, it still uses the Angular 2 internal object _factoriesRaynell
This does not work in Angular 9. _factories property is no longer available. Our app stopped loading dynamic components after we upgraded from Angular 8 to 9. :(Walley
@AjayAmbre You don't need entryComponents in Angular 9. Just create a dictionary with mapping between id and components like I described here #40529092Ruthenious
@Ruthenious was facing the same problem like Ajay Ambre but the implementation with a dictionary solved my problem (and actually simplified my code)Fancy
this.resolver['_factories'] is undefind in angular 9 (Ivy compailer)Chippy
this.resolver['_factories'] is undefind in angular 9 (Ivy compailer), for get factories need somthing like this.resolver['ngModule']['instance']['__proto__']['constructor']['__annotations__'][0]['entryComponents']Chippy
D
13

I know this post is old, but a lot of things have changed in Angular and I didn't really like any of the solutions from an ease of use and safety. Here's my solution that I hope you like better. I'm not going to show the code to instantiate the class because those examples are above and the original Stack Overflow question already showed a solution and was really asking how to get the Class instance from the Selector.

export const ComponentLookupRegistry: Map<string, any> = new Map();

export const ComponentLookup = (key: string): any => {
    return (cls) => {
        ComponentLookupRegistry.set(key, cls);
    };
};

Place the above Typescript Decorator and Map in your project. And you can use it like so:

import {ComponentLookup, ComponentLookupRegistry} from './myapp.decorators';

@ComponentLookup('MyCoolComponent')
@Component({
               selector:        'app-my-cool',
               templateUrl:     './myCool.component.html',
               changeDetection: ChangeDetectionStrategy.OnPush
           })
export class MyCoolComponent {...}

Next, and this is important, you need to add your component to entryComponents in your module. This allows the Typescript Decorator to get called during app startup.

Now anywhere in your code where you want to use Dynamic Components (like several of the above examples) when you have a Class Reference, you just get it from your map.

const classRef = ComponentLookupRegistry.get('MyCoolComponent');  
// Returns a reference to the Class registered at "MyCoolComponent

I really like this solution because your KEY that you register can be the component selector, or something else that's important to you or registered with your server. In our case, we needed a way for our server to tell us which component (by string), to load into a dashboard.

Delectate answered 7/2, 2020 at 13:15 Comment(6)
Hi Joe, have you tried this with an Angular 9 optimized (prod) build yet? I've followed this same pattern since Angular 7, but I found that the decorator code has been optimized out in production builds now, so it doesn't execute at all.Ballew
Have you found a solution?Babineaux
@Babineaux At the risk of necro'ing a thread long dead, I've gotten this working with Angular 12 in an AoT/Optimized/Ivy application using almost the entire code exactly as written above by Joe Firebaugh (many thanks!), with one slight change/addition: Because AoT/Ivy will tree-shake away classes it thinks are unused (and dynamically loaded components fit that bill), I was forced to modify my main module.ts file to include a "fake" static variable like this: static entryComponents = [MyCoolComponent]; Doing this forces a ref to the class, which keeps it in the bundle, and it works.Five
@Five so basically, this static variable will be an array of all component classes that you wish to instantiate dynamically?Jewbaiting
this is a better solution without relying on angular internal implementationHammad
@Five can you provide a bit more context and how your code? I would like to ensure this works in angular 16 pleaseDiamante
L
2

I looked far and wide for solution that satisfies Angular 9 requirements for dynamically loaded modules and I came up with this

import { 
    ComponentFactory, 
    Injectable, 
    Injector, 
    ɵcreateInjector as createInjector,
    ComponentFactoryResolver,
    Type
} from '@angular/core';

export class DynamicLoadedModule {
    public exportedComponents: Type<any>[];

    constructor(
        private resolver: ComponentFactoryResolver
    ) {
    }

    public createComponentFactory(componentName: string): ComponentFactory<any> {
        const component = (this.exportedComponents || [])
            .find((componentRef) => componentRef.name === componentName);          

        return this.resolver.resolveComponentFactory(component);
    }
}

@NgModule({
    declarations: [LazyComponent],
    imports: [CommonModule]
})
export class LazyModule extends DynamicLoadedModule {
    constructor(
        resolver: ComponentFactoryResolver
    ) {
        super(resolver);
    }
    
}


@Injectable({ providedIn: 'root' })
export class LazyLoadUtilsService {
    constructor(
        private injector: Injector
    ) {
    }

    public getComponentFactory<T>(component: string, module: any): ComponentFactory<any> {
        const injector = createInjector(module, this.injector);
        const sourceModule: DynamicLoadedModule = injector.get(module);

        if (!sourceModule?.createComponentFactory) {
            throw new Error('createFactory not defined in module');
        }

        return sourceModule.createComponentFactory(component);
    }
}

Usage

async getComponentFactory(): Promise<ComponentFactory<any>> {
    const modules = await import('./relative/path/lazy.module');
    const nameOfModuleClass = 'LazyModule';
    const nameOfComponentClass = 'LazyComponent';
    return this.lazyLoadUtils.getComponentFactory(
        nameOfComponentClass ,
        modules[nameOfModuleClass]
    );
}
Lubberly answered 1/10, 2020 at 6:38 Comment(0)
F
0

It's also possible to access through import:

someComponentLocation.ts - contains enum of possible components:

export * from './someComponent1.component'
export * from './someComponent2.component'
export * from './someComponent3.component';

importer component:

import * as possibleComponents from './someComponentLocation'
...

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

constructor(private resolver: ComponentFactoryResolver){}

then you can create instance of component for example:

let inputComponent = possibleComponents[componentStringName];
if (inputComponent) {
    let inputs = {model: model};
    let inputProviders = Object.keys(inputs).map((inputName) => { return { provide: inputName, useValue: inputs[inputName] }; });
    let resolvedInputs = ReflectiveInjector.resolve(inputProviders);
    let injector: ReflectiveInjector = ReflectiveInjector.fromResolvedProviders(resolvedInputs, this.dynamicInsert.parentInjector);
    let factory = this.resolver.resolveComponentFactory(inputComponent as any);
    let component = factory.create(injector);
    this.dynamicInsert.insert(component.hostView);
}

note that component has to be in @NgModule entryComponents

Fons answered 7/11, 2017 at 12:18 Comment(6)
so how do we get that import dynamically? I believe that is not really practical when you just start with a string of the component name.Smug
More info on this? what is this.dynamicInsert variable? Can you provide a minimum working sample code?Nationalism
@Andre you are right. It's not really dynamic version. But sometimes you can define set of known components by static array.Fons
@Nationalism I've updated the post little bit. dynamicInsert is typical element <ng-template #dynamicInsert></ng-template>Fons
@PetrŠpaček thanks for the answer. and how would you assign inputs and outputs for a component that is loaded like that?Nationalism
@Nationalism sorry for delay, you can easily access to created component instance: component.instance.yourCustomInput = value; try to debug it in chromeFons
M
0

i user anthor way to done this, may be helpful for you.

1.first defined a class which use as name map componet and class RegisterNMC for moduleName map nmc

export class NameMapComponent {
  private components = new Map<string, Component>();

  constructor(components: Component[]) {
    for (let i = 0; i < components.length; i++) {
      const component = components[i];
      this.components.set(component.name, component);
    }
  }

  getComponent(name: string): Component | undefined {
    return this.components.get(name);
  }

  setComponent(component: Component):void {
    const name = component.name;
    this.components.set(name, component);
  }

  getAllComponent(): { [key: string]: Component }[] {
    const components: { [key: string]: Component }[] = [];
    for (const [key, value] of this.components) {
      components.push({[key]: value});
    }
    return components;
  }
}

export class RegisterNMC {
  private static nmc = new Map<string, NameMapComponent>();

  static setNmc(name: string, value: NameMapComponent) {
    this.nmc.set(name, value);
  }

  static getNmc(name: string): NameMapComponent | undefined {
    return this.nmc.get(name);
  }

}

type Component = new (...args: any[]) => any;
  1. in the ngMgdule file,you must put the components which be dynamically load in entryCompoent.

    const registerComponents = [WillBeCreateComponent]; const nmc = new NameMapComponent(registerComponents); RegisterNMC.setNmc('component-demo', nmc);

3.in the container component

@ViewChild('insert', {read: ViewContainerRef, static: true}) insert: ViewContainerRef;

  nmc: NameMapComponent;
  remoteData = [
    {name: 'WillBeCreateComponent', options: '', pos: ''},
  ];

  constructor(
    private resolve: ComponentFactoryResolver,
  ) {
    this.nmc = RegisterNMC.getNmc('component-demo');

  }

  ngOnInit() {
    of(this.remoteData).subscribe(data => {
      data.forEach(d => {
        const component = this.nmc.getComponent(d.name);
        const componentFactory = this.resolve.resolveComponentFactory(component);
        this.insert.createComponent(componentFactory);
      });
    });
  }

it's fine, hopefully can help you ^_^!

Marinetti answered 19/3, 2020 at 9:10 Comment(1)
I followed this approach using angular 11 and it doesn't work as the contents of the name map get erased when a prod build is done. Unless you have solved this?Litman

© 2022 - 2024 — McMap. All rights reserved.