iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 15
3
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
0
elvisyip
iT邦新手 5 級 ‧ 2021-01-13 14:17:05

想問一下 todo-list.component.html 部份
當輸入 ng serve 的時候
會出現錯誤

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

Error: src/app/todo-list/todo-list.component.html:5:103 - error TS2345: Argument of type 'EventTarget | null' is not assignable to parameter of type 'HTMLInputElement'.
Type 'null' is not assignable to type 'HTMLInputElement'.

另也附上 todo-list-component.ts

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

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(): void {
  }

  addTodo(inputRef: HTMLInputElement): void {

    const todo = inputRef.value.trim();

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

  getList(): string[] {
    return this.todoListService.getList();
  }

}
看更多先前的回應...收起先前的回應...
elvisyip iT邦新手 5 級 ‧ 2021-01-13 14:30:08 檢舉

最後在 todo-list.component.ts 的addTodo函式改成以下就正常了
可能是Angular版本關係吧? 相隔了2-3年..

addTodo(inputRef: any): void {

    const todo = inputRef.value.trim();

    if (todo) {
      this.todoListService.add(todo);
      inputRef.value = '';
    }
}
Leo iT邦新手 3 級 ‧ 2021-01-13 14:42:11 檢舉

Hi elvisyip,

非常不建議你把 inputRef 的型別改成 any 噢,
從錯誤訊息中可以知道要怎麼改:

Error: src/app/todo-list/todo-list.component.html:5:103 - error TS2345: Argument of type 'EventTarget | null' is not assignable to parameter of type 'HTMLInputElement'. Type 'null' is not assignable to type 'HTMLInputElement'.

這段錯誤訊息的意思是, $event.targe 的型別是 EventTarget | null ,所以它無法指定給我們定義型別為 HTMLInputElement 的參數 inputRef ,尤其是 null 的部份。

因此我建議可以改成這樣會比較好:

addTodo(inputRef: HTMLInputElement | null): void {
  if (inputRef) {
    return;
  }
  const todo = inputRef.value.trim();
  if (todo) {
    this.todoListService.add(todo);
    inputRef.value = '';
  }
}

你可以試試看 :)

elvisyip iT邦新手 5 級 ‧ 2021-01-13 15:42:45 檢舉
addTodo(inputRef: HTMLInputElement | null): void {

    if(inputRef){
      return;
    }
    
    const todo = inputRef.value.trim();

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

不過導致了另1個問題出現
Object is possibly 'null'.
const todo = inputRef.value.trim();
inputRef.value = '';
以上2句的 inputRef 都有同1個錯誤

Leo iT邦新手 3 級 ‧ 2021-01-15 11:22:02 檢舉

Hi elvisyip,

抱歉,我給你的程式碼有個判斷式寫錯

if(!inputRef) 才對,不是 if(inputRef)

意即當 inputRefnull or undefined 時,就不會再繼續執行。

0
joanne_muyun
iT邦新手 5 級 ‧ 2021-08-04 17:06:49

Leo 大您好:

我依照文中方法撰寫 html,但是在 $event.target 處會出現如下錯誤:
Type 'null' is not assignable to type 'HTMLInputElement'.

看起來跟樓上大大的錯誤一樣,所以我根據您給他的回覆,把 component 的 addTodo 改成 addTodo(todoThing: HTMLInputElement | null),結果出現新的錯誤如下:
Type 'EventTarget' is missing the following properties from type 'HTMLInputElement': accept, align, alt, autocomplete, and 284 more

可以請問這要怎麼解決嗎? 謝謝!

以下附上 code 供參考

  • 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>
    
  • component
    import { Component, OnInit } from '@angular/core';
    
    import { ListContent } from './list-content.model';
    
    @Component({
        selector: 'app-todo-list',
        templateUrl: './todo-list.component.html',
        styleUrls: ['./todo-list.component.css']
    })
    export class TodoListComponent implements OnInit {
    
        constructor() { }
    
        ngOnInit(): void {
        }
    
        addTodo(todoThing: any): void {
            if(!todoThing){
                return;
            }
    
            const todo = todoThing.value.trim();
            this.todoListService.addList(todo);
            todoThing.value = '';
        }
    }
    
看更多先前的回應...收起先前的回應...
Leo iT邦新手 3 級 ‧ 2021-08-04 17:53:49 檢舉

Hi joanne_muyun,

我猜想這是因為現在新版本的 Angular 預設已經是使用 strict mode 的 TypeScript ,所以你才會在這邊被擋下來,我建議你在 template 裡改成 addTodo($event) ,然後 ts 應該是 addTodo(event: KeyboardEvent): void { ... } ,型別的部分你在 function 裡處理即可。

Leo 大您好:

感謝您的回覆!根據您的回覆,我把 template 那邊改成 addTodo($event),component 那邊則參考官方文件改成以下:

addTodo(event: KeyboardEvent): void {
        const todoThing = (event.target as HTMLInputElement);
        // 主要只改了上面兩行
        if(!todoThing){
            return;
        }

        const todo = todoThing.value.trim();
        this.todoListService.addList(todo);
        todoThing.value = '';
}

結果出現了對應 template 中 $event 的報錯:
Type 'Event' is missing the following properties from type 'KeyboardEvent': altKey, char, charCode, code, and 16 more.

不知道是不是我型別處理的方式不對呢?
感謝您!

Wisely iT邦新手 5 級 ‧ 2021-09-02 17:27:05 檢舉

我試了修改如下便可以正常執行
<input
class="new-todo"
placeholder="What needs to be done?"
autofocus
(keyup)="addTodo($event)"
>

addTodo(event: KeyboardEvent): void
{
const todoThing = event.target as HTMLInputElement;
if (!todoThing)
{
return;
}
if (event.key === 'Enter')
{
const todo = todoThing.value.trim();
this.todoListService.add(todo);
todoThing.value='';
}
}

Leo iT邦新手 3 級 ‧ 2021-09-03 11:05:51 檢舉

Hi wisely_7club,

非常感謝您幫忙回覆/images/emoticon/emoticon12.gif

0
evafly0508
iT邦新手 5 級 ‧ 2021-10-18 10:13:55

Leo大您好,
想請問為什麼list不直接宣告為public而是宣告為private再透過getList()方法取得值呢?
如果宣告為public好像就可以在component.ts中取得list的值了
是為了保護list的值不被service.ts檔案以外的地方改變嗎?
麻煩您解答了,謝謝!

  • component.ts
getList():string[]{
    return this.todoListService.list;
}
Leo iT邦新手 3 級 ‧ 2021-10-18 11:16:55 檢舉

Hi evafly0508,

沒錯唷!雖然這樣的防止效果僅限於開發期間 XD

你也可以照你的想法做沒問題的,每個人都有自己的設計,只要在大方向上沒有錯誤,又或者是說不會難以維護即可。

我要留言

立即登入留言