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



Книга Знакомимся с основами 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, избежав лишней ручной работы со стилями.

636   0  

Comments

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