Знакомимся с основами Angular через создание простого приложения

Готовы сегодня создать что-нибудь интересное? Я тоже!

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

Я глубоко убеждена, что единственный способ научиться чему-либо — это начать заниматься этим на практике. Только методом смелых проб и ошибок можно шаг за шагом выстроить пирамиду опыта и вырасти из новичка в рок-звезду.

“Обучение  —  это активный процесс, и учимся мы путем активной деятельности. Как итог  —  в виде устойчивых навыков оседают только те знания, которые применяются на практике”,  —  Дэйл Карнеги.

Что будем изучать?

Мы создадим базовую версию приложения для отслеживания привычек, где будут содержаться соответствующие записи, которые можно будет добавлять, редактировать и удалять. Это будет пошаговая инструкция, в которой мы поочередно соберем все компоненты UI и бэкенд, чтобы вы могли наглядно проследить процесс их объединения воедино.

Здесь мы:

  • настроим приложение Angular с нуля  —  без стартовых файлов, полностью разобрав весь процесс;
  • установим Angular Material и используем ряд его компонентов для создания красивого UI;
  • включим в шаблон UI некоторые из структурных директив Angular для показа и скрытия элементов на основе состояния компонента;
  • реализуем простую реактивную форму для получения входных данных.

В конце урока у вас будет рабочее приложение, управляющее списком привычек, которые можно добавлять, редактировать и удалять. Поехали!

Для тех, кто не хочет читать: демо-версия приложения доступна на StackBlitz.

Базовая настройка

Если вы ранее не создавали приложение Angular на своей машине, то убедитесь, что у вас установлены Node.js и Angular CLI.

Откройте предпочтительный для вас терминал. Я в данном уроке буду использовать его встроенный в VSCode вариант.

Перейдите в каталог, где хотите создать приложение и введите следующую команду:

Вам будет задано два вопроса:

  1. Would you like to add Angular routing? (Добавить маршрутизацию Angular?)  —  для подтверждения введите y и нажмите ввод.
  2. Which stylesheet format would you like to use? (Какой формат таблицы стилей использовать?)  —  выберите предпочтительный с помощью клавиш стрелок. Мне нравится вариант SCSS.

После генерации этих пакетов можно переходить к настройке Angular Material (AM). Для начала перейдите в каталог приложения командой cd и добавьте эту библиотеку в проект:

Вам также понадобится ответить на ряд вопросов:

  1. Choose the Deep Purple/Amber pre-built theme (Выберите тему Deep Purple/Amber).
  2. Set up global Angular Material typography styles? (Настроить глобальные стили оформления Angular Material?)  —  для подтверждения введите y и нажмите ввод.
  3. Set up browser animations for Angular Material? (Настроить анимации браузера для Angular Material?)  —  для подтверждения введите y и нажмите ввод.

После этого введите команду “ng serve”, чтобы увидеть весь сгенерированный Angular код. 

Пустяки, не так ли? А теперь пора перейти к собственному написанию кода. 

Создание элементов UI

Создание нашего удобного красивого UI мы начнем с добавления пары компонентов Angular Material.

Toolbar

В файле app.component.html удалите весь сгенерированный код и добавьте:

<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>

Так как для создания элементов mat-toolbar и mat-icon мы используем AM, вам также понадобится добавить в файл app.module.ts следующее:

//... другие импорты...
import { MatIconModule } from '@angular/material/icon';
import { MatToolbarModule } from '@angular/material/toolbar';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    //...другие импорты...
    MatIconModule,
    MatToolbarModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Примечание: я буду опускать некоторые импорты, чтобы сократить свои вставки с GitHub. Весь код вы сможете найти в конце урока.

Затем добавьте в app.component.scss следующее:

.toolbar h1 {
    padding-left: 5px;
}

Далее перейдите в styles.scss и добавьте в стили background-color: #f5f0fe; для элемента body.

Сохраните все и полюбуйтесь, какую крутую панель инструментов мы создали.

Если изменений не произошло, то стоит попробовать остановить выполнение приложения, введя CTRL+С в терминале, и заново выполнить ng serve. Иногда вносимые в app.module.ts изменения требуют перезапуска.

