[從 0 開始的 Angular 生活]No.29 定義 Angular 元件的輸出介面 - @Output()

前言

在物件導向程式的領域,有個稱為 OCP ( Open Closed Principle ) 的原則,中文稱為開放封閉原則。這原則說的是,在進行物件導向程式設計時要能符合開放擴充但封閉修改的要素,這樣子才能把每個不同的物件獨立切開、互不干擾。

這段前言又跟這篇文章有什麼關係呢?

截至目前為止的結構

可整理出如下列表

  • AppModule - 根模組
    • AppComponent
      • ArticleList
        • ArticleHeader
        • ArticleBody

而元件一層包一層的好處是,一次只要關注一個想要修改的地方就好了,不管應用程式多複雜透過元件化的架構,就可以把一份複雜的網頁切割成多個元件,每個元件只單純負責幾件事情就好。

製作刪除文章的功能

假使想要實作出刪除文章的按鈕,想要將這個按鈕放在 ArticleHeader 的 Template 內,點擊後能夠將刪除的資訊傳給父元件 ArticleList ,最後達成刪除文章的效果。

添加刪除按鈕

首先在 ArticleHeader 的 Template 內做出一顆按鈕。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<header class="post-header">
<h2 class="post-title">
<a [href]="item.href">{{ item.subject?.title|lowercase|slice:-20 }}</a>
<small>{{item.subject?.subtite}}</small>
</h2>
<div class="post-info clearfix">
<span class="post-date"><i class="glyphicon glyphicon-calendar"></i>{{item.date|date:'yyyy-MM-dd HH:mm'}}</span>
<span class="post-author"><i class="glyphicon glyphicon-user"></i><a
href="http://blog.miniasp.com/author/will.aspx">{{item.author}}</a></span>
<span class="post-category"><i class="glyphicon glyphicon-folder-close"></i><a
[href]="item['category-link']">{{item.category}}</a></span>
</div>
<span><button>刪除</button></span>
</header>

但是這樣是沒辦法刪除的,因為資料是由父元件 ArticleList 傳遞給子元件 ArticleHeader ,子元件本身並不具有這份資料。

解決辦法是將要刪除哪筆篇文章的資料往上傳遞給父元件,透過父元件刪除該篇文章。

白話來說,以刪除文章這個功能而言:

  • 子元件 ArticleHeader 扮演的角色是:透過點擊事件通知父元件哪一篇文章的刪除按鈕被點擊了。
  • 接著父元件 ArticleList 接收到訊息後把對應資料刪除,重新透過 ngFor 渲染網頁。

註冊 ArticleHeader 內的事件

從父元件傳值給子元件是透過屬性繫結;從子元件傳值給父元件則必須透過事件繫結。

在 Angular 元件內要註冊一個事件必須先宣告一個屬性,同時宣告一個 @Output 裝飾器,跟之前介紹過的 @Input 一樣。

首先來到 ArticleHeader 的 class 內進行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Component, OnInit, Input, Output } from '@angular/core';

@Component({
selector: 'app-article-header',
templateUrl: './article-header.component.html',
styleUrls: ['./article-header.component.scss']
})
export class ArticleHeaderComponent implements OnInit {
@Input()
item;

@Output()
delete;
constructor() { }

ngOnInit() {
}

}

這樣就完成了 delete 事件的註冊,接著要將這個事件繫結到父元件上。

事件的名稱可以自由命名。

在父元件上繫結 delete 事件

剛才已經在子元件內註冊了 delete 事件,此時在 ArticleList 內已經可以從自動完成中看到對應的選項。

ArticleList 內的 Template

1
2
3
4
5
6
<!-- Article START-->
<article class="post" id="post{{idx}}" *ngFor="let item of atticleData; let idx = index">
<app-article-header [item]="item" (delete)="doDelete($event)"></app-article-header>
<app-article-body [item]="item"></app-article-body>
</article>
<!-- Article END-->

當接收到 delete 事件發出的通知時,就會觸發 ArticleList 內的 doDelete() 方法,而且還要讓它傳入一個 $event 參數,這樣才能知道使用者點擊的是哪一篇文章的刪除按鈕。

完成 ArticleHeader 內的 delete 事件

剛才雖然已經註冊 delete 事件,但還有很多細節尚待處理。

  • 例如剛才宣告的 delete 屬性需要 new 一個 EventEmitter 物件
    • EventEmitter 是一個事件的發射器。
    • EventEmitter 後面可以接 <any> ,代表要傳送的資料可以是任意型別

