iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 16
1
Modern Web

Angular 深入淺出三十天系列 第 16

[Angular 深入淺出三十天] Day 15 - Angular小學堂(三之三)

回顧一下我們目前完成的效果:

Imgur

完成了新增待辦事項的功能之後,接下來當然是要來能夠註記是否已完成該項待辦事項囉!如果你有操作的話其實會發現,目前清單前面的 Checkbox 是可以打勾的,但實質上並無真正的註記效果。

怎麼說呢?我們可以到原本 TodoMVC 的頁面操作看看就知道了。

發現了嗎?當我們將待辦事項前的 checkbox 打勾之後,待辦事項其實應該會被畫上刪除線,且字體顏色應該要變淡。

我們先來做這個功能吧!這個功能說起來其實滿簡單的,只要在 checkbox 打勾之後,在元素上多加一個 class 就能完成。

只不過我們目前的資料結構並不足以完成這件事,因為我們目前只有將待辦事項的事項名稱記下來而已,並沒有記錄該事項是否已完成。

所以我們先從資料開始下手,先建立一個叫做 Todo 的資料物件模型:

ng generate class todo-list/todo --type model

在新增類別的指令額外加上 --type 的參數是為了要讓 CLI 幫我們產生檔案時,檔案名稱會變成 [filename].[type].ts 這樣的命名方式,檔案內容並不會有任何變化。
如果沒有加上 --type 的話,CLI 在產生檔案時,檔案名稱只會是 [filename].ts 而已。

這時候 CLI 就會建立一個名為 todo.model.ts 的檔案,裡面大概長這樣:

/**
 * 待辦事項的資料物件模型
 *
 * @export
 * @class Todo
 */
export class Todo {

}

接著新增兩個私有屬性,用以記錄待辦事項的事項名稱以及完成與否:

export class Todo {

  /**
   * 事項名稱
   *
   * @private
   * @memberof Todo
   */
  private title = '';

  /**
   * 完成與否
   *
   * @private
   * @memberof Todo
   */
  private completed = false;

}

我個人習慣在宣告屬性時就先給預設值,一方面是讓 VSCode 知道這個變數的資料型態是什麼;一方面是減少預期之外的行為。
至於屬性宣告為私有屬性是因為我不希望外部可以隨便修改這裡面的值,如果你不介意,可以不用加上 private 的宣告就是公有的囉!

再來是覆寫一下建構式:

/**
 * Creates an instance of Todo.
 * 
 * @param {string} title - 待辦事項的名稱
 * @memberof Todo
 */
constructor(title: string) {
  this.title = title || ''; // 為避免傳入的值為 Falsy 值,稍作處理
}

關於 Truthy 與 Falsy ,請參考 MDN 的 TruthyFalsy 說明文件。

由於屬性宣告為私有屬性,所以寫個 getter 讓外部可以取得該屬性:

/**
 * 此事項是否已經完成
 *
 * @readonly
 * @type {boolean}
 * @memberof Todo
 */
get done(): boolean {
  return this.completed;
}

/**
 * 取得事項名稱
 *
 * @returns {string}
 * @memberof Todo
 */
getTitle(): string {
  return this.title;
}

在這裡我特意使用兩種方式來讓外部可以取得內部私有的屬性,稍後可以稍微留意一下使用方式,比較一下差別。

除此之外,由於是 checkbox 的關係,可以打勾之後再取消打勾,所以我們再加上一個 toggleCompletion 的函式來讓外部可以使用:

/**
 * 來回切換完成狀態
 *
 * @memberof Todo
 */
toggleCompletion(): void {
  this.completed = !this.completed;
}

資料物件模型做好了之後,我們來調整一下原本的程式碼,先開啟 todo-list.service.ts ,將程式碼調整成像是這樣:

import { Injectable } from '@angular/core';

// Class
import { Todo } from './todo.model';

@Injectable({
  providedIn: 'root'
})
export class TodoListService {

  private list: Todo[] = [];

  constructor() { }

  /**
   * 取得待辦事項清單
   *
   * @returns {Todo[]}
   * @memberof TodoListService
   */
  getList(): Todo[] {
    return this.list;
  }

  /**
   * 新增待辦事項
   *
   * @param {string} title - 待辦事項的標題
   * @memberof TodoListService
   */
  add(title: string): void {

    // 避免傳入的 title 是無效值或空白字串,稍微判斷一下
    if (title || title.trim()) {
      this.list.push(new Todo(title));
    }

  }

}

接著是 todo-list.component.ts

import { Component, OnInit } from '@angular/core';

// Service
import { TodoListService } from './todo-list.service';

// Class
import { Todo } from './todo.model';

