Тестовое задание на Angular. Работа с формами.

Aleksandr Serenko
F.A.F.N.U.R
Published in
4 min readJul 5, 2021

В данной статье рассмотрим работу с формами в 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

--

--

Aleksandr Serenko
F.A.F.N.U.R

Senior Front-end Developer, Angular evangelist, Nx apologist, NodeJS warlock