Wiemy już w jaki sposób tworzyć podstawowe komponenty i wykorzystywać je do budowania naszych aplikacji w Angular 2. Dzisiaj omówimy pierwszy ze sposobów umożliwiający prostą komunikację pomiędzy nimi – właściwości oznaczone dekoratorem @Input()
oraz @Output()
.
Dane wejściowe @Input()
W Angular 2 komponent rodzica ma możliwość przekazania do dziecka danych, które mogą determinować zachowanie komponentu lub w ogóle – pozwolić na jego odpowiednie wyrenderowanie. Jest to możliwe dzięki adnotacji @Input()
, a jej wykorzystanie jest banalnie proste:
@Component({ selector: 'simple-component' })
export class SimpleComponent {
@Input() title : string;
}
Teraz przekazanie instancji komponentu SimpleComponent
parametru title
ogranicza się do poniższego zapisu:
<simple-component title="Example"></simple-component>
Oczywiście, nic nie stoi na przeszkodzie, aby nasze parametry miały określoną wartość domyślną:
@Input() title = "Default title";
Zdarzenia @Output()
Wiemy już w jaki sposób przekazać dane z rodzica do dziecka. Adnotacja @Output()
pozwala nam osiągnąć odwrotny rezultat – przekazać wartości od dziecka do rodzica – przy pomocy zdarzenia. Przykład:
@Output() titleChanged = new EventEmitter<string>();
Warto podkreślić, że EventEmitter
jest klasą generyczną – szablonem. Powyżej określiliśmy, że parametr jaki będzie przekazywał w postaci argumentu, będzie typu string
. Teraz, aby przekazać metodę, która wywoła się przy emisji zdarzenia, wystarczy poniższy zapis:
<simple-component (titleChanged)="titleChanged($event)"></simple-component>
Aby wyemitować zdarzenie, musimy wywołać metodę emit()
obiektu titleChanged
:
this.titleChanged.emit(newTitle);
Poskutkuje to wywołaniem funkcji titleChanged
rodzica i przekazaniem mu w postaci argumentu – nowego tytułu.
Praktyka, nie teoria
Myślę, że to wystarczający wstęp teoretyczny – czas zająć się praktyką. W ramach ćwiczeń stworzymy prostą aplikację w stylu Todo, która będzie pozwalała na tworzenie nowych zadań oraz usuwanie już istniejących. Po raz kolejny – aby maksymalnie ograniczyć czas poświęcony na style CSS – wykorzystamy bibliotekę Materialize. Końcowy rezultat powinien być taki, jak na poniższym obrazku:
Rozpoczniemy od komponentu, który będzie reprezentował pojedyncze zadanie – TodoItemComponent
. Niech na pojedyncze zadanie składa się nazwa oraz status. Dla wygody wygenerujemy sobie enumerator TodoStatus
:
ng g enum todo-status
export enum TodoStatus {
TODO,
IN_PROGRESS,
IN_REVIEW,
BUG,
DONE
}
Oraz interfejs – Todo
, wymuszający na obiektach posiadanie odpowiednich składowych:
ng g interface todo
export interface Todo {
name: string,
status: TodoStatus
}
Nasz komponent powinien otrzymać od rodzica obiekt spełniający interfejs Todo
oraz metodę, która będzie wywoływana podczas usuwania zadania. Dodatkowo w zależności od statusu, powinien dobierać odpowiednią klasę CSS oraz wyświetlać prawidłowy tekst. Otwieramy plik todo-item.component.ts
i realizujemy postawione założenia:
@Component({
selector: 'todo-item',
templateUrl: './todo-item.component.html',
styleUrls: ['./todo-item.component.css']
})
export class TodoItemComponent {
@Output() removeTodo = new EventEmitter<Todo>();
@Input() item: Todo;
get statusName(): string {
const { status } = this.item;
switch(status) {
case TodoStatus.DONE: return "DONE";
case TodoStatus.BUG: return "BUG";
case TodoStatus.IN_PROGRESS: return "IN PROGRESS";
case TodoStatus.IN_REVIEW: return "IN REVIEW";
default: return "TODO";
}
}
get classNames() {
const statusName = this.statusName.split(" ").pop().toLowerCase();
return {
'status': true,
[`status-${statusName}`]: true
}
}
public remove(): void {
this.removeTodo.emit(this.item);
}
}
Widzimy, że ze składowych @Input()
i @Output()
korzystamy tak samo, jak gdybyśmy posługiwali się zwykłymi polami klasy. Pozostaje nam szablon komponentu:
<h5>{{ item.name }}</h5>
<span [ngClass]="classNames">{{ statusName }}</span>
<i class="material-icons" (click)="remove()">close</i>
To wszystko – nasz pierwszy komponent jest gotowy. Przechodzimy do formularza, dzięki któremu będziemy w stanie dodawać nowe zadania do kolekcji. Komponent powinien emitować zdarzenia addTodo()
i przekazywać w postaci argumentu – nowy obiekt spełniający interfejs Todo
– jeżeli pole Name
nie pozostało puste.
@Component({
selector: 'add-todo',
templateUrl: './add-todo.component.html',
styleUrls: ['./add-todo.component.css']
})
export class AddTodoComponent {
@Output() addTodo = new EventEmitter<Todo>();
todo: Todo;
constructor() {
this.setInitial();
}
public add(): void {
const { name, status } = this.todo;
if(name.length > 0) {
this.todo.status = +status;
this.addTodo.emit(this.todo);
this.setInitial();
}
}
private setInitial(): void {
this.todo = {
name: "",
status: TodoStatus.TODO
}
}
}
Pola input
oraz select
powiążemy z obiektem todo
przy pomocy dwustronnego wiązania, które osiągniemy wykorzystując [(ngModel)]
(jeżeli zagadnienie bindingu jest Ci obce, zerknij tutaj). Kod szablonu komponentu:
<div class="card">
<div class="row">
<div class="form-control col s8">
<input placeholder="Name" [(ngModel)]="todo.name" type="text" class="validate">
</div>
<div class="form-control col s8">
<select [(ngModel)]="todo.status">
<option value="0">TODO</option>
<option value="1">IN PROGRESS</option>
<option value="2">IN REVIEW</option>
<option value="3">BUG</option>
<option value="4">DONE</option>
</select>
</div>
<div class="form-control col s8">
<button type="button" class="btn red darken-1" (click)="add()">Add item</button>
</div>
</div>
</div>
Ostatnia prosta
Nasze komponenty są już gotowe. Pozostało nam złożyć to wszystko w całość i odpowiednio reagować na emitowane zdarzenia. Nasz root component
powinien posiadać listę zadań Todo
oraz implementację dwóch, głównych metod, których zadaniem jest dodawanie lub usuwanie obiektów z kolekcji.
Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
public todos: Todo[];
constructor() {
this.todos = [];
}
public addTodo(todo: Todo) {
this.todos.unshift(todo);
}
public removeTodo(todo: Todo) {
this.todos = this.todos.filter(item => item !== todo);
}
}
To wszystko. Pozostaje szablon, w którym jako rodzic przekażemy dzieciom odpowiednie dane wejściowe i podepniemy pod emitowane zdarzenia nasze implementacje metod addTodo()
i removeTodo()
.
<div class="container">
<h2>Todo App</h2>
<add-todo (addTodo)="addTodo($event)"></add-todo>
<div class="todo-list">
<ul class="collection">
<li *ngFor="let todo of todos" class="collection-item">
<todo-item [item]="todo" (removeTodo)="removeTodo($event)"></todo-item>
</li>
</ul>
</div>
</div>
Nasza prosta aplikacja powinna już funkcjonować zgodnie z założeniami.
Podsumowanie
Celem artykułu było zaprezentowanie podstawowej możliwości komunikacji pomiędzy komponentami w konfiguracji rodzic – dziecko. Jest to jeden z kilku sposobów i sprawdza się w najprostszych przypadkach. W kolejnych artykułach zajmiemy się pozostałymi rozwiązaniami – referencją lokalną, shared service i koncepcją znaną przede wszystkim programistom Reacta – Redux’em.
Pełny kod źródłowy dostępny na GitHub.