@Component({
  selector: 'app-todo-list',
  templateUrl: './todo-list.component.html',
  styleUrls: ['./todo-list.component.css']
})
export class TodoListComponent implements OnInit {

  constructor(private todoListService: TodoListService) { }

  ngOnInit() {
  }

  /**
   * 新增代辦事項
   *
   * @param {HTMLInputElement} inputRef - 輸入框的元素實體
   * @memberof TodoListComponent
   */
  addTodo(inputRef: HTMLInputElement): void {

    const todo = inputRef.value.trim();

    if (todo) {
      this.todoListService.add(todo);
      inputRef.value = '';
    }

  }

  /**
   * 取得待辦事項清單
   *
   * @returns {Todo[]}
   * @memberof TodoListComponent
   */
  getList(): Todo[] {
    return this.todoListService.getList();
  }

}

最後則是將 todo-list.component.html 裡的 {{ todo }} 改為 {{ todo.getTitle() }}

到目前為止,我們只是重構了一下程式碼,所以先來測試功能有沒有壞掉。

如果有遇到問題都可以在下方留言給我噢!

好!功能都正常之後,我們再把資料綁到 Template 上:

<ul class="todo-list">
  <li
    *ngFor="let todo of getList()"
    [class.completed]="todo.done"
  >
    <div class="view">
      <input
        class="toggle"
        type="checkbox"
        (click)="todo.toggleCompletion()"
        [checked]="todo.done"
      >
      <label>{{ todo.getTitle() }}</label>
      <button class="destroy"></button>
    </div>
  </li>
</ul>

有注意到 donetoggleCompletion 明明都是函式,卻有著不一樣的使用方式嗎?

來看看效果:

Imgur

很好,完成功能且運作正常!

接下來我們希望能夠可以有刪除的功能,所以我們打開 todo-list.service.ts 實作刪除的函式:

/**
 * 移除待辦事項
 *
 * @param {number} index - 待辦事項的索引位置
 * @memberof TodoListService
 */
remove(index: number): void {
  this.list.splice(index, 1);
}

再來是 todo-list.component.ts

/**
 * 移除待辦事項
 *
 * @param {number} index - 待辦事項的索引位置
 * @memberof TodoListComponent
 */
remove(index: number): void {
  this.todoListService.remove(index);
}

最後在按鈕上加上事件的綁定:

<button
  class="destroy"
  (click)="remove(i)"
></button>

再來看一下效果:

Imgur

很好!刪除的功能也順利完成了!

今天就先到這邊,稍微吸收一下,我們明天再來把功能做得更完善吧!

明天見!


上一篇
[Angular 深入淺出三十天] Day 14 - Angular小學堂(三之二)
下一篇
[Angular 深入淺出三十天] Day 16 - Angular小學堂(三之四)
系列文
Angular 深入淺出三十天33

1 則留言

0
jackson09
iT邦新手 5 級 ‧ 2018-12-21 15:40:38

Leo大大 小弟有個關於刪除功能的問題
目前在做練習 仿照上文的刪除鍵做法 請問Leo大大有看出問題嗎
https://ithelp.ithome.com.tw/upload/images/20181221/20113704i6YhtfHn6k.pnghttps://ithelp.ithome.com.tw/upload/images/20181221/20113704UANyJ7U7Bt.pnghttps://ithelp.ithome.com.tw/upload/images/20181221/20113704CO1wO248Ez.png

看更多先前的回應...收起先前的回應...
Leo iT邦新手 4 級‧ 2018-12-21 15:51:45 檢舉

Hi Jackson,

有阿,

首先是在 app.component.tsremove 函式裡,參數 index 一定是 undefined,因為那需要把 app.component.html 的 Line 21 改成這樣才能正確傳入索引值:

<ng-container *ngFor="let message of messages; let i = index">

再來就是你想問的問題,remove 這個函式本身就不是陣列的函式,要馬使用陣列的原生函式處理,要馬就是自己做函式處理(但也會用到原生函式)

了解 感謝你的回答
另外想問 type="reset"的部分
延續上圖練習 我將"清空所有項目"鍵設type="reset"能把 from裡面的資料清空
但若將type ="reset"放在刪除鍵卻無法 請問為何?

Leo iT邦新手 4 級‧ 2018-12-22 18:17:18 檢舉

Hi Jackson,

那是因為我們已經寫好 function 去處理啦~~

所以雖然都是 type="reset" ,但實際做的事情還是要看我們是怎麼去處理。

jakeuj iT邦新手 5 級‧ 2019-01-07 17:40:00 檢舉

文中
*ngFor="let todo of getList()
少了
;let i = index

Leo iT邦新手 4 級‧ 2019-01-08 10:39:13 檢舉

/images/emoticon/emoticon12.gif

我要留言

立即登入留言