Сайт визитка на Angular. Модуль оформления заказа.

Aleksandr Serenko
F.A.F.N.U.R
Published in
7 min readMar 27, 2022
Сайт визитка на Angular. Модуль оформления заказа

В данной статье разберем реализацию модуля оформления заказа. Создадим форму с данными клиента и реализуем API отправки заявки на сервер.

Процесс заказа представляет собой список товаров в корзине и контактную информацию о клиенте. Для того, чтобы получить список товаров, клиенту необходимо добавить выбранные товары в корзину. И чтобы оформить заказ, пользователю необходимо заполнить форму с личными данными.

Интерфесы

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

Создадим библиотеку:

nx g lib orders/common

Создадим файл order.interface.ts:

Сущность клиента представляет собой следующий набор данных:

export interface Customer {
name: string;
phone: string;
email: string;
city: string;
postcode: string;
address: string;
}

Где:

  • name — полное имя клиента;
  • phone — номер телефона;
  • email — электронный адрес;
  • city — город;
  • address — адрес;
  • postcode — почтовый индекс.

Для создания товара используется сущность OrderCreate:

export interface OrderCreate {
customer: Customer;
cartProducts: CartProduct[];
}

Как и было сказано ранее, что заказ это список товаров в корзине и личные данные клиента.

Для полной информации интерфейс Order содержит еще список выбранных товаров.

export interface Order extends OrderCreate {
products: Product[];
}

После успешного оформления заказа, появляется информация о заказе — OrderDetails.

export interface OrderDetails {
id: string;
}

Ключевым свойством является номер заказа — id.

Order Api Service

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

Создадим библиотеку для OrderApi:

nx g lib orders/api

Создадим сервис OrderApiService:

nx g s orders-api --project=orders-api

Сервис содержит всего один метод — createOrder, который принимает Order и возвращает OrderDetails.

Отметим, так как для приложения используется SSR, то и API по созданию заказа будет реализовано там же. В простом варианте можно просто сделать отправку сообщения в telegram или на email.

Модуль лишь подключает OrderApiService:

Order State

Добавим feature state для управления процессом оформления заказа.

Сгенерируем новую библиотеку для orders state:

nx g lib orders/state

Сгенерируем state:

nx g @nrwl/angular:ngrx order --module=libs/orders/state/src/lib/orders-state.module.ts

Изменим order.actions.ts:

  • init — экшен, который запускает восстановление данных о клиенте, если они ранее были введены
  • restore — экшен, который читает из localStorage значение клиента и записывает его в state;
  • createOrder — экшен, который запускает эффект по созданию заказа;
  • createOrderSuccess — экшен, который выполняется в случае успешного оформления заказа;
  • creareOrderFailure — экшен, который запускается если при оформлении заказа произошла ошибка;
  • updateCustomer — экшен, который вызывается каждый раз при изменении данных о клиенте.

Изменим order.reducer.ts:

State хранит все 2 значения:

  • customer — информация о клиенте;
  • orderCreating — флаг, который говорит о том, идет ли сейчас процесс оформления заказа.

Редьюсер обрабатывает следующие экшены: restore, updateCustomer, createOrder, createOrderSuccess и createOrderFailure.

Актуализируем файл order.selectors.ts:

Как и было сказано ранее, селекторы предоставляют доступ к двум свойствам: customer и orderCreating.

Обновим эффекты в файле order.effects.ts:

Как видно из примера, были определены следующие эффекты:

  • init$ — эффект, который запускает восстановление данных из localStorage;
  • createOrder$ — эффект, который запускает процесс оформления заказа;
  • createOrderSuccess$ — эффект, который при успешном оформлении заказа удаляет информацию о клиенте;
  • updateCustomer$ — эффект, который при изменении личных данных клиента, сохраняет их в localStorage.

Все эффекты просты в реализации, которые поверяя условия, вызывают те или иные экшены.

Например, эффект оформления заказа:

createOrder$ = createEffect(() => {
return this.actions$.pipe(
ofType(OrderActions.createOrder),
concatLatestFrom(() => this.store.select(selectProducts)),
fetch({
run: ({ order }, products) => {
return this.ordersApiService
.createOrder({ ...order, products })
.pipe(map((orderDetails) => OrderActions.createOrderSuccess({ orderDetails })));
},
onError: (action, error) => OrderActions.createOrderFailure({ error }),
})
);
});

