不是我的原创,也不是不是我的原创,是在理解原文的基础上修改而来的,还对某些api进行了升级
可以使用容器的createComponent方法来创建组件,但是这个方法还可以有甚多别的参数,比如传递服务参数和内容投影参数,这次就学习下。
如我们有一个组件AdvComponent,希望当鼠标放上的时候动态创建一个组件TooltipComponent,TooltipComponent中还包括一个内容投影的部分即组件ForTooltipComponent。
AdvComponent组件
@Component({
selector: 'app-root',
template: `
<div class="container">
<p tooltipp [tooltipInput]="component">来来来</p>
</div>
`
})
export class AdvComponent {
component = ForTooltipComponent;
}
为了实现鼠标放上之后的效果,添加了一个指令:tooltip,指令上有一个输入属性:tooltipInput,把ForTooltipComponent组件传递了过去,ForTooltipComponent是这样子的:
@Component({
selector: 'for',
template: 'for--{{content}}'
})
export class ForTooltipComponent implements OnInit, OnChanges {
content = Math.random();
constructor(public cd: ChangeDetectorRef) { //ChangeDetectorRef很有用
}
}
ChangeDetectorRef很有用,先不解释。
TooltipDirective指令
@Directive({
selector: '[tooltipp]'
})
export class TooltipDirective implements OnDestroy {
@Input('tooltipInput') content: any;
private componentRef: ComponentRef<TooltipComponent>;
constructor(private element: ElementRef,
private renderer: Renderer2,
private injector: Injector,
private resolver: ComponentFactoryResolver,
private vcr: ViewContainerRef,
) {}
。。。
}
在指令中给元素添加事件,就用到了HostListener了,我们还要在这个回调函数中动态创建一个组件:
@HostListener('mouseenter')
mouseenter() {
const factory = this.resolver.resolveComponentFactory(TooltipComponent);
const injector = Injector.create([
{
provide: 'tooltipConfig',
useValue: {
random: Math.random() // p元素
}
}
]);
this.componentRef = this.vcr.createComponent(factory, undefined, injector, this.getContentProjection());
}
其中最关键的就是createComponent方法的调用了,这次传递了四个参数,从官网文档中可以查看这个方法的参数说明https://angular.io/api/core/ViewContainerRef#createComponent。
1. 第一个参数是组件工厂,和上一篇用法一样
2. 第二个参数是index,指明新建的组件的在容器中的index
3. 第三个参数定义了新创建的组件的一个服务,这里的话就给传递了一个随机数而已
4. 第四个参数是新创建的组件里面的内容投影
TooltipComponent组件
这个组件要可以接收内容投射,
@Component({
template: `
<div>
服务来的random:{{random}},,,
后面是投影:
<ng-content></ng-content>
</div>
`,
})
export class TooltipComponent implements OnInit, AfterViewInit {
random: '';
constructor(@Inject('tooltipConfig') private config) {
// tooltipConfig 是自己配置的Injector
}
ngAfterViewInit() {
setTimeout(() => {
console.log();
this.random = this.config.random;
});
}
}
对random的赋值放到了AfterViewInit里面的setTimeout里面。其实对于这个场景完全可以放到别的地方。放到这里是为了演示一个error:
当放到AfterViewInit钩子中的时候,如果不放到setTimeout中会报错:
TooltipComponent.html:2 ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked.
这个error的原因比较大,一两句话说不清楚,关键我自己也没完全明白,等我明白了再写一篇解释吧。
内容投影部分
这个内容投影想要投一个组件过去,那么这个组件要动态创建。
不过和之前不同的是,并不是直接用容器来创建,而是使用组件工厂的create方法。
getContentProjection() {
const factory = this.resolver.resolveComponentFactory(this.content);
const componentRef = factory.create(this.injector);
// 这个地方先省略一行代码 --------1
// return [[componentRef._viewRef.rootNodes[0]]]
return [[componentRef.location.nativeElement]];
}
create方法至少需要一个参数,所以这里传了this.injector过去。也可以根据情况传递一个Injector.create()的返回的injector实例过去。
投影的内容必须是Node类型,不然会报错:TooltipComponent.html:2 ERROR TypeError: Failed to execute ‘appendChild’ on ‘Node’: parameter 1 is not of type ‘Node’.
现在看看效果:
是不是很诡异???
ForTooltipComponent的模板中写的明明是for–{{content}},content也赋值成了随机数了啊,为什么没显示出来呢???
其实这涉及到angular中的动态创建组件和变更检测的问题,我还没完全明白,等我明白了再写写,反正解决方法是这样的:在故意省略代码的地方(1)加上:
(<ForTooltipComponent>componentRef.instance).cd.detectChanges(); // 不然模板中 -- 后面的内容显示不出来
参考文献:
https://netbasal.com/create-advanced-components-in-angular-e0655df5dde6