Недавно у нас была статья по созданию и запуску приложения. Работает оно отлично, но вот его структуру стоило бы улучшить. На данный момент весь код для представления и логики находится в одном компоненте, в результате чего по мере роста и усложнения приложения, его содержимое станет понимать все труднее.
Такое приложение нуждается в обновлении.
Так как программа развивается, ее сложность будет продолжать возрастать, пока не будут предприняты меры по ее сдерживанию или уменьшению.” — Мэнни Леман.
План нашего урока
Этот урок подчеркивает важность поддержания чистоты и структурированности приложений Angular во избежание скопления в одном файле общей кучи кода.
В нем:
- Я покажу пример недавно созданного приложения, в котором не помешало бы доработать структуру и разделить код по компонентам, с которыми мы сможем работать.
- Мы научимся использовать события Output для поддержания осведомленности родительского компонента об изменении данных.
- Задействуем свойства Input для передачи данных от родительского компонента к дочернему.
Весь код написанного нами ранее приложения сейчас содержится в компоненте app. В ходе добавления новых возможностей и постепенного изменения приложения с этой все нарастающей мешаниной кода станет очень сложно работать. Поэтому по мере развития программы важно инкапсулировать код по соответствующим компонентам, чтобы он не отбивался от рук.
Почему компоненты?
- Компонент — это часть кода, служащая своей отдельной задаче. Он включает только необходимые для этого составляющие и может задействоваться как самостоятельное представление.
- Помещая код в компоненты, мы делаем его переиспользуемым. Например, если вам потребуется использовать заданную форму элемента UI в нескольких местах кода, то можно существенно снизить повторяемость, поместив ее в компонент.
- Компоненты упрощают тестирование кода. Они позволяют легче группировать связанную логику и писать более сфокусированные тесты, что опять же облегчает понимание кода в дальнейшем, когда вы возвращаетесь к нему для внесения изменений или расширения функционала.
- Глядя на HTML-код в
app.component.html
, можно уже выделить очевидные разделы, которые готовы стать компонентами:
<!-- Основной Nav -->
<div class="toolbar-container">
<mat-toolbar class="toolbar" color="primary">
<mat-icon aria-hidden="false" aria-label="check mark icon">fact_check</mat-icon>
<h1>Habit Tracker</h1>
</mat-toolbar>
</div>
<!-- Форма для добавления/редактирования -->
<div class="add-form-container" *ngIf="adding || editing">
<mat-card>
<mat-card-title>Add New Habit</mat-card-title>
<hr />
<form [formGroup]="habitForm" (ngSubmit)="onSubmit()">
<!-- Code omitted for brevity-->
</form>
</mat-card>
</div>
<!-- Список всех привычек -->
<div class="all-habits" *ngIf="!adding && !editing">
<h1>All Habits</h1>
<button mat-raised-button color="accent" (click)="adding = !adding">
Add New Habit
</button>
<div *ngFor="let habit of habits; let i = index;">
<mat-card>
<!-- Код опущен в целях сокращения-->
</mat-card>
</div>
</div>
В стартовых файлах я реструктурировала приложение на четыре компонента:
app
— родительский компонент.toolbar
— содержит код для навигации и меняться на протяжении урока не будет.all-habits
— этот дочерний компонент содержит весь код для управления списком привычек.habit-form
— этот дочерний компонент инкапсулирует форму, которую мы будем применять для добавления и редактирования привычек.
Дополнительно я переместила данные привычки в отдельный файл с экспортируемым const, который будет использоваться компонентами совместно.
Настройка
Среда разработки
- Если вы работали с приложением Angular на своей машине, то убедитесь, что у вас установлены Node.js и Angular CLI.
- Я покажу, как получить стартовые файлы с помощью Git, который также потребуется установить, если ранее вы им не пользовались.
Стартовые файлы
Стартовый проект можно взять из этого репозитория GitHub. Чтобы осуществить это из командной строки, перейдите в расположение, куда хотите скачать приложение, и введите следующую команду Git:
git clone https://github.com/jessipearcy/habit-tracker-components-split
Так вы скопируете файлы в нужный каталог. Далее выполните следующие две команды, чтобы перейти в этот каталог и установить необходимые для запуска приложения Angular пакеты:
cd habit-tracker-components-split
npm ci
По завершению установки пакетов, введите в командной строке ng serve
, нажмите ввод и перейдите на страницу http://localhost:4200
, где должно отобразиться запущенное приложение.
Уточним, что у нас есть для начала
- Шаблон компонента app содержит три дочерних компонента.
- Источник данных для нашего списка привычек находится в экспортируемом const в
habits.ts
— это делает данный массив привычек доступным для взаимодействия и управления со стороны разных компонентов. - На данный момент app отображает все перечисленные в нем компоненты — мы же добавим свойства и структурные директивы, которые на основе действий пользователей будут определять, что конкретно должно отображаться.
Использование событий Output и свойств Input
Реализация структурных директив для управления представлением
Сейчас мы отображаем форму и список привычек одновременно, что занимает очень много пространства в представлении. Нам же нужно видеть форму редактирования только при необходимости. Рассмотрим это как возможность научиться использовать *ngIf
в сочетании с <ng-template>
, чтобы показывать/скрывать код.
Добавьте в app.component.ts
свойство-флаг, которое в положении true
будет показывать форму, а в положении false
список. По умолчанию мы установим это свойство как false
:
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
public formOpen = false;
ngOnInit(): void {}
}
В app.component.html
мы настроим условие if/else при помощи структурных директив Angular:
<app-habit-form *ngIf="formOpen; else allHabits"></app-habit-form>
<ng-template #allHabits>
<app-all-habits></app-all-habits>
</ng-template>
Условие else
в выражении *ngIf
относится к локальной ссылке, которую мы установили в <ng-template>
. Использование <ng-template>
означает, что компонент all-habits
не только не показывается в представлении, но по факту не отрисовывается в DOM вообще.
Добавление в all-habits
события Output
И список привычек, и форма теперь являются дочерними компонентами app. События Output позволяют дочерним компонентам уведомлять своих родителей об изменениях в данных. Мы добавим такое событие в all-habits
, а затем настроим его прослушивание в app
.
Добавьте в all-habits.component.ts
свойство при помощи декоратора Output()
и установите его как new EventEmitter
. Не забудьте импортировать EventEmitter
из @angular/core
. Затем мы будем отправлять данное событие в функцию.
import { Component, OnInit, Output, EventEmitter } from '@angular/core';
// ... код опущен
export class AllHabitsComponent implements OnInit {
@Output() addEvent = new EventEmitter();
public habits: Habit[];
constructor() {}
ngOnInit(): void {
this.habits = HABITS;
}
onAdd() {
this.addEvent.emit();
}
// ... код опущен
}
Теперь обновим HTML-шаблон для вызова функции onAdd()
при нажатии кнопки Add New Habit:
<div class="all-habits">
<h1>All Habits</h1>
<button mat-raised-button color="accent" (click)="onAdd()">
Add New Habit
</button>
<div *ngFor="let habit of habits; let i = index">
<mat-card>
<mat-card-title>
<mat-icon
class="habit-icon"
color="accent"
aria-hidden="false"
aria-label="circle check mark icon"
>check_circle_outline</mat-icon
>
{{ habit.name }}
</mat-card-title>
<div class="detail-options">
<mat-icon
class="habit-icon"
color="primary"
>edit</mat-icon
>
<!--...Код опущен...-->
Теперь, когда отправку события мы наладили, нужно наладить его прослушивание в родительском компоненте.
Пропишите слушателя событий в app.component.html
, который в ответ на событие будет вызывать новый метод.
<app-habit-form *ngIf="formOpen"></app-habit-form>
<app-all-habits *ngIf="!formOpen"
(addEvent)="onAdding()"></app-all-habits>
Затем добавьте этот метод в app.component.ts
для получения события и выполнения действия:
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
public formOpen = false;
ngOnInit(): void {}
onAdding() {
this.formOpen = true;
}
}
Вот теперь у нас должна появится возможность отображать форму по клику.
Отправка события из формы
Форме тоже нужна возможность сообщать родительскому компоненту, когда необходимо прекратить ее показ и вернуться к списку.
Давайте добавим в habit-form.component.ts
новое событие Output и обновим метод exitform()
, чтобы при вызове он это событие отправлял:
export class HabitFormComponent implements OnInit {
@Output() onExit = new EventEmitter();
// ...код опущен...
exitForm() {
this.habitForm.reset();
this.onExit.emit();
}
}
Еще нужно наладить сброс формы, чтобы при переключении между ней и списком всегда иметь свежие данные.
Мы уже вызываем exitform()
в шаблоне habit-form
, поэтому нужно лишь начать прослушивать это событие в компоненте app
:
<app-habit-form *ngIf="formOpen; else allHabits"
(onExit)="closeForm()"></app-habit-form>
Добавьте в app.component.ts
метод closeForm()
:
export class AppComponent implements OnInit {
public formOpen = false;
ngOnInit(): void {}
onAdding() {
this.formOpen = true;
}
closeForm() {
this.formOpen = false;
}
}
Вот теперь мы можем как открывать, так и закрывать форму добавления привычек.
Отправка события Output с данными
Из стартового проекта была унаследована возможность добавления новых привычек, так что можете ее проверить — работать она должна исправно. Тем не менее нам дополнительно нужен способ сообщать форме, что мы находимся в режиме редактирования, и предоставлять ей всю необходимую информацию для обновления существующего элемента.
Это непростая задача, так как данные о привычке, которые нам нужно передавать форме, будут поступать из all-habits
, а нам нужно вводить эти данные из app
. Для решения данной задачи потребуется несколько шагов:
- Отправить из
all-habits
событие, содержащее данные привычки. - Сохранить эту привычку в
app
. - Ввести привычку в элемент
habit-form
в шаблоне. - Заполнить данные из входной привычки.
- Обновить существующую привычку в списке.
Начнем с отправки события при нажатии пользователем кнопки edit:
all-habits.component.ts
export class AllHabitsComponent implements OnInit {
@Output() addEvent = new EventEmitter();
@Output() editEvent = new EventEmitter<Habit>();
public habits: Habit[];
constructor() {}
ngOnInit(): void {
this.habits = HABITS;
}
onAdd() {
this.addEvent.emit();
}
onEdit(habit: Habit) {
this.editEvent.emit(habit);
}
public onDelete(index: number) {
this.habits.splice(index, 1);
}
}
all-habits.component.html
<!--...Код опущен...-->
<div *ngFor="let habit of habits; let i = index">
<mat-card>
<mat-card-title>
<mat-icon
class="habit-icon"
color="accent"
aria-hidden="false"
aria-label="circle check mark icon"
>check_circle_outline</mat-icon
>
{{ habit.name }}
</mat-card-title>
<div class="detail-options">
<mat-icon
class="habit-icon"
color="primary"
(click)="onEdit(habit)"
>edit</mat-icon
>
<mat-icon class="habit-icon" color="warn" (click)="onDelete(i)"
>remove_circle</mat-icon
>
</div>
<!--...Код опущен...-->
Несколько пояснений к изменениям:
- В этом событии Output мы добавляем тип. Причина в том, что при отправке события нам нужно передать объект привычки, и он будет иметь тип
Habit
. - Также обратите внимание, что наша функция
onEdit()
получает параметр, и у нас есть возможность обращаться к конкретной привычке, передавая этой функции нужную привычку из*ngFor
в HTML.
Теперь давайте настроим в app
прослушивание и сохранение события:
app.component.html
<ng-template #allHabits>
<app-all-habits
(addEvent)="onAdding()"
(editEvent)="onEditing($event)">
</app-all-habits>
</ng-template>
app.component.ts
import { Component, OnInit } from '@angular/core';
import { Habit } from './models/habit';
// ...Код опущен...
export class AppComponent implements OnInit {
public formOpen = false;
public editHabit: Habit;
// ...Код опущен...
onEditing(habit: Habit) {
this.editHabit = habit;
this.formOpen = true;
}
closeForm() {
this.formOpen = false;
this.editHabit = null;
}
}
Интересные изменения:
- Не забудьте импортировать модель
Habit
. - Мы обновили
closeForm()
для обнуления свойстваeditHabit
при закрытии формы. Это предотвратит случайную передачу нежелательных данных в форму при ее следующем открытии.
Добавление Input в элемент
Теперь если вы все сохранили и проверили, то наше событие клика должно срабатывать, на что мы должны получать данные в компоненте app — но форма при открытии по-прежнему пуста! Вот здесь и появляется декоратор input.
Для начала добавим его в элемент habit-form
в app.component.html
:
<app-habit-form
*ngIf="formOpen; else allHabits"
(onExit)="closeForm()"
[habit]="editHabit">
</app-habit-form>
Переменная внутри квадратных скобок слева ссылается на свойство habit
дочернего компонента, который мы собираемся создать, и для нее устанавливается значение свойства editHabit
компонента app, которое на данный момент находится в области видимости.
Добавление Input в форму
Для подбора и использования вводимого свойства мы обновим компонент habit-form
:
export class HabitFormComponent implements OnInit {
@Output() onExit = new EventEmitter();
@Input() habit: Habit;
public editingIndex: number;
public habits: Habit[];
public habitForm = new FormGroup({
name: new FormControl(''),
frequency: new FormControl(''),
description: new FormControl(''),
});
ngOnInit(): void {
this.habits = HABITS;
if (this.habit) {
this.editingIndex = this.habits.indexOf(this.habit);
this.setEditForm(this.habit);
}
}
public setEditForm(habit: Habit) {
this.habitForm.patchValue({
name: habit.name,
frequency: habit.frequency,
description: habit.description,
});
}
public onSubmit() {
const habit = this.habitForm.value as Habit;
if (this.habit) {
this.habits.splice(this.editingIndex, 1, habit);
} else {
this.habits.push(habit);
}
this.exitForm();
}
//...Код опущен...
Примечания:
- При нажатии кнопки Add New Habit свойство input будет null или undefined, в связи с чем легко проверить его существование и понять, находимся ли мы в режиме редактирования.
- Обратите внимание, что мы проверяем индекс объекта привычки, используя метод
.indexOf()
, которому передаем весь объект. - Для обновления существующей привычки мы задействуем
.splice()
, удаляя элемент из массива и заменяя его на значения, отправленные в форме. - Если привычка передана не была, мы понимаем, что находимся в режиме добавления, поэтому можно просто добавить новую привычку в массив.
Отлично! Теперь вы умеете создавать, обновлять, редактировать и удалять привычки, передавая данные через все семейство компонентов Angular. Неплохая работа.
Дополнительная практика
В качестве дополнительных упражнений попробуйте разделить другой компонент этого приложения. all-habits
содержит список привычек, который можно отлично уместить в собственный компонент. Попробуйте инкапсулировать этот код и разобраться, как обеспечить соответствующее обновление списка при добавлении в него элементов и их редактировании.
Читайте также:
- Знакомимся с основами Angular через создание простого приложения
- Зачем и как реализовать ленивую загрузку компонентов в Angular
- Стратегии обнаружения изменений в Angular - «onPush» и «Default»
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Jessi Pearcy: Inputs and Outputs: Working With Angular Components