Тестовое задание на Angular. Создание страницы в админ панели.
В данной статье приведем реализацию страницы админ панели для сущности номер (room
).
В предыдущей статье был создан layout
для админ страниц. Используем его при создании страницы.
Реализация AdminRoomPage
Создадим модуль и компонент:
Как и во всем приложении, используем два экрана:
<app-admin-rooms-actions automation-id="admin-rooms-actions"></app-admin-rooms-actions>
<ng-container *ngIf="isDesktopScreen; then desktopTpl; else mobileTpl"></ng-container>
<ng-template #desktopTpl>
<app-admin-rooms-table automation-id="admin-rooms-table" [data]="rooms" *ngIf="isDesktopScreen"></app-admin-rooms-table>
</ng-template>
<ng-template #mobileTpl>
<app-admin-rooms-list automation-id="admin-rooms-list" [data]="rooms" *ngIf="!isDesktopScreen"></app-admin-rooms-list>
</ng-template>
В десктопной версии выводим таблицу с сущностями, в мобильной версии список сущностей с возможностью изменить их.
Для получения сущностей используется RoomManamger:
this.roomManager.roomsExtended$
.pipe(
tap((rooms) => {
this.rooms = rooms;
this.changeDetectorRef.markForCheck();
}),
takeUntil(this.destroy$)
)
.subscribe();
RoomManager был рассмотрен ранее в статьях, но приведем его еще раз, для полноты понимания:
Как видно из реализации, сначала получаются данные, а потом выводятся в компонентах:
<ng-template #desktopTpl>
<app-admin-rooms-table automation-id="admin-rooms-table" [data]="rooms" *ngIf="isDesktopScreen"></app-admin-rooms-table>
</ng-template>
<ng-template #mobileTpl>
<app-admin-rooms-list automation-id="admin-rooms-list" [data]="rooms" *ngIf="!isDesktopScreen"></app-admin-rooms-list>
</ng-template>
Реализация AdminRoomTable
Создадим модуль и компонент таблицы:
В данном случае используется модуль Angular Material Table, который создает и заполняет таблицу. Опишем столбцы в таблице:
displayedColumns: string[] = [
RoomField.ID,
RoomField.Photos,
RoomField.Price,
RoomField.Created,
RoomField.Updated,
RoomField.Building,
RoomField.Person,
'actions',
];
При изменении данных всегда обновляем таблицу:
@Input() set data(rooms: RoomExtended[] | null) {
this.rooms = rooms;
if (!this.dataSource) {
this.dataSource = new MatTableDataSource<RoomExtended>();
}
this.dataSource.data = this.rooms?.length ? this.rooms : [];
this.changeDetectorRef.markForCheck();
}
Для отображения определенных ячеек таблицы, для каждого поля создадим макет:
<ng-container [matColumnDef]="RoomField.ID">
<th mat-header-cell automation-id="admin-room-id-label" *matHeaderCellDef>ID</th>
<td mat-cell automation-id="admin-room-id-value" *matCellDef="let element">
{{ element.id }}
</td>
</ng-container>
В примере выше приведен шаблон для вывода ID
сущности.
Для вывода полей с датами, шаблоны будут похожи на:
<ng-container [matColumnDef]="RoomField.Created">
<th mat-header-cell *matHeaderCellDef>
Дата создания
</th>
<td mat-cell *matCellDef="let room">
{{ room.created | date: 'longDate' }}
</td>
</ng-container>
Задав все поля, получим следующее:
Реализация AdminRoomList
Создадим модуль и компонент списка:
Список представляет собой просто вывод сущностей с помощью AdminRoomCardComponent.
Реализация AdminRoomCard
Создадим модуль и компонент карточки:
Как видно из примера, для отображения карточки используется Angular Material Card
.
<mat-card>
<mat-card-header>
...
</mat-card-header>
<mat-card-content>
...
</mat-card-content>
<mat-card-actions>
...
</mat-card-actions>
</mat-card>
В шапке выводим ссылку на собственника:
<a
automation-id="admin-room-card-avatar"
mat-card-avatar
class="room-card-avatar"
[routerLink]="NavigationPath.AdminPersonsPage | navPath"
[queryParams]="{ person: room.buildingExtended.personExtended.id }"
[ngStyle]="room.buildingExtended.personExtended.avatar | backgroundImage"
>
</a>
Имя собственника как заголовок:
<mat-card-title automation-id="admin-room-card-title">{{ room.buildingExtended.personExtended | personFullName }}</mat-card-title>
И адрес как подзаголовок:
<mat-card-subtitle automation-id="admin-room-card-subtitle">
{{ room.buildingExtended.city }}, {{ room.buildingExtended.address }}
</mat-card-subtitle>
В качестве изображения выводим карусель:
<div mat-card-image>
<app-carousel automation-id="admin-room-card-carousel" class="room-card-carousel" [images]="room.photos"></app-carousel>
</div>
В контенте соответствующие особенности апартаментов:
<div class="room-card-props">
<div automation-id="admin-room-card-price" class="room-card-prop">{{ room.price }} ₽</div>
<div automation-id="admin-room-card-created" class="room-card-prop">{{ room.created | date: 'longDate' }}</div>
</div>
<div automation-id="admin-room-card-description">
{{ room.description }}
</div>
В качестве действий, выводим компонент, который позволяет изменять и удалять апартаменты:
<app-admin-room-actions automation-id="admin-room-actions" [room]="room"></app-admin-room-actions>
Реализация AdminRoomActions
Создадим модуль и компонент:
В данном случае имеются три кнопки:
<button automation-id="admin-room-view" type="button" mat-icon-button (click)="onView()">
<mat-icon color="primary">visibility</mat-icon>
</button>
<button automation-id="admin-room-edit" type="button" mat-icon-button (click)="onEdit()"><mat-icon color="accent">edit</mat-icon></button>
<button automation-id="admin-room-remove" type="button" mat-icon-button (click)="onRemove()">
<mat-icon color="warn">delete</mat-icon>
</button>
При нажатии на первую кнопку отображается детальная информация об апартаментах. При нажатии на вторую кнопку, открывается форма редактирования сущности, а при нажатии на третью кнопку спрашивается подтверждение удаления сущности и при получении одобрения, сущность удаляется.
Соответственно каждое действие показывает popup
:
onView(): void {
this.matDialog.open(AdminRoomViewDialogComponent, { data: this.room, width: '100%' });
}
onEdit(): void {
this.matDialog.open(AdminRoomEditDialogComponent, { data: this.room });
}
onRemove(): void {
const dialogRef = this.matDialog.open(AdminRoomRemoveDialogComponent, { data: this.room });
dialogRef
.afterClosed()
.pipe(
tap((result) => {
if (result) {
this.roomManager.removeRoom(this.room);
}
}),
take(1),
takeUntil(this.destroy$)
)
.subscribe();
}
Как видно из реализации, при клике на удаление происходит отслеживание закрытия окна. И только в том случае, если нажали удалить в popup’е, то тогда происходит удаление.
Реализация AdminRoomClearDialog
Создадим модуль и компонент:
В компоненте выводится две кнопки, где нужно либо подтвердить удаление, либо отменить.
<h2 mat-dialog-title automation-id="admin-room-clear-dialog-title">Внимание</h2>
<div mat-dialog-content automation-id="admin-room-clear-dialog-content">
<p>Вы действительно хотите удалить все номера?</p>
</div>
<div mat-dialog-actions align="center">
<button
automation-id="admin-room-clear-dialog-cancel"
type="button"
class="admin-dialog-action"
color="primary"
mat-raised-button
(click)="onCancel()"
>
Нет
</button>
<button
automation-id="admin-room-clear-dialog-success"
type="button"
class="admin-dialog-action"
color="warn"
mat-raised-button
(click)="onSuccess()"
>
Да
</button>
</div>
Как видно из реализации компонент обрабатывает событие закрытие попапа:
onCancel(): void {
this.dialogRef.close(false);
}onSuccess(): void {
this.dialogRef.close(true);
}
Реализация AdminRoomCreateDialog
Создадим модуль и компонент:
В компоненте выводится форма создания апартаментов и кнопки с подтверждением и отменой:
<mat-dialog-content>
<app-admin-room-form automation-id="admin-room-form" [form]="form"></app-admin-room-form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button automation-id="admin-room-cancel" mat-raised-button mat-dialog-close>Отмена</button>
<button automation-id="admin-room-add" mat-raised-button color="primary" (click)="onSubmit()">Добавить</button>
</mat-dialog-actions>
Если форма заполнена и валидна, то создаем новые апартаменты, иначе подсвечиваем незаполненные поля.
onSubmit(): void {
this.form.markAllAsTouched();
if (this.form.valid) {
this.roomFacade.addRoom(this.form.value);
} else {
this.formErrorsService.scrollToFirstError(this.form, ROOMS_IDS);
}
this.changeDetectorRef.markForCheck();
}
Реализация AdminRoomForm
Создадим модуль и компонент:
Список доступных полей:
export enum RoomField {
ID = 'id',
Building = 'building',
Person = 'person',
Guests = 'guests',
Bedrooms = 'bedrooms',
Bathrooms = 'bathrooms',
Beds = 'beds',
Price = 'price',
Description = 'description',
Photos = 'photos',
Amenities = 'amenities',
Created = 'created',
Updated = 'updated',
}
Создадим форму и конфиг:
export const ROOM_FORM_CONFIG: Record<string, any> = {
[RoomField.Person]: [null, [Validators.required]],
[RoomField.Building]: [null, [Validators.required]],
[RoomField.Price]: [null, [Validators.required, Validators.min(0)]],
[RoomField.Description]: [null, [Validators.required, Validators.minLength(1)]],
[RoomField.Guests]: [null, [Validators.required, Validators.min(0)]],
[RoomField.Bedrooms]: [null, [Validators.required, Validators.min(0)]],
[RoomField.Beds]: [null, [Validators.required, Validators.min(0)]],
[RoomField.Bathrooms]: [null, [Validators.required, Validators.min(0)]],
[RoomField.Photos]: [null, [Validators.required]],
[RoomField.Amenities]: [null, []],
};
export function createRoomForm(form: FormGroup): void {
for (const [key, value] of Object.entries(ROOM_FORM_CONFIG)) {
form.addControl(key, new FormControl(value[0], value[1]));
}
}
И сама форма создается в момент инициализации компонента:
ngOnInit(): void {
if (!this.form) {
this.form = new FormGroup({});
}
createRoomForm(this.form);
if (this.room) {
this.form.patchValue({
...this.room,
[RoomField.Person]: this.room.buildingExtended.person,
});
}
}
Отметим, что createRoomForm просто добавляет поля в форму.
Компонент выводит элементы формы:
<ng-container *ngIf="form">
<app-admin-room-person automation-id="admin-room-person" [control]="form | extractFormControl: RoomField.Person"></app-admin-room-person>
<app-admin-room-building
automation-id="admin-room-building"
[control]="form | extractFormControl: RoomField.Building"
></app-admin-room-building>
<app-row mode="md">
<app-column [modes]="{ md: 6 }">
<app-admin-room-price automation-id="admin-room-price" [control]="form | extractFormControl: RoomField.Price"></app-admin-room-price>
</app-column>
<app-column [modes]="{ md: 6 }">
<app-admin-room-guests
automation-id="admin-room-guests"
[control]="form | extractFormControl: RoomField.Guests"
></app-admin-room-guests>
</app-column>
</app-row>
<app-row mode="md">
<app-column [modes]="{ md: 6 }">
<app-admin-room-bedrooms
automation-id="admin-room-bedrooms"
[control]="form | extractFormControl: RoomField.Bedrooms"
></app-admin-room-bedrooms>
</app-column>
<app-column [modes]="{ md: 6 }">
<app-admin-room-beds automation-id="admin-room-beds" [control]="form | extractFormControl: RoomField.Beds"></app-admin-room-beds>
</app-column>
</app-row>
<app-row mode="md">
<app-column [modes]="{ md: 6 }">
<app-admin-room-bathrooms
automation-id="admin-room-bathrooms"
[control]="form | extractFormControl: RoomField.Bathrooms"
></app-admin-room-bathrooms>
</app-column>
<app-column [modes]="{ md: 6 }">
<app-admin-room-amenities
automation-id="admin-room-amenities"
[control]="form | extractFormControl: RoomField.Amenities"
></app-admin-room-amenities>
</app-column>
</app-row>
<app-admin-room-photos automation-id="admin-room-photos" [control]="form | extractFormControl: RoomField.Photos"></app-admin-room-photos>
<app-admin-room-description
automation-id="admin-room-description"
[control]="form | extractFormControl: RoomField.Description"
></app-admin-room-description>
</ng-container>
Соответственно создание формы делегировано на уровень выше.
Создадим элементы формы, где просто используем Angular Material
для отображения и ReactiveForms
.
AdminRoomAmenities:
AdminRoomBathrooms:
AdminRoomBedrooms:
AdminRoomBeds:
AdminRoomBuilding:
AdminRoomDescription:
AdminRoomGuests:
AdminRoomPerson:
AdminRoomPhotos:
AdminRoomPrice:
Реализация AdminRoomEditDialog
Создадим модуль и компонент:
По аналогии с созданием новых апартаментов, выводим форму и подставляем значения:
ngOnInit(): void {
this.roomFacade.roomChanged$
.pipe(
tap(() => {
this.matDialogRef.close(true);
this.matSnackBar.open('Номер успешно изменен!', '', { duration: 5000 });
}),
takeUntil(this.destroy$)
)
.subscribe();
}onSubmit(): void {
this.form.markAllAsTouched();
if (this.form.valid) {
this.roomFacade.changeRoom({ ...this.form.value, [RoomField.ID]: this.room.id });
} else {
this.formErrorsService.scrollToFirstError(this.form, ROOMS_IDS);
}
this.changeDetectorRef.markForCheck();
}
Реализация AdminRoomRemoveDialog
Создадим модуль и компонент:
Аналогично с компонентом очистки, в компоненте используется две кнопки, где можно либо подтвердить либо отменить операцию.
onCancel(): void {
this.dialogRef.close(false);
}
onSuccess(): void {
this.dialogRef.close(true);
}
Реализация AdminRoomViewDialog
Создадим модуль и компонент:
В данном компоненте выводится подробная информация о компоненте.
Ссылки
Вернуться к оглавлению — Введение.
Следующая статья — Тестирование сервисов.
Предыдущая статья — Создание admin лейаута.
Все исходники на 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