Сайт визитка на 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
или на
Модуль лишь подключает 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