В данном случае, при запуске экшена createOrder, эффект запускает метод оформления заказа createOrder из OrderApiService. В случае успешного ответа от сервера, будет испущен экшен createOderSucces, иначе эффект вызовет экшен createOrderFailure.

Обновим order.facade.ts:

В фасаде предоставляет доступ к селекторам customer и orderCreating. Также есть observable для событий оформления заказа — createOrderSuccess$ и createOrderFailure$.

Order UI

Наличие OrderState позволяет добавить несколько компонентов для оформления заказа.

Order Info

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

Создадим библиотеку для компонента:

nx g lib orders/ui/info

Добавим компонент в библиотеку:

nx g c order-info --project=orders-ui-info

Для реализации используется MatTableModule, который создает таблицу.

<table automation-id="table" mat-table [dataSource]="cartProducts" *ngIf="cartProducts$ | async; let cartProducts">
<ng-container matColumnDef="title">
<th mat-header-cell *matHeaderCellDef i18n="Order info|Title head">Title</th>
<td mat-cell *matCellDef="let element">{{ element.product?.title }}</td>
<td mat-footer-cell *matFooterCellDef colspan="3" i18n="Order info|Total label">Total</td>
</ng-container>

<ng-container matColumnDef="size">
<th mat-header-cell *matHeaderCellDef i18n="Order info|Size head">Size</th>
<td mat-cell *matCellDef="let element">{{ element.size }}</td>
</ng-container>

<ng-container matColumnDef="count">
<th mat-header-cell *matHeaderCellDef i18n="Order info|Count head">Count</th>
<td mat-cell *matCellDef="let element">{{ element.count }}</td>
</ng-container>

<ng-container matColumnDef="price">
<th mat-header-cell *matHeaderCellDef i18n="Order info|Price head">Price</th>
<td mat-cell *matCellDef="let element">{{ element.product?.price | currency }}</td>
<td mat-footer-cell *matFooterCellDef>
{{ cartProducts | cartTotalPrice | async | currency }}
</td>
</ng-container>

<tr mat-header-row *matHeaderRowDef="columns"></tr>
<tr mat-row *matRowDef="let row; columns: columns"></tr>

<tr mat-footer-row *matFooterRowDef="columnsFooter"></tr>
</table>

MatTable мощный, сложный компонент. Подробнее можно ознакомиться в документации — Angular Material Table.

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

export class OrderInfoComponent implements OnInit {
readonly columns = ['title', 'size', 'count', 'price'];
readonly columnsFooter = ['title', 'price'];

cartProducts$!: Observable<(CartProduct & { product?: Product })[]>;

constructor(private readonly cartFacade: CartFacade, private readonly productFacade: ProductFacade) {}

ngOnInit(): void {
this.cartProducts$ = combineLatest([
this.cartFacade.cartProducts$.pipe(isNotNullOrUndefined()),
this.productFacade.productsEntities$.pipe(isNotNullOrUndefined()),
]).pipe(
map(([cartProducts, products]) => cartProducts.map((cartProduct) => ({ ...cartProduct, product: products[cartProduct.productId] })))
);
}
}

В результате получим:

Order Form

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

Создадим библиотеку для компонента:

nx g lib orders/ui/form

Добавим компонент:

nx g c order-form --project=orders-ui-form

В шаблоне формы можно увидеть, что все элементы формы являются отдельными компонентами:

<mat-card>
<mat-card-title i18n="Order form|Title">Order Form</mat-card-title>
<mat-card-content>
<form automation-id="form" [formGroup]="form" (ngSubmit)="onSubmit()">
<banshop-order-name automation-id="name" [control]="form | banshopExtractFormControl: fields.Name"></banshop-order-name>
<banshop-row tablet>
<banshop-column tablet="6">
<banshop-order-phone automation-id="phone" [control]="form | banshopExtractFormControl: fields.Phone"></banshop-order-phone>
</banshop-column>
<banshop-column tablet="6">
<banshop-order-email automation-id="email" [control]="form | banshopExtractFormControl: fields.Email"></banshop-order-email>
</banshop-column>
</banshop-row>
<banshop-order-city automation-id="city" [control]="form | banshopExtractFormControl: fields.City"></banshop-order-city>
<banshop-order-address automation-id="address" [control]="form | banshopExtractFormControl: fields.Address"></banshop-order-address>
<banshop-order-postcode
automation-id="postcode"
[control]="form | banshopExtractFormControl: fields.Postcode"
></banshop-order-postcode>
<banshop-order-actions>
<button
automation-id="submit"
type="submit"
mat-raised-button
color="primary"
i18n="Order form|Submit button"
[disabled]="submitted"
>
Order
</button>
</banshop-order-actions>
</form>
</mat-card-content>
</mat-card>

