Тестовое задание на Angular. Работа с формами.
В данной статье рассмотрим работу с формами в Angular и создадим форму бронирования.
В предыдущей статье был обзор создания страницы апартаментов. Была выведена вся информация о выбранном варианте кроме блока с бронированием. Блок бронирования представляет собой форму, в которой есть 2 поля: срок бронирования (даты начала и конца бронирования).
Создание BookingCard
Создадим модуль и компонент:
В данном компоненте, нам интересен блок в шаблоне:
<app-room-booking-form [room]="room">
<app-room-booking-price [room]="room"></app-room-booking-price>
</app-room-booking-form>
Рассмотрим RoomBookingForm
. Опишем интерфейс формы:
export enum BookingField {
Period = 'period',
PeriodStart = 'start',
PeriodEnd = 'end',
Guests = 'guests',
}export interface BookingDetails {
[BookingField.Period]: {
[BookingField.PeriodStart]: string;
[BookingField.PeriodEnd]: string;
};
[BookingField.Guests]: number;
}
В данном случае мы имеем форму из двух полей:
- поле период, которое является составным и имеет два значения, дата начала заезда и дата выезда из апартаментов
- поле количество гостей, от которого зависит стоимость.
В Angular есть мощный инструментарий для создания и управления форм представленный двумя модулями: FormsModule и ReactiveForms.
В формах angular есть две ключевых сущности: FormControl и FormGroup.
FormControl
— это любой элемент формы — input, select, checkbox, radio, range, textarea и др. FormGroup
— коллекция FormControl’ов, представленная в виде объекта, где имя свойства будет именем соответствующего FormControl
, а значением объекта будет сам FormControl
. Другими словами FormGroup
позволяет вкладывать формы в форму.
FormContol
, как и FormGroup
имеет ряд полезных свойств:
- Набор валидаторов, которые позволяют определять является ли введенное значение валидным;
- Набор состояний (
touched
,dirty
, и т.д.), которые могут сообщить о действиях с формой и элементом; - Ряд функций для установления состояний (
disabled
,touched
); - Каждый элемент формы имеет ссылку на родителя (у корневой формы, значение будет
null
).
Обязательно посмотрите официальную документацию Angular, связанную с формами. Это позволит вам сэкономить много времени при реализации форм.
Создадим FormGroup
.
FormGroup
можно создать двумя способами:
- Создание с помощью новых экземпляров
- Создание с помощью сервиса FormBuilder’а
В данном случае используется первый подход. Как видно из примера, сначала создается главная форма, которая является FormGroup
, и где элементами формы являются FormGroup
, который представляет собой FormContol’ы даты начала и конца бронирования, а также FormContol
для количества гостей.
Добавим шаблон RoomBookingForm
:
Разберем компонент по шагам. Сначала при инициализации компонента создаем форму:
form!: FormGroup;ngOnInit(): void {
this.form = getRoomBookingForm();
}
где функция getRoomBookingForm() просто вынесена отдельно:
export function getRoomBookingForm(): FormGroup {
return new FormGroup({
[BookingField.Period]: new FormGroup({
[BookingField.PeriodStart]: new FormControl(null, [Validators.required]),
[BookingField.PeriodEnd]: new FormControl(null, [Validators.required]),
}),
[BookingField.Guests]: new FormControl(null, [Validators.required, Validators.min(1)]),
});
}
В шаблоне форма выводится следующим образом:
<app-room-booking-date
[control]="form | extractFormGroup: BookingField.Period">
</app-room-booking-date>
<app-room-booking-guest
[control]="form | extractFormControl: BookingField.Guests"
[room]="room">
</app-room-booking-guest>
...
<div class="room-booking-actions">
<button
automation-id="room-booking-submit"
class="room-booking-action"
type="button"
mat-raised-button
color="warn"
(click)="onBooking()"
>
Забронировать
</button>
</div>
Как видно из реализации, были созданы 2 компонента, которые являются реализацией каждого из полей.
RoomBookingDate
— это реализация поля выбора диапазона бронированияRoomBookingGuest
— это реализация поля количества гостей.
Кнопка “Забронировать” вызывает обработчик onBooking():
onBooking(): void {
if (this.form.valid) {
this.matDialog.open(RoomBookingDialogComponent);
} else {
this.showError = true;
}
this.changeDetectorRef.markForCheck();
}
Из примера видно, что сначала проверяется — является ли форма валидной. Если форма валидна, то тогда показываем RoomBookingDialog
, иначе показываем ошибку в форме:
<div class="room-booking-error" *ngIf="showError">
Выберите период и количество гостей.
</div>
Рассмотрим реализацию элементов формы.
Создадим RoomBookingGuest
модуль и компонент:
Компонент использует Angular Material для создания компонента:
<mat-form-field class="ui-field" appearance="fill" *ngIf="control">
<mat-label automation-id="room-booking-guests-label">Гостей</mat-label>
<mat-select automation-id="room-booking-guests-value" [formControl]="control">
<mat-option *ngFor="let guest of guests" [value]="guest">
{{ guest }}
</mat-option>
</mat-select>
</mat-form-field>
MatFormField
создает рамку вокруг элемента, MatLabel
добавляет label, а MatSelect
соответственно реализует сам select.
Подробнее с работой Angular Material можно ознакомиться в документации.
В компоненте гостей, в select’е выводим количество гостей (1, 2, 3, ..)
@Input() room!: RoomExtended;
get guests(): number[] {
return Array.from({ length: this.room?.guests ?? 1 }, (value, key) => key + 1);
}
Отметим, что Array.from создаст массив, где начальными значениями будет key + 1.
Создадим компонент и модуль для выбора диапазона.
Как и в случае с количеством гостей, будем использовать Angular Material. Используем стандартный datepicker
:
<mat-form-field class="ui-field" appearance="fill" *ngIf="control">
<mat-label automation-id="room-booking-date-label">Прибытие - Выезд</mat-label>
<mat-date-range-input
automation-id="room-booking-date-range"
[formGroup]="control"
[rangePicker]="tripDate"
[comparisonStart]="control.value.start"
[comparisonEnd]="control.value.end"
[min]="minDate"
[max]="maxDate"
>
<input automation-id="room-booking-date-start" matStartDate placeholder="Прибытие" [formControlName]="BookingField.PeriodStart" />
<input automation-id="room-booking-date-end" matEndDate placeholder="Выезд" [formControlName]="BookingField.PeriodEnd" />
</mat-date-range-input>
<mat-datepicker-toggle automation-id="room-booking-datepicker-toggle" matSuffix [for]="tripDate"></mat-datepicker-toggle>
<mat-date-range-picker automation-id="room-booking-range-picker" [touchUi]="!isDesktopScreen" #tripDate></mat-date-range-picker>
</mat-form-field>
Как видно из реализации, создается input, а также элементы, которые открывают datepicker
.
Из особенностей, укажем, что в мобильной версии используется touchUi
.
Реализация RoomBookingPriceService
После того, как есть форма, добавим небольшой сервис, который будет рассчитывать стоимость по заданным параметрам.
В данном случае имеем следующую логику:
- определяем количество дней бронирования
- в качестве комиссии берем 15% за использование сервисом
- в качестве комиссии за уборку — 1%
Теперь свяжем расчет стоимости с изменением формы, вызовом соответствующего фасада:
this.form.valueChanges
.pipe(
filter(() => this.form.valid),
tap(() => {
this.showError = false;
this.edited = true;
this.bookingService.setBookingDetails(this.form.value);
this.changeDetectorRef.markForCheck();
}),
takeUntil(this.destroy$)
)
.subscribe();
Теперь при изменении количества гостей и срока бронирования, будут отображаться новые условия.
Ссылки
Вернуться к оглавлению — Введение.
Следующая статья — Навигация в приложении.
Предыдущая статья — Создание страницы апартаментов.
Все исходники на github/fafnur/barinb.
Группа в Medium: https://medium.com/fafnur
Группа в Vkontakte: https://vk.com/fafnur
Группа в Facebook: https://www.facebook.com/groups/fafnur/
Telegram канал: https://t.me/f_a_f_n_u_r
Twitter: https://twitter.com/Fafnur1
LinkedIn: https://www.linkedin.com/in/fafnur