Использование иконок Angular Material 

Среди прочих крутых возможностей AM можно выделить бесплатный функционал иконок. По этому поводу я хочу пояснить несколько моментов:

  • отображаемое имя иконки указывается текстом между тегами <mat-icon>;
  • все доступные варианты иконок можно найти на странице Material Design icons;
  • По умолчанию иконка в элементе будет наследовать цвет шрифта родительского элемента. Вы же можете использовать один из цветов темы, добавив атрибут цвета. Например, <mat-icon color="primary"> окрасит иконку в основной цвет темы.

Создание списка

А теперь мы обратимся за помощью к TypeScript, который позволит настроить в шаблоне отображение дат. В этом руководстве не затрагиваются темы локального сохранения информации, поэтому пока что для экспериментирования мы настроим временный список.

Нам нужна модель, сообщающая приложению описание каждой привычки (habit). В директории проекта создайте каталог models, в нем файл habit.ts, и в него добавьте:

export class Habit {
    name: string;
    frequency: string;
    description: string;
}

Вернитесь к app.component.ts и добавьте с помощью нашего нового типа свойство массива:

import { Component } from '@angular/core';

import { Habit } from './models/habit';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  public habits: Habit[] = [
    <Habit>{
      name: '15 Minute Walk',
      frequency: 'Daily',
      description:
        'This habit makes my kitchen look nice and makes my day better the next morning.',
    },
    <Habit>{
      name: 'Weed the Garden',
      frequency: 'Weekly',
      description:
        'The weeds get so out of hand if they wait any longer, and I like how nice our home looks with a clean lawn.',
    },
  ];
}

Не забудьте импортировать тип Habit.

В app.component.html под панелью инструментов вставьте этот HTML:

<div class="all-habits">
  <h1>All Habits</h1>
  <div *ngFor="let habit of habits">
    <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
        >
        <mat-icon class="habit-icon" color="warn"
          >remove_circle</mat-icon
        >
      </div>
      <mat-card-content>
        <p>
          <span class="detail-label">Frequency:</span> {{ habit.frequency }}
        </p>
        <p>
          <span class="detail-label">Why is this habit important to me?</span>
          <br />{{ habit.description }}
        </p>
      </mat-card-content>
    </mat-card>
  </div>
</div>

Теперь из-за добавления этих новых компонентов AM в шаблон нам будет не хватать несколько импортов. Чтобы это исправить, импортируйте MatCardModule в app.module.ts:

//...другие импорты...

import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatToolbarModule } from '@angular/material/toolbar';

