Icednut's Note

Angular 2 - 사례로 살펴보는 @Input과 @Output의 이해 2

2016-12-09

Sample App Sample App

문제

1
2
3
4
5
6
7
8
9
10
11
12
.
├── first-tab
│   ├── first-tab.component.css
│   ├── first-tab.component.html
│   └── first-tab.component.ts
├── second-tab
│   ├── second-tab.component.css
│   ├── second-tab.component.html
│   └── second-tab.component.ts
├── tab-menu.component.css
├── tab-menu.component.html
└── tab-menu.component.ts
  • 스크린샷에 보이는 것 처럼 한 페이지에 Tab으로 구분된 Form이 구성되어 있다. 제출하기 버튼 클릭했을 때 모든 탭의 Form 데이터
    를 얻어오려면 어떻게 해야될까?

풀이

  • 위 상황만 따지고 볼 때 핵심은 제출하기 버튼 클릭 시 모든 탭에 있는 Form 데이터를 가져오도록 코드를 작성하면 될 것 같다.
  • 아래와 같이 Tab들은 자식 컴포넌트로 구성이 될텐데 부모 컴포넌트에서 자식 컴포넌트의 Form 데이터는 어떻게 가져올 것인가?
tab-menu.component.html
1
2
3
4
5
6
7
8
9
10
<div>
<tabset class="container">
<tab heading="1단계">
<first-tab></first-tab>
</tab>
<tab heading="2단계">
<second-tab></second-tab>
</tab>
</tabset>
</div>
  • 문제의 핵심은 부모 컴포넌트(tab-menu.component)에서 자식 컴포넌트(first-tab.component & second-tab.component)의 값을 언제 어떻게 가져올 것인가로 압축할 수 있다.

부모 컴포넌트에서 자식 컴포넌트의 값을 가져오는 방법 2가지

  • 방법 1. 모델 공유
  • 방법 2. 클로저 이벤트 사용

일단 위 2가지 방법이 그나마 효율적인 방법이라고 생각한다. (위 2가지 방법 말고도 다른 방법이 있겠지만…) 먼저 첫 번째 방법 부터 살펴보자.

방법 1. 모델 공유 - 부모 컴포넌트에 자식 컴포넌트들의 Form 모델을 선언하는 방법

말 그대로 부모 컴포넌트에서 자식 컴포넌트들의 Form 데이터가 담긴 모델을 갖고 있도록 정의하는 방법이다. 코드로 살펴보면 다음과 같다.

1) 부모 컴포넌트(tab-menu.component.ts)에서 자식 컴포넌트의 데이터를 받을 모델 멤버변수를 선언한다.

  • 여기서 firstTabForm과 secondTabForm에 대한 부모컴포넌트의 멤버 필드를 선언하고 인스턴스화 한다.
  • 이렇게 인스턴스화 된 멤버들을 자식들에게 공유하는 방식이다.
tab-menu.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Component, OnInit } from '@angular/core';
import { FirstTabForm } from './firstTabForm';
import { SecondTabForm } from './secondTabForm';
@Component({
selector: 'tab-menu',
templateUrl: './tab-menu.component.html',
styleUrls: ['./tab-menu.component.css']
})
export class TabMenuComponent implements OnInit {
firstTabForm: FirstTabForm = new FirstTabForm();
secondTabForm: SecondTabForm = new SecondTabForm();
ngOnInit() {
}
}

2) 부모 컴포넌트의 멤버 변수를 자식 컴포넌트로 전달한다.

  • 전달할 때는 아래와 같이 자식 컴포넌트의 @Input 멤버 필드에 전달한다.
tab-menu.component.html
1
2
3
4
5
6
7
8
9
10
<div>
<tabset class="container">
<tab heading="1단계">
<first-tab [childForm]="firstTabForm"></first-tab>
</tab>
<tab heading="2단계">
<second-tab [childForm]="secondTabForm"></second-tab>
</tab>
</tabset>
</div>
first-tab.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Component, OnInit, Input } from '@angular/core';
import { FirstTabForm } from '../firstTabForm';
@Component({
selector: 'first-tab',
templateUrl: './first-tab.component.html',
styleUrls: ['./first-tab.component.css']
})
export class FirstTabComponent implements OnInit {
@Input()
childForm: FirstTabForm;
constructor() { }
ngOnInit() {
}
}
second-tab.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Component, OnInit, Input } from '@angular/core';
import { SecondTabForm } from '../secondTabForm';
@Component({
selector: 'second-tab',
templateUrl: './second-tab.component.html',
styleUrls: ['./second-tab.component.css']
})
export class SecondTabComponent implements OnInit {
@Input()
childForm: SecondTabForm;
constructor() { }
ngOnInit() {
}
}

