建立專案元件
元件提供了一種組織應用程式的方法。本文將引導您建立一個元件來處理列表中的單個專案,並新增檢查、編輯和刪除功能。本文將介紹 Angular 事件模型。
| 先決條件 | 熟悉核心 HTML、CSS 和 JavaScript 語言,瞭解 終端/命令列。 |
|---|---|
| 目標 | 瞭解有關元件的更多資訊,包括事件如何處理更新。新增檢查、編輯和刪除功能。 |
建立新元件
在命令列中,使用以下 CLI 命令建立一個名為 item 的元件
ng generate component item
ng generate component 命令會建立一個元件和資料夾,其名稱由您指定。在此處,資料夾和元件名稱為 item。您可以在 app 資料夾中找到 item 目錄
src/app/item ├── item.component.css ├── item.component.html ├── item.component.spec.ts └── item.component.ts
與 AppComponent 一樣,ItemComponent 由以下檔案組成
item.component.html用於 HTMLitem.component.ts用於邏輯item.component.css用於樣式item.component.spec.ts用於測試元件
您可以在 item.component.ts 中的 @Component() 裝飾器元資料中看到對 HTML 和 CSS 檔案的引用。
@Component({
selector: 'app-item',
standalone: true,
imports: [],
templateUrl: './item.component.html',
styleUrl: './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>
第一個輸入是一個複選框,以便使用者可以在專案完成時將專案勾選。複選框 <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>
當用戶單擊編輯按鈕時,editable 變為 true,這將從 DOM 中刪除此 <div> 及其子級。如果使用者沒有單擊編輯,而是單擊刪除,則 ItemComponent 會引發一個事件,通知 AppComponent 刪除操作。
下一個 <div> 上也有一個 *ngIf,但設定為 editable 值為 true。在這種情況下,如果 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,則 description 已經在 <input> 中。這樣,當用戶編輯專案時,<input> 的值已經是 eat。
<input> 上的模板變數 #editedItem 表示 Angular 將使用者在此 <input> 中鍵入的任何內容儲存在一個名為 editedItem 的變數中。如果使用者選擇按 Enter 鍵而不是單擊儲存,則 keyup 事件將呼叫 saveItem() 方法並傳入 editedItem 值。
當用戶單擊取消按鈕時,editable 切換為 false,這將從 DOM 中刪除用於編輯的輸入和按鈕。當 editable 為 false 時,Angular 將帶有編輯和刪除按鈕的 <div> 放回 DOM 中。
單擊儲存按鈕將呼叫 saveItem() 方法。saveItem() 方法將從 #editedItem 元素中獲取值,並將專案的 description 更改為 editedItem.value 字串。
準備 AppComponent
在下一節中,您將新增依賴於 AppComponent 和 ItemComponent 之間通訊的程式碼。在 app.component.ts 檔案的頂部附近新增以下行,以匯入 Item
import { Item } from "./item";
import { ItemComponent } from "./item/item.component";
然後,透過將以下內容新增到同一檔案的類中來配置 AppComponent
remove(item: Item) {
this.allItems.splice(this.allItems.indexOf(item), 1);
}
remove() 方法使用 JavaScript Array.splice() 方法從相關專案的 indexOf 中刪除一個專案。通俗地說,這意味著 splice() 方法從陣列中刪除該專案。有關 splice() 方法的更多資訊,請參閱 Array.prototype.splice() 文件。
向 ItemComponent 新增邏輯
要使用 ItemComponent UI,您必須向元件新增邏輯,例如函式以及資料進出的方式。在 item.component.ts 中,按如下方式編輯 JavaScript 匯入
import { Component, Input, Output, EventEmitter } from "@angular/core";
import { CommonModule } from "@angular/common";
import { Item } from "../item";
新增 Input、Output 和 EventEmitter 允許 ItemComponent 與 AppComponent 共享資料。透過匯入 Item,ItemComponent 可以理解 item 是什麼。您可以更新 @Component 以在 app/item/item.component.ts 中使用 CommonModule,以便我們可以使用 ngIf 指令
@Component({
selector: 'app-item',
standalone: true,
imports: [CommonModule],
templateUrl: './item.component.html',
styleUrl: './item.component.css',
})
在 item.component.ts 中,用以下內容替換生成的 ItemComponent 類
export class ItemComponent {
editable = false;
@Input() item!: Item;
@Output() remove = new EventEmitter<Item>();
saveItem(description: string) {
if (!description) return;
this.editable = false;
this.item.description = description;
}
}
editable 屬性有助於切換使用者可以在其中編輯專案的模板部分。editable 是模板中與 *ngIf 語句中相同的屬性,*ngIf="editable"。當您在模板中使用屬性時,還必須在類中宣告它。
@Input()、@Output() 和 EventEmitter 促進了兩個元件之間的通訊。@Input() 作為資料進入元件的門戶,@Output() 作為資料離開元件的門戶。@Output() 必須是 EventEmitter 型別,以便元件在有資料準備與其他元件共享時可以引發事件。
注意: 類屬性宣告中的 ! 稱為 明確賦值斷言。此運算子告訴 TypeScript item 欄位始終已初始化,而不是 undefined,即使 TypeScript 無法從建構函式的定義中判斷出來也是如此。如果您的程式碼中沒有包含此運算子,並且您具有嚴格的 TypeScript 編譯設定,則應用程式將無法編譯。
使用 @Input() 指定屬性的值可以來自元件外部。使用 @Output() 與 EventEmitter 結合使用,指定屬性的值可以離開元件,以便其他元件可以接收該資料。
saveItem() 方法以 description(型別為 string)作為引數。description 是使用者在編輯列表中的專案時輸入到 HTML <input> 中的文字。此 description 是與具有 #editedItem 模板變數的 <input> 相同的字串。
如果使用者沒有輸入值,而是單擊儲存,saveItem() 不會返回任何內容,也不會更新 description。如果您沒有此 if 語句,使用者可以在 HTML <input> 中什麼都不輸入就單擊儲存,並且 description 將變為空字串。
如果使用者輸入文字並單擊儲存,saveItem() 將 editable 設定為 false,這會導致模板中的 *ngIf 刪除編輯功能並再次呈現編輯和刪除按鈕。
儘管應用程式應該在此時編譯,但您需要在 AppComponent 中使用 ItemComponent,以便您可以在瀏覽器中看到新功能。
在 AppComponent 中使用 ItemComponent
在父子關係的上下文中將一個元件包含在另一個元件中,可以讓您靈活地在需要的地方使用元件。
AppComponent 作為應用程式的外殼,您可以在其中包含其他元件。
要在 AppComponent 中使用 ItemComponent,請將 ItemComponent 選擇器置於 AppComponent 模板中。Angular 在 @Component() 裝飾器的元資料中指定元件的選擇器。在此示例中,我們已將選擇器定義為 app-item
@Component({
selector: 'app-item',
// ...
要在 AppComponent 中使用 ItemComponent 選擇器,您需要新增元素 <app-item>,該元素對應於您為元件類定義的選擇器,新增到 app.component.html。用以下更新版本替換 app.component.html 中的當前無序列表 <ul>
<h2>
{{items.length}}
<span *ngIf="items.length === 1; else elseBlock">item</span>
<ng-template #elseBlock>items</ng-template>
</h2>
<ul>
<li *ngFor="let i of items">
<app-item (remove)="remove(i)" [item]="i"></app-item>
</li>
</ul>
更改 app.component.ts 中的 imports 以包括 ItemComponent 以及 CommonModule
@Component({
standalone: true,
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
imports: [CommonModule, ItemComponent],
})
<h2> 中的雙大括號語法 {{}} 將插值 items 陣列的長度,並顯示數字。
<h2> 中的 <span> 使用 *ngIf 和 else 來確定 <h2> 應該顯示“item”還是“items”。如果列表中只有一個專案,則顯示包含“item”的 <span>。否則,如果 items 陣列的長度不是 1,則 <ng-template>(我們已將其命名為 elseBlock,語法為 #elseBlock)將顯示,而不是 <span>。當您不希望預設情況下呈現內容時,可以使用 Angular 的 <ng-template>。在這種情況下,當 items 陣列的長度不是 1 時,*ngIf 將顯示 elseBlock,而不是 <span>。
<li> 使用 Angular 的重複器指令 *ngFor 來遍歷 items 陣列中的所有專案。Angular 的 *ngFor 與 *ngIf 一樣,是另一個幫助您在編寫更少程式碼的同時更改 DOM 結構的指令。對於每個 item,Angular 會重複 <li> 及其內部的所有內容,其中包括 <app-item>。這意味著對於陣列中的每個專案,Angular 都會建立另一個 <app-item> 例項。對於陣列中的任意數量的專案,Angular 將建立與之數量相等的 <li> 元素。
您也可以在其他元素上使用 *ngFor,例如 <div>、<span> 或 <p>,僅舉幾例。
AppComponent 具有用於刪除專案的 remove() 方法,該方法繫結到 ItemComponent 中的 remove 屬性。方括號 [] 中的 item 屬性將 i 之間的值繫結到 AppComponent 和 ItemComponent 之間。
現在,您應該能夠從列表中編輯和刪除專案。當您新增或刪除專案時,專案的數量也應該改變。為了使列表更易於使用,請向 ItemComponent 新增一些樣式。
向 ItemComponent 新增樣式
您可以使用元件的樣式表來新增特定於該元件的樣式。以下 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;
}