Adding a Component to a TemplateRef using a structural Directive

心已入冬 提交于 2019-11-30 20:21:57

Demo

I've set up a demo on StackBlitz. The Spinner component, Busy directive and the consumer are all in app.component.ts for brevity.

Setup

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) { }

Passing the boolean

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;

Creating consumer's content

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 the component

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

Entry components

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],
  ...
})

Full code

@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)
    }
  }

}

Usage examples (check out the demo as well)

<div *xuiBusy="true">
  <span>This is some content</span>
</div>

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