3) 자식 컴포넌트의 템플릿에 선언되어 있는 input, select 등 입출력을 담당하는 엘리먼트에 양방향 바인딩을 걸어준다.

  • 사실 input 바인딩만 걸어줘도 되는데 추후 저장 후 수정과 같은 페이지를 만들 것을 대비하여 양방향 바인딩을 걸어준다. (이게 비효율적으로 보이면 단방향 바인딩만 해줘도 된다.)
first-tab.component.html
1
2
3
4
5
6
7
8
9
<div class="form-group">
<input type="text" class="form-control" placeholder="이름(Name)" [(ngModel)]="childForm.name">
</div>
<div class="form-group">
<input type="email" class="form-control" placeholder="이메일(Email)" [(ngModel)]="childForm.email">
</div>
<div class="form-group">
<input type="text" class="form-control" placeholder="주소(Address)" [(ngModel)]="childForm.address">
</div>
second-tab.component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div class="form-group">
<label for="personCount">예약 인원</label>
<select class="form-control" id="personCount" [(ngModel)]="childForm.count">
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
<option>5</option>
</select>
</div>
<div class="form-group">
<label for="groupName">모임명</label>
<input type="email" class="form-control" id="groupName" [(ngModel)]="childForm.groupName">
</div>

4) 부모 컴포넌트로 데이터가 제대로 들어오는지 확인

  • 부모 컴포넌트 쪽에서 데이터가 제대로 들어왔는지 디버깅을 해보자.
tab-menu.component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div>
<tabset class="container">
<tab heading="1단계">
<first-tab [childForm]="firstTabForm"></first-tab>
</tab>
<tab heading="2단계">
<second-tab [childForm]="secondTabForm"></second-tab>
</tab>
</tabset>
<div>
<button class="btn btn-primary" type="button" style="width:140px;" (click)="saveRequest()">제출하기</button>
<button class="btn btn-info" type="button" style="width:140px;">초기화</button>
</div>
</div>
tab-menu.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
@Component({
selector: 'tab-menu',
templateUrl: './tab-menu.component.html',
styleUrls: ['./tab-menu.component.css']
})
export class TabMenuComponent implements OnInit {
...
saveRequest() {
console.log(`${this.firstTabForm.name} : ${this.firstTabForm.email} : ${this.firstTabForm.address}`);
console.log(`${this.secondTabForm.count} : ${this.secondTabForm.groupName}`);
}
}

방법 1 전체 소스
https://github.com/icednut/angular2-exercise/tree/master

2. 클로저 이벤트 사용 - 자식 컴포넌트에서 Form 데이터 셋팅 시 부모 컴포넌트에 이벤트 emit 시 Form 데이터 셋팅 클로저를 넘기는 방법

부모 컴포넌트 페이지에서 제출하기버튼 클릭 시 자식 컴포넌트들에게 이벤트를 발생하는데 이 때 내보내는 이벤트 타입을 클로저로 지정하는 방법이다. 말로 들으면 이해가 안갈지도 모르는데 코드를 살펴보면 다음과 같다.

1) 제출하기 버튼 클릭 핸들러 추가

tab-menu.component.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div>
<tabset class="container">
<tab heading="1단계">
<first-tab></first-tab>
</tab>
<tab heading="2단계">
<second-tab></second-tab>
</tab>
</tabset>
<div>
<button class="btn btn-primary" type="button" (click)="saveRequest()">제출하기</button>
<button class="btn btn-info" type="button">초기화</button>
</div>
</div>

tab-menu.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'tab-menu',
templateUrl: './tab-menu.component.html',
styleUrls: ['./tab-menu.component.css']
})
export class TabMenuComponent implements OnInit {
...
ngOnInit() {
}
saveRequest() {
// TODO 1: 여기서 first-tab 컴포넌트와 second-tab 컴포넌트에 Form 데이터를 가져올 이벤트를 발생(emit)한다.
// TODO 2: 자식 컴포넌트들(first-tab, second-tab)에서는 부모 컴포넌트(tab-menu)로 Form 데이터를 전달한다.
}
}

2) 부모 컴포넌트의 멤버 필드로 데이터를 셋팅하는 로직이 담긴 클로저 이벤트를 발생한다.

