Проблема
В результате работы с фреймворком Angular, мы декомпозируем наше web-приложение. И по этому у нас возникает ситуация, когда нам нужно передавать данные между компонентами.
@Input()
Что бы передать данные в дочерний компонент, мы можем
использовать декоратор @Input()
. Он позволит нам
передать данные из родительского компонента в дочерний. Рассмотрим
простой пример:
import { Input, Component} from '@angular/core'; @Component({ selector: 'app-child', template: `<h1>Title: {{ title }}</h1>`})export class ChildComponent { @Input() title: string;}
В дочернем компоненте мы мы "задекорировали" нужное нам свойство
title
. Не забываем импортировать декоратор:
import { Input} from '@angular/core';
Осталось только передать параметр title
в дочерний
компонент из родительского:
import { Component } from '@angular/core'; @Component({ selector: 'app-component', template: `<app-child [title]="title" [userAge]="age"></app-child>`})export class AppComponent { public title = 'Hello world!';}
Параметры из класса мы передаем с помощью квадратных скобок
[title]="title"
, простую строку мы можем передать и
без использования квадратных скобок title="Hello
world"
. Мы научились передавать параметры из родительского в
дочерний, но что если нам надо сделать все наоборот?
@Output()
Благодаря директиве @Output()
мы можем привязаться
к событиям дочернего компонента. На первый взгляд не очень понятно,
так что давайте рассмотрим пример:
import { Component } from '@angular/core'; @Component({ selector: 'app-counter', template: `<h1>Count: {{ count }}</h1> <app-add (buttonClick)="onAdd()"></app-add>`})export class AppCounter { public count = 0;public onAdd(): void { this.count++;}}
import { Component, EventEmitter, Output } from '@angular/core';@Component({ selector: 'app-add', template: `<button (click)="add()"></button>`;})export class AppAdd { @Output() buttonClick = new EventEmitter();public add(): void { this.buttonClick.emit();}}
Думаю данный код требует некоторых объяснений. При клике на
кнопку в компоненте AppAdd
срабатывает событие
click
, которое вызывает функцию add()
.
Код this.buttonClick.emit() вызовет событие
buttonClick
в компоненте AppCounter
.
Очень важно правильно импортировать EventEmitter
:
import { EventEmitter } from '@angular/core';
Но есть одно "но", мы не передали никакую информацию в родительский компонент. Рассмотрим уже другой вариант в котором мы будем передавать информацию в родительский компонент:
import { Component } from '@angular/core';@Component({ selector: 'app-better-counter', template: `<h1>Count: {{ count }}</h1><app-buttons (buttonClick)="onChange($event)"></app-buttons>`})export class BetterCounterComponent { public count = 0;public onChange(isAdd: boolean): void { if (isAdd) { this.count++; } else { this.count--; } }}
import { Component, EventEmitter, Output } from '@angular/core';@Component({ selector: 'app-buttons', template: `<button (click)="change(true)"></button> <button (click)="change(false)"></button>`})export class ButtonsComponent { @Output() buttonClick = new EventEmitter<boolean>();public change(change: boolean): void { this.buttonClick.emit(change);}}
Давайте рассмотрим список внесенных изменений:
-
Добавили тип передаваемых данных
new EventEmitter<
boolean>()
-
В метод
emit
передали нужную информациюthis.buttonClick.emit(change)
-
Принимаем данные как
$event
в родительском компоненте(buttonClick)="onChange($event)"
@Input()
и @Output()
достаточно
удобно, но не в ситуации, когда на надо передать данные в дочерний
компонент, дочернего компонента и т.д., или же компоненты находятся
в разных частях приложения.
Сервисы и RxJs
Одними из лучших вариантов обмена данных остаются сервисы. Создадим простой сервис который бы мог оповещать компоненты про изменение данных, а так же передавать значения:
import { Injectable } from '@angular/core';import { Subject } from 'rxjs';@Injectable({ providedIn: 'root',})export class SimpleService { public count$ = new Subject<number>();public changeCount(count: number) { this.count$.next(count); }}
Наш сервис готов. В нём мы создадим переменную
count$
. Знак доллара - это договорённость между
программистами в обозначениях потоков. Теперь простыми словами про
Subject
. Subject
- это труба, по которой
мы можем передавать данные. Данные получают компоненты, которые
оформили подписку на Subject
. Давайте посмотрим, как
изменять count
из компонента:
import { SimpleService } from './services/simple.service.ts';@Component({ selector: 'app-any', template: ``})export class AnyComponentComponent { constructor( private readonly simpleService: SimpleService ) {} public setAnyCount(): void { this.simpleService.changeCount(Math.random());}}
Мы передали результат Math.random()
и пустили его
по всем подписчикам. Теперь посмотрим как следить за этими
изменениями:
import { Component, OnInit } from '@angular/core';import { SimpleService } from './services/simple.service.ts';@Component({ selector: 'app-other', template: ``})export class OtherComponentComponent implements OnInit { constructor( private readonly simpleService: SimpleService ) {} ngOnInit(): void { this.simpleService.count$.subscribe((count) => this.log(count)); } private log(data: number): void { console.log(data); }}
На инициализации мы подписываемся на изменения
count
, и при каждом вызове
count$.next(...)
где-либо сработает функция которую мы
передали в subscribe
. Единственная проблема которая
осталась в коде - утечка памяти. При переходе между страницами
нашего приложения, компонент будет дестроится, а когда он нам снова
понадобится произойдёт повторная инициализация. Старая подписка не
пропала, а новые с каждым разом будут только добавляться. Функция
log()
будет запускаться столько раз, сколько у нас
есть подписок. Если бы мы имели там какой-нибудь сложный
функционал, то пользовать приложения заметил бы снижение
производительности. Этого можно избежать, отписавшись от
count$
на OnDestroy
. Для этого вынесем
подписку в переменную и вызовем у неё метод
unsubscribe()
:
import { Component, OnInit, OnDestroy } from '@angular/core';import { SimpleService } from './services/simple.service.ts';import { Subsription } from 'rxjs';@Component({ selector: 'app-other', template: ``})export class OtherComponentComponent implements OnInit, OnDestroy { private subs: Subsription; constructor( private readonly simpleService: SimpleService ) {} ngOnInit(): void { this.subs = this.simpleService.count$.subscribe((count) => this.log(count)); } ngOnDestroy(): void { this.subs.unsubscribe();} private log(data: number): void { console.log(data); }}
Мы можем подписаться на множество Subject из компонента, подписаться на один и тот же Subject из разных компонентов.
Итог
Мы можем обмениваться данными между компонентов с помощью
@Input()
, @Output()
, а также RxJs. В
данной статье я опустил store
, так как статья
рассчитана на новичков. Советую попрактиковаться в данной теме, что
бы улучшить свои навыки.