建立一個 item 元件

元件可以用來幫助你組織你的應用程式。這篇文章會引導你建立一個元件,用來管理清單列表中的個別項目,包含加入核取方塊、編輯和刪除功能。這邊也會介紹 Angular 事件模型。

預備知識: 熟悉主要的 HTMLCSSJavaScript 語言和terminal/command line (en-US)知識。
學習目標: 掌握更多元件知識,包含如何使用事件來處理資料更新,以及加入核取方塊、編輯和刪除的功能。

建立一個新的元件

使用如下的 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 將含有 EditDelete 按鈕的 <div> 放回 DOM 中。

點擊 Save 按鈕將調用 saveItem() 方法。 saveItem()方法從 <input> 中的範本變數 #editedItem 取得值,並將該項目的 description 更改為 editedItem.value 的值。

準備 AppComponent

在下一章節,您將添加用來溝通 AppComponentItemComponent 的程式碼。首先將以下內容添加到 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()

Add logic to ItemComponent

To use the ItemComponent UI, you must add logic to the component such as functions, and ways for data to go in and out.

In item.component.ts, edit the JavaScript imports as follows:

import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Item } from "../item";

The addition of Input, Output, and EventEmitter allows ItemComponent to share data with AppComponent. By importing Item, ItemComponent can understand what an item is.

Further down item.component.ts, replace the generated ItemComponent class with the following:

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;
  }
}

The editable property helps toggle a section of the template where a user can edit an item. editable is the same property in the HTML as in the *ngIf statement, *ngIf="editable". When you use a property in the template, you must also declare it in the class.

@Input(), @Output(), and EventEmitter facilitate communication between your two components. An @Input() serves as a doorway for data to come into the component, and an @Output() acts as a doorway for data to go out of the component. An @Output() has to be of type EventEmitter, so that a component can raise an event when there's data ready to share with another component.

Use @Input() to specify that the value of a property can come from outside of the component. Use @Output() in conjunction with EventEmitter to specify that the value of a property can leave the component so that another component can receive that data.

The saveItem() method takes as an argument a description. The description is the text that the user enters into the HTML <input> when editing an item in the list. This description is the same string from the <input> with the #editedItem template variable.

If the user doesn't enter a value but clicks Save, saveItem() returns nothing and does not update the description. If you didn't have this if statement, the user could click Save with nothing in the HTML <input>, and the description would become an empty string.

If a user enters text and clicks save, saveItem() sets editable to false, which causes the *ngIf in the template to remove the edit feature and render the Edit and Delete buttons again.

Though the application should compile at this point, you need to use the ItemComponent in AppComponent so you can see the new features in the browser.

Use the ItemComponent in the AppComponent

Including one component within another in the context of a parent-child relationship gives you the flexibility of using components wherever you need them.

The AppComponent serves as a shell for the application where you can include other components.

To use the ItemComponent in AppComponent, put the ItemComponent selector in the AppComponent template. Angular specifies the selector of a component in the metadata of the @Component() decorator. In this example, the selector is app-item:

@Component({
  selector: 'app-item',
  templateUrl: './item.component.html',
  styleUrls: ['./item.component.css']
})

To use the ItemComponent selector within the AppComponent, you add the element, <app-item>, which corresponds to the selector you defined for the component class to app.component.html. Replace the current unordered list in app.component.html with the following updated version:

<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>

The double curly brace syntax, {{}}, in the <h2> interpolates the length of the items array and displays the number.

The <span> in the <h2> uses an *ngIf and else to determine whether the <h2> should say "item" or "items". If there is only a single item in the list, the <span> containing "item" displays. Otherwise, if the length of the items array is anything other than 1, the <ng-template>, which we've named elseBlock, with the syntax #elseBlock, shows instead of the <span>. You can use Angular's <ng-template> when you don't want content to render by default. In this case, when the length of the items array is not 1, the *ngIf shows the elseBlock and not the <span>.

The <li> uses Angular's repeater directive, *ngFor, to iterate over all of the items in the items array. Angular's *ngFor like *ngIf, is another directive that helps you change the structure of the DOM while writing less code. For each item, Angular repeats the <li> and everything within it, which includes <app-item>. This means that for each item in the array, Angular creates another instance of <app-item>. For any number of items in the array, Angular would create that many <li> elements.

You can use an *ngFor on other elements, too, such as <div>, <span>, or <p>, to name a few.

The AppComponent has a remove() method for removing the item, which is bound to the remove property in the ItemComponent. The item property in the square brackets, [], binds the value of item between the AppComponent and the ItemComponent.

Now you should be able to edit and delete items from the list. When you add or delete items, the count of the items should also change. To make the list more user-friendly, add some styles to the ItemComponent.

為 ItemComponent 添加樣式

你可以使用元件的 styles sheet 去增加該元件的樣式。下面的 CSS 增加了基本的樣式,對按鈕添加 flexbox 屬性和客製化了核取方塊。

將下面的樣式程式碼貼至 item.component.css

.item {
  padding: .5rem 0 .75rem 0;
  text-align: left;
  font-size: 1.2rem;
}

.btn-wrapper {
  margin-top: 1rem;
  margin-bottom: .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: .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: .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: .15em; left: .22em;
  font-size: 1.3em;
  line-height: 0.8;
  color: #0d8dee;
  transition: all .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;
}

結論

您現在應該擁有一個樣式化的Angular待辦事項列表應用程序,該應用程序可以添加,編輯和刪除項目。下一步是加入過濾功能,以便您可以查看符合特定條件的項目。

In this module