Основная часть работы в Angular выполняется в компонентах. Как минимум у вас должен быть один компонент, который называется корневым. Мы уже создали два компонента в нашем приложении: app.component.ts (он и есть корневой) и all-products.component.ts. Корневой компонент определяется в файле app.module.ts, в свойстве bootstrap декоратора @NgModule

По большому счету можно создать всего один компонент и писать весь код только в нем. Но в средних и больших проектах принято разбивать функциональность на разные компоненты, каждый из которых будет отвечать за какой-то определенный функционал. В нашем приложении для удобства и наглядности был один компонент, который содержит и таблицу с товарами и форму для добавления новых товаров. В этом разделе мы перенесем форму в отдельный компонент.

Компонент это обычный класс, пока к нему не будет применен декоратор @Component() с метаинформацией. Основные метаданные мы рассмотрели в этой главе, не так часто используемые:

  • template — возможно задание шаблона прямо в декораторе. В таком случае не нужно будет создавать отдельный файл для шаблона. Недостаток в том, что для более менее больших шаблонов, даже около 15-20 строк кода, этим вариантом пользоваться неудоно
  • styles — то же, что и с шаблоном — определения стилей прямо в файле компонента.
  • providers — инициализация сервисов, которые будут использоваться в данном компоненте.
  • animations — задание анимации для данного компонента

С полным списком свойств данного декоратора можно ознакомиться в официальной документации.

Также в предыдущих главах мы говорили о том, что есть 2 способа создания компонентов. Один из них — вручную создать файл компонента, файлы шаблона и стилей и затем импортировать и подключить в файле app.module.ts. Если же вы используете в проекте Angular-CLI, то гораздо удобнее и быстрее создавать компоненты с помощью специальной команды: ng g c component-name --spec false. В таком случае Angular за нас создаст необходимые файлы и подключит их в модуле.

Подготовка проекта

Вы можете скачать уже готовый проект с изменениями в шаблоне тут. Или проделать это самостоятельно. Вкратце опишу что нужно сделать.

  1. Для начала нам нужно создать новый компонент. Создаем папку add-product, в ней запускаем командную строку (правый клик и в контекстном меню выбираем Git Bash Here), запускаем команду ng g c add-product --spec false
  2. Перенести из шаблона компонента all-products.component.html код отвечающий за форму (начиная с div с классами col-md-12 mt-2 add-new-item)
  3. Перенести стили из файла all-products.components.css
  4. Перенести логику из основного файла all-products.components.ts (импорты классов для групп FormControl, FormGroup, Validators, определение переменной form, инициализация формы в ngOnInit, методы checkLength() и onSubmit())

Передача данных между компонентами Angular

Теперь у нас есть 3 компонента:

  1. app — это основной компонент нашего приложения, в котором подключены два других компонента (т.е. это родительский компонент).
  2. all-products — это компонент, который отвечает за вывод всех доступных товаров (дочерний компонент, child component)
  3. add-product — компонент с формой добавления нового товара (дочерний компонент, child component)

Как теперь выглядит наше приложение

Components - child and parent

Сейчас мы только для примера, чтобы показать как происходит обмен данными между компонентами, добавим следующий функционал. В файле шаблона основного компонента добавим следующий код:

Листинг 9.1 | Файл: src/app/app.component.html
<app-all-products [clickCount]="counter"></app-all-products>
<app-add-product (clickChange)="onClicked($event)"></app-add-product>

Затем в typescript файле основного компонента, добавим следующий код:

Листинг 9.2 | Файл: src/app/app.component.ts
...
export class AppComponent {
    counter: number = 0;

    public onClicked(counter){
      this.counter = counter;
    }
}

Теперь добавим изменения в компонент all-products. Начнем с шаблона:

Листинг 9.3 | Файл: src/app/all-products/all-products.component.html
<div class="container">
  <div class="row">
    <h3 class="col-md-12 mr-3 mt-2">{{ title }}</h3>
    <p class="col-md-12">Вы кликнули {{ clickCount }} раз</p>
...

