Input и Output в компонентах Angular



Книга Input и Output в компонентах Angular

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


Такое приложение нуждается в обновлении.


Так как программа развивается, ее сложность будет продолжать возрастать, пока не будут предприняты меры по ее сдерживанию или уменьшению.” — Мэнни Леман.


План нашего урока


Этот урок подчеркивает важность поддержания чистоты и структурированности приложений 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 содержит список привычек, который можно отлично уместить в собственный компонент. Попробуйте инкапсулировать этот код и разобраться, как обеспечить соответствующее обновление списка при добавлении в него элементов и их редактировании.




1088   0  

Comments

    Ничего не найдено.