添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

昨天我們已經順利讓 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 的部份,程式碼太長我就不貼在這裡佔篇幅了,請直接點我下載。

筆者不小心把原檔刪除了,還請直接參考 TodoMVC 的原始碼

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

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

有了輸入框之後,接下來就是要將使用者所輸入的待辦事項新增到清單內。我們希望使用者輸入完待辦事項後,直接按下 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 是指這個函式回傳值的資料類型,意思是沒有任何回傳值。

我們來看看效果:

看起來似乎是有達到效果,不過稍微防呆一下應該會更好。不急,我們先讓使用者可以真的把待辦事項顯示在清單上。讓我們先在 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">
        <div class="view">
          <input class="toggle" type="checkbox">
          <label>這裡要顯示待辦事項</label>
          <button class="destroy"></button>
  </section>
  <!-- 清單區域結束 -->
</section>

這時候畫面應該會變成:

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

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

ng generate service todo-list/todo-list

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

剛建好的 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>
</section>

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

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

來看看效果吧:

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

來看一下目前程式碼吧!

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>
  </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);

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

<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> </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();
                            

最後在 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 = '';

非常不建議你把 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 = '';

你可以試試看 :)

從錯誤訊息中可以知道要怎麼改: `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` 的部份。 因此我建議可以改成這樣會比較好: ```typescript addTodo(inputRef: HTMLInputElement | null): void { if (inputRef) { return; const todo = inputRef.value.trim(); if (todo) { this.todoListService.add(todo); inputRef.value = ''; 你可以試試看 :)

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 供參考

<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 大您好:

    感謝您的回覆!根據您的回覆,我把 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.

    不知道是不是我型別處理的方式不對呢?

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

  • component.ts
  • getList():string[]{
        return this.todoListService.list;
                        
    
  •