Pankaj Kapare
Pankaj Kapare

Reputation: 7802

How to load component dynamically using component name in angular2?

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?

Upvotes: 29

Views: 36665

Answers (5)

Imants Volkovs
Imants Volkovs

Reputation: 836

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]
    );
}

Upvotes: 2

H.jame
H.jame

Reputation: 75

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 ^_^!

Upvotes: 0

Joe Firebaugh
Joe Firebaugh

Reputation: 131

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.

Upvotes: 13

Petr Spacek
Petr Spacek

Reputation: 787

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

Upvotes: 0

yurzui
yurzui

Reputation: 214047

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

Upvotes: 43

Related Questions