We are building an angular 4 component library and one of the components is a Busy component. The purpose of the component is to allow a developer to create an overlay on any given HTML element which contains a spinner graphic.
<div *xuiBusy="isBusy">...</div>
When the value of isBusy
is true we want to append to the inner content of the div
so that we can present the overlay elements on top of the content.
We have been able to append the component to the ViewContainerRef
however this inserts the busy element as a sibling to the div
rather than within the div
as desired.
ngOnInit(): void {
const compFactory = this._componentFactory.resolveComponentFactory(XuiBusyComponent);
const comp = this._viewContainer.createComponent(compFactory);
What the consumer does:
<div *xuiBusy="isBusy">
<span>This is the content</span>
</div>
When isBusy
is set to true we want to alter the markup to look something like this. Notice that <spinner>
has been added to the div
element.
<div *xuiBusy="isBusy">
<span>This is the content</span>
<spinner>Please wait...</spinner> <-- inserted by directive
</div>
Any advice is appreciated!
I've set up a demo on StackBlitz. The Spinner component, Busy directive and the consumer are all in app.component.ts
for brevity.
The structural directive needs to inject the following:
TemplateRef
, a reference to the template that the structural directive lies on (in desugared syntax);ViewContainerRef
, a reference to the container of views which can be rendered inside the view that the structural directive encapsulates;ComponentFactoryResolver
, a class which knows how to dynamically create instances of component from code.Injecting in Angular is done via a constructor.
constructor(private templateRef: TemplateRef<void>,
private vcr: ViewContainerRef,
private cfr: ComponentFactoryResolver) { }
We need an input in order to pass data from the outside. In order to make the syntax pretty and obvious (especially when a directive expects a single input such as in this case), we can name the input exactly the same as our selector for the directive.
To rename the input, we can pass a bindingPropertyName
to the Input
decorator.
@Input('xuiBusy') isBusy: boolean;
The content which consumer can be dynamically created by using the createEmbeddedView
method defined on the ViewContainerRef
class. The first parameter is the only mandatory one, and it accepts a template reference that the inserted view will be based on: this is the templateRef
which we injected in our case.
this.vcr.createEmbeddedView(this.templateRef)
Creating a component dynamically requires a bit more ceremony, because you first need to resolve a factory which knows how to span a component instance.
For this, we use the resolveComponentFactory
method on the instance of the injected ComponentFactoryResolver
.
const cmpFactory = this.cfr.resolveComponentFactory(SpinnerComponent)
Now we can use the resulting factory in order to createComponent
in a similar fashion we created the embedded view.
this.vcr.createComponent(cmpFactory)
Of course, this should happen only if the isBusy
flag is set to true
, so we wrap this in a branch.
if (this.isBusy) {
const cmpFactory = this.cfr.resolveComponentFactory(SpinnerComponent)
this.vcr.createComponent(cmpFactory)
}
Angular needs to compile our component before they can be used in the application. If the component is never referenced in the template, Angular won't know it needs to compile it. This is the case with our Spinner component, as we're only adding it dynamically from code.
To tell explicitly Angular to compile a component, add it to NgModule.entryComponents
.
@NgModule({
...
entryComponents: [SpinnerComponent],
...
})
@Directive({selector: '[xuiBusy]'})
export class BusyDirective implements OnInit {
@Input('xuiBusy') isBusy: boolean;
constructor(private templateRef: TemplateRef<void>,
private vcr: ViewContainerRef,
private cfr: ComponentFactoryResolver) { }
ngOnInit() {
this.vcr.createEmbeddedView(this.templateRef)
if (this.isBusy) {
const cmpFactory = this.cfr.resolveComponentFactory(SpinnerComponent)
this.vcr.createComponent(cmpFactory)
}
}
}
<div *xuiBusy="true">
<span>This is some content</span>
</div>
<div *xuiBusy="false">
<span>This is some content</span>
</div>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With