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?

5 Answers 5

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

Sign up to request clarification or add additional context in comments.

19 Comments

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);
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?
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!!!
Here this.resolver['_factories'], the _factories data member is private. Please correct me if I am wrong.
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
|
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.

6 Comments

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.
Have you found a solution?
@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 so basically, this static variable will be an array of all component classes that you wish to instantiate dynamically?
this is a better solution without relying on angular internal implementation
|
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]
    );
}

Comments

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

6 Comments

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.
More info on this? what is this.dynamicInsert variable? Can you provide a minimum working sample code?
@Andre you are right. It's not really dynamic version. But sometimes you can define set of known components by static array.
@cdarken I've updated the post little bit. dynamicInsert is typical element <ng-template #dynamicInsert></ng-template>
@cdarken sorry for delay, you can easily access to created component instance: component.instance.yourCustomInput = value; try to debug it in chrome
|
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 ^_^!

1 Comment

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?

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.