選擇正確的 EventEmitter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';

@Component({
selector: 'app-article-header',
templateUrl: './article-header.component.html',
styleUrls: ['./article-header.component.scss']
})
export class ArticleHeaderComponent implements OnInit {
@Input()
item;

@Output()
delete = new EventEmitter<any>();
constructor() { }

ngOnInit() {
}

}

什麼時候發射 delete 的事件?自然是按下刪除按鈕的時候
回到 ArticleHeader 的 Template :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<header class="post-header">
<h2 class="post-title">
<a [href]="item.href">{{ item.subject?.title|lowercase|slice:-20 }}</a>
<small>{{item.subject?.subtite}}</small>
</h2>
<div class="post-info clearfix">
<span class="post-date"><i class="glyphicon glyphicon-calendar"></i>{{item.date|date:'yyyy-MM-dd HH:mm'}}</span>
<span class="post-author"><i class="glyphicon glyphicon-user"></i><a
href="http://blog.miniasp.com/author/will.aspx">{{item.author}}</a></span>
<span class="post-category"><i class="glyphicon glyphicon-folder-close"></i><a
[href]="item['category-link']">{{item.category}}</a></span>
</div>
<span><button (click)="deleteArticle()">刪除</button></span>
</header>

當點擊按鈕時透過點擊事件,觸發 ArticleHeader 內的 deleteArticle 方法。
因此我們必須實作 deleteArticle 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';

@Component({
selector: 'app-article-header',
templateUrl: './article-header.component.html',
styleUrls: ['./article-header.component.scss']
})
export class ArticleHeaderComponent implements OnInit {
@Input()
item;

@Output()
delete = new EventEmitter<any>();
constructor() { }

ngOnInit() {
}

deleteArticle(){
this.delete.emit(this.item);
}
}

deleteArticle 方法被觸發時,透過 delete.emit() 這個事件發射器,將 this.item 向父元件射出。

而父元件上的 $event 接收的資料就會是剛才被射出的 this.item

實作 doDelete 方法

回到 ArticleList 的 class 內,實作刪除文章。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { Component, OnInit } from '@angular/core';

@Component({
selector: 'app-article-list',
templateUrl: './article-list.component.html',
styleUrls: ['./article-list.component.scss']
})
export class ArticleListComponent implements OnInit {
atticleData: Array<any>;
constructor() { }
ngOnInit() {
/* tslint:disable */
this.atticleData = [
{
"id": 1,
"href": "http://blog.miniasp.com/post/2016/04/30/Visual-Studio-Code-from-Command-Prompt-notes.aspx",
"subject": {
"title": "從命令提示字元中開啟 Visual Studio Code 如何避免顯示惱人的偵錯訊息",
"subtite": "Visual Studio Code"
},
"date": "2016/04/30 18:05",
"author": "GHJKL",
"category": "Visual Studio",
"category-link": "http://blog.miniasp.com/category/Visual-Studio.aspx",
"summary": "<p>由於我的 Visual Studio Code 大部分時候都是在命令提示字元下啟動,所以只要用 <strong><font color='#ff0000' face='Consolas'>code .</font></strong>就可以快速啟動 Visual Studio Code 並自動開啟目前所在資料夾。不過不知道從哪個版本開始,我在啟動 Visual Studio Code 之後,卻開始在原本所在的命令提示字元視窗中出現一堆惱人的偵錯訊息,本篇文章試圖解析這個現象,並提出解決辦法。</p><p>... <a class='more' href='http://blog.miniasp.com/post/2016/04/30/Visual-Studio-Code-from-Command-Prompt-notes.aspx#continue'>繼續閱讀</a>...</p>"
},
...
];
}
doDelete(item){
this.atticleData = this.atticleData.filter((data)=>{
return data.id !== item.id;
})
}
}

因為 atticleData 屬性並沒有明確的型別,所以使用自動完成並沒有提示可以使用什麼方法,因此手動補上 Array<any> 代表這是一個可以傳入任何資料的陣列。

最後透過 ES6 的 filter 方法篩選被刪除的文章,達成本次需求。

搞定~測試看看吧!

把文章全刪除了

小結

可以很明顯的看出,從子元件將資料傳遞給父元件是要透過比較多道手續的,但整體而言也不難理解,就花點時間好好地記住吧。

0%