27

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?

Marco
  • 1,073
  • 9
  • 22
Pankaj Kapare
  • 7,486
  • 5
  • 40
  • 56

5 Answers5

42

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

alexalejandroem
  • 1,094
  • 12
  • 17
yurzui
  • 205,937
  • 32
  • 433
  • 399
  • Its throwing error at x.name. Property 'name' does not exist on type '{}'. – Pankaj Kapare Oct 18 '16 at 18:38
  • 2
    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); – Pankaj Kapare Oct 18 '16 at 18:48
  • Updated. Don't forget to import `Type` – yurzui Oct 18 '16 at 18:53
  • 1
    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? – Tom Schreck Dec 14 '16 at 16:18
  • 2
    I figured it out. I needed to add var factoryClass = >factories.find((x: any) => x.name === this.comp); The Plunker example is different. Thank you for this approach!!! – Tom Schreck Dec 14 '16 at 16:45
  • In the documentation, ` _factories` is not mentioned to get all components. I can see in the doc( https://github.com/angular/angular/blob/910c0d9ee7e1f556f773e37e7385d5dd58ef79fd/packages/core/src/linker/component_factory_resolver.ts#L45 ) 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. – Hitesh Kumar Apr 11 '17 at 09:10
  • @HiteshKumar You should know about all your factories. Use object as map. Don't use private api – yurzui Apr 11 '17 at 09:20
  • 2
    Here `this.resolver['_factories']`, the `_factories` data member is private. Please correct me if I am wrong. – Hitesh Kumar Apr 11 '17 at 09:23
  • 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!` – Inigo Aug 31 '17 at 18:39
  • 6
    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 type – Mathew Thompson Sep 12 '17 at 10:43
  • 1
    @mattytommo Check out this answer https://stackoverflow.com/questions/40528592/ng2-dynamically-creating-a-component-based-on-a-template/40662376#40662376 – yurzui Sep 12 '17 at 10:49
  • I agree with @HiteshKumar in that this is a usage of an Angular internal. I have a similar question/answer here https://stackoverflow.com/questions/40222579/angular-2-resolve-component-factory-with-a-string/40247193#40247193 But this answer technically works better. Unfortunately as of now I haven't found a way to cleanly do this :( – dudewad Sep 15 '17 at 20:25
  • @dudewad Did you check this? https://stackoverflow.com/questions/40528592/ng2-dynamically-creating-a-component-based-on-a-template/40662376#40662376 – yurzui Sep 15 '17 at 20:28
  • @yurzui 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 _factories – dudewad Sep 15 '17 at 20:45
  • 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. :( – Ajay Ambre Feb 11 '20 at 22:06
  • 1
    @AjayAmbre You don't need entryComponents in Angular 9. Just create a dictionary with mapping between id and components like I described here https://stackoverflow.com/questions/40528592/ng2-dynamically-creating-a-component-based-on-a-template/40662376#40662376 – yurzui Feb 12 '20 at 16:41
  • @yurzui was facing the same problem like Ajay Ambre but the implementation with a dictionary solved my problem (and actually simplified my code) – brz Mar 02 '20 at 10:33
  • 1
    `this.resolver['_factories']` is undefind in angular 9 (Ivy compailer) – izik f Apr 22 '20 at 08:11
  • 1
    `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']` – izik f Apr 22 '20 at 10:00
11

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.

Joe Firebaugh
  • 111
  • 1
  • 3
  • 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. – Andy Kachelmeier Feb 17 '20 at 15:30
  • Have you found a solution? – bartolja Apr 01 '20 at 17:31
  • @bartolja 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. – Londovir Jun 15 '21 at 20:42
  • @Londovir so basically, this static variable will be an array of all component classes that you wish to instantiate dynamically? – AsGoodAsItGets Jul 05 '21 at 10:33
  • this is a better solution without relying on angular internal implementation – Shaybc Nov 15 '21 at 00:18
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]
    );
}
Imants Volkovs
  • 838
  • 11
  • 20
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

Petr Spacek
  • 629
  • 6
  • 8
  • 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. – Andre Elrico Jan 17 '19 at 20:32
  • More info on this? what is `this.dynamicInsert` variable? Can you provide a minimum working sample code? – cdarken Mar 02 '19 at 23:02
  • @Andre you are right. It's not really dynamic version. But sometimes you can define set of known components by static array. – Petr Spacek Mar 04 '19 at 07:51
  • 1
    @cdarken I've updated the post little bit. dynamicInsert is typical element – Petr Spacek Mar 04 '19 at 08:13
  • @PetrŠpaček thanks for the answer. and how would you assign inputs and outputs for a component that is loaded like that? – cdarken Mar 06 '19 at 17:01
  • 1
    @cdarken sorry for delay, you can easily access to created component instance: component.instance.yourCustomInput = value; try to debug it in chrome – Petr Spacek Apr 01 '19 at 12:06
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 ^_^!

H.jame
  • 79
  • 1
  • 6
  • 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? – Murray Furtado Mar 26 '21 at 17:38