Тут мы будем выводить количество кликов по кнопке, которую мы позже добавим к нашей форме.

Для того, чтобы компонент мог принимать какие-то данные из другого компонента, нам прежде всего нужно импортировать декоратор @Input и затем объявить входное свойство с помощью этого декоратора:

Листинг 9.4 | Файл: src/app/all-products/all-products.component.ts
import { Component, OnInit, Input } from '@angular/core';

import { DataService } from '../shared/data.service';

@Component({
  selector: 'app-all-products',
  templateUrl: './all-products.component.html',
  styleUrls: ['./all-products.component.css']
})
export class AllProductsComponent implements OnInit {

  title = 'Каталог товаров';

  @Input() clickCount: number;
...

Сейчас давайте добавим изменения в наш третий компонент с формой:

Листинг 9.5 | Файл: src/app/add-product/add-product.component.html
...
      <button class="btn btn-success" type="submit" [disabled]="!form.valid">Добавить</button>
      <button class="btn btn-info ml-2" type="submit" (click)="clickMe()">Click me</button>
...

Вот это кнопка, клики по которой мы будем отслеживать. Теперь нам еще нужно добавить в файл компонента декоратор @Output для привязки к событию компонента:

Листинг 9.6 | Файл: src/app/add-product/add-product.component.ts
import { Component, OnInit, Output, EventEmitter } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';

...

  clickCount: number = 0;
  @Output() clickChange: EventEmitter<number> = new EventEmitter();

  clickMe() { 
    this.clickCount++;
    this.clickChange.emit(this.clickCount);
  }
...

Теперь при клике по одному компоненту, значение переменной передается в другой компонент.

Элемент <ng-content>

Если у вас возникла необходимость добавить контент (html или просто текст) в родительском элементе, но он должен отображаться в дочернем, тогда можно воспользоваться компонентом ng-content.
Давайте заголовок формы «Добавление нового товара» перенесем в родительский компонент — app.

Листинг 9.7 | Файл: src/app/app.component.html
<app-all-products [clickCount]="counter"></app-all-products>
<app-add-product (clickChange)="onClicked($event)">
  <h4 class="text-center">Добавление нового товара</h4><hr>
</app-add-product>

А в компоненте формы вместо этой строки добавим элемент ng-content

Листинг 9.8 | Файл: src/app/add-product/add-product.component.html
<div class="container">
  <div class="col-md-12 mt-2 add-new-item">
    <ng-content></ng-content>
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
...

После этих изменений, если вы взглянете на наше приложение, то ничего не изменилось. Все работает без ошибок, но теперь контент выводится из родительского элемента в дочернем.

@ContentChild

Бывают ситуации, когда вам может понадобиться получить значение какого-то элемента (или элемент целиком) из родительского в дочернем. В этом нам поможет декоратор @ContentChild.
Давайте из нашего примера получим заголовок формы в наш компонент. Изменим шаблон основного компонента, добавив ему локальную переменную #test.

Листинг 9.9 | Файл: src/app/app.component.html
<app-all-products [clickCount]="counter"></app-all-products>
<app-add-product (clickChange)="onClicked($event)">
  <h4 class="text-center" #test>Добавление нового товара</h4><hr>
</app-add-product>

Для того, чтобы воспользоваться вышеупомянутым декоратором, нам нужно импортировать его и ElementRef из библиотеки Angular

Листинг 9.10 | Файл: src/app/add-product/add-product.component.ts
import { Component, OnInit, Output, EventEmitter, ContentChild, ElementRef } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
...
...
form: FormGroup;
@ContentChild('test') test: ElementRef;

ngOnInit() {
  	this.form = new FormGroup({
  		title: new FormControl('', [Validators.required, this.checkLength]),
  		details: new FormGroup({
  			price: new FormControl('100', [Validators.min(1), Validators.max(1500)]),
  			quantity: new FormControl('1', Validators.max(100))
  		})
  	});

    console.log(this.test.nativeElement.innerText);
}
...

В качестве свойства этому декоратору нужно предать имя нашей локальной переменной. Также задать имя переменной, которое мы хотим использовать в нашем компоненте, я задал также test, но можно использовать любое другое. Тип данной переменной должен быть ElementRef.

В методе ngOnInit выведем в консоль значение нашего тега. Если вы выведите в консоль просто this.test, то сможете посмотреть сколько у него разных свойств. Можно получить доступ и к классам, и к атрибутам элемента и т.д. Ну и конечно же вы можете изменять переменную ‘test’ так как вам нужно.

@ViewChild

Для того, чтобы использовать локальную переменную в родительском файле компонента, мы можем воспользоваться декоратором @ViewChild. Мы ту же переменную test теперь выведем в консоль в AppComponent.

Листинг 9.11 | Файл: src/app/app.component.ts
import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
    counter: number = 0;

