Источник: Nuances of Programming
Недавно у нас была статья по созданию и запуску приложения. Работает оно отлично, но вот его структуру стоило бы улучшить. На данный момент весь код для представления и логики находится в одном компоненте, в результате чего по мере роста и усложнения приложения, его содержимое станет понимать все труднее.
Такое приложение нуждается в обновлении.
Так как программа развивается, ее сложность будет продолжать возрастать, пока не будут предприняты меры по ее сдерживанию или уменьшению.” — Мэнни Леман.
План нашего урока
Этот урок подчеркивает важность поддержания чистоты и структурированности приложений 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 содержит список привычек, который можно отлично уместить в собственный компонент. Попробуйте инкапсулировать этот код и разобраться, как обеспечить соответствующее обновление списка при добавлении в него элементов и их редактировании.
Читайте также:
Перевод статьи Jessi Pearcy: Inputs and Outputs: Working With Angular Components