angular的插槽的一种简单用法

2,078 阅读4分钟

「这是我参与2022首次更文挑战的第36天,活动详情查看:[2022首次更文挑战] (juejin.cn/post/705288… "juejin.cn/post/705288…

angular 用了一年了,结果还不怎么会用它的插槽,暂且就用select。 templateOut等搞搞明白了再来。

ng-content 标签

这个插槽用法简单,和vue的slot 差不多。

在组件里的预留位置放置<ng-content select="[name=sider]"></ng-content>

然后,使用组件的时候。 假设组件标签是app-co

<app-co >
    <div name="sider"></div>
</app-co>

和 vue相同,如果不设置 select 那就是个默认插槽<ng-content ></ng-content>
使用的时候,直接放在组件标签即可。

image.png

select 属性

这个select属性类似具名插槽,但是select的值,是css选择器,和document.querySelector()的参数是一样的,

select="head" 表示选择第一个head标签
select=".head" 表示选择第一个class="head"的标签
select="#head" 表示选择第一个id="head"的标签
select="[name=head]" 表示选择第一个name="head"的标签
select="[slot=head]" 表示选择第一个slot="head"的标签

最后那个写法是不是可以和vue以假乱真,^_^。

但是要注意都得写在组件标签内.

ContentChildren

说到插槽,就要说一个典型的组件tabset。点击tab栏切换,好像挺简单的。但是有个地方要注意,用于点击的tab,肯定是在外层组件的,但是却是根据内部组件的数据来渲染的。因此,我们需要在外部组件就获取子组件的数据,并且还要监听子组件的变化。

期望的tab组件如下

        <app-tabs>
            <app-tab *ngFor="let bar of bars" [barTitle] = "bar.title">
                {{bar.content}}
            </app-tab>
        </app-tabs>

很明显, 这个tabset组件由外层tabs 和内层tab组成。并且在我改变了遍历的bars数组之后,要能改变对应的tab栏和当前显示的内容。这就要使用 contentChildren 了。

用于从内容 DOM 中获取元素或指令的 `QueryList`。每当添加、删除或移动子元素时,此查询列表都会更新,并且其可观察对象 changes 都会发出新值。

在调用 `ngAfterContentInit` 回调之前设置的内容查询。

我们先定义好内部组件tab.就是一个插槽,然后接受一个tab标题,一个点击事件的回调函数。 我这里直接使用display来控制,当前展示的tab页。如果需要滚动动画可以考虑,Element.scrollIntoView()这个方法。

import { Component, Input, OnInit } from '@angular/core';

@Component({
  selector: 'app-tab',
  template: `
  <main [style.display]="display">
    <ng-content>

    </ng-content>
   </main>
  `,
  styles: [
    ``
  ]
})
export class TabComponent implements OnInit {

  constructor() { }
  @Input()barTitle: '' ;
  display = 'none' ;
  
  @Input()clickcb = () => {} ;
  ngOnInit(): void {
  }

}

然后需要在外部tabs组件里获取子组件的数据,并渲染。 获取的使用方法就是装饰器contentChildren,结果是QueryList,这并不是一个数组,需要调用它的toArray方法转为数组。总体来说这个组件写的比较失败,


import { AfterContentInit, AfterViewInit, ChangeDetectorRef, Component, ContentChildren, Input, OnInit, QueryList, ViewChild, ViewChildren, ViewRef } from '@angular/core';
import { TabComponent } from './tab.component';
import { delay, filter, first, startWith, takeUntil } from 'rxjs/operators';

@Component({
  selector: 'app-tabs',
  template: `
  <div class="top">
    <div class="l">
      <!-- 这里只是要个标题而已  遍历一个轻量的数组会不会好些-->
      <span (click)="toggle(index)" *ngFor="let v of bars; let index  = index" [ngClass]="{active:curIndex === index}" class="tab-title">
        {{v}}
      </span>

    </div>

    <div class="r">
      <ng-content select="[slot=nzTabBarExtraContent]"></ng-content>
    </div>
</div>
<main style="padding:5px 0;" >
    <ng-content>

    </ng-content>
</main>
  `,
  styles: [
    `
    .top{
      display: flex;
      justify-content: space-between;
    }
    .top .l{
      display: flex;
      gap:10px ;
    }
    .tab-title{
    padding: 10px 15px 8px 15px;
    position: relative;
    border-bottom: 2px solid transparent;
    cursor: pointer ;
}
.tab-title:hover{
  color: #409eff;
}
.tab-title.active{
  color: #409eff;
  border-bottom-color:#409eff ;
}
    `
  ]
})
export class TabsComponent implements OnInit, AfterViewInit, AfterContentInit {
  bars = [];
  curIndex = 0;
  @Input()
   set defualtIndex(v){
     /* 这是为了主动设置 活跃的tab 应该有更好的写法 */
     this.curIndex = v ;
     this.tabArray.length && this.toggle(v) ;
    
    }
  tabArray: TabComponent[] = [];
  
  constructor( private cd: ChangeDetectorRef) { }
  @ContentChildren(TabComponent) tabs: QueryList<TabComponent>;
  ngOnInit(): void {

  }
  toggle(index: number): void{
    this.curIndex = index ;
    this.tabArray.forEach((tab) => {
      tab.display = 'none' ;
    });
    this.tabArray[index].display = 'block' ;
    this.tabArray[index].clickcb() ;
  }
  ngAfterViewInit(): void {


  }
  ngAfterContentChecked(): void {
    /* 这个方法会调用多次 并且初始化时会在 init方法后调用 */

}
/* 
变更检测期间
更新所有子组件/指令的绑定属性
调用所有子组件/指令的三个生命周期钩子:ngOnInit,OnChanges,ngDoCheck
更新当前组件的 DOM
为子组件执行变更检测(译者注:在子组件上重复上面三个步骤,依次递归下去)
为所有子组件/指令调用当前组件的 ngAfterViewInit 生命周期钩子

属性值突变的罪魁祸首是子组件或指令,*/
  ngAfterContentInit(): void {
    /* 断点调试发现是子组件先触发这个init  然后是父组件 符合函数调用特征 */
    this.updateChidren(null)
    this.tabs.changes.pipe(startWith(this.tabs)).subscribe((data)=> {
      this.updateChidren(data) ;
    })
  }

  updateChidren(data){
    this.tabArray  =  this.tabs.toArray();
    this.bars = [] ;
    this.tabArray .forEach((tab, i) => {
      this.bars.push( tab.barTitle) ;
      tab.display = 'none' ;

    });
    /* 这里刚好在变更检测之后才去更改  就会导致ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked */

      this.tabArray[this.curIndex].display = 'block' ;
      // this.cd.detectChanges() ;// 增加一个变更检测  加了之后疯狂报错应该这个时候有些数据又没了
     //  this.tabs.notifyOnChanges(); // 通知变更吗 结果栈溢出了,无限触发检查? 应该是不能和上面的混用, 不行
 
    console.log(this.tabArray, this.bars, data) ;
  
  }



}