建立一個 item 元件
元件可以用來幫助你組織你的應用程式。這篇文章會引導你建立一個元件,用來管理清單列表中的個別項目,包含加入核取方塊、編輯和刪除功能。這邊也會介紹 Angular 事件模型。
預備知識: | 熟悉主要的 HTML,CSS 和 JavaScript 語言和terminal/command line知識。 |
---|---|
學習目標: | 掌握更多元件知識,包含如何使用事件來處理資料更新,以及加入核取方塊、編輯和刪除的功能。 |
建立一個新的元件
使用如下的 CLI 指令,在命令行建立一個名為 item
元件:
ng generate component item
指令 ng generate component
創建了以你指定名稱的元件及對應資料夾。這邊的元件和資料夾名稱為 item
。你可以在 app
資料夾下找到 item
目錄。
與 AppComponent
一樣, ItemComponent
是由下列文件組成:
- 用於 HTML 的
item.component.html
- 用於邏輯的
item.component.ts
- 用於樣式的
item.component.css
你可以在 item.component.ts
的 @Component()
的裝飾器中找到 HTML 和 CSS 文件的參照位置。
@Component({
selector: 'app-item',
templateUrl: './item.component.html',
styleUrls: ['./item.component.css'],
})
為 ItemComponent 添加 HTML
ItemComponent
元件能讓使用者檢查已完成的項目,並對其進行編輯或刪除。
為了增加管理項目的標記,使用下面程式碼替換 item.component.html
中的佔位符內容。
<div class="item">
<input
[id]="item.description"
type="checkbox"
(change)="item.done = !item.done"
[checked]="item.done" />
<label [for]="item.description">{{item.description}}</label>
<div class="btn-wrapper" *ngIf="!editable">
<button class="btn" (click)="editable = !editable">Edit</button>
<button class="btn btn-warn" (click)="remove.emit()">Delete</button>
</div>
<!-- This section shows only if user clicks Edit button -->
<div *ngIf="editable">
<input
class="sm-text-input"
placeholder="edit item"
[value]="item.description"
#editedItem
(keyup.enter)="saveItem(editedItem.value)" />
<div class="btn-wrapper">
<button class="btn" (click)="editable = !editable">Cancel</button>
<button class="btn btn-save" (click)="saveItem(editedItem.value)">
Save
</button>
</div>
</div>
</div>
第一個 input 是一個核取方塊,讓用戶可以在完成該項目後勾選以核對。核取方塊的 <input>
和 <label>
中的雙大括號 {{}}
表示 Angular 的內嵌繫結。
Angular 使用 {{item.description}}
從 items
陣列中獲取當前 item
的描述。下一節將詳細解釋元件如何共享數據。
接下來的用於編輯和刪除當前項目的兩個按鈕位於 <div>
內。 <div>
內的 *ngIf
,是內置的 Angular 結構型指令,可動態更改 DOM 的結構。
*ngIf
表示如果 editable
的值為 false,則此 <div>
會出現在 DOM 中。如果 editable
的值為 true,則 Angular 將從 DOM 中移除該 <div>
。
<div class="btn-wrapper" *ngIf="!editable">
<button class="btn" (click)="editable = !editable">Edit</button>
<button class="btn btn-warn" (click)="remove.emit()">Delete</button>
</div>
當用戶點擊 Edit 按鈕時,editable
的值變為 true,這將從 DOM 中移除此 <div>
和它的子元素。如果用戶點擊 Delete 而不是點擊 Edit,則 ItemComponent
將觸發一個刪除事件,用來通知 AppComponent
做刪除動作。
在下一個 <div>
裡也放上了 *ngIf
,不過它的判斷條件是當 editable
為 true 的情況下,Angular 會將該 <div>
和其子元素 <input>
和 <button>
放入 DOM 中。
<!-- This section shows only if user clicks Edit button -->
<div *ngIf="editable">
<input
class="sm-text-input"
placeholder="edit item"
[value]="item.description"
#editedItem
(keyup.enter)="saveItem(editedItem.value)" />
<div class="btn-wrapper">
<button class="btn" (click)="editable = !editable">Cancel</button>
<button class="btn btn-save" (click)="saveItem(editedItem.value)">
Save
</button>
</div>
</div>
設置 [value]="item.description"
,<input>
的值將綁定到當前項目的 description
屬性。此綁定使項目的 description
成為<input>
的值。因此如果將 description
設為 eat
, 因為 <input>
已經和 description
綁定。所以,當用戶編輯項目時,<input>
的值已被設為 eat
。
<input>
上的範本變數 #editedItem
表示 Angular 將用戶在此 <input>
中輸入的內容儲存在名為 editedItem
的變數中。如果用戶在輸入後選擇按 Enter 而不是點擊 Save,則 keyup
事件將調用 saveItem()
方法並傳遞 editedItem
變數的值。
當用戶點擊 Cancel 按鈕時,editable
的值將切換為 false
,連帶從 DOM 中移除編輯相關的輸入框和按鈕。當 editable
的值為 false
時,Angular 將含有 Edit 和 Delete 按鈕的 <div>
放回 DOM 中。
點擊 Save 按鈕將調用 saveItem()
方法。 saveItem()
方法從 <input>
中的範本變數 #editedItem
取得值,並將該項目的 description
更改為 editedItem.value
的值。
準備 AppComponent
在下一章節,你將添加用來溝通 AppComponent
和 ItemComponent
的程式碼。首先將以下內容添加到 app.component.ts
中來配置 AppComponent:
remove(item) {
this.allItems.splice(this.allItems.indexOf(item), 1);
}
上面 remove()
方法使用了 JavaScript Array.splice()
方法,並透過 indexOf
取得欲刪除項目的陣列索引中位置,以從陣列中刪除該項目。
簡單來說,splice()
方法從陣列中刪除了該項目。 splice()
的更多訊息請參閱 MDN Web 文章:Array.prototype.splice()
。
在 ItemComponent 添加邏輯
使用 ItemComponent
UI, 你必須在元件中添加邏輯,就跟在 function 中寫輸入與輸出的方式一樣。
在 item.component.ts
,引入 JavaScript,如下所示:
import { Component, Input, Output, EventEmitter } from "@angular/core";
import { Item } from "../item";
加入 Input
, Output
,和 EventEmitter
使 ItemComponent
可以與 AppComponent
共享數據,
透過匯入 Item
, 就可以知道在 ItemComponent
的 item
為何。
繼續看 item.component.ts
,把生成的 ItemComponent
替換成以下內容:
export class ItemComponent {
editable = false;
@Input() item: Item;
@Input() newItem: string;
@Output() remove = new EventEmitter<Item>();
saveItem(description) {
if (!description) return;
this.editable = false;
this.item.description = description;
}
}
editable
屬性有助於切換模板,讓使用者可以編輯其項目。HTML 中的 editable
屬性與 *ngIf
語句中的屬性相同, *ngIf="editable"
。當你在模板使用此屬性時,你也必須在 class 中宣告它。
@Input()
、@Output()
和 EventEmitter
促進兩個元件中的溝通,一個 @Input()
服務元件做為資料傳進的入口,然後一個 @Output()
是將元件資料傳到外層。@Output()
必須是 EventEmitter
的類型,資料可以透過事件分享至其他組件。
使用 @Input()
指定外部元件要傳進之屬性的值,將 @Output()
與 EventEmitter
結合使用可將該元件指定屬性的值傳出,使得另一個元件可以接收其資料。
saveItem()
方法是在 description
取得一個引數,此description
為使用者輸入 HTML 的 <input>
標籤編輯清單的項目時的文字,
此 description
與 <input>
中帶有 #editedItem
範本變數的字符串相同。
如果使用者沒有輸入任何的值但點擊 Save 時,saveItem()
不會回傳任何東西與更新 description
。如果你沒有用 if
,使用者就可以在 HTML 的 <input>
沒有值的時候點擊 Save,並且 description
會是空字串。
如果使用者輸入文字並點擊儲存, saveItem()
會設定 editable
是 false,這會導致模板中的*ngIf
移除編輯功能並重新渲染 Edit 跟 Delete 的按鈕
儘管程式現在可以編譯,你必須在 AppComponent
中使用 ItemComponent
才能在瀏覽器看到新功能。
在 AppComponent 中使用 ItemComponent
在父子關係的情境下,可將一個組件包含在另一個組件中,讓你靈活地使用它們。
AppComponent
就像個應用程式的外殼,可在內部加入其他元件
要在 AppComponent
中使用 ItemComponent
,需將 ItemComponent
選擇器放到 AppComponent
中。Angular 在元件共享數據的元件中使用 @Component()
裝飾器,此選擇器為 app-item
:
@Component({
selector: 'app-item',
templateUrl: './item.component.html',
styleUrls: ['./item.component.css']
})
要在 AppComponent
中使用 ItemComponent
選擇器時,你要增加元素 <app-item>
,它對應你在 app.component.html
中對元件類別定義的選擇器。用以下更新的版本替換在 app.component.html
中未排序清單:
<h2>
{{items.length}}
<span *ngIf="items.length === 1; else elseBlock">item</span>
<ng-template #elseBlock>items</ng-template>
</h2>
<ul>
<li *ngFor="let item of items">
<app-item (remove)="remove(item)" [item]="item"></app-item>
</li>
</ul>
雙括號 {{}}
,在 <h2>
內顯示 items
的長度與數目。
在 <h2>
中 <span>
使用 *ngIf
與 else
決定 <h2>
是否要呈現 "item" 或 "items"。如果在列表中只有一個項目, 則 會顯示包含 <span> 的內容。當items 陣列不等於 1 時,被我們命名為 elseBlock 的 <ng-template>,將顯示 #elseBlock,而不是 <span>。當你不想內容在預設渲染的時候,可以使用 Angular 的 <ng-template> ,因 #elseBlock 不是 <span>,是使用 <ng-template>。在此範例中,若 item 陣列長度不是 1 ,則 *ngIf 會顯示 elseBlock 而不顯示 <span>。
在 <li>
使用 Angular 的結構型指令 *ngFor
會在 items
陣列迭代所有的項目,Angular 的 *ngFor
與 *ngIf
指令相似,是另一個可以協助你用更少的程式碼改變 DOM 元素架構,每一個 item
,Angular 會重複 <li>
與其所有的內容,其中包含 <app-item>
。這代表 Angular 為陣列中的每一個項目建立另一個 <app-item>
實體。Angular 會建立與 items
陣列中的數量相同的項目的 <li>
元素。
你可使用 *ngFor
在其他的元素上,像是在 <div>
、<span>
或是 <p>
,以此類推。
在 AppComponent
有一個移除項目的 remove()
的方法,是綁定 ItemComponent
中 remove 的屬性,此 item
屬性是在中括號內 []
,用來綁定 item
在 AppComponent
與 ItemComponent
之間的值。
現在你應該知道如何編輯和刪除在列表中的項目。當你新增或刪除項目時,項目的數量也會更動,為了使列表更易於使用,請在 ItemComponent
中新增些樣式。
為 ItemComponent 添加樣式
你可以使用元件的 styles sheet 去增加該元件的樣式。下面的 CSS 增加了基本的樣式,對按鈕添加 flexbox 屬性和客製化了核取方塊。
將下面的樣式程式碼貼至 item.component.css
。
.item {
padding: 0.5rem 0 0.75rem 0;
text-align: left;
font-size: 1.2rem;
}
.btn-wrapper {
margin-top: 1rem;
margin-bottom: 0.5rem;
}
.btn {
/* menu buttons flexbox styles */
flex-basis: 49%;
}
.btn-save {
background-color: #000;
color: #fff;
border-color: #000;
}
.btn-save:hover {
background-color: #444242;
}
.btn-save:focus {
background-color: #fff;
color: #000;
}
.checkbox-wrapper {
margin: 0.5rem 0;
}
.btn-warn {
background-color: #b90000;
color: #fff;
border-color: #9a0000;
}
.btn-warn:hover {
background-color: #9a0000;
}
.btn-warn:active {
background-color: #e30000;
border-color: #000;
}
.sm-text-input {
width: 100%;
padding: 0.5rem;
border: 2px solid #555;
display: block;
box-sizing: border-box;
font-size: 1rem;
margin: 1rem 0;
}
/* Custom checkboxes
Adapted from https://css-tricks.com/the-checkbox-hack/#custom-designed-radio-buttons-and-checkboxes */
/* Base for label styling */
[type="checkbox"]:not(:checked),
[type="checkbox"]:checked {
position: absolute;
left: -9999px;
}
[type="checkbox"]:not(:checked) + label,
[type="checkbox"]:checked + label {
position: relative;
padding-left: 1.95em;
cursor: pointer;
}
/* checkbox aspect */
[type="checkbox"]:not(:checked) + label:before,
[type="checkbox"]:checked + label:before {
content: "";
position: absolute;
left: 0;
top: 0;
width: 1.25em;
height: 1.25em;
border: 2px solid #ccc;
background: #fff;
}
/* checked mark aspect */
[type="checkbox"]:not(:checked) + label:after,
[type="checkbox"]:checked + label:after {
content: "\2713\0020";
position: absolute;
top: 0.15em;
left: 0.22em;
font-size: 1.3em;
line-height: 0.8;
color: #0d8dee;
transition: all 0.2s;
font-family: "Lucida Sans Unicode", "Arial Unicode MS", Arial;
}
/* checked mark aspect changes */
[type="checkbox"]:not(:checked) + label:after {
opacity: 0;
transform: scale(0);
}
[type="checkbox"]:checked + label:after {
opacity: 1;
transform: scale(1);
}
/* accessibility */
[type="checkbox"]:checked:focus + label:before,
[type="checkbox"]:not(:checked):focus + label:before {
border: 2px dotted blue;
}