Angular 튜터리얼 - Master/Detail

Master/Detail

이 페이지에서는 Tour of Heroes 앱을 확장하여 Heroes 목록을 표시하고 사용자가 Hero를 선택하고 Hero의 세부 정보를 표시하도록 합니다.

이 페이지를 끝내면 앱은 이 라이브 예제/예제 다운로드처럼 보일 것입니다.

어디까지 했나?

Tour of Heroes 페이지를 계속하기 전에 The Hero Editor 이후에 아래와 같은 구조인지 확인하십시오. 구조가 일치하지 않으면 그 페이지로 돌아가서 무엇을 놓쳤는지 파악하십시오.

1
2
3
4
5
6
7
8
9
10
11
12
angular-tour-of-heroes
┣ src
┃ ┣ app
┃ ┃ ┣ app.component.ts
┃ ┃ ┗ app.module.ts
┃ ┣ main.ts
┃ ┣ index.html
┃ ┣ styles.css
┃ ┣ systemjs.config.js
┃ ┗ tsconfig.json
┣ node_modules ...
┗ package.json

앱 실행과 트랜스파일을 유지하기

터미널 창에서 다음 명령을 입력하십시오.

1
npm start

이 명령은 TypeScript 컴파일러를 “watch mode”에서 실행하여 코드가 변경되면 자동으로 다시 컴파일 하도록합니다. 또한 이 명령은 앱이 브라우저에서 동시에 실행되고 코드가 변경되면 브라우저를 새로 고칩니다.

다시 컴파일하거나 새로 고치지 않고 브라우저를 일시 정지하지 않아 Tour of Heroes 앱을 계속 만들 수 있습니다.

Hero들 표시하기

Heroes의 목록을 표시하기 위해 Hero 템플릿 view를 추가합니다.

Hero들 생성

10 명의 Hero로 구성된 배열을 만듭니다.

src/app/app.component.ts (hero array)

1
2
3
4
5
6
7
8
9
10
11
12
const HEROES: Hero[] = [
{ id: 11, name: 'Mr. Nice' },
{ id: 12, name: 'Narco' },
{ id: 13, name: 'Bombasto' },
{ id: 14, name: 'Celeritas' },
{ id: 15, name: 'Magneta' },
{ id: 16, name: 'RubberMan' },
{ id: 17, name: 'Dynama' },
{ id: 18, name: 'Dr IQ' },
{ id: 19, name: 'Magma' },
{ id: 20, name: 'Tornado' }
];

HEROES 배열은 이전 페이지에서 정의된 Hero 타입입니다. 나중에는 앱이 웹 서비스에서 Hero들의 목록을 가져 오지만 지금은 Mock Hero를 표시 합니다.

Hero들 노출

AppComponent에 Hero들을 노출하는 바인딩을 위해 public 프로퍼티를 만듭니다.

app.component.ts (hero array property)

1
heroes = HEROES;

TypeScript는 HEROES의 array에서 유추하기 때문에 heroes 타입은 따로 정의하지 않습니다.

궁극적으로 Hero의 이름은 데이터 서비스에서 나올 것이므로 Hero 데이터는 클래스 구현과 분리됩니다.

Template에 Hero 이름 표시하기

정렬되지 않은 목록에 Hero 이름을 표시하려면 title 아래, detail 위에 다음 HTML을 삽입합니다.

app.component.ts (heroes template)

1
2
3
4
5
6
<h2>My Heroes</h2>
<ul class="heroes">
<li>
<!-- each hero goes here -->
</li>
</ul>

이제 Hero 이름으로 템플릿을 채울 수 있습니다.

ngFor를 이용한 Hero들 목록

목표는 Component의 Hero 배열을 템플릿에 바인딩하고, 반복하여 개별적으로 표시하는 것입니다.

내장 Directive *ngFor를 추가하여 <li>태그를 수정합니다.

app.component.ts (ngFor)

1
<li *ngFor="let hero of heroes">

ngFor의 접두사(*)는 이 구문에서 중요한 부분입니다. <li> 요소와 그 하위 요소가 master 템플릿을 구성함을 나타냅니다.

ngFor DirectiveComponentheroes 배열을 반복하고 그 배열의 각 Hero에 대해 이 템플릿 인스턴스를 렌더링합니다.

표현식의 let hero 부분은 hero를 템플릿 입력 변수로 식별합니다. 이 변수는 각 반복마다 현재 Hero 아이템을 보유합니다. 템플릿내에서 이 변수를 참조하여 현재 Hero의 프로퍼티에 액세스할 수 있습니다.

ngFor 및 템플릿 입력 변수에 대한 자세한 내용은 데이터 표시 페이지의 배열 속성 표시와 템플릿 구문 페이지의 *ngFor 섹션과, 템플릿 구문 페이지의 ngFor 섹션을 참조하십시오.

<li>태그 안에 hero 템플릿 변수를 사용하여 Hero의 프로퍼티를 나타내는 내용을 추가합니다.

app.component.ts (ngFor template)

1
2
3
<li *ngFor="let hero of heroes">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>