@NgModule({
  declarations: [AppComponent],
  imports: [
    //...другие импорты...
    MatCardModule,
    MatIconModule,
    MatToolbarModule,
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

Внесите в app.component.scss следующие классы:

.all-habits {
    text-align: center;
}

.all-habits h1 {
    margin-top: 1em;
}

.all-habits mat-card {
    width: 80%;
    margin: 1em auto;
    text-align: left;
}

.habit-icon {
    vertical-align: middle;
    padding-bottom: 3px;
    font-weight: 600;
}

.detail-label {
    font-weight: 500;
}

.detail-options {
    position: absolute;
    top: 14px;
    right: 10px;
}

.detail-options mat-icon {
    padding-right: 5px;
}

button:hover, .detail-options {
    cursor: pointer;
}

Таким образом Angular и AM предоставили нам целый набор бесплатных компонентов, стилей и поведений, что изначально сделало приложение достаточно продуманным.

  • Обратите внимание на установленную нами структурную директиву *ngFor, которая перебирает массив привычек.
  • Мы также ввели элемент mat-card, который предоставляет удобные способы разбивки содержимого карточки, делая ее более организованной. О дочерних компонентах mat-card можно узнать в документации.

Настройка формы

Для управления списком привычек нам понадобится форма, которая позволит добавлять и редактировать записи. 

Пока что мы поместим HTML-фрагмент для формы между кодом панели инструментов и All Habits (обратите внимание, чтобы он оказался над областью тега div с class="all-habits"):

<div class="add-form-container">
  <mat-card>
    <mat-card-title>Add New Habit </mat-card-title>
    <hr />
    <form>
      <mat-card-content>
        <mat-form-field appearance="fill">
          <mat-label>Title</mat-label>
          <input matInput />
        </mat-form-field>
        <br />
        <mat-form-field appearance="fill">
          <mat-label>Frequency</mat-label>
          <mat-select>
            <mat-option value="Daily">Daily</mat-option>
            <mat-option value="Weekly">Weekly</mat-option>
            <mat-option value="Monthly">Monthly</mat-option>
          </mat-select>
        </mat-form-field>
        <br />
        <mat-form-field appearance="fill">
          <mat-label>Description</mat-label>
          <textarea
            matInput
            placeholder="Why is this habit important to you?"
          ></textarea>
        </mat-form-field>
      </mat-card-content>
      <mat-card-actions>
        <button mat-raised-button color="accent" type="submit">Save</button>
        <button mat-raised-button>Cancel</button>
      </mat-card-actions>
    </form>
  </mat-card>
</div>

Для исправления красных волнистых подчеркиваний, выдаваемых элементами формы и кнопок, импортируйте MatButtonModule, MatInputModule и MatSelectModule в корневой модуль:

//...другие импорты

import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatToolbarModule } from '@angular/material/toolbar';

@NgModule({
  declarations: [AppComponent],
  imports: [
    //...другие импорты
    MatButtonModule,
    MatCardModule,
    MatIconModule,
    MatInputModule,
    MatSelectModule,
    MatToolbarModule,
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

Стилизуем мы это все, добавив в app.component.scss следующее:

.add-form-container {
    padding: 4em;
    text-align: center;
    max-width: 400px;
    margin: auto;
}

.add-form-container mat-card-title {
    margin: 1em !important;
}

.add-form-container button {
    margin-top: 10px !important;
}

mat-card-content {
    margin-bottom: 0;
}

form {
    padding-top: 1em;
}

mat-form-field {
    width: 90%;
}

button {
    width: 80%;
    max-width: 300px;
    margin-bottom: 1em;
}

button:hover, .detail-options {
    cursor: pointer;
}

Форма готова! Давайте кое-что по этому фрагменту проясним:

  • В mat-form-field есть много прекрасных вариантов для плейсхолдеров, ярлыков и стилей. Подробности в документации.
  • Вместо отдельного именования входных элементов AM вы добавляете их как директивы или атрибуты в обычные входные элементы HTML, как мы видим в случае с textarea в коде выше: <textarea matInput>. Подробности о входных элементах Angular Material описаны в документации.
  • Кнопки работают аналогичным образом. Вы можете задействовать для них различные стили, которые добавляются в качестве атрибутов к элементу <button>. В этом случае мы используем директиву mat-raised-button, если же вам больше по нраву отсутствие теней, то можете использовать mat-flat-button. Кнопки также могут иметь атрибут color, в котором доступны классы цветов primary, ascent и warn. С различными опциями настройки кнопок и стилей можете, опять же, ознакомиться в документации.

Добавление UX 

Сейчас наша форма смещает список привычек вниз, и мы его не видим. А нужно ли нам его видеть в процессе добавления новой привычки? На мой взгляд, нет. 

Для начала обратимся к TypeScript коду и добавим переменную, указывающую, находимся ли мы в режиме добавления привычки, определив ее по умолчанию как false:

export class AppComponent {
  public adding = false;
  
  public habits: Habit[] = [
    <Habit>{
      name: '15 Minute Walk',
      frequency: 'Daily',
      description:
        'This habit makes my kitchen look nice and makes my day better the next morning.',
    },
    <Habit>{
      name: 'Weed the Garden',
      frequency: 'Weekly',
      description:
        'The weeds get so out of hand if they wait any longer, and I like how nice our home looks with a clean lawn.',
    },
  ];
}

Теперь, чтобы не отображать постоянно форму, добавим кнопку Add New Habit, которая эту форму будет вызывать. Для этого обновим раздел all-habits:

<div class="all-habits">
  <h1>All Habits</h1>
  <button mat-raised-button color="accent" (click)="adding = !adding">
    Add New Habit
  </button>
  <div *ngFor="let habit of habits">
    <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>
      <mat-card-content>
        <p>
          <span class="detail-label">Frequency:</span> {{ habit.frequency }}
        </p>
        <p>
          <span class="detail-label">Why is this habit important to me?</span>
          <br />{{ habit.description }}
        </p>
      </mat-card-content>
    </mat-card>
  </div>
</div>

Обратите внимание, что для кнопки присутствует событие клика. При нажатии на нее, активируется переменная adding, позволяя нам переключаться между режимом просмотра привычек и их добавления. Чтобы все это организовать нужным образом, мы используем структурную директиву *ngif, с помощью которой будем скрывать список All Habits и показывать форму, когда adding будет true:

<div class="add-form-container" *ngIf="adding">
  <mat-card>
    <mat-card-title>Add New Habit</mat-card-title>
    <hr />
    <form>
      <mat-card-content>
        <mat-form-field appearance="fill">
          <mat-label>Title</mat-label>
          <input matInput />
        </mat-form-field>
        <br />
        <mat-form-field appearance="fill">
          <mat-label>Frequency</mat-label>
          <mat-select>
            <mat-option value="Daily">Daily</mat-option>
            <mat-option value="Weekly">Weekly</mat-option>
            <mat-option value="Monthly">Monthly</mat-option>
          </mat-select>
        </mat-form-field>
        <br />
        <mat-form-field appearance="fill">
          <mat-label>Description</mat-label>
          <textarea
            matInput
            placeholder="Why is this habit important to you?"
          ></textarea>
        </mat-form-field>
      </mat-card-content>
      <mat-card-actions>
        <button mat-raised-button color="accent" type="submit">Save</button>
        <button mat-raised-button>Cancel</button>
      </mat-card-actions>
    </form>
  </mat-card>
</div>

<div class="all-habits" *ngIf="!adding">
  <h1>All Habits</h1>
  <button mat-raised-button color="accent" (click)="adding = !adding">
    Add New Habit
  </button>
  <div *ngFor="let habit of habits">
    <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
        >
        <mat-icon class="habit-icon" color="warn"
          >remove_circle</mat-icon
        >
      </div>
      <mat-card-content>
        <p>
          <span class="detail-label">Frequency:</span> {{ habit.frequency }}
        </p>
        <p>
          <span class="detail-label">Why is this habit important to me?</span>
          <br />{{ habit.description }}
        </p>
      </mat-card-content>
    </mat-card>
  </div>
</div>

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

Отлично! Теперь, когда мы выстроили все элементы UI, пришло время добавить приложению динамичности и заставить кнопки выполнять их основную работу. Я понимаю, что все это долго, но мы уже почти закончили.

Основная функциональность

Подключение реактивной формы

А теперь веселая часть. Пора привязать к нашему TypeScript-коду реактивную форму, что даст нам возможность управлять данными в списке привычек.

Для настройки этой формы добавьте в app.component.ts следующий код:

import { Component } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';

import { Habit } from './models/habit';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  public adding = false;

  public habitForm = new FormGroup({
    name: new FormControl(''),
    frequency: new FormControl(''),
    description: new FormControl(''),
  });

  public habits: Habit[] = [
    <Habit>{
      name: '15 Minute Walk',
      frequency: 'Daily',
      description:
        'This habit makes my kitchen look nice and makes my day better the next morning.',
    },
    <Habit>{
      name: 'Weed the Garden',
      frequency: 'Weekly',
      description:
        'The weeds get so out of hand if they wait any longer, and I like how nice our home looks with a clean lawn.',
    },
  ];

  public onSubmit() {
    this.habits.push(this.habitForm.value as Habit);
    this.adding = false;
  }
}

На что обратить внимание:

  • Не забудьте импортировать FormGroup и FormControl из @abgular/core.
  • Тип FormGroup предоставляет возможность управления несколькими входными элементами формы в одном месте. 
  • Определение FormControls с пустыми строками подразумевает, что значение соответствующих входных элементов при инициализации формы будет пустым. Мы научимся инициализировать их со значением, когда дойдем до редактирования.
  • Весь богатый арсенал возможностей о Reactive Forms описан в документации.
  • Обратите внимание на метод onSubmit(). Мы имеем возможность обратиться к значению FormGroup и выполняем его приведение к модели Habit, получая удобную автоподстановку от IntelliSense и проверку типов. 

Теперь нужно прикрепить форму TypeScript к шаблону HTML.

<div class="add-form-container" *ngIf="adding">
  <mat-card>
    <mat-card-title>Add New Habit</mat-card-title>
    <hr />
    <form [formGroup]="habitForm" (ngSubmit)="onSubmit()">
      <mat-card-content>
        <mat-form-field appearance="fill">
          <mat-label>Title</mat-label>
          <input matInput formControlName="name" />
        </mat-form-field>
        <br />
        <mat-form-field appearance="fill">
          <mat-label>Frequency</mat-label>
          <mat-select formControlName="frequency">
            <mat-option value="Daily">Daily</mat-option>
            <mat-option value="Weekly">Weekly</mat-option>
            <mat-option value="Monthly">Monthly</mat-option>
          </mat-select>
        </mat-form-field>
        <br />
        <mat-form-field appearance="fill">
          <mat-label>Description</mat-label>
          <textarea
            matInput
            formControlName="description"
            placeholder="Why is this habit important to you?"
          ></textarea>
        </mat-form-field>
      </mat-card-content>
      <mat-card-actions>
        <button mat-raised-button color="accent" type="submit">Save</button>
        <button mat-raised-button>Cancel</button>
      </mat-card-actions>
    </form>
  </mat-card>
</div>

Чтобы HTML распознал привязку [formGroup], нужно добавить в импорты app.module.ts модули FormModule и ReactiveFormModule:

//...другие импорты

import { FormsModule , ReactiveFormsModule } from '@angular/forms';

@NgModule({
  declarations: [AppComponent],
  imports: [
    //...другие импорты
    FormsModule,
    ReactiveFormsModule,
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

Теперь у нас не только есть рабочая реактивная форма, но мы также можем добавлять в список новые привычки. 

После всего проделанного важно обратить внимание на следующее:

  • Обязательное добавление привязки [formGroup] к элементу form.
  • (ngSubmit) прослушивает все события клика кнопок с типом submit, так что нет необходимости добавлять отдельное событие клика для кнопки save.
  • Имя, которое вы указываете в атрибуте formControlName, должно в точности соответствовать именам элементов управления, которые вы определили в TypeScript коде.

Добавление метода редактирования

На данный момент мы можем добавлять привычки, но нам еще нужно поместить в интерфейс иконку карандаша, которая будет открывать форму редактирования и обновлять соответствующую запись в списке. Сначала добавим в app.component.ts метод редактирования:

export class AppComponent {
  public adding = false;
  public editing = false;
  public editingIndex: number;

  public habitForm = new FormGroup({
    name: new FormControl(''),
    frequency: new FormControl(''),
    description: new FormControl(''),
  });

  public habits: Habit[] = [
    <Habit>{
      name: '15 Minute Walk',
      frequency: 'Daily',
      description:
        'This habit makes my kitchen look nice and makes my day better the next morning.',
    },
    <Habit>{
      name: 'Weed the Garden',
      frequency: 'Weekly',
      description:
        'The weeds get so out of hand if they wait any longer, and I like how nice our home looks with a clean lawn.',
    },
  ];

  public onSubmit() {
    const habit = this.habitForm.value as Habit;

    if (this.editing) {
      this.habits.splice(this.editingIndex, 1, habit);
    } else {
      this.habits.push(habit);
    }

    this.editing = false;
    this.adding = false;
  }

  public setEditForm(habit: Habit, index: number) {
    this.habitForm.patchValue({
      name: habit.name,
      frequency: habit.frequency,
      description: habit.description,
    });
    this.editing = true;
    this.editingIndex = index;
  }
}

Несколько пояснений:

  • Обратите внимание на применение patchValue в методе setEditForm. Это позволяет нам напрямую устанавливать значения всей группы форм.
  • Также взгляните на параметры, которые мы передаем в тот же метод,  —  они задействуются в наших HTML-вставках. Параметр index будет указывать индекс передаваемой привычки, что позволит нам находить ее в существующем массиве для замены. 
  • Подробнее о методе .splice() можете узнать на GeeksforGeeks.
  • Заметьте, что новый флаг editing также устанавливается в коде отправки формы, чтобы при сохранении переключаться обратно к основному списку.

Теперь перейдем к шаблону:

<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()">
      <mat-card-content>
        <mat-form-field appearance="fill">
          <mat-label>Title</mat-label>
          <input matInput formControlName="name" />
        </mat-form-field>
        <br />
        <mat-form-field appearance="fill">
          <mat-label>Frequency</mat-label>
          <mat-select formControlName="frequency">
            <mat-option value="Daily">Daily</mat-option>
            <mat-option value="Weekly">Weekly</mat-option>
            <mat-option value="Monthly">Monthly</mat-option>
          </mat-select>
        </mat-form-field>
        <br />
        <mat-form-field appearance="fill">
          <mat-label>Description</mat-label>
          <textarea
            matInput
            formControlName="description"
            placeholder="Why is this habit important to you?"
          ></textarea>
        </mat-form-field>
      </mat-card-content>
      <mat-card-actions>
        <button mat-raised-button color="accent" type="submit">Save</button>
        <button mat-raised-button>Cancel</button>
      </mat-card-actions>
    </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-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)="setEditForm(habit, i)"
          >edit</mat-icon
        >
        <mat-icon class="habit-icon" color="warn"
          >remove_circle</mat-icon
        >
      </div>
      <mat-card-content>
        <p>
          <span class="detail-label">Frequency:</span> {{ habit.frequency }}
        </p>
        <p>
          <span class="detail-label">Why is this habit important to me?</span>
          <br />{{ habit.description }}
        </p>
      </mat-card-content>
    </mat-card>
  </div>
</div>

Какие здесь изменения:

  • Для редактирования списка мы используем ту же форму, что и для внесения в него элементов, поэтому просто добавили в обе проверки *ngIf флаг editing .
  • Изменения, внесенные в *ngFor, позволяют нам задействовать одну из очень удобных возможностей Angular  —  получать индекс выбранного элемента, определив содержащую его значение переменную прямо в той же строке. После мы передаем эту переменную в функцию setEditForm(), сообщая таким образом форме, какую привычку собираемся редактировать. 
  • В завершении мы добавили функцию запуска редактирования в качестве события клика для кнопки иконки карандаша. 

Прекрасно! Теперь займемся удалением.

Добавление метода удаления

Добавьте в TypeScript код метод onDelete(), который по аналогии с setEditForm будет получать индекс из *ngFor.

export class AppComponent {
  public adding = false;
  public editing = false;
  public editingIndex: number;

  public habitForm = new FormGroup({
    name: new FormControl(''),
    frequency: new FormControl(''),
    description: new FormControl(''),
  });

  public habits: Habit[] = [
    <Habit>{
      name: '15 Minute Walk',
      frequency: 'Daily',
      description:
        'This habit makes my kitchen look nice and makes my day better the next morning.',
    },
    <Habit>{
      name: 'Weed the Garden',
      frequency: 'Weekly',
      description:
        'The weeds get so out of hand if they wait any longer, and I like how nice our home looks with a clean lawn.',
    },
  ];

  public onSubmit() {
    const habit = this.habitForm.value as Habit;

    if (this.editing) {
      this.habits.splice(this.editingIndex, 1, habit);
    } else {
      this.habits.push(habit);
    }

    this.editing = false;
    this.adding = false;
  }

  public setEditForm(habit: Habit, index: number) {
    this.habitForm.patchValue({
      name: habit.name,
      frequency: habit.frequency,
      description: habit.description,
    });
    this.editing = true;
    this.editingIndex = index;
  }

  public onDelete(index: number) {
    this.habits.splice(index, 1);
  }

Далее в шаблоне добавьте к иконке удаления событие клика:

<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-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)="setEditForm(habit, i)"
          >edit</mat-icon
        >
        <mat-icon 
          class="habit-icon" 
          color="warn"
          (click)="onDelete(i)"
          >remove_circle</mat-icon
        >
      </div>
      <mat-card-content>
        <p>
          <span class="detail-label">Frequency:</span> {{ habit.frequency }}
        </p>
        <p>
          <span class="detail-label">Why is this habit important to me?</span>
          <br />{{ habit.description }}
        </p>
      </mat-card-content>
    </mat-card>
  </div>
</div>

Теперь у нас появилась возможность удаления.

Кнопка отмены и исправление бага

Итак, мы вышли на финишную прямую и впереди настройка последних фрагментов.

Нам осталось сделать две вещи: организовать работу кнопки отмены (cancel) и попутно исправить небольшой баг. Вы могли заметить, что если после редактирования привычки сразу перейти к созданию новой, то форма заполняется данными из только что редактированной записи. Почему это происходит?

На данный момент при сохранении новых записей мы не очищаем поля формы, поэтому данные просто в них “подвисают”. Давайте создадим решение, которое будет отвечать за очищение при сохранении или нажатии кнопки отмены, которую также потребуется добавить.

В app.component.ts мы пропишем метод exitForm(), который используем в функции onSubmit():

export class AppComponent {
  public adding = false;
  public editing = false;
  public editingIndex: number;

  public habitForm = new FormGroup({
    name: new FormControl(''),
    frequency: new FormControl(''),
    description: new FormControl(''),
  });

  public habits: Habit[] = [
    <Habit>{
      name: '15 Minute Walk',
      frequency: 'Daily',
      description:
        'This habit makes my kitchen look nice and makes my day better the next morning.',
    },
    <Habit>{
      name: 'Weed the Garden',
      frequency: 'Weekly',
      description:
        'The weeds get so out of hand if they wait any longer, and I like how nice our home looks with a clean lawn.',
    },
  ];

  public onSubmit() {
    const habit = this.habitForm.value as Habit;

    if (this.editing) {
      this.habits.splice(this.editingIndex, 1, habit);
    } else {
      this.habits.push(habit);
    }

    this.editing = false;
    this.adding = false;
    this.exitForm();
  }

  public setEditForm(habit: Habit, index: number) {
    this.habitForm.patchValue({
      name: habit.name,
      frequency: habit.frequency,
      description: habit.description,
    });
    this.editing = true;
    this.editingIndex = index;
  }

  public onDelete(index: number) {
    this.habits.splice(index, 1);
  }

  exitForm() {
    this.adding = false;
    this.editing = false;
    this.habitForm.reset();
  }
}

Наша группа форм содержит функцию reset(), которая будет сбрасывать входные элементы в исходное состояние и обнулять все значения в полях.

Далее мы включим в форму добавления кнопку отмены с событием клика exitForm():

<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()">
      <mat-card-content>
        <mat-form-field appearance="fill">
          <mat-label>Title</mat-label>
          <input matInput formControlName="name" />
        </mat-form-field>
        <br />
        <mat-form-field appearance="fill">
          <mat-label>Frequency</mat-label>
          <mat-select formControlName="frequency">
            <mat-option value="Daily">Daily</mat-option>
            <mat-option value="Weekly">Weekly</mat-option>
            <mat-option value="Monthly">Monthly</mat-option>
          </mat-select>
        </mat-form-field>
        <br />
        <mat-form-field appearance="fill">
          <mat-label>Description</mat-label>
          <textarea
            matInput
            formControlName="description"
            placeholder="Why is this habit important to you?"
          ></textarea>
        </mat-form-field>
      </mat-card-content>
      <mat-card-actions>
        <button mat-raised-button color="accent" type="submit">Save</button>
        <button mat-raised-button (click)="exitForm()">Cancel</button>
      </mat-card-actions>
    </form>
  </mat-card>
</div>

Вот и все!

Вывод

Во-первых, я благодарю вас за то, что проделали со мной этот путь! Давайте подведем его итог.

  • Мы научились использовать структурные директивы Angular для управления показом заданного содержимого на основе состояния и для динамического отображения элементов списка.
  • Мы использовали реактивную форму, чтобы управлять полями формы и значениями из кода TypeScript, а не в HTML-шаблоне.
  • С помощью компонентов Angular Material мы легко создали приятный UI, избежав лишней ручной работы со стилями.

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

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


Перевод статьи Jessi Pearcy: Learn Angular Basics by Building a Simple App Using Angular Material

Предыдущая статьяДокеризируем среду разработки в VS Code
Следующая статьяПочему каркасы бесполезны?