Dynamic component loader - shared service for the resolving components

*爱你&永不变心* 提交于 2019-12-08 10:11:53

问题


I have to use ComponentFactoryResolver to add components dynamically. For my example, I have a modal window which I dynamically loaded after pressing some button on the view. And code for this action like this:

let componentFactory = this.componentFactoryResolver.resolveComponentFactory(XYZComponent);
let viewContainerRef = this.containerHost.viewContainerRef;
viewContainerRef.clear();
let componentRef = viewContainerRef.createComponent(componentFactory);

let _XYZComponent = componentRef.instance as XYZComponent;

_XYZComponent.close.subscribe(() => {
    componentRef.destroy();
});

Every time when I would like to use this modal window I need to put the similar code, only with the different component name.

I would like to create a shared service for this but I can't find a good solution for my case I mean dynamically loaded components.

Any idea how to create a good service to the use this code?


回答1:


Since you will be loading components dynamically, you have to register these components somewhere, and that is inside the entryComponents of your modal component decorator. Also, since these components are typescript classes, you need to import them from whatever component you're calling your modal. In order to handle this in just one place, we're going to import these into a single file and then export them inside an array.

So here you will keep all the possible dynamic components, lets call this file dynamic-components.ts:

import { FooComponent } from './path/to/foo';
import { BarComponent } from './path/to/bar';
import { BazComponent } from './path/to/baz';

// export all dynamic components
export const dynamicComponents = [
  FooComponent, BarComponent, BazComponent
]

Then in your modal component, you can spread these components into the entryComponents property

import { dynamicComponents } from './path/to/dynamic-components';
@Component({
  //...
  entryComponents: [ ...dynamicComponents ]
})
export class ModalComponent {}

So far this should be all known to you.

Now, inside your modal component, you can create a method that will render a component taking as parameter the component name and also some metadata to handle props dynamically, by props I mean @Input() and @Output() decorated properties. This will make your modal more flexible, since you will be able to render components with different inputs and outputs.

So instead of hardcoding the component in the method like you're doing now, you'll have to extract it from the dynamicComponents array.

Since Javascript classes are sugar syntax for functions, all non-anonymous functions have a name property, this way you can match the name parameter provided by your function with the name of the components in dynamicComponents.

export class ModalComponent { 
  //...
  createComponent(name: string, metadata: any) {
    const viewContainerRef = this.entryContainer.viewContainerRef;
    const cmpClass = dynamicComponents.find(cmp => cmp.name === name);
    const cmpToCreate = new DynamicComponent(cmpClass, metadata);

    const componentFactory = this.cmpFactoryResolver.resolveComponentFactory(cmpToCreate.component)

    viewContainerRef.clear();

    const cmpRef = viewContainerRef.createComponent(componentFactory);


    // patch input values ...
    for ( let input in metadata.inputs ) {
      if ( metadata.inputs.hasOwnProperty(input) ) {
        cmpRef.instance[input] = metadata.inputs[input];
      }
    }


    // and subscribe to outputs
    for ( let output in metadata.outputs ) {
      if ( metadata.outputs.hasOwnProperty(output) ) {
        console.log('hasOuput', metadata.outputs[output]);
        cmpRef.instance[output].subscribe(metadata.outputs[output]);
      }
    }
  }
}

A couple things to mention. Here's the definition for the DynamicComponent class:

export class DynamicComponent {
  constructor(public component: Type<any>, data: any) {}
}

The reason for creating this helper class, is because resolveComponentFactory is expecting a component parameter with Type<T>, and the result of dynamicComponents.find() is a union type, so if we don't want the typescript compiler to complain, we should patch this class.

The rest of the function is pretty much what you have, except for the metadata parameter. Now, if the components you are instantiating in your modal have inputs and outputs, unless you design these components specifically to meet some criteria, might have different inputs and outputs. So that's what the metada parameter is, just an object with inputs and outputs. I guess it's clearer when you actually call the method, like this:

export class SomeComponentThatRendersTheModal() {
  renderFooComponent() {
    // I don't know how you call your modal, so I'll just assume there's a modal service or whatever
    this.modalService.openModal();
    this.modalService.createComponent(
      'FooComponent', { 
        inputs  : { fooInputTest  : 'kitten' },
        outputs : { fooOutputTest : handleOutput } 
      }
    );
  }

  // You can pass this method as the subscription `next` handler
  handleOutput(emittedEvent) {
    // ...
  }
}

Where FooComponent is something like this:

@Component({
  selector: 'foo',
  template: `
    <h1>Foo Component, here's the input: " {{ fooInputTest }} "</h1>
    <button (click)="fooOutputTest.emit('Greetings from foo')">Foo output test</button>
  `
})
export class FooComponent {
  @Input()
  fooInputTest: any;

  @Output()
  fooOutputTest: EventEmitter<any> = new EventEmitter<any>();
}

Now, of course you can change the way the metadata looks or how you handle patching the input values or what you pass as handlers to the outputs, but this is the basic foundation of how you can create a different components dynamically.


Of course I've set up a demo too. Hope it helps.

12/14/07 Edit:

Apparently accessing the name property of a Function object doesn't really work for production (you'll get a no component factory found error), since after uglyfying your code, the names of function get mangled and won't match. There's an issue comment on the angular repo explaining some workarounds for this problem.



来源:https://stackoverflow.com/questions/47754534/dynamic-component-loader-shared-service-for-the-resolving-components

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!