Готовы сегодня создать что-нибудь интересное? Я тоже!
На данный момент я тружусь в роли Angular-разработчика, создавая для сотрудников нашей компании инструменты, которые помогают им более эффективно и комфортно решать насущные задачи. Работать с Angular я начала только в мае 2020, но ввиду своего глубокого увлечения фронтенд-разработкой планирую освоить этот навык профессионально.
Я глубоко убеждена, что единственный способ научиться чему-либо — это начать заниматься этим на практике. Только методом смелых проб и ошибок можно шаг за шагом выстроить пирамиду опыта и вырасти из новичка в рок-звезду.
“Обучение — это активный процесс, и учимся мы путем активной деятельности. Как итог — в виде устойчивых навыков оседают только те знания, которые применяются на практике”, — Дэйл Карнеги.
Что будем изучать?
Мы создадим базовую версию приложения для отслеживания привычек, где будут содержаться соответствующие записи, которые можно будет добавлять, редактировать и удалять. Это будет пошаговая инструкция, в которой мы поочередно соберем все компоненты UI и бэкенд, чтобы вы могли наглядно проследить процесс их объединения воедино.
Здесь мы:
- настроим приложение Angular с нуля — без стартовых файлов, полностью разобрав весь процесс;
- установим Angular Material и используем ряд его компонентов для создания красивого UI;
- включим в шаблон UI некоторые из структурных директив Angular для показа и скрытия элементов на основе состояния компонента;
- реализуем простую реактивную форму для получения входных данных.
В конце урока у вас будет рабочее приложение, управляющее списком привычек, которые можно добавлять, редактировать и удалять. Поехали!
Для тех, кто не хочет читать: демо-версия приложения доступна на StackBlitz.
Базовая настройка
Если вы ранее не создавали приложение Angular на своей машине, то убедитесь, что у вас установлены Node.js и Angular CLI.
Откройте предпочтительный для вас терминал. Я в данном уроке буду использовать его встроенный в VSCode вариант.
Перейдите в каталог, где хотите создать приложение и введите следующую команду:
Вам будет задано два вопроса:
- Would you like to add Angular routing? (Добавить маршрутизацию Angular?) — для подтверждения введите y и нажмите ввод.
- Which stylesheet format would you like to use? (Какой формат таблицы стилей использовать?) — выберите предпочтительный с помощью клавиш стрелок. Мне нравится вариант SCSS.
После генерации этих пакетов можно переходить к настройке Angular Material (AM). Для начала перейдите в каталог приложения командой cd и добавьте эту библиотеку в проект:
Вам также понадобится ответить на ряд вопросов:
- Choose the Deep Purple/Amber pre-built theme (Выберите тему Deep Purple/Amber).
- Set up global Angular Material typography styles? (Настроить глобальные стили оформления Angular Material?) — для подтверждения введите y и нажмите ввод.
- 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, избежав лишней ручной работы со стилями.
Читайте также:
- Зачем и как реализовать ленивую загрузку компонентов в Angular
- Стратегии обнаружения изменений в Angular - «onPush» и «Default»
- Резолвер в Angular для предвыборки данных
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Jessi Pearcy: Learn Angular Basics by Building a Simple App Using Angular Material