tab-menu.component.ts
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
36
import { Component, OnInit } from '@angular/core';
import { FirstTabForm } from './firstTabForm';
import { SecondTabForm } from './secondTabForm';
@Component({
selector: 'tab-menu',
templateUrl: './tab-menu.component.html',
styleUrls: ['./tab-menu.component.css']
})
export class TabMenuComponent implements OnInit {
...
firstTabForm: FirstForm;
secondTabForm: SecondForm;
firstTabFormSelectEvent: EventEmitter<any> = new EventEmitter<any>();
secondTabFormSelectEvent: EventEmitter<any> = new EventEmitter<any>();
ngOnInit() {
}
saveRequest() {
// TODO 1: 여기서 first-tab 컴포넌트와 second-tab 컴포넌트에 Form 데이터를 가져올 이벤트를 발생(emit)한다.
this.firstTabFormSelectEvent.emit(it => {
this.firstTabForm = it;
console.log(`${this.firstTabForm.name} : ${this.firstTabForm.email} : ${this.firstTabForm.address}`);
});
this.secondTabFormSelectEvent.emit(it => {
this.secondTabForm = it;
console.log(`${this.secondTabForm.count} : ${this.secondTabForm.groupName}`);
}); // emit의 파라미터를 자세히 살펴보면 클로저를 넘기고 있다.
// TODO 2: 자식 컴포넌트들(first-tab, second-tab)에서는 부모 컴포넌트(tab-menu)로 Form 데이터를 전달한다.
}
}

3) 이벤트를 자식컴포넌트에서 subscribe 하기 위해서 서비스 클래스(formService.ts)로 따로 뺀다.

tab-menu.component.ts
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
import { Component, OnInit, Output, EventEmitter } from '@angular/core';
import { FirstTabForm } from './firstTabForm';
import { SecondTabForm } from './secondTabForm';
import { FormService } from './formService';
@Component({
selector: 'tab-menu',
templateUrl: './tab-menu.component.html',
styleUrls: ['./tab-menu.component.css']
})
export class TabMenuComponent implements OnInit {
firstTabForm: FirstTabForm;
secondTabForm: SecondTabForm;
constructor(
private formService: FormService
) { }
ngOnInit() {
}
saveRequest() {
this.formService.selectFirstForm(it => {
this.firstTabForm = it;
console.log(`${this.firstTabForm.name} : ${this.firstTabForm.email} : ${this.firstTabForm.address}`);
});
this.formService.selectSecondForm(it => {
this.secondTabForm = it;
console.log(`${this.secondTabForm.count} : ${this.secondTabForm.groupName}`);
});
}
}

formService.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { Injectable, EventEmitter } from '@angular/core';
@Injectable()
export class FormService {
firstTabFormSelectEvent: EventEmitter<any> = new EventEmitter<any>();
secondTabFormSelectEvent: EventEmitter<any> = new EventEmitter<any>();
selectFirstForm(event: any) {
this.firstTabFormSelectEvent.emit(event);
}
selectSecondForm(event: any) {
this.secondTabFormSelectEvent.emit(event);
}
}

4) 자식 컴포넌트에서는 해당 이벤트를 subscribe 하는데 이 때 자식 컴포넌트의 데이터를 넘기기 위해 받은 이벤트를 실행한다.

first-tab.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Component, OnInit, Input } from '@angular/core';
import { FirstTabForm } from '../firstTabForm';
import { FormService } from '../formService';
@Component({
selector: 'first-tab',
templateUrl: './first-tab.component.html',
styleUrls: ['./first-tab.component.css']
})
export class FirstTabComponent implements OnInit {
@Input()
childForm: FirstTabForm = new FirstTabForm();
constructor(private formService: FormService) { }
ngOnInit() {
this.formService.onSelectFirstForm(eventBody => {
eventBody(this.childForm);
});
}
}

formService.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Injectable, EventEmitter } from '@angular/core';
@Injectable()
export class FormService {
firstTabFormSelectEvent: EventEmitter<any> = new EventEmitter<any>();
secondTabFormSelectEvent: EventEmitter<any> = new EventEmitter<any>();
selectFirstForm(event: any) {
this.firstTabFormSelectEvent.emit(event);
}
selectSecondForm(event: any) {
this.secondTabFormSelectEvent.emit(event);
}
onSelectFirstForm(subscribeFunc: any) {
this.firstTabFormSelectEvent.subscribe(subscribeFunc);
}
onSelectSecondForm(subscribeFunc: any) {
this.secondTabFormSelectEvent.subscribe(subscribeFunc);
}
}

방법 2 전체 소스
https://github.com/icednut/angular2-exercise/tree/closure_event

방법1, 방법2 말고도 더 좋은 방법이 있겠지만 이번 포스트는 여기서 마무리!

Tags: angular2 input output