В компоненте сначала создается новая форма:

this.form = new FormGroup({
[CustomerField.Name]: new FormControl(null, [Validators.required]),
[CustomerField.Phone]: new FormControl(null, [Validators.required]),
[CustomerField.Email]: new FormControl(null, [Validators.required, Validators.email]),
[CustomerField.City]: new FormControl(null, [Validators.required]),
[CustomerField.Address]: new FormControl(null, [Validators.required]),
[CustomerField.Postcode]: new FormControl(null, [Validators.required]),
});

Затем идет подписка на данные в state. Если в state есть данные о клиенте, то они добавляются в форму:

this.orderFacade.customer$
.pipe(
take(1),
isNotNullOrUndefined(),
tap((customer) => this.form.patchValue(customer)),
takeUntil(this.destroy$)
)
.subscribe();

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

this.form.valueChanges
.pipe(
isNotNullOrUndefined(),
debounceTime(1000),
tap((customer) => this.orderFacade.updateCustomer(customer)),
takeUntil(this.destroy$)
)
.subscribe();

Если заказ оформлен успешно, то клиенту показывается соответствующий диалог, который вызывается с помощью сервиса:

this.orderFacade.createOrderSuccess$
.pipe(
tap((orderDetails) => {
this.submitted = false;
this.orderNotifyService.openSuccessDialog(orderDetails);
void this.navigationService.navigateByUrl(this.navigationService.getPaths().home);
this.changeDetectorRef.markForCheck();
}),
takeUntil(this.destroy$)
)
.subscribe();

Если произошла ошибка при оформлении заказа, то показывается окно с ошибкой:

this.orderFacade.createOrderFailure$
.pipe(
tap(() => {
this.submitted = false;
this.orderNotifyService.openFailureDialog();
this.changeDetectorRef.markForCheck();
}),
takeUntil(this.destroy$)
)
.subscribe();

При клике по кнопке оформить заказ, идет вызов метода onSubmit().

onSubmit(): void {
this.form.markAllAsTouched();
if (this.form.valid && !this.submitted) {
this.submitted = true;
this.orderFacade.createOrder(this.form.value);
}

this.changeDetectorRef.markForCheck();
}

Метод проверяет форму на валидность данных. Если данные валидны, то тогда идет оформление заказа. Иначе показываются ошибки, где пользователя просят заполнить обязательные поля.

Добавим элементы формы:

В результате получим форму:

Order Notify

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

Добавим библиотеку:

nx g lib orders/ui/notify

Создадим сервис:

nx g s order-notify --project=orders-ui-notify

Вывод информации происходит с помощью диалоговых окон.

Создадим два окна:

nx g m order-notify-failure --project=orders-ui-notify
nx g c order-notify-failure --project=orders-ui-notify
nx g m order-notify-success --project=orders-ui-notify
nx g c order-notify-success --project=orders-ui-notify

Добавим отображение:

В итоге получим:

Order Page

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

Добавим библиотеку:

nx g lib orders/page

Добавим компонент:

nx g c order-page --project=orders-page

Добавим модуль роутинга:

nx g m order-page-routing --project=orders-page

Из реализации компонента видно, что страница заказа всего-лишь выводит некий набор компонентов:

<h1 automation-id="title" i18n="Order page|Order title">Making an order</h1>
<banshop-order-info automation-id="info"></banshop-order-info>
<banshop-order-form automation-id="form"></banshop-order-form>

Order Guard

В конце добавим проверку доступности страницы оформления заказа.

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

Добавим библиотеку:

nx g lib orders/guards

Добавим гуард order.guard.ts:

Как и было сказано ранее, гуард проверяет есть ли товары в корзине и либо разрешает клиенту посетить страницу, либо отравляет его на главную.

Ссылки

Оглавление

Предыдущая статья — Модуль корзины.

Следующая статья — Модуль чата.

Все исходники находятся на github, в репозитории:

Для того, чтобы посмотреть состояние проекта на момент написания статьи, нужно выбрать соответствующий тег — article.

Подписывайтесь на блог, чтобы не пропустить новые статьи про Angular, и веб-разработку. Medium | Telegram| VK |Tw| Ln

--

--

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

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