Добавить в корзинуПозвонить
Найти в Дзене
Nuances of programming

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

Источник: Nuances of Programming Недавно у нас была статья по созданию и запуску приложения. Работает оно отлично, но вот его структуру стоило бы улучшить. На данный момент весь код для представления и логики находится в одном компоненте, в результате чего по мере роста и усложнения приложения, его содержимое станет понимать все труднее. Такое приложение нуждается в обновлении. Так как программа развивается, ее сложность будет продолжать возрастать, пока не будут предприняты меры по ее сдерживанию или уменьшению.” — Мэнни Леман. План нашего урока Этот урок подчеркивает важность поддержания чистоты и структурированности приложений Angular во избежание скопления в одном файле общей кучи кода. В нем: Весь код написанного нами ранее приложения сейчас содержится в компоненте app. В ходе добавления новых возможностей и постепенного изменения приложения с этой все нарастающей мешаниной кода станет очень сложно работать. Поэтому по мере развития программы важно инкапсулировать код по соответст
Оглавление

Источник: 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;
}
}

Вот теперь у нас должна появится возможность отображать форму по клику.

-2

Отправка события из формы

Форме тоже нужна возможность сообщать родительскому компоненту, когда необходимо прекратить ее показ и вернуться к списку.

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

Читайте также:

Читайте нас в Telegram, VK

Перевод статьи Jessi Pearcy: Inputs and Outputs: Working With Angular Components