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

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

Читайте нас в Telegram, VK и Яндекс.Дзен


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

Предыдущая статьяКомпилятор VS интерпретатор: ключевые отличия
Следующая статья3 признака того, что ваш ИИ-проект обречен