    public onClicked(counter){
      this.counter = counter;
    }

    @ViewChild('test') test: ElementRef;

    ngOnInit() {
    	console.log(this.test);
    }

}

Если вы теперь заглянете в консоль, то тут как раз и будет ElementRef. Там очень много различных свойств.

Жизненный цикл компонента

При загрузке компонента, он имеет несколько этапов, которые срабатывают поочередно. Эти этапы называются жизненными циклами. Вам нужно понимать какие из них наступают раньше, а какие позже.

Самый первый сработает конструктор нашего класса constructor() {}. Давайте добавим в наш компонент App, все методы и импортируем каждый из библиотеки Angular, а затем рассмотрим, для чего они нужны. Также мы удалим код в этом компоненте, который мы добавляли ранее для примера (и лучше удалить код из шаблона и файла компонента AddProduct — тот где мы использовали @ContentChild и вывод переменной test).

Листинг 9.12 | Файл: src/app/app.component.ts
import { Component, OnInit, OnChanges, SimpleChanges, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnChanges, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy {
    counter: number = 0;

    public onClicked(counter){
      this.counter = counter;
    }

    constructor() { console.log('constructor') }

    ngOnInit() { console.log('ngOnInit') }

    ngOnChanges(changes: SimpleChanges) { console.log('ngOnChanges', changes) }

    ngDoCheck() { console.log('ngDoCheck') }

    ngAfterContentInit() { console.log('ngAfterContentInit') }

    ngAfterContentChecked() { console.log('ngAfterContentChecked') }

    ngAfterViewInit() { console.log('ngAfterViewInit') }

    ngAfterViewChecked() { console.log('ngAfterViewChecked') }

    ngOnDestroy() { console.log('ngOnDestroy') }

}
Имплементироваться от каждого события необязательно, но это желательно делать, чтобы было более наглядно какие жизненные циклы использует компонент

Вот результат данного вывода в консоль, в порядке выполнения:
Жизненные циклы компонента
Здесь не хватает только метода onDestroy, он срабатывает последним и чаще всего используется, чтобы «убить» процессы прослушки.

Теперь давайте рассмотрим каждый метод:

Имя Описание
ngOnInit Производит инициализацию свойств и выполняется один раз при запуске компонента.
ngOnChanges Этот метод вызывается при изменении значений свойств, а также перед ngOnInit. Может принимать параметр типа SimpleChanges, в таком случае можно будет получить как текущее значение так и предыдущее
ngDoCheck Вызывается, когда Angular запускает процесс обнаружения изменений
ngAfterContentInit Вызывается один раз, после инициализации контента (метода ngDoCheck)
ngAfterContentChecked Метод вызывается после анализа контента директивы в процессе обнаружения изменений
ngAfterViewInit вызывается фреймворком Angular после инициализации представления компонента, а также представлений дочерних компонентов.
ngAfterViewChecked вызывается фреймворком Angular после проверки на изменения в представлении компонента, а также проверки представлений дочерних компонентов
ngOnDestroy Метод вызывается непосредственно перед тем, как Angular уничтожает компонент

Методы ngDoCheck, ngAfterContentChecked и ngAfterViewChecked будут срабатывать несколько раз.

Ваши вопросы и комментарии:

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *