iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 16
4
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(); let i = index"
    [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

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

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

明天見!

錯誤更新記錄

  • 2019/09/05 15:41 - 非常感謝邦友 jakeujjacky123200 的提醒,補上漏掉的 let i = index; 語法。

上一篇
[Angular 深入淺出三十天] Day 14 - Angular小學堂(三之二)
下一篇
[Angular 深入淺出三十天] Day 16 - Angular小學堂(三之四)
系列文
Angular 深入淺出三十天33
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中
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邦新手 3 級 ‧ 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 這個函式本身就不是陣列的函式,要馬使用陣列的原生函式處理,要馬就是自己做函式處理(但也會用到原生函式)

jackson09 iT邦新手 5 級 ‧ 2018-12-21 16:10:11 檢舉

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

Leo iT邦新手 3 級 ‧ 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邦新手 3 級 ‧ 2019-01-08 10:39:13 檢舉

/images/emoticon/emoticon12.gif

0
linsslinss2004
iT邦新手 5 級 ‧ 2019-07-12 16:36:44

Hi Leo大大:
我不管打甚麼ng的指令都會出現

"ng : 無法辨識 'ng' 詞彙是否為 Cmdlet、函數、指令檔或可執行程式的名稱。請檢查名稱拼字是否正確,如果包含路徑的話,請確認路徑是
否正確,然後再試一次。"
這個error訊息,請問該怎麼解決

Leo iT邦新手 3 級 ‧ 2019-07-12 17:00:12 檢舉

Hi linsslinss2004,

這通常是因為:

  1. 沒有裝 @angular/cli
  2. 命令列工具不支援該語法(我猜你應該是用 windows 最普通的那種命令列工具),可能要換一種命令列工具試試看

以上只是我的猜測,你可以檢查看看~

0
jacky123200
iT邦新手 5 級 ‧ 2019-09-05 15:38:05

雖然文中
*ngFor="let todo of getList()
少了
;let i = index
但是為什麼在這可以讀取到i呢? (click)="remove(i)"

另外一個問題是
get done(): boolean {
return this.completed;
}
為什麼get 和done()是分開的?
這個是什麼語法?
謝謝解答

Leo iT邦新手 3 級 ‧ 2019-09-05 15:48:58 檢舉

Hi jacky123200,

非常感謝你的提醒,原來文章裡漏掉了這段語法,而且之前已經有邦友提醒過了,我居然還沒有改到!!

/images/emoticon/emoticon04.gif

那個 get 是一種宣告,表示接下來的這個函式是一個 getter ,這種函式在使用時,不用像一般函式那樣需要加上 () 才能執行。

例如:

get done(): boolean {
  return this.completed;
}

console.log(this.done); 

// 與

done(): boolean {
  return this.completed;
}

console.log(this.done());

謝謝

0
obelisk0114
iT邦新手 5 級 ‧ 2019-11-04 18:20:49
[class.completed]="todo.done"

是什麼 ? 看起來不像 property binding
one way binding 除了 event binding 和 插值, 其他好像不能傳入方法 ?

(click)="todo.toggleCompletion()"
[checked]="todo.done"

可以用像是 [(ngModel)]="todo.toggleCompletion()" 的形式嗎 ?
上面那個和 [ngModel]="todo.toggleCompletion()" 都失敗

看更多先前的回應...收起先前的回應...
Leo iT邦新手 3 級 ‧ 2019-11-04 18:36:54 檢舉

Hi obelisk0114,

  1. 只要是用 [] 包起來的基本都算是 property binding 。
  2. 可以傳入方法。
  3. 想要使用 [ngModel] or [(ngModel)] 要在 Module 裡 import FormsModule
  1. 我是問
[class.completed]="todo.done"

是什麼意思 ?
2. 從 Angular 的官網有這句

Similarly, you cannot use property binding to call a method on the target element.

https://angular.io/guide/template-syntax#property-binding-property

但是 [checked]="todo.done" 沒有報錯, 所以看起來只要是單純取值就可以 ?
3. import FormsModule 還是會失敗
https://ithelp.ithome.com.tw/upload/images/20191105/20122639EJoPegLd81.png
https://ithelp.ithome.com.tw/upload/images/20191105/20122639aMlfVKd9rt.png

Leo iT邦新手 3 級 ‧ 2019-11-06 21:49:29 檢舉

Hi obelisk0114,

<li [class.completed]="todo.done"></li>

<!-- todo.done 為 true 時 -->
<li class="completed"></li>

<!-- todo.done 為 false 時 -->
<li></li>
  1. 錯誤訊息很明顯的是說 24 行有問題

在 Angular 裡, [] 是 input 、 () 是 output , [(ngModel)] 是雙向綁定,表示其同時具有 input 以及 output 的功能,而且這個 output 還是直接會把值塞回去的那種。

所以你覺得 todo.toggleCompletion() 的回傳結果是可以被指定值的嗎?

也就是說 [(ngModel)] 只能用在 component 的 property, 不能用在 <input>checked (checked!completed 使用 two-way binding) ?

Leo iT邦新手 3 級 ‧ 2019-11-07 19:41:01 檢舉

不,我指的是,要直接給它變數,它才能將值指定給該變數

後來把 private completedprivate 移除,可以用 [(ngModel)] 做出原來的效果。我之前搞錯了,感謝大大的說明

Leo iT邦新手 3 級 ‧ 2019-11-18 17:43:09 檢舉

很高興能夠幫得上忙^^

/images/emoticon/emoticon01.gif

0
messboy000
iT邦新手 4 級 ‧ 2019-12-24 16:51:29

請教一下動態gif檔是用哪一款程式呢? 真的方便易懂,謝謝

Leo iT邦新手 3 級 ‧ 2019-12-27 11:46:58 檢舉

Hi messboy000,

就只是螢幕錄影之後,在線上找個影片轉gif檔的網站而已 :)

0
smile98
iT邦新手 5 級 ‧ 2020-06-22 04:47:57

Hi Leo 大大:
想請問這段是什麼意思呢?

constructor(title: string) {
this.title = title || ''; // 為避免傳入的值為 Falsy 值,稍作處理
}

this.title 等於title 或者''嗎?
什麼時候會等於''呢? 當沒有title的時候嗎?

改成以下這個,好像也沒有出現什麼錯誤
constructor(title: string) {
this.title = title;
}

先謝謝大大地回答:)

Leo iT邦新手 3 級 ‧ 2020-06-22 10:36:24 檢舉

Hi smile98,

this.title 等於title 或者''嗎?

是的沒錯。

什麼時候會等於''呢? 當沒有title的時候嗎?

是的,當傳入的 title 值為 falsy 的時候,就會等於 ''

關於什麼是 falsy ,原文內有連結,請參考。

改成以下這個,好像也沒有出現什麼錯誤
constructor(title: string) {
this.title = title;
}

這就要看你對於錯誤定義囉,基本上如果用 new Todo(0)new Todo(undefined)new Todo(null) 這三個方式去建立這個 todo ,就可能會造成錯誤。

基本上,這樣寫就是個防呆機制,可做可不做,自由心證囉。

0
muyun
iT邦新手 5 級 ‧ 2021-08-04 16:52:33

Leo 大您好:

感謝您撰文分享,內容非常清楚,十分受用
let i = index的地方似乎還沒補上XD"
如果是我眼拙漏看的話先說聲抱歉!!

Leo iT邦新手 3 級 ‧ 2023-09-18 16:20:35 檢舉

我搜尋了一下是有找到,還是你是指有別的地方漏掉了呢?

0
alan8034
iT邦新手 5 級 ‧ 2023-09-18 16:16:21

Leo大大您好,我跟著完成後,我沒有可以按叉叉的button耶,我的html是也有跟著您刻出來壓~~

看更多先前的回應...收起先前的回應...
Leo iT邦新手 3 級 ‧ 2023-09-18 16:21:44 檢舉

可能是因為 CSS 的原因?我在上一篇有回你了,你把它補上試試?

alan8034 iT邦新手 5 級 ‧ 2023-09-18 16:56:32 檢舉

Leo大大~~我兩邊都有加惹你貼給我的那段,但還是都沒有叉叉可以給我用,我也有把checkbox勾選起來QQ

alan8034 iT邦新手 5 級 ‧ 2023-09-18 17:35:53 檢舉

Leo大大我找到問題惹,是我class打錯,所以沒有吃到css,謝謝你我要繼續看你的文章惹~~

Leo iT邦新手 3 級 ‧ 2023-09-19 10:00:09 檢舉

嗯嗯,加油加油!

我要留言

立即登入留言