[功能介紹-2] 資料繫結的模版語法

Template Syntax

在上一篇的Angular架構中有提到,透過模版語法,template可以與component做許多的溝通。

那這一篇就會介紹Angular內建的模版語法參考。

對於tempalte.html來說,所有的html標籤都可以使用,除了<script>以外,這是為了維護模版的安全性,去除template被攻擊的風險,因此在template中所有的<script>會被忽略,並跳出警告。

接下來我們來介紹所有Angular的模板語法

{{…}}

以下為一個範例

大括號之間值通常是組件屬性的名稱。Angular使用相應組件屬性的字符串值替換該名稱。
在上面的例子中,Angular會取元件裡title和heroImageUrl的屬性,並會取代{{title}}及{{heroImageUrl}},因此在頁面上會顯示一個大的應用程序標題,然後一個英雄影像的位置。

在{{…}}之間,我們也可以使用模板表達式去轉換要顯示的值。
例如在刮弧中做運算:

或者也可以呼叫component的function getVal():

大多的運算符都可以用在表達式裡面,除了一些會影響到component的值的運算符,如=、+=、-=之類。

有時候{{…}}裡面要綁定的數值也可以是在template定義的(使用#符號),請見下面的範例

「註」:如果在元件裡已經有變數名稱叫做hero,而template裡又有一個hero,在template會優先使用template內定義的變數。
在表達式中不能引用任何除了undefined外的全域變數,如window或document,也不能用consolo.log,只能使用元件內的屬性或方法,或者template裡上下文內的成員

在使用{{…}}時,有下面四個原則要注意:

  • No visible side effects:不應該改變任何元件內的值,在rendering整個表式示時應該是穩定的
  • Quick execution:表達式的運算應該要很快,因為它會在許多狀況下被呼叫,因此若是裡面含有許多複雜運算時,請考慮用快取以增加效能。
  • Simplicity:雖然可以在{{…}}裡面寫很複雜的運算但是不建議,最多在裡面使用!符號,不然還是建議將運算放到元件內去計算,以利閱讀及開發
  • Idempotent: idempotent的意思是如果相同的操作再執行第二遍第三遍,結果還是跟第一遍的結果一樣 (也就是說不管執行幾次,結果都跟只有執行一次一樣)。

(event)=”statement”

(event)=”statement”是Template statements。事件綁定是利用這樣的方式去做的,下面是一個範例:

與{{…}}不同的是,在這樣的語法中,是可以去改變元件的值的,並且被改變的值會透過單向綁定{{…}}顯示在畫面上。因此,(event)=”statement”的statement是能夠有支援=運算符的,但是+=、-=和++、–是不被允許的。

語句上下文也可以引用模板自己的上下文的屬性。在以下範例中,將模版$event對象,模板輸入變量(let hero)和模板引用變量(#heroForm)傳遞給組件的事件處理方法。
下面是一個範例

[target]=”statement”

以下是一個範例:

這樣的語法,能夠讓isUnchanged為true時,顯示這樣的畫面

綁定方法整理

資料方向 語法 類型
單向綁定
從資料源到view
Interpolation
Property
Attribute
Class
Style
單向綁定
從view的目標到資料源
Event
雙向綁定 Two-way

HTML attribute與DOM property

HTML attribute和DOM property(物件屬性)的區別對於理解Angular綁定是很重要的。一旦使用插值({{...}}),就不是使用HTML attribute,而是在設置DOM property。

一些HTML attribute可以1:1的對應到一個DOM property,例如:id
一些HTML attribute沒有相應的DOM property,例如:colspan(無法使用插值)。
一些DOM property沒有相應的HTML attribute,例如:textContent
許多DOM property似乎對應到HTML attribute…但不是以你想像的方式!

HTML attribute的value指定初始值; DOM property的value屬性是當前值

例如,當瀏覽器執行下面HTML,它創建一個對應的DOM節點,其value屬性初始化為“Bob”。

當用戶在輸入框中輸入“Sally”時,DOM property的value變成“Sally”。
但是,HTML attribute的value保持不變,如下所示:

disabled屬性是另一個特殊的例子。按鈕的disabled 屬性是false默認的,所以按鈕被啟用。當你添加disabled 屬性時,它的存在會初始化按鈕的disabled 屬性,true所以按鈕被禁用。添加和刪除disabled屬性禁用並啟用按鈕。屬性的值是不相關的,這就是為什麼你不能用下面的語法來將按鈕設為enable。

因此,HTML attribute和DOM property是不一樣的,即使它們具有相同的名稱。

綁定目標整理

類型 目標 例子
屬性 元素屬性
組件屬性
指令屬性

事件 元素事件
組件事件
指令事件
雙向 事件和財產
屬性 屬性(例外)
class 屬性
樣式 style 屬性

Property binding or interpolation?

屬性綁定與插值常常能達到相同的功效,如下面的範例:

一般而言,為了易讀性,會使用插值({{…}})。但當沒有要綁定的元素屬性時,必須使用屬性綁定。例如:

會得到這個錯誤
Template parse errors:
Can't bind to 'colspan' since it isn't a known native property

這是因為插值只能設定properties而不能設定attributes。
這時可以改成

則可以正常顯示如下圖

Built-in structural directives

常見的結構指令如下:

  • NgIf – 有條件地從DOM中添加或刪除一個元素,要注意,這和css的show、hide不一樣,當元素被dom移除時,是沒有辦法去操作DOM元素裡的物件的。
  • NgSwitch – 一組在不同視圖之間切換的指令
  • NgForOf – 為列表中的每個項目重複一個模板

Template reference variables ( #var )

在Angular也可以使用#開頭來將使用者在網頁上input輸入的值轉為一個變數,如下:

這個功能在做表單驗證時非常的方便

允許外部元件讀取元件內的屬性

要讓元件內的屬性能夠給其他元件使用,或者讀取其他元件的屬性,可以在component.ts內宣告

或者這樣也可以

輸入屬性通常接收數據值。 輸出屬性會發送事件,如EventEmitter。
下面的圖顯示元件屬性的輸入和輸出的範例。

safe navigation operator ( ?. )

為了防止出現null reference exception,我們可以使用?.,當值為空值時,會直接傳回空白,可以避免產生不必要的exception
以下為範例

參考資料

完整綁定範例資料請見此:線上範例

[功能介紹-1] Angular架構

Angular是一個框架,用來編寫html的應用程式,可以用javascript或typescript去編寫。

下圖是官網上所繪製的Angular架構圖:

首先先看最中間那一塊,是由template、metadata、component所構成的,這三個是一個component必備的元素。
所謂的元件可以看之前創建hero那篇文章,我們可以用下面指令創一個元件

創完一個元件後,可以看見下面這些檔案

Templates

裡面的component.html檔就是template,它看起來像是一個html的檔案,可以在裡面用資料綁定與事件綁定與controller裡的物件做繫結。
下面是一個template的範例:

可以注意到上面有些地方與一般的html不相同,例如像是*ngFor、{{hero.name}}、(click)="selectHero(hero)"等…
這就是架構圖畫面上用來連繫Component以及Template的兩個箭頭,property binding以及event binding。

例如click事件繫結是(click)=’functionName()’,物件繫結可以用{{data}})。
透過這樣的繫結可以讓template將使用者操作的事件傳給component,component也可以將資料的更動即時的反饋到template所顯示的資料上。

下圖是非常清楚的binding類型列圖

其中{{hero.name}}為值繫結,可以綁定component裡的值。
[hero]="selectedHero"為property binding,可以將某個元件裡的變數塞進一個HTML元素的屬性裡。。
(click)="selectHero(hero)" event binding可以呼叫component裡的function

這個則是雙向數據綁定,在雙向綁定中,與屬性綁定一樣,數據屬性值將從組件輸入到輸入框中。用戶的更改也會返回到組件,將屬性重置為最新值,就像事件綁定一樣。

數據綁定在模板及其組件之間的通信中起著重要的作用。

數據綁定對於父組件和子組件之間的通信也很重要。

Component

而component.ts檔則是component內容,裡面會有一些屬性或方法來供template呼叫,下面是一個Component的簡單範例:

當用戶在應用程序中切換畫面時,Angular會創建,更新和銷毀組件。你的應用程序可以通過可選參加這個生命週期的每個時刻動作的lifecycle hooks,像ngOnInit()。

Metadata

metadata則是在component.ts裡由@Component開頭的區塊來宣告,裡面會定義這個Component在要如何在別的元件的template裡被引用,templateUrl則是定義自己這個元件要顯示的html模版位置及styleUrls是css檔案位置。
例如下面這個metadata宣告

就可以用下面的方式來顯示這個元件

Modules

接著來介紹左上方的區塊

Angular應用程序是模組化的,Angular稱自己為NgModules的模組化系統。NgModules是一個很大的議題,在後面會有另一篇文章專門介紹ngModules。
一個小的application至少會有一個模組,稱為root module。雖然有些小的專案可能就只有一個模組,但大多大的專案都會有多個模組,稱為feature modules,一個模組內會是相同工作範疇的一組元件,它們有許多工作流程或功能上緊密相關,彼此協同運作。
當我們執行了這樣的指令來創建一個新的專案時

可以看到src/app資料夾內有app.module.ts這個檔案,這個檔案就是angular預設的根模組。
其內容如下:

一個NgModule,無論是root module還是feature modules,該class裡一定會有@ngModule的宣告區域,在angular裡會有許多@開頭的宣告,這樣的宣告稱為decorator,可以在元件或模組裡設定許多metadata。關於ngModule更多說明可以看@NgModule的說明關於decorators更多的說明則可以看這邊,這兩項之後都會有專門的文章來介紹。

NgModule是一個用來描述這個模組裡有那些component的metadata,是一個decorator function。
在ngModule裡最重要的屬性有下面這五點:

  • declarations:屬於這個模組的成員。Angular有三種成員:components, directives,以及pipes。
  • exports:要將declarations宣告的那一些成員公開。讓其他模組引用此模組時,可以存取該成員的public function。
  • imports:需引用的模組,所有在這個模組內的元件要引用的模組都要在此宣告
  • providers:要引用的Service需在此宣告
  • bootstrap:這個屬性只有根模組需要設定,在此設定在一開始要顯示的application view

註:上面的範例中,因為有設定bootstrap代表其為一個根組件,而根組件是不需要exports出去的,因為不會有任何其他modules需要用到它。

而要啟動整個應用程序可以在main.ts中加上這段

在js裡我們透過export一個class來供其他modules使用。並在其他js檔案利用import來將這個class引入。

NgModules與JavaScript modules是完全不同的兩樣東西,我們可以同時透過ngModule與JS Modules來交互使用以達成我們所需要達成的目的。

詳細了解Web上的JavaScript模組系統

Angular libraries


angular內建了許多可供應用的模組,我們稱為library module。
所有的library module都以@angular為前綴開頭,可以使用npm來安裝管理它們。
例如要使用Component功能要引用@angular/core的Component

或是使用BrowserModule

Directives


在架構圖中的template右邊,可以看到有一個directive指向template。directive是透過Angular使用內建或自訂directive用來自己定義html元素,並簡化dom操作的功能。可以讓template裡去使用。
一個directive會由@Directive來宣告。其實directive與component的本質是相同的,只是component是一個有template的directive,而directive沒有。我們可以視component為directive的擴展,擴展了template的功能。不過因為component在angular是非常重要的,和directive有不同的意義,因此會將component以及directive在架構上分開來。

directives分為架構型的和屬性型的:
下面是兩個內建架構型的directive範例

*ngFor 會讓angular重覆寫許多的 <li>將heroes裡的資料跑過一圈
*ngIf 則會讓angular只有在selectedHero的值為true時才會顯示app-hero-detail元件
而屬性型的directive則例如像是做雙向繫結的ngModel,下面的片段程式會將hero.name的值塞入input的value屬性內,並且監聽使用者修改input的值的事件將修改傳回至hero.name

Services


架構圖的左下區是很多的Service注入至Component裡。幾乎所有功能都可以是service,但是它應該是目的明確且狹義的功能。
例如:記錄服務、數據服務、消息服務、稅計算器、應用程序配置等…

以下是一個範例,在這個例子中可以看到我們可以利用getHeroes()和service取得Hero列表,而Service則負責與Backend溝通由API取得資料並且回傳給component

Dependency injection

依賴注入是一種設計模式,可參考這篇文章了解:理解 Dependency Injection 實作原理
在Angular中,大多數依賴是服務,Angular的元件使用依賴注入為自己提供他們需要的服務,對於元件來說,服務必須是完全依賴的,component會在元件內使用service所提供的方法來取得自己所需的資料。

HeroService依賴注入的過程看起來像這樣:

injector會有一個所有Service的集合。如果所需要的Service不在這個集合中,那麼injector將創建一個Service並加進Service集合裡。當component所需要的Service都已經取得後,只要在constructor設定該服務在此元件內的名稱,就可以在元件裡自由的使用服務了。

下面是一個依賴注入的範例

例如上面的程式碼就會讓該component裡的程式碼可以使用service.getHeroes()來取得英雄列表。

在上圖中,上面要被injector選擇的那些service是怎麼來的呢?我們可在根模組或者自己所在的模組裡去提供所有需用到的服務,這樣在這個模組內的所有元件都可以使用這個服務,如下:
src / app / app.module.ts

或者也可以在該元件的metadata裡用providers來設定這個元件要使用這個服務,如下:

參考資料

[新手教程-7] 使用http來與API溝通

Angular處理http是使用rx(Reactive Programming)來實作的,類別的名稱為rxjs
在閱讀此篇之前,建議可以了解一下何謂Reactive Programming,其核心概念為何,這樣會比較容易理解本篇的內容
推薦閱讀:Reactive Programming 簡介與教學(以 RxJS 為例)官網 ReactiveX
RxJS教學:30 天精通 RxJS

使用http來取得api資料

將src/app/hero.service.ts取得資料的方式改由API取得

上面程式碼註解地方代表有修改過的地方。
所有的HttpClient方法都返回一個Observable的物件。一般來說,Observable物件會傳送多次資料給接收者,但http.get所取得的值為例外,因為http方法是發出要求並收到回應後就不會再有動作,因此它只會傳一次資料給取得這個資料的接收者。並且需要在被subscribe後才會啟動動作
http.get預設接收的格式為json,會自動對照所取得的json轉化成一個對應屬性的物件以方便取得資料。

處理錯誤

如果在取得api時發生網路錯誤或其他問題導致無法順利取得伺服器資料時,可以用下面的方法來偵聽錯誤
首先,導入rxjs/operators類別

接著,使用pipe方法擴展Observable並在裡面下達catchError()來處理錯誤的狀況

下面則是catchError的內容

在上面的T是類型參數,在這個例子中,T代表Hero[]。這可以讓程式在打api失敗時依舊可取得符合應用程式所期望的類型的回傳值。

使用http修改伺服器資料

取得資料時使用get,而修改資料時使用put,其參數意義如下:

其中httpOptions較常設定的是headers,也就是宣告傳去的格式

下面是使用updateHero的範例

在RxJS中,有兩個角色,ObservableSubscription,Observable負責產生資料,創建後不會馬上啟動,而在關注(subscribe)後開始啟動。

使用http新增伺服器資料

新增資料在rxjs裡是使用http.post()

使用addHero方法的範例如下:

使用http刪除伺服器資料

刪除資料我們使用http.delete(),範例如下

而呼叫deleteHero的地方則要這樣寫:

上面我們可以看到在呼叫deleteHero時,即便刪除完成後沒有要多做任何時,仍然需要加上subscribe()
就像上面說過的,所有rxjs的動作都會在有人subscribe後才會呼叫,因此如果忽略subscribe(),http將不會將刪除請求發送到伺服器!
Observable需要等到有東西subscribe它,才會被執行。

使用搜尋功能

於src/app/hero.service.ts增加搜索功能

創建一個HeroSearchComponent的class

修改src/app/hero-search/hero-search.component.html

上面的程式碼中,要注意的是這一行

如果光使用for迴圈去使用heroes$,Observable不會做任何事,async透過|這個pipe來自動做subscribe的動作,我們可以不用再次的透過subscribe()來讓Observable被執行
而heroes$則是告知這個for迴圈操作的對象是一個Observable而不是一般的值。

接著我們修改src/app/hero-search/hero-search.component.ts

上面的程式碼中,heroes也需要寫為heroes$並宣告它是一個Observable

searchTerms在這邊被宣告為RxJS裡的Subject.

一個Subject自己本身是observable,並且接收一個observable為參數。因此我們可以對Subject做許多和Observable相同的動作。
也可以使用next(value)去傳值至searchTerms裡,對任何的Observable物件也都能這樣操作。

在這邊的search()方法與keyup事件綁定,searchTerms會返回一個Observable的觀察結果。

另外searchTerms的寫法

注意:使用switchMap(),每個有資格的鍵事件都可以觸發HttpClient.get()方法調用。即使在請求之間暫停了300毫秒,也可能有多個HTTP請求在運行,並且可能不會按照發送的順序返回。
switchMap()保留最初的請求順序,同時只返回最近的HTTP方法調用的observable。來自先前搜尋的結果被取消並被丟棄。
不過,取消先前的searchHeroes() Observable 並不會中止掛起的HTTP請求。不需要的結果在到達應用程序代碼之前就被丟棄了。

最後成品樣子如下:

完整範例檔請見: live example / download example.