Тестовое задание на Angular. Создание страницы в админ панели.

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

В данной статье приведем реализацию страницы админ панели для сущности номер (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

--

--

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

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