브라우저가 새로 고침되면 Heroes의 목록이 나타납니다.

Hero들 스타일

사용자에게 그들이 Hovering하는 Hero와 선택한 Hero를 시각적으로 표현해야 합니다.

Component에 스타일을 추가하기 위해 @Component Decoratorstyles 프로퍼티를 다음의 CSS 클래스로 설정합니다.

src/app/app.component.ts (styles)

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
37
38
39
40
41
42
43
44
45
46
47
48
49
styles: [`
.selected {
background-color: #CFD8DC !important;
color: white;
}
.heroes {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 15em;
}
.heroes li {
cursor: pointer;
position: relative;
left: 0;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
}
.heroes li.selected:hover {
background-color: #BBD8DC !important;
color: white;
}
.heroes li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.heroes .text {
position: relative;
top: -3px;
}
.heroes .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color: #607D8B;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
`]

여러줄 문자열을 위해 백틱 표기법을 사용해야합니다.

이러한 스타일은 별도의 파일에 추가하여 사용하면 훨씬 유용합니다. 이후 페이지에서 스타일을 별도의 파일로 옮깁니다.

Component에 스타일을 지정하면 특정 Component로 범위가 지정됩니다. 이 스타일은 AppComponent에만 적용되며 외부 HTML에는 영향을 미치지 않습니다.

Hero들을 표시하기 위한 템플릿은 다음과 같습니다.

src/app/app.component.ts (styled heroes)

1
2
3
4
5
6
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>

Hero 선택하기

이제 앱에서 Heroes 목록과 상세 보기에 한명의 Hero를 표시합니다. 하지만 목록과 세부정보 보기는 연결되어 있지 않습니다. 사용자가 목록에서 Hero를 선택하면 선택한 Hero가 세부정보 보기에 나타나야 합니다. 이 UI 패턴을 “master/detail”이라고 합니다. 이 경우, master는 Heroes 목록이고 detail은 선택된 Hero입니다.

다음으로 click 이벤트에 바인딩된 selectedHero Component 프로퍼티를 통해 마스터를 세부 정보에 연결합니다.

Click 이벤트 핸들

다음과 같이 <li> 클릭 이벤트 바인딩을 추가합니다.

app.component.ts (template excerpt)

1
2
3
<li *ngFor="let hero of heroes" (click)="onSelect(hero)">
...
</li>

괄호는 <li>요소의 클릭 이벤트를 대상으로 식별합니다. onSelect(hero) 표현식은 AppComponent 메소드인 onSelect()를 호출하여 템플릿 입력 변수 hero를 파라미터로 전달합니다. 이것은 이전에 ngFor Directive에서 정의한 것과 동일한 hero 변수입니다.

사용자 입력 페이지 및 템플릿 구문 페이지의 이벤트 바인딩 섹션에서 이벤트 바인딩에 대해 자세히 알수 있습니다.

선택된 Hero를 노출하는 Click 핸들러 추가

더이상 한명의 Hero만 표시하지 않으므로 hero 프로퍼티가 필요하지 않습니다. 당신은 Hero들의 목록을 보여주고 있습니다. 그러나 사용자는 Hero중 하나를 클릭하여 선택할 수 있습니다. 그러므로 hero 프로퍼티를 다음과 같은 간단한 selectedHero 프로퍼티로 대체합니다.

src/app/app.component.ts (selectedHero)

1
selectedHero: Hero;

사용자가 Hero를 선택하기 전에 Hero의 이름을 모두 선택 해제 해야하므로 hero와 마찬가지로 selectedHero도 초기화할 필요 없습니다.

selectedHero 프로퍼티를 사용자가 클릭하는 hero로 설정하는 onSelect() 메서드를 추가합니다.

src/app/app.component.ts (onSelect)

1
2
3
onSelect(hero: Hero): void {
this.selectedHero = hero;
}

템플릿은 여전히 오래된 hero 프로퍼티를 참조합니다. 다음과 같이 새로운 selectedHero 프로퍼티에 바인딩합니다.

app.component.ts (template excerpt)

1
2
3
4
5
6
<h2>{{selectedHero.name}} details!</h2>
<div><label>id: </label>{{selectedHero.id}}</div>
<div>
<label>name: </label>
<input [(ngModel)]="selectedHero.name" placeholder="name"/>
</div>

ngIf를 이용해 빈 상세보기 숨기기

앱이 로드되면 selectedHeroundefined입니다.. selectedHero는 사용자가 Hero의 이름을 클릭할 때 초기화됩니다. Angular는 undefinedselectedHero의 프로퍼티를 표시할 수 없으며 다음 오류를 브라우저의 콘솔에 표시합니다.

1
EXCEPTION: TypeError: Cannot read property 'name' of undefined in [null]

템플릿에 selectedHero.name이 표시 되더라도 선택한 Hero가 있을 때까지 Hero 상세 보기를 DOM 밖에서 유지해야합니다.

템플릿의 HTML Hero 상세 보기를 <div>로 묶습니다. 그런 다음 내장 ngIf Directive를 추가하고 ComponentselectedHero 프로퍼티를 설정합니다.

src/app/app.component.ts (ngIf)

1
2
3
4
5
6
7
8
<div *ngIf="selectedHero">
<h2>{{selectedHero.name}} details!</h2>
<div><label>id: </label>{{selectedHero.id}}</div>
<div>
<label>name: </label>
<input [(ngModel)]="selectedHero.name" placeholder="name"/>
</div>
</div>

ngIf 앞에 별표 (*)를 잊지 마십시오.

앱은 더 이상 오류가 발생하지 않고 브라우저에 Hero들의 이름 목록이 다시 표시됩니다.

선택된 Hero가 없을 때 ngIf Directive는 Hero 상세 보기 HTML을 DOM에서 제거합니다. 문제가될 Hero 상세 보기나 바인딩이 없습니다.

사용자가 Hero를 선택하면 selectedHero가 정의되고 ngIf는 Hero 상세 보기 내용을 DOM에 넣고 중첩된 바인딩을 평가합니다.

ngIfngFor에 대한 자세한 내용은 Directive 구조 페이지 및 템플릿 구문 페이지의 기본 제공 Directive 섹션을 참조하십시오.

선택된 Hero의 스타일

선택한 Hero의 세부 정보가 목록 아래에 표시되지만 목록 내에서 선택한 Hero를 식별하는 것은 어렵습니다.

위에서 추가 한 styles 메타 데이터에는 selected라는 사용자 정의 CSS 클래스가 있습니다. 선택한 Hero를 더 잘 보이게 하려면 사용자가 Hero 이름을 클릭하면 이 선택된 클래스를 <li>에 적용합니다. 예를 들어, 사용자가 “Magneta”를 클릭하면 다음과 같이 독특하지만 미묘한 배경색으로 렌더링해야합니다.

템플릿에서 다음 [class.selected] 바인딩을 <li>에 추가합니다.

app.component.ts (setting the CSS class)

1
[class.selected]="hero === selectedHero"

표현식 (hero === selectedHero)가 true 일 때, Angular는 selected CSS 클래스를 추가합니다. 그리고 표현식이 false 일 때 Angular는 selected 클래스를 제거합니다.

템플릿 구문 가이드에서 [class] 바인딩에 대해 자세히 읽어보십시오.

<li>의 최종 버전은 다음과 같습니다.

app.component.ts (styling each hero)

1
2
3
4
5
<li *ngFor="let hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>

“Magneta”를 클릭하면 목록이 다음과 같이 표시됩니다.

다음은 현재의 app.component.ts입니다.

src/app/app.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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { Component } from '@angular/core';
export class Hero {
id: number;
name: string;
}
const HEROES: Hero[] = [
{ id: 11, name: 'Mr. Nice' },
{ id: 12, name: 'Narco' },
{ id: 13, name: 'Bombasto' },
{ id: 14, name: 'Celeritas' },
{ id: 15, name: 'Magneta' },
{ id: 16, name: 'RubberMan' },
{ id: 17, name: 'Dynama' },
{ id: 18, name: 'Dr IQ' },
{ id: 19, name: 'Magma' },
{ id: 20, name: 'Tornado' }
];
@Component({
selector: 'my-app',
template: `
<h1>{{title}}</h1>
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
<div *ngIf="selectedHero">
<h2>{{selectedHero.name}} details!</h2>
<div><label>id: </label>{{selectedHero.id}}</div>
<div>
<label>name: </label>
<input [(ngModel)]="selectedHero.name" placeholder="name"/>
</div>
</div>
`,
styles: [`
.selected {
background-color: #CFD8DC !important;
color: white;
}
.heroes {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 15em;
}
.heroes li {
cursor: pointer;
position: relative;
left: 0;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
}
.heroes li.selected:hover {
background-color: #BBD8DC !important;
color: white;
}
.heroes li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.heroes .text {
position: relative;
top: -3px;
}
.heroes .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color: #607D8B;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
`]
})
export class AppComponent {
title = 'Tour of Heroes';
heroes = HEROES;
selectedHero: Hero;
onSelect(hero: Hero): void {
this.selectedHero = hero;
}
}

어디까지 왔나?

이 페이지에서 작업한 내용은 다음과 같습니다.

  • Tour of Heroes 앱은 선택 가능한 Hero의 목록을 표시합니다.
  • Hero를 선택하고 Hero의 상세 보기를 표시하는 기능을 추가했습니다.
  • Component의 템플릿에서 ngIfngFor라는 기본 제공 Directive를 사용하는 방법을 배웠습니다.

앱은 이 라이브 예제/예제 다운로드와 비슷해야합니다.

앞으로의 여정

Tour of Heroes 앱을 확장했지만 아직 완료되지 않았습니다. 일반적인 앱은 하나의 단독 Component를 사용하지 않습니다. 다음 페이지에서는 앱을 하위 Component로 분할하여 함께 사용하게됩니다.


이 내용은 나중에 참고하기 위해 제가 공부하며 정리한 내용입니다.
의역, 오역, 직역이 있을 수 있음을 알려드립니다.
This post is a translation of this original article [https://angular.io/tutorial/toh-pt2]

참고

공유하기