iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 15
2
Modern Web

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

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

Imgur

昨天我們已經順利讓 TodoListComponent 可以順利在 AppComponent 裡使用了,接下來為了方便大家練習,我們直接從 TodoMVC 的 Source 借 HTML 與 CSS 來用。

我們先用以下的 HTML 替換掉原本在 todo-list.component.html 裡的 HTML:

<section class="todoapp">

  <header class="header">
	<h1>todos</h1>
    <input
      class="new-todo"
      placeholder="What needs to be done?"
      autofocus
    >
  </header>

</section>

然後再把一些全系統共用的樣式設定貼到 src/ 底下的 style.css 裡:

html,
body {
	margin: 0;
	padding: 0;
}

button {
	margin: 0;
	padding: 0;
	border: 0;
	background: none;
	font-size: 100%;
	vertical-align: baseline;
	font-family: inherit;
	font-weight: inherit;
	color: inherit;
	-webkit-appearance: none;
	appearance: none;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
}

body {
	font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
	line-height: 1.4em;
	background: #f5f5f5;
	color: #4d4d4d;
	min-width: 230px;
	max-width: 550px;
	margin: 0 auto;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
	font-weight: 300;
}

:focus {
	outline: 0;
}

.hidden {
	display: none;
}

再來是 todo-list.component.css 的部份,程式碼太長我就不貼在這裡佔篇幅了,請直接點我下載。

完成後應該會看到以下畫面:

Imgur

到目前為止我們所做的事是先將主要的輸入框做出來,這樣才有個地方讓我們輸入待辦的事項。

有了輸入框之後,接下來就是要將使用者所輸入的待辦事項新增到清單內。我們希望使用者輸入完待辦事項後,直接按下 enter 就可以將其所輸入的待辦事項加入到清單內。

所以我們先在輸入框上綁定一個 keyup.enter 的事件,並指定 addTodo 函式去處理這個事件,且將 $event.target 當做參數傳入:

<input
  class="new-todo"
  placeholder="What needs to be done?"
  autofocus
  (keyup.enter)="addTodo($event.target)"
>

其實這一段在原本的程式碼裡是使用 [(ngModel)] 來處理雙向綁定,但我在這裡故意採用另外一種方式來告訴大家,既然是事件綁定,其實就有個 $event 的參數可以使用。然後我們可以從 $event.target 來取得觸發當前事件的元素實體,進而取得這個元素的值。

接著我們再到 todo-list.component.ts 裡,實作這個 addTodo 函式:

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

為避免有朋友看不懂上述程式碼,我簡單說明一下:

  • addTodo 是函式名稱,應該沒有人不知道吧?!

  • inputRef 指的是我們在 Template 使用 $event.target 取到的當前觸發事件的這個元素實體。

  • HTMLInputElementinputRef 的資料類型。我習慣會替參數宣告資料類型,也建議大家這麼做。因為 VSCode 會幫你檢查你傳入的參數型別有沒有問題 (如果是從 Template 傳入的倒是檢查不到),而且也會提示你這個參數有什麼屬性跟方法可以使用,可以節省時間且降低因打錯字造成 Bug 風險的,非常貼心!

  • void 是指這個函式回傳值的資料類型,意思是沒有任何回傳值。

我們來看看效果:

Imgur

看起來似乎是有達到效果,不過稍微防呆一下應該會更好。不急,我們先讓使用者可以真的把待辦事項顯示在清單上。讓我們先在 todo-list.component.html 裡加上:

<section class="todoapp">

  <header class="header">
	<h1>todos</h1>
    <input
      class="new-todo"
      placeholder="What needs to be done?"
      autofocus
      (keyup.enter)="addTodo($event.target)"
    >
  </header>

  <!-- 清單區域開始 -->
  <section class="main">

    <ul class="todo-list">
      <li>
        <div class="view">
          <input class="toggle" type="checkbox">
          <label>這裡要顯示待辦事項</label>
          <button class="destroy"></button>
        </div>
      </li>
    </ul>

  </section>
  <!-- 清單區域結束 -->

</section>

這時候畫面應該會變成:

Imgur

接下來我想要新增一個 Service ,將之後 CRUD 的部份都交給這個 Service 來處理,Component 只要專心處理畫面的顯示就好。

所以輸入以下指令來新增 TodoListService:

ng generate service todo-list/todo-list

之所以會是 todo-list/todo-list 是因為,我想要讓 Angular CLI 建立好這個 Service 之後,直接放在 todo-list 資料夾裡面:

Imgur

剛建好的 TodoListService 長這樣:

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

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

  constructor() { }

}

然後我們來宣告一個私有的變數 list ,準備用來存放我們所有的代辦事項:

private list: string[] = [];

接著我們新增一個能將使用者所輸入的待辦事項存放到 list 裡的函式:

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

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

}

不過因為 list 是私有變數的關係,所以我們需要再新增一個函式來取得存放在 list 裡的資料:

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

好的,這樣 TodoListService 就大致有個雛形了!接下來我們到 TodoListComponent 裡來注入這個 TodoListService 。

先將 TodoListService 引入:

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

接著直接在 constructor 函式裡加入 todoListService 這個參數並將其資料類型宣告為 TodoListService :

constructor(private todoListService: TodoListService) { }

當我們在 constructor 宣告參數的時候, TypeScript 預設會幫我們建立一個同名變數,並把參數指定給那個同名變數。

意思是,上面一行其實做了像是這樣子的事:

class TodoListComponent {

  private todoListService: TodoListService;

  constructor(private todoListService: TodoListService) {
    this.todoListService = todoListService;
  }

}

不過因為 TypeScript 其實會幫我們處理,所以就不用寫那麼多了。

接著我們來實際使用 todoListService 新增待辦事項:

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

  const todo = inputRef.value.trim();

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

}

除了新增,也要讓 TodoListComponent 能夠把待辦事項的清單顯示在畫面上,所以再新增一個取得清單的函式:

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

然後我們到 todo-list.component.html 裡將資料綁到畫面上:

<section class="main" *ngIf="getList().length">

  <ul class="todo-list">
    <li *ngFor="let todo of getList()">
      <div class="view">
        <input class="toggle" type="checkbox">
        <label>{{ todo }}</label>
        <button class="destroy"></button>
      </div>
    </li>
  </ul>

</section>

因為希望清單裡面有待辦事項時才顯示,所以我們使用 *ngIf 這個結構型的 Directive ,令其在存放清單裡的資料大於 0 時才會將整個 <section></section> 載入。

然後用之前學過的 *ngFor 來把清單裡的所有待辦事項逐筆迴圈出來,並將待辦事項用插值表達式 {{ todo }} 來將資料綁在 <label></label> 裡。

來看看效果吧:

Imgur

所以目前為止,我們已經完成了待辦事項的新增與清單的顯示。

來看一下目前程式碼吧!

todo-list.component.html 的部份:

<section class="todoapp">

	<header class="header">
		<h1>todos</h1>
    <input
      class="new-todo"
      placeholder="What needs to be done?"
      autofocus
      (keyup.enter)="addTodo($event.target)"
    >
  </header>

  <section class="main" *ngIf="getList().length">

    <ul class="todo-list">
      <li *ngFor="let todo of getList()">
        <div class="view">
          <input class="toggle" type="checkbox">
          <label>{{ todo }}</label>
          <button class="destroy"></button>
        </div>
      </li>
    </ul>

  </section>

</section>

todo-list.component.ts 的部份:

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

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

@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 {string[]}
   * @memberof TodoListComponent
   */
  getList(): string[] {
    return this.todoListService.getList();
  }

}

todo-list.service.ts 的部份:

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

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

  private list: string[] = [];

  constructor() { }

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

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

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

  }

}

那今天就到這邊,想一下、吸收一下,明天再來完成剩下的部份!

明天見!


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

尚未有邦友留言

立即登入留言