问题
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 class
es, 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