本文介紹了 ng-bootstrap 項目中,tabset 的實現分析。git
<ngb-tabset>
做爲容器元素,其中的每一個頁籤以一個 <ngb-tab>
元素定義,在 <ngb-tabset>
中包含若干個 <ngb-tab>
子元素。github
在 <ngb-tab>
元素中,使用 <ng-template>
模板來定義內容,內容分爲兩種:標題和內容。bootstrap
標題使用 [ngbTabTitle]
指令來聲明,或者在 <ngb-tab>
元素上使用 title
屬性聲明。api
內容使用 [ngbTabContent]
指令聲明。app
<ngb-tabset> <ngb-tab title="Simple"> <ng-template ngbTabContent> <p>Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat salvia cillum iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.</p> </ng-template> </ngb-tab> <ngb-tab> <ng-template ngbTabTitle><b>Fancy</b> title</ng-template> <ng-template ngbTabContent>Food truck fixie locavore, accusamus mcsweeney's marfa nulla single-origin coffee squid. <p>Exercitation +1 labore velit, blog sartorial PBR leggings next level wes anderson artisan four loko farm-to-table craft beer twee. Qui photo booth letterpress, commodo enim craft beer mlkshk aliquip jean shorts ullamco ad vinyl cillum PBR. Homo nostrud organic, assumenda labore aesthetic magna delectus mollit. Keytar helvetica VHS salvia yr, vero magna velit sapiente labore stumptown. Vegan fanny pack odio cillum wes anderson 8-bit, sustainable jean shorts beard ut DIY ethical culpa terry richardson biodiesel. Art party scenester stumptown, tumblr butcher vero sint qui sapiente accusamus tattooed echo park.</p> </ng-template> </ngb-tab> <ngb-tab title="Disabled" [disabled]="true"> <ng-template ngbTabContent> <p>Sed commodo, leo at suscipit dictum, quam est porttitor sapien, eget sodales nibh elit id diam. Nulla facilisi. Donec egestas ligula vitae odio interdum aliquet. Duis lectus turpis, luctus eget tincidunt eu, congue et odio. Duis pharetra et nisl at faucibus. Quisque luctus pulvinar arcu, et molestie lectus ultrices et. Sed diam urna, egestas ut ipsum vel, volutpat volutpat neque. Praesent fringilla tortor arcu. Vivamus faucibus nisl enim, nec tristique ipsum euismod facilisis. Morbi ut bibendum est, eu tincidunt odio. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris aliquet odio ac lorem aliquet ultricies in eget neque. Phasellus nec tortor vel tellus pulvinar feugiat.</p> </ng-template> </ngb-tab> </ngb-tabset>
能夠看到,外層元素是 <ngb-tabset>
。iphone
每一個 tab 使用元素 <ngb-tab>
定義,tab 的內容使用 <ng-template>
模板定義, tab 中的內容分爲兩個部分:標題和內容。ide
下面是使用模板的標題flex
<ng-template ngbTabTitle><b>Fancy</b> title</ng-template>
標題也能夠在 ngb-tab
上使用 [title]
屬性定義。例如:ui
<ngb-tab title="Disabled" [disabled]="true">
內容部分定義,這裏使用了指令 [ngbTabContent]
便於識別。this
<ng-template ngbTabContent> <p>Sed commodo, leo at suscipit dictum, quam est porttitor sapien, eget sodales nibh elit id diam. </p> </ng-template>
從前面的使用能夠看出,全部 tab 的定義都是 ngb-tabset
元素的內容,它們在使用時定義,而不是在 ngb-tabse
本身的模板中定義。
因此找到它們須要使用 ContentChildren 來找到。
@ContentChildren(NgbTab) tabs: QueryList<NgbTab>;
不使用 ContentChild 的緣由是它沒有提供 descendants
的支持。
在 bootstrap 中,每一個頁籤 實際上渲染成兩個部分,一個標題的列表,和當前顯示的內容。
標題列表使用一個 ul
來處理。其中使用循環來將全部的標題顯示出來。
而 titleTpl
是由模板定義的,因此,使用了 [ngTemplateOutlet]
來渲染出來。
<ul [class]="'nav nav-' + type + (orientation == 'horizontal'? ' ' + justifyClass : ' flex-column')" role="tablist"> <li class="nav-item" *ngFor="let tab of tabs"> <a [id]="tab.id" class="nav-link" [class.active]="tab.id === activeId" [class.disabled]="tab.disabled" href (click)="select(tab.id); $event.preventDefault()" role="tab" [attr.tabindex]="(tab.disabled ? '-1': undefined)" [attr.aria-controls]="(!destroyOnHide || tab.id === activeId ? tab.id + '-panel' : null)" [attr.aria-selected]="tab.id === activeId" [attr.aria-disabled]="tab.disabled"> {{tab.title}}<ng-template [ngTemplateOutlet]="tab.titleTpl?.templateRef"></ng-template> </a> </li> </ul>
title 部分並列使用了兩種來源
{{tab.title}}<ng-template [ngTemplateOutlet]="tab.titleTpl?.templateRef"></ng-template>
內容部分,因爲具體內容也是使用模板定義出來,因此這裏也是使用 [ngTemplateOutlet]
渲染出來。
<div class="tab-content"> <ng-template ngFor let-tab [ngForOf]="tabs"> <div class="tab-pane {{tab.id === activeId ? 'active' : null}}" *ngIf="!destroyOnHide || tab.id === activeId" role="tabpanel" [attr.aria-labelledby]="tab.id" id="{{tab.id}}-panel"> <ng-template [ngTemplateOutlet]="tab.contentTpl?.templateRef"></ng-template> </div> </ng-template> </div>
投影內容須要在 Content
類型的事件中處理。
ngAfterContentChecked() { // auto-correct activeId that might have been set incorrectly as input let activeTab = this._getTabById(this.activeId); this.activeId = activeTab ? activeTab.id : (this.tabs.length ? this.tabs.first.id : null); }
指令的定義很是簡單,就是獲取模板的引用,以便後繼使用。
能夠看到屬性名稱爲 templateRef
@Directive({selector: 'ng-template[ngbTabTitle]'}) export class NgbTabTitle { constructor(public templateRef: TemplateRef<any>) {} }
這是 [ngbTabContent]
的定義,與上面相同,依然是定義了屬性 templateRef
。
@Directive({selector: 'ng-template[ngbTabContent]'}) export class NgbTabContent { constructor(public templateRef: TemplateRef<any>) {} }
元素型的指令,因此連模板都沒有了。
@Directive({selector: 'ngb-tab'})
內容是投影進來的。
因爲在 tab
中使用了模板,而且使用指令來標識出來,它們定義在組件的模板以內,因此這裏使用了 ContentChildren
來識別。
@ContentChildren(NgbTabTitle, {descendants: false}) titleTpls: QueryList<NgbTabTitle>; @ContentChildren(NgbTabContent, {descendants: false}) contentTpls: QueryList<NgbTabContent>
之後就能夠使用 titleTpls
和 contentTpls
來使用模板了。
因爲是內容,須要在 content
的事件中處理,實際上,在每一個頁籤中,咱們只有一個標題和一個內容的聲明。
ngAfterContentChecked() { // We are using @ContentChildren instead of @ContentChild as in the Angular version being used // only @ContentChildren allows us to specify the {descendants: false} option. // Without {descendants: false} we are hitting bugs described in: // https://github.com/ng-bootstrap/ng-bootstrap/issues/2240 this.titleTpl = this.titleTpls.first; this.contentTpl = this.contentTpls.first; }