結(jié)構(gòu)型指令的職責(zé)是 HTML 布局。 它們塑造或重塑 DOM 的結(jié)構(gòu),比如添加、移除或維護(hù)這些元素。
像其它指令一樣,你可以把結(jié)構(gòu)型指令應(yīng)用到一個(gè)宿主元素上。 然后它就可以對(duì)宿主元素及其子元素做點(diǎn)什么。
結(jié)構(gòu)型指令非常容易識(shí)別。 在這個(gè)例子中,星號(hào)(*
)被放在指令的屬性名之前。
Path:"src/app/app.component.html (ngif)"
<div *ngIf="hero" class="name">{{hero.name}}</div>
沒(méi)有方括號(hào),沒(méi)有圓括號(hào),只是把 *ngIf
設(shè)置為一個(gè)字符串。
在這個(gè)例子中,你將學(xué)到星號(hào)(*
)這個(gè)簡(jiǎn)寫(xiě)方法,而這個(gè)字符串是一個(gè)微語(yǔ)法,而不是通常的模板表達(dá)式。 Angular 會(huì)解開(kāi)這個(gè)語(yǔ)法糖,變成一個(gè) <ng-template>
標(biāo)記,包裹著宿主元素及其子元素。 每個(gè)結(jié)構(gòu)型指令都可以用這個(gè)模板做點(diǎn)不同的事情。
三個(gè)常用的內(nèi)置結(jié)構(gòu)型指令 —— NgIf
、NgFor
和NgSwitch...
。 你在模板語(yǔ)法一章中學(xué)過(guò)它,并且在 Angular 文檔的例子中到處都在用它。下面是模板中的例子:
Path:"src/app/app.component.html (built-in)"
<div *ngIf="hero" class="name">{{hero.name}}</div>
<ul>
<li *ngFor="let hero of heroes">{{hero.name}}</li>
</ul>
<div [ngSwitch]="hero?.emotion">
<app-happy-hero *ngSwitchCase="'happy'" [hero]="hero"></app-happy-hero>
<app-sad-hero *ngSwitchCase="'sad'" [hero]="hero"></app-sad-hero>
<app-confused-hero *ngSwitchCase="'confused'" [hero]="hero"></app-confused-hero>
<app-unknown-hero *ngSwitchDefault [hero]="hero"></app-unknown-hero>
</div>
指令的拼寫(xiě)形式
你將看到指令同時(shí)具有兩種拼寫(xiě)形式大駝峰 UpperCamelCase
和小駝峰 lowerCamelCase
,比如你已經(jīng)看過(guò)的 NgIf
和 ngIf
。 這里的原因在于,NgIf
引用的是指令的類(lèi)名,而 ngIf
引用的是指令的屬性名*。
指令的類(lèi)名拼寫(xiě)成大駝峰形式(NgIf
),而它的屬性名則拼寫(xiě)成小駝峰形式(ngIf
)。 本章會(huì)在談?wù)撝噶畹膶傩院凸ぷ髟頃r(shí)引用指令的類(lèi)名,在描述如何在 HTML 模板中把該指令應(yīng)用到元素時(shí),引用指令的屬性名。
還有另外兩種 Angular 指令,在本開(kāi)發(fā)指南的其它地方有講解:(1) 組件 (2) 屬性型指令。
組件可以在原生 HTML 元素中管理一小片區(qū)域的 HTML。從技術(shù)角度說(shuō),它就是一個(gè)帶模板的指令。
屬性型指令會(huì)改變某個(gè)元素、組件或其它指令的外觀或行為。 比如,內(nèi)置的
NgStyle
指令可以同時(shí)修改元素的多個(gè)樣式。
你可以在一個(gè)宿主元素上應(yīng)用多個(gè)屬性型指令,但只能應(yīng)用一個(gè)結(jié)構(gòu)型指令。
NgIf
是一個(gè)很好的結(jié)構(gòu)型指令案例:它接受一個(gè)布爾值,并據(jù)此讓一整塊 DOM 樹(shù)出現(xiàn)或消失。
Path:"src/app/app.component.html (ngif-true)"
<p *ngIf="true">
Expression is true and ngIf is true.
This paragraph is in the DOM.
</p>
<p *ngIf="false">
Expression is false and ngIf is false.
This paragraph is not in the DOM.
</p>
ngIf
指令并不是使用 CSS 來(lái)隱藏元素的。它會(huì)把這些元素從 DOM 中物理刪除。 使用瀏覽器的開(kāi)發(fā)者工具就可以確認(rèn)這一點(diǎn)。
可以看到第一段文字出現(xiàn)在了 DOM 中,而第二段則沒(méi)有,在第二段的位置上是一個(gè)關(guān)于“綁定”的注釋。
當(dāng)條件為假時(shí),NgIf
會(huì)從 DOM 中移除它的宿主元素,取消它監(jiān)聽(tīng)過(guò)的那些 DOM 事件,從 Angular 變更檢測(cè)中移除該組件,并銷(xiāo)毀它。 這些組件和 DOM 節(jié)點(diǎn)可以被當(dāng)做垃圾收集起來(lái),并且釋放它們占用的內(nèi)存。
指令也可以通過(guò)把它的 display
風(fēng)格設(shè)置為 none
而隱藏不需要的段落。
Path:"src/app/app.component.html (display-none)"
<p [style.display]="'block'">
Expression sets display to "block".
This paragraph is visible.
</p>
<p [style.display]="'none'">
Expression sets display to "none".
This paragraph is hidden but still in the DOM.
</p>
當(dāng)不可見(jiàn)時(shí),這個(gè)元素仍然留在 DOM 中。
對(duì)于簡(jiǎn)單的段落,隱藏和移除之間的差異影響不大,但對(duì)于資源占用較多的組件是不一樣的。 當(dāng)隱藏掉一個(gè)元素時(shí),組件的行為還在繼續(xù) —— 它仍然附加在它所屬的 DOM 元素上, 它也仍在監(jiān)聽(tīng)事件。Angular 會(huì)繼續(xù)檢查哪些能影響數(shù)據(jù)綁定的變更。 組件原本要做的那些事情仍在繼續(xù)。
雖然不可見(jiàn),組件及其各級(jí)子組件仍然占用著資源,而這些資源如果分配給別人可能會(huì)更有用。 在性能和內(nèi)存方面的負(fù)擔(dān)相當(dāng)可觀,響應(yīng)度會(huì)降低,而用戶(hù)卻可能無(wú)法從中受益。
當(dāng)然,從積極的一面看,重新顯示這個(gè)元素會(huì)非???。 組件以前的狀態(tài)被保留著,并隨時(shí)可以顯示。 組件不用重新初始化 —— 該操作可能會(huì)比較昂貴。 這時(shí)候隱藏和顯示就成了正確的選擇。
但是,除非有非常強(qiáng)烈的理由來(lái)保留它們,否則你會(huì)更傾向于移除用戶(hù)看不見(jiàn)的那些 DOM 元素,并且使用 NgIf
這樣的結(jié)構(gòu)型指令來(lái)收回用不到的資源。
同樣的考量也適用于每一個(gè)結(jié)構(gòu)型指令,無(wú)論是內(nèi)置的還是自定義的。 你應(yīng)該提醒自己慎重考慮添加元素、移除元素以及創(chuàng)建和銷(xiāo)毀組件的后果。
你可能注意到了指令名的星號(hào)(*
)前綴,并且困惑于為什么需要它以及它是做什么的。
這里的 *ngIf
會(huì)在 hero
存在時(shí)顯示英雄的名字。
Path:"src/app/app.component.html (asterisk)"
<div *ngIf="hero" class="name">{{hero.name}}</div>
星號(hào)是一個(gè)用來(lái)簡(jiǎn)化更復(fù)雜語(yǔ)法的“語(yǔ)法糖”。 從內(nèi)部實(shí)現(xiàn)來(lái)說(shuō),Angular 把 *ngIf
屬性 翻譯成一個(gè) <ng-template>
元素 并用它來(lái)包裹宿主元素,代碼如下:
Path:"src/app/app.component.html (ngif-template)"
<ng-template [ngIf]="hero">
<div class="name">{{hero.name}}</div>
</ng-template>
*ngIf
指令被移到了 <ng-template>
元素上。在那里它變成了一個(gè)屬性綁定 [ngIf]
。<div>
上的其余部分,包括它的 class
屬性在內(nèi),移到了內(nèi)部的 <ng-template>
元素上。第一種形態(tài)永遠(yuǎn)不會(huì)真的渲染出來(lái)。 只有最終產(chǎn)出的結(jié)果才會(huì)出現(xiàn)在 DOM 中。
Angular 會(huì)在真正渲染的時(shí)候填充 <ng-template>
的內(nèi)容,并且把 <ng-template>
替換為一個(gè)供診斷用的注釋。
NgFor
和NgSwitch...
指令也都遵循同樣的模式。
Angular 會(huì)把 *ngFor
用同樣的方式把星號(hào)(*
)語(yǔ)法的 template
屬性轉(zhuǎn)換成 <ng-template>
元素。
這里有一個(gè) NgFor
的全特性應(yīng)用,同時(shí)用了這兩種寫(xiě)法:
Path:"src/app/app.component.html (inside-ngfor)"
<div *ngFor="let hero of heroes; let i=index; let odd=odd; trackBy: trackById" [class.odd]="odd">
({{i}}) {{hero.name}}
</div>
<ng-template ngFor let-hero [ngForOf]="heroes" let-i="index" let-odd="odd" [ngForTrackBy]="trackById">
<div [class.odd]="odd">({{i}}) {{hero.name}}</div>
</ng-template>
它明顯比 ngIf
復(fù)雜得多,確實(shí)如此。 NgFor
指令比本章展示過(guò)的 NgIf
具有更多的必選特性和可選特性。 至少 NgFor
會(huì)需要一個(gè)循環(huán)變量(let hero)和一個(gè)列表(heroes)。
你可以通過(guò)把一個(gè)字符串賦值給 ngFor
來(lái)啟用這些特性,這個(gè)字符串使用 Angular 的微語(yǔ)法。
ngFor
字符串之外的每一樣?xùn)|西都會(huì)留在宿主元素(<div&
)上,也就是說(shuō)它移到了<ng-template&
內(nèi)部。 在這個(gè)例子中,[class.odd]="odd"
留在了<div&
上。
Angular 微語(yǔ)法能讓你通過(guò)簡(jiǎn)短的、友好的字符串來(lái)配置一個(gè)指令。 微語(yǔ)法解析器把這個(gè)字符串翻譯成 <ng-template>
上的屬性:
let
關(guān)鍵字聲明一個(gè)模板輸入變量,你會(huì)在模板中引用它。本例子中,這個(gè)輸入變量就是 hero
、i
和 odd
。 解析器會(huì)把 let hero
、let i
和 let odd
翻譯成命名變量 let-hero
、let-i
和 let-odd
。of
和 trackby
,把它們首字母大寫(xiě)(of
-> Of
, trackBy
-> TrackBy
), 并且給它們加上指令的屬性名(ngFor
)前綴,最終生成的名字是 ngForOf
和 ngForTrackBy
。 這兩個(gè)最終生成的名字是 NgFor
的輸入屬性,指令據(jù)此了解到列表是 heroes
,而 track-by
函數(shù)是 trackById
。NgFor
指令在列表上循環(huán),每個(gè)循環(huán)中都會(huì)設(shè)置和重置它自己的上下文對(duì)象上的屬性。 這些屬性包括但不限于 index
和 odd
以及一個(gè)特殊的屬性名 $implicit
(隱式變量)。let-i
和 let-odd
變量是通過(guò) let i=index
和 let odd=odd
來(lái)定義的。 Angular 把它們?cè)O(shè)置為上下文對(duì)象中的 index
和 odd
屬性的當(dāng)前值。let-hero
的上下文屬性。它的來(lái)源是隱式的。 Angular 將 let-hero
設(shè)置為此上下文中 $implicit
屬性的值, 它是由 NgFor
用當(dāng)前迭代中的英雄初始化的。NgFor
指令的其它屬性和上下文屬性。NgForOf
指令實(shí)現(xiàn)了 NgFor
。請(qǐng)到 NgForOf API 參考手冊(cè)中了解 NgForOf
指令的更多屬性及其上下文屬性。
當(dāng)你編寫(xiě)自己的結(jié)構(gòu)型指令時(shí),也可以利用這些微語(yǔ)法機(jī)制。 例如,Angular 中的微語(yǔ)法允許你寫(xiě)成 <div *ngFor="let item of items">{{item}}</div>
而不是 <ng-template ngFor let-item [ngForOf]="items"><div>{{item}}</div></ng-template>
。 以下各節(jié)提供了有關(guān)約束、語(yǔ)法和微語(yǔ)法翻譯方式的詳細(xì)信息。
微語(yǔ)法必須滿(mǎn)足以下要求:
當(dāng)你編寫(xiě)自己的結(jié)構(gòu)型指令時(shí),請(qǐng)使用以下語(yǔ)法:
*:prefix="( :let | :expression ) (';' | ',')? ( :let | :as | :keyExp )*"
下表描述了微語(yǔ)法的每個(gè)組成部分。
組成部分 | 描述 |
---|---|
prefix |
HTML 屬性鍵(attribute key) |
key |
HTML 屬性鍵(attribute key) |
local |
模板中使用的局部變量名 |
export |
指令使用指定名稱(chēng)導(dǎo)出的值 |
expression |
標(biāo)準(zhǔn) Angular 表達(dá)式 |
keyExp = :key ":"? :expression ("as" :local)? ";"? |
let = "let" :local "=" :export ";"? |
as = :export "as" :local ";"? |
將微語(yǔ)法轉(zhuǎn)換為常規(guī)的綁定語(yǔ)法,如下所示:
微語(yǔ)法 | 翻譯結(jié)果 |
---|---|
prefix 和裸表達(dá)式 | [prefix]="expression" |
keyExp | [prefixKey] "表達(dá)式" (let-prefixKey="export"),注意 prefix 已經(jīng)加成了 key |
let | let-local="export" |
下表說(shuō)明了 Angular 會(huì)如何解開(kāi)微語(yǔ)法。
微語(yǔ)法 | 解語(yǔ)法糖后 |
---|---|
*ngFor="let item of [1,2,3]" | <ng-template ngFor let-item [ngForOf]="[1,2,3]"> |
*ngFor="let item of [1,2,3] as items; trackBy: myTrack; index as i" | <ng-template ngFor let-item [ngForOf]="[1,2,3]" let-items="ngForOf" [ngForTrackBy]="myTrack" let-i="index"> |
*ngIf="exp" | <ng-template [ngIf]="exp"> |
*ngIf="exp as value" | <ng-template [ngIf]="exp" let-value="ngIf"> |
注:
- 這些微語(yǔ)法機(jī)制在你寫(xiě)自己的結(jié)構(gòu)型指令時(shí)也同樣有效。
模板輸入變量是這樣一種變量,你可以在單個(gè)實(shí)例的模板中引用它的值。 這個(gè)例子中有好幾個(gè)模板輸入變量:hero
、i
和 odd
。 它們都是用 let
作為前導(dǎo)關(guān)鍵字。
模板輸入變量和模板引用變量是不同的,無(wú)論是在語(yǔ)義上還是語(yǔ)法上。
你使用 let
關(guān)鍵字(如 let hero
)在模板中聲明一個(gè)模板輸入變量。 這個(gè)變量的范圍被限制在所重復(fù)模板的單一實(shí)例上。 事實(shí)上,你可以在其它結(jié)構(gòu)型指令中使用同樣的變量名。
而聲明模板引用變量使用的是給變量名加 #
前綴的方式(#var)。 一個(gè)引用變量引用的是它所附著到的元素、組件或指令。它可以在整個(gè)模板的任意位置訪問(wèn)。
模板輸入變量和引用變量具有各自獨(dú)立的命名空間。let hero
中的 hero
和 #hero
中的 hero
并不是同一個(gè)變量。
有時(shí)你會(huì)希望只有當(dāng)特定的條件為真時(shí)才重復(fù)渲染一個(gè) HTML 塊。 你可能試過(guò)把 *ngFor
和 *ngIf
放在同一個(gè)宿主元素上,但 Angular 不允許。這是因?yàn)槟阍谝粋€(gè)元素上只能放一個(gè)結(jié)構(gòu)型指令。
原因很簡(jiǎn)單。結(jié)構(gòu)型指令可能會(huì)對(duì)宿主元素及其子元素做很復(fù)雜的事。當(dāng)兩個(gè)指令放在同一個(gè)元素上時(shí),誰(shuí)先誰(shuí)后?NgIf
優(yōu)先還是 NgFor
優(yōu)先?NgIf
可以取消 NgFor
的效果嗎? 如果要這樣做,Angular 應(yīng)該如何把這種能力泛化,以取消其它結(jié)構(gòu)型指令的效果呢?
對(duì)這些問(wèn)題,沒(méi)有辦法簡(jiǎn)單回答。而禁止多個(gè)結(jié)構(gòu)型指令則可以簡(jiǎn)單地解決這個(gè)問(wèn)題。 這種情況下有一個(gè)簡(jiǎn)單的解決方案:把 *ngIf
放在一個(gè)"容器"元素上,再包裝進(jìn) *ngFor
元素。 這個(gè)元素可以使用ng-container
,以免引入一個(gè)新的 HTML 層級(jí)。
Angular 的 NgSwitch
實(shí)際上是一組相互合作的指令:NgSwitch
、NgSwitchCase
和 NgSwitchDefault
。
例子如下:
Path:"src/app/app.component.html (ngswitch)"
<div [ngSwitch]="hero?.emotion">
<app-happy-hero *ngSwitchCase="'happy'" [hero]="hero"></app-happy-hero>
<app-sad-hero *ngSwitchCase="'sad'" [hero]="hero"></app-sad-hero>
<app-confused-hero *ngSwitchCase="'confused'" [hero]="hero"></app-confused-hero>
<app-unknown-hero *ngSwitchDefault [hero]="hero"></app-unknown-hero>
</div>
一個(gè)值(hero.emotion
)被被賦值給了 NgSwitch
,以決定要顯示哪一個(gè)分支。
NgSwitch
本身不是結(jié)構(gòu)型指令,而是一個(gè)屬性型指令,它控制其它兩個(gè) switch
指令的行為。 這也就是為什么你要寫(xiě)成 [ngSwitch]
而不是 *ngSwitch
的原因。
NgSwitchCase
和 NgSwitchDefault
都是結(jié)構(gòu)型指令。 因此你要使用星號(hào)(*
)前綴來(lái)把它們附著到元素上。 NgSwitchCase
會(huì)在它的值匹配上選項(xiàng)值的時(shí)候顯示它的宿主元素。 NgSwitchDefault
則會(huì)當(dāng)沒(méi)有兄弟 NgSwitchCase
匹配上時(shí)顯示它的宿主元素。
指令所在的元素就是它的宿主元素。
<happy-hero&
是*ngSwitchCase
的宿主元素。<unknown-hero&
是*ngSwitchDefault
的宿主元素。
像其它的結(jié)構(gòu)型指令一樣,NgSwitchCase
和 NgSwitchDefault
也可以解開(kāi)語(yǔ)法糖,變成 <ng-template>
的形式。
Path:"src/app/app.component.html (ngswitch-template)"
<div [ngSwitch]="hero?.emotion">
<ng-template [ngSwitchCase]="'happy'">
<app-happy-hero [hero]="hero"></app-happy-hero>
</ng-template>
<ng-template [ngSwitchCase]="'sad'">
<app-sad-hero [hero]="hero"></app-sad-hero>
</ng-template>
<ng-template [ngSwitchCase]="'confused'">
<app-confused-hero [hero]="hero"></app-confused-hero>
</ng-template >
<ng-template ngSwitchDefault>
<app-unknown-hero [hero]="hero"></app-unknown-hero>
</ng-template>
</div>
星號(hào)(*
)語(yǔ)法比不帶語(yǔ)法糖的形式更加清晰。 如果找不到單一的元素來(lái)應(yīng)用該指令,可以使用<ng-container>
作為該指令的容器。
雖然很少有理由在模板中使用結(jié)構(gòu)型指令的屬性形式和元素形式,但這些幕后知識(shí)仍然是很重要的,即:Angular 會(huì)創(chuàng)建 <ng-template>
,還要了解它的工作原理。 當(dāng)需要寫(xiě)自己的結(jié)構(gòu)型指令時(shí),你就要使用 <ng-template>
。
<ng-template>
是一個(gè) Angular 元素,用來(lái)渲染 HTML。 它永遠(yuǎn)不會(huì)直接顯示出來(lái)。 事實(shí)上,在渲染視圖之前,Angular 會(huì)把 <ng-template>
及其內(nèi)容替換為一個(gè)注釋。
如果沒(méi)有使用結(jié)構(gòu)型指令,而僅僅把一些別的元素包裝進(jìn) <ng-template>
中,那些元素就是不可見(jiàn)的。 在下面的這個(gè)短語(yǔ)"Hip! Hip! Hooray!"中,中間的這個(gè) "Hip!"(歡呼聲) 就是如此。
Path:"src/app/app.component.html (template-tag)"
<p>Hip!</p>
<ng-template>
<p>Hip!</p>
</ng-template>
<p>Hooray!</p>
Angular 抹掉了中間的那個(gè) "Hip!",讓歡呼聲顯得不再那么熱烈了。
結(jié)構(gòu)型指令會(huì)讓 <ng-template>
正常工作,在你寫(xiě)自己的結(jié)構(gòu)型指令時(shí)就會(huì)看到這一點(diǎn)。
通常都需要一個(gè)根元素作為結(jié)構(gòu)型指令的宿主。 列表元素(<li>
)就是一個(gè)典型的供 NgFor
使用的宿主元素。
Path:"src/app/app.component.html (ngfor-li)"
<li *ngFor="let hero of heroes">{{hero.name}}</li>
當(dāng)沒(méi)有這樣一個(gè)單一的宿主元素時(shí),你就可以把這些內(nèi)容包裹在一個(gè)原生的 HTML 容器元素中,比如 <div>
,并且把結(jié)構(gòu)型指令附加到這個(gè)"包裹"上。
Path:"src/app/app.component.html (ngif)"
<div *ngIf="hero" class="name">{{hero.name}}</div>
但引入另一個(gè)容器元素(通常是 <span>
或 <div>
)來(lái)把一些元素歸到一個(gè)單一的根元素下,通常也會(huì)帶來(lái)問(wèn)題。注意,是"通常"而不是"總會(huì)"。
這種用于分組的元素可能會(huì)破壞模板的外觀表現(xiàn),因?yàn)?CSS 的樣式既不曾期待也不會(huì)接受這種新的元素布局。 比如,假設(shè)你有下列分段布局。
Path:"src/app/app.component.html (ngif-span)"
<p>
I turned the corner
<span *ngIf="hero">
and saw {{hero.name}}. I waved
</span>
and continued on my way.
</p>
而你的 CSS 樣式規(guī)則是應(yīng)用于 <p>
元素下的 <span>
的。
Path:"src/app/app.component.css (p-span)"
p span { color: red; font-size: 70%; }
這樣渲染出來(lái)的段落就會(huì)非常奇怪。
本來(lái)為其它地方準(zhǔn)備的 p
span
樣式,被意外的應(yīng)用到了這里。
另一個(gè)問(wèn)題是:有些 HTML 元素需要所有的直屬下級(jí)都具有特定的類(lèi)型。 比如,<select>
元素要求直屬下級(jí)必須為 <option>
,那就沒(méi)辦法把這些選項(xiàng)包裝進(jìn) <div>
或 <span>
中。
如果這樣做:
Path:"src/app/app.component.html (select-span)"
<div>
Pick your favorite hero
(<label><input type="checkbox" checked (change)="showSad = !showSad">show sad</label>)
</div>
<select [(ngModel)]="hero">
<span *ngFor="let h of heroes">
<span *ngIf="showSad || h.emotion !== 'sad'">
<option [ngValue]="h">{{h.name}} ({{h.emotion}})</option>
</span>
</span>
</select>
下拉列表就是空的。瀏覽器不會(huì)顯示 <span> 中的 <option>。
Angular 的 <ng-container>
是一個(gè)分組元素,但它不會(huì)污染樣式或元素布局,因?yàn)?Angular 壓根不會(huì)把它放進(jìn) DOM 中。
下面是重新實(shí)現(xiàn)的條件化段落,這次使用 <ng-container>
。
Path:"src/app/app.component.html (ngif-ngcontainer)"
<p>
I turned the corner
<ng-container *ngIf="hero">
and saw {{hero.name}}. I waved
</ng-container>
and continued on my way.
</p>
這次就渲染對(duì)了。
現(xiàn)在用 <ng-container>
來(lái)根據(jù)條件排除選擇框中的某個(gè) <option>
。
Path:"src/app/app.component.html (select-ngcontainer)"
<div>
Pick your favorite hero
(<label><input type="checkbox" checked (change)="showSad = !showSad">show sad</label>)
</div>
<select [(ngModel)]="hero">
<ng-container *ngFor="let h of heroes">
<ng-container *ngIf="showSad || h.emotion !== 'sad'">
<option [ngValue]="h">{{h.name}} ({{h.emotion}})</option>
</ng-container>
</ng-container>
</select>
下拉框也工作正常。
注:
-ngModel
指令是在 Angular 的FormsModule
中定義的,你要在想使用它的模塊的imports: [...]
元數(shù)據(jù)中導(dǎo)入FormsModule
。
<ng-container>
是一個(gè)由 Angular 解析器負(fù)責(zé)識(shí)別處理的語(yǔ)法元素。 它不是一個(gè)指令、組件、類(lèi)或接口,更像是 JavaScript 中 if
塊中的花括號(hào)。
if (someCondition) {
statement1;
statement2;
statement3;
}
沒(méi)有這些花括號(hào),JavaScript 只會(huì)執(zhí)行第一句,而你原本的意圖是把其中的所有語(yǔ)句都視為一體來(lái)根據(jù)條件執(zhí)行。 而 <ng-container>
滿(mǎn)足了 Angular 模板中類(lèi)似的需求。
你需要寫(xiě)一個(gè)名叫 UnlessDirective
的結(jié)構(gòu)型指令,它是 NgIf
的反義詞。 NgIf
在條件為 true
的時(shí)候顯示模板內(nèi)容,而 UnlessDirective
則會(huì)在條件為 false
時(shí)顯示模板內(nèi)容。
Path:"src/app/app.component.html (appUnless-1)"
<p *appUnless="condition">Show this sentence unless the condition is true.</p>
創(chuàng)建指令很像創(chuàng)建組件。
Directive
裝飾器(而不再是 Component
)。Input
、TemplateRef
和 ViewContainerRef
,你在任何結(jié)構(gòu)型指令中都會(huì)需要它們。這里是起點(diǎn):
Path:"src/app/unless.directive.ts (skeleton)"
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({ selector: '[appUnless]'})
export class UnlessDirective {
}
指令的選擇器通常是把指令的屬性名括在方括號(hào)中,如 [appUnless]
。 這個(gè)方括號(hào)定義出了一個(gè) CSS 屬性選擇器。
該指令的屬性名應(yīng)該拼寫(xiě)成小駝峰形式,并且?guī)в幸粋€(gè)前綴。 但是,這個(gè)前綴不能用 ng,因?yàn)樗粚儆?Angular 本身。 請(qǐng)選擇一些簡(jiǎn)短的,適合你自己或公司的前綴。 在這個(gè)例子中,前綴是 app。
指令的類(lèi)名用 Directive
結(jié)尾,參見(jiàn)風(fēng)格指南。 但 Angular 自己的指令例外。
像這個(gè)例子一樣的簡(jiǎn)單結(jié)構(gòu)型指令會(huì)從 Angular 生成的 <ng-template>
元素中創(chuàng)建一個(gè)內(nèi)嵌的視圖,并把這個(gè)視圖插入到一個(gè)視圖容器中,緊挨著本指令原來(lái)的宿主元素 <p>
(譯注:注意不是子節(jié)點(diǎn),而是兄弟節(jié)點(diǎn))。
你可以使用TemplateRef
取得 <ng-template>
的內(nèi)容,并通過(guò)ViewContainerRef
來(lái)訪問(wèn)這個(gè)視圖容器。
你可以把它們都注入到指令的構(gòu)造函數(shù)中,作為該類(lèi)的私有屬性。
Path:"src/app/unless.directive.ts (ctor)"
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef) { }
該指令的使用者會(huì)把一個(gè) true/false
條件綁定到 [appUnless]
屬性上。 也就是說(shuō),該指令需要一個(gè)帶有 @Input
的 appUnless
屬性。
Path:"src/app/unless.directive.ts (set)"
@Input() set appUnless(condition: boolean) {
if (!condition && !this.hasView) {
this.viewContainer.createEmbeddedView(this.templateRef);
this.hasView = true;
} else if (condition && this.hasView) {
this.viewContainer.clear();
this.hasView = false;
}
}
一旦該值的條件發(fā)生了變化,Angular 就會(huì)去設(shè)置 appUnless
屬性。因?yàn)椴荒苡?appUnless
屬性,所以你要為它定義一個(gè)設(shè)置器(setter
)。
如果條件為假,并且以前尚未創(chuàng)建過(guò)該視圖,就告訴視圖容器(ViewContainer
)根據(jù)模板創(chuàng)建一個(gè)內(nèi)嵌視圖。
如果條件為真,并且視圖已經(jīng)顯示出來(lái)了,就會(huì)清除該容器,并銷(xiāo)毀該視圖。
沒(méi)有人會(huì)讀取 appUnless
屬性,因此它不需要定義 getter
。
完整的指令代碼如下:
Path:"src/app/unless.directive.ts (excerpt)"
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
/**
* Add the template content to the DOM unless the condition is true.
*/
@Directive({ selector: '[appUnless]'})
export class UnlessDirective {
private hasView = false;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef) { }
@Input() set appUnless(condition: boolean) {
if (!condition && !this.hasView) {
this.viewContainer.createEmbeddedView(this.templateRef);
this.hasView = true;
} else if (condition && this.hasView) {
this.viewContainer.clear();
this.hasView = false;
}
}
}
把這個(gè)指令添加到 AppModule
的 declarations
數(shù)組中。
然后創(chuàng)建一些 HTML 來(lái)試用一下。
Path:"src/app/app.component.html (appUnless)"
<p *appUnless="condition" class="unless a">
(A) This paragraph is displayed because the condition is false.
</p>
<p *appUnless="!condition" class="unless b">
(B) Although the condition is true,
this paragraph is displayed because appUnless is set to false.
</p>
當(dāng) condition
為 false
時(shí),頂部的段落就會(huì)顯示出來(lái),而底部的段落消失了。 當(dāng) condition
為 true
時(shí),頂部的段落被移除了,而底部的段落顯示了出來(lái)。
你可以通過(guò)在指令定義中添加模板守護(hù)屬性來(lái)改進(jìn)自定義指令的模板類(lèi)型檢查。這些屬性可以幫助 Angular 模板類(lèi)型檢查器在編譯期間發(fā)現(xiàn)模板中的錯(cuò)誤,避免這些失誤導(dǎo)致運(yùn)行期錯(cuò)誤。
使用類(lèi)型守護(hù)屬性可以告訴模板類(lèi)型檢查器你所期望的類(lèi)型,從而改進(jìn)該模板的編譯期類(lèi)型檢查。
ngTemplateGuard_(someInputProperty)
允許你為模板中的輸入表達(dá)式指定一個(gè)更準(zhǔn)確的類(lèi)型。ngTemplateContextGuard
靜態(tài)屬性聲明了模板上下文的類(lèi)型。本節(jié)提供了這兩種類(lèi)型守護(hù)屬性的例子。
模板中的結(jié)構(gòu)型指令會(huì)根據(jù)輸入表達(dá)式來(lái)控制是否要在運(yùn)行時(shí)渲染該模板。為了幫助編譯器捕獲模板類(lèi)型中的錯(cuò)誤,你應(yīng)該盡可能詳細(xì)地指定模板內(nèi)指令的輸入表達(dá)式所期待的類(lèi)型。
類(lèi)型守護(hù)函數(shù)會(huì)把輸入表達(dá)式所期待的類(lèi)型窄化為在運(yùn)行時(shí)可能傳給指令的子類(lèi)型。你可以提供這樣一個(gè)函數(shù)來(lái)幫助類(lèi)型檢查器在編譯期間推斷出該表達(dá)式的正確類(lèi)型。
例如,NgIf
的實(shí)現(xiàn)使用類(lèi)型窄化來(lái)確保只有當(dāng) *ngIf
的輸入表達(dá)式為真時(shí),模板才會(huì)被實(shí)例化。為了提供具體的類(lèi)型要求,NgIf
指令定義了一個(gè)靜態(tài)屬性 ngTemplateGuard_ngIf: 'binding'
。binding
值是一種常見(jiàn)的類(lèi)型窄化的例子,它會(huì)對(duì)輸入表達(dá)式進(jìn)行求值,以滿(mǎn)足類(lèi)型要求。
要為模板中的指令提供一個(gè)更具體的輸入表達(dá)式類(lèi)型,就要把 ngTemplateGuard_xx
屬性添加到該指令中,其靜態(tài)屬性名的后綴(xx)是 @Input
字段名。該屬性的值既可以是針對(duì)其返回類(lèi)型的通用類(lèi)型窄化函數(shù),也可以是字符串 "binding" 就像 NgIf
一樣。
例如,考慮以下結(jié)構(gòu)型指令,它以模板表達(dá)式的結(jié)果作為輸入。
Path:"src/app/IfLoadedDirective"
export type Loaded = { type: 'loaded', data: T };
export type Loading = { type: 'loading' };
export type LoadingState = Loaded | Loading;
export class IfLoadedDirective {
@Input('ifLoaded') set state(state: LoadingState) {}
static ngTemplateGuard_state(dir: IfLoadedDirective, expr: LoadingState): expr is Loaded { return true; };
export interface Person {
name: string;
}
@Component({
template: `
{{ state.data }}
`,
})
export class AppComponent {
state: LoadingState;
}
在這個(gè)例子中,LoadingState<T>
類(lèi)型允許兩種狀態(tài)之一,Loaded<T>
或 Loading
。此表達(dá)式用作該指令的 state 輸入是一個(gè)總括類(lèi)型 LoadingState
,因?yàn)榇颂幍募虞d狀態(tài)是未知的。
IfLoadedDirective
定義聲明了靜態(tài)字段 ngTemplateGuard_state
,表示其窄化行為。在 AppComponent 模板中,*ifLoaded
結(jié)構(gòu)型指令只有當(dāng)實(shí)際的 state
是 Loaded<Person>
類(lèi)型時(shí),才會(huì)渲染該模板。類(lèi)型守護(hù)允許類(lèi)型檢查器推斷出模板中可接受的 state
類(lèi)型是 Loaded<T>
,并進(jìn)一步推斷出 T
必須是 Person
一個(gè)實(shí)例。
如果你的結(jié)構(gòu)型指令要為實(shí)例化的模板提供一個(gè)上下文,可以通過(guò)提供靜態(tài)的 ngTemplateContextGuard
函數(shù)在模板中給它提供合適的類(lèi)型。下面的代碼片段展示了該函數(shù)的一個(gè)例子。
Path:"src/app/myDirective.ts"
@Directive({…})
export class ExampleDirective {
// Make sure the template checker knows the type of the context with which the
// template of this directive will be rendered
static ngTemplateContextGuard(dir: ExampleDirective, ctx: unknown): ctx is ExampleContext { return true; };
// …
}
import { Component } from '@angular/core';
import { Hero, heroes } from './hero';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
heroes = heroes;
hero = this.heroes[0];
condition = false;
logs: string[] = [];
showSad = true;
status = 'ready';
trackById(index: number, hero: Hero): number { return hero.id; }
}
<h1>Structural Directives</h1>
<p>Conditional display of hero</p>
<blockquote>
<div *ngIf="hero" class="name">{{hero.name}}</div>
</blockquote>
<p>List of heroes</p>
<ul>
<li *ngFor="let hero of heroes">{{hero.name}}</li>
</ul>
<hr>
<h2 id="ngIf">NgIf</h2>
<p *ngIf="true">
Expression is true and ngIf is true.
This paragraph is in the DOM.
</p>
<p *ngIf="false">
Expression is false and ngIf is false.
This paragraph is not in the DOM.
</p>
<p [style.display]="'block'">
Expression sets display to "block".
This paragraph is visible.
</p>
<p [style.display]="'none'">
Expression sets display to "none".
This paragraph is hidden but still in the DOM.
</p>
<h4>NgIf with template</h4>
<p><ng-template> element</p>
<ng-template [ngIf]="hero">
<div class="name">{{hero.name}}</div>
</ng-template>
<hr>
<h2 id="ng-container"><ng-container></h2>
<h4>*ngIf with a <ng-container></h4>
<button (click)="hero = hero ? null : heroes[0]">Toggle hero</button>
<p>
I turned the corner
<ng-container *ngIf="hero">
and saw {{hero.name}}. I waved
</ng-container>
and continued on my way.
</p>
<p>
I turned the corner
<span *ngIf="hero">
and saw {{hero.name}}. I waved
</span>
and continued on my way.
</p>
<p><i><select> with <span></i></p>
<div>
Pick your favorite hero
(<label><input type="checkbox" checked (change)="showSad = !showSad">show sad</label>)
</div>
<select [(ngModel)]="hero">
<span *ngFor="let h of heroes">
<span *ngIf="showSad || h.emotion !== 'sad'">
<option [ngValue]="h">{{h.name}} ({{h.emotion}})</option>
</span>
</span>
</select>
<p><i><select> with <ng-container></i></p>
<div>
Pick your favorite hero
(<label><input type="checkbox" checked (change)="showSad = !showSad">show sad</label>)
</div>
<select [(ngModel)]="hero">
<ng-container *ngFor="let h of heroes">
<ng-container *ngIf="showSad || h.emotion !== 'sad'">
<option [ngValue]="h">{{h.name}} ({{h.emotion}})</option>
</ng-container>
</ng-container>
</select>
<br><br>
<hr>
<h2 id="ngFor">NgFor</h2>
<div class="box">
<p class="code"><div *ngFor="let hero of heroes; let i=index; let odd=odd; trackBy: trackById" [class.odd]="odd"></p>
<div *ngFor="let hero of heroes; let i=index; let odd=odd; trackBy: trackById" [class.odd]="odd">
({{i}}) {{hero.name}}
</div>
<p class="code"><ng-template ngFor let-hero [ngForOf]="heroes" let-i="index" let-odd="odd" [ngForTrackBy]="trackById"/></p>
<ng-template ngFor let-hero [ngForOf]="heroes" let-i="index" let-odd="odd" [ngForTrackBy]="trackById">
<div [class.odd]="odd">({{i}}) {{hero.name}}</div>
</ng-template>
</div>
<hr>
<h2 id="ngSwitch">NgSwitch</h2>
<div>Pick your favorite hero</div>
<p>
<label *ngFor="let h of heroes">
<input type="radio" name="heroes" [(ngModel)]="hero" [value]="h">{{h.name}}
</label>
<label><input type="radio" name="heroes" (click)="hero = null">None of the above</label>
</p>
<h4>NgSwitch</h4>
<div [ngSwitch]="hero?.emotion">
<app-happy-hero *ngSwitchCase="'happy'" [hero]="hero"></app-happy-hero>
<app-sad-hero *ngSwitchCase="'sad'" [hero]="hero"></app-sad-hero>
<app-confused-hero *ngSwitchCase="'confused'" [hero]="hero"></app-confused-hero>
<app-unknown-hero *ngSwitchDefault [hero]="hero"></app-unknown-hero>
</div>
<h4>NgSwitch with <ng-template></h4>
<div [ngSwitch]="hero?.emotion">
<ng-template [ngSwitchCase]="'happy'">
<app-happy-hero [hero]="hero"></app-happy-hero>
</ng-template>
<ng-template [ngSwitchCase]="'sad'">
<app-sad-hero [hero]="hero"></app-sad-hero>
</ng-template>
<ng-template [ngSwitchCase]="'confused'">
<app-confused-hero [hero]="hero"></app-confused-hero>
</ng-template >
<ng-template ngSwitchDefault>
<app-unknown-hero [hero]="hero"></app-unknown-hero>
</ng-template>
</div>
<hr>
<h2><ng-template></h2>
<p>Hip!</p>
<ng-template>
<p>Hip!</p>
</ng-template>
<p>Hooray!</p>
<hr>
<h2 id="appUnless">UnlessDirective</h2>
<p>
The condition is currently
<span [ngClass]="{ 'a': !condition, 'b': condition, 'unless': true }">{{condition}}</span>.
<button
(click)="condition = !condition"
[ngClass] = "{ 'a': condition, 'b': !condition }" >
Toggle condition to {{condition ? 'false' : 'true'}}
</button>
</p>
<p *appUnless="condition" class="unless a">
(A) This paragraph is displayed because the condition is false.
</p>
<p *appUnless="!condition" class="unless b">
(B) Although the condition is true,
this paragraph is displayed because appUnless is set to false.
</p>
<h4>UnlessDirective with template</h4>
<p *appUnless="condition">Show this sentence unless the condition is true.</p>
<p *appUnless="condition" class="code unless">
(A) <p *appUnless="condition" class="code unless">
</p>
<ng-template [appUnless]="condition">
<p class="code unless">
(A) <ng-template [appUnless]="condition">
</p>
</ng-template>
button {
min-width: 100px;
font-size: 100%;
}
.box {
border: 1px solid gray;
max-width: 600px;
padding: 4px;
}
.choices {
font-style: italic;
}
code, .code {
background-color: #eee;
color: black;
font-family: Courier, sans-serif;
font-size: 85%;
}
div.code {
width: 400px;
}
.heroic {
font-size: 150%;
font-weight: bold;
}
hr {
margin: 40px 0
}
.odd {
background-color: palegoldenrod;
}
td, th {
text-align: left;
vertical-align: top;
}
p span { color: red; font-size: 70%; }
.unless {
border: 2px solid;
padding: 6px;
}
p.unless {
width: 500px;
}
button.a, span.a, .unless.a {
color: red;
border-color: gold;
background-color: yellow;
font-size: 100%;
}
button.b, span.b, .unless.b {
color: black;
border-color: green;
background-color: lightgreen;
font-size: 100%;
}
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { heroSwitchComponents } from './hero-switch.components';
import { UnlessDirective } from './unless.directive';
@NgModule({
imports: [ BrowserModule, FormsModule ],
declarations: [
AppComponent,
heroSwitchComponents,
UnlessDirective
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
export interface Hero {
id: number;
name: string;
emotion?: string;
}
export const heroes: Hero[] = [
{ id: 1, name: 'Dr Nice', emotion: 'happy'},
{ id: 2, name: 'Narco', emotion: 'sad' },
{ id: 3, name: 'Windstorm', emotion: 'confused' },
{ id: 4, name: 'Magneta'}
];
import { Component, Input } from '@angular/core';
import { Hero } from './hero';
@Component({
selector: 'app-happy-hero',
template: `Wow. You like {{hero.name}}. What a happy hero ... just like you.`
})
export class HappyHeroComponent {
@Input() hero: Hero;
}
@Component({
selector: 'app-sad-hero',
template: `You like {{hero.name}}? Such a sad hero. Are you sad too?`
})
export class SadHeroComponent {
@Input() hero: Hero;
}
@Component({
selector: 'app-confused-hero',
template: `Are you as confused as {{hero.name}}?`
})
export class ConfusedHeroComponent {
@Input() hero: Hero;
}
@Component({
selector: 'app-unknown-hero',
template: `{{message}}`
})
export class UnknownHeroComponent {
@Input() hero: Hero;
get message() {
return this.hero && this.hero.name ?
`${this.hero.name} is strange and mysterious.` :
'Are you feeling indecisive?';
}
}
export const heroSwitchComponents =
[ HappyHeroComponent, SadHeroComponent, ConfusedHeroComponent, UnknownHeroComponent ];
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
/**
* Add the template content to the DOM unless the condition is true.
*
* If the expression assigned to `appUnless` evaluates to a truthy value
* then the templated elements are removed removed from the DOM,
* the templated elements are (re)inserted into the DOM.
*
* <div *appUnless="errorCount" class="success">
* Congrats! Everything is great!
* </div>
*
* ### Syntax
*
* - `<div *appUnless="condition">...</div>`
* - `<ng-template [appUnless]="condition"><div>...</div></ng-template>`
*
*/
@Directive({ selector: '[appUnless]'})
export class UnlessDirective {
private hasView = false;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef) { }
@Input() set appUnless(condition: boolean) {
if (!condition && !this.hasView) {
this.viewContainer.createEmbeddedView(this.templateRef);
this.hasView = true;
} else if (condition && this.hasView) {
this.viewContainer.clear();
this.hasView = false;
}
}
}
<ng-container>
對(duì)元素進(jìn)行分組。*
)語(yǔ)法解開(kāi)成 <ng-template>
。NgIf
、NgFor
和 NgSwitch
的工作原理。<ng-template>
。UnlessDirective
。
更多建議: