Сайт визитка на Angular. Модуль чата.
В данной статье создадим макет чата службы поддержки. Создадим feature state
, а также добавим методы добавления новых сообщений.
Чат на сайте достаточно частое явление на сайтах. Чтобы все смотрелось колоритно, встроим чат в приложение.
Возможно стоило бы реализовать интеграцию стороннего решения с собственным API, но это сложно и очень индивидуально. Идейно, никто не мешает добавить интеграцию с telegram, написав бота — но это не в рамках данной статьи.
Интерфейсы
Создадим интерфейсы, которые будут описывать сущности чата и сообщений.
Создадим новую библиотеку:
nx g lib chat/common
Создадим файл chat.interface.ts
:
Chat
— чат представляет собой простую сущность, в которой есть только набор сообщений.ChatMessage
— сущность сообщения, которая содержит 4 свойства: номер сообщения, дату создания, текст сообщения и флаг того, что сообщение является сообщением клиента (иначе сообщение написано службой поддержки).ChatMessageCreate
— сущность создания сообщения, которая содержит 2 свойства: текст сообщения и признак клиента.
Chat State
Добавим state, который будет хранить сообщения, а также предоставит возможность создавать новые.
Добавим библиотеку:
nx g lib chat/state
Добавим feature state:
nx g @nrwl/angular:ngrx chat --module=libs/chat/state/src/lib/chat-state.module.ts
Изменим chat.actions.ts
:
init
— экшен, который запускает эффект восстановления сообщений изlocalStorage
;restore
— экшен, который получает набор сообщений и записывает их в state;createMessage
— экшен, который запускает эффект, который добавляет новое сообщение;createMessageSuccess
— экшен, который вызывается в случае успешного добавления сообщения;createMessageFailure
— экшен, который вызывается в случае ошибки добавления сообщения.
Отредактируем chat.reducer.ts
:
В основном в reducer
’е управляется добавление нового комментария в state
. Помимо сообщений, state содержит 2 свойства:
loaded
— признак, который говорит о том, что сообщения загружены/восстановлены;messageCreating
— переменная, которая говорит о том, что в данный момент идет добавление нового сообщения.
Актуализируем chat.selectors.ts
:
selectLoaded
— селектор, который предоставляет доступ к признаку загрузки сообщений;selectChatMessages
— селектор, который возвращает массив сообщений;selectChatMessagesEntities
— селектор, который возвращает словарь сообщений.
Добавим эффекты в chat.effects.ts
:
init$
— эффект, который восстанавливает сообщения изlocalStorage
;restore$
— эффект, который проверяет список сообщений. И если еще не было ни одного сообщения, то создается новое сообщение от имени службы поддержки;createMessage$
— эффект, который добавляет новое сообщение;createMessageSuccess$
— эффект, который подписывается на событие успешного добавления сообщения и сохраняет весь список сообщений вlocalStorage
.
Обновим фасад chat.facade.ts
:
Фасад предоставляет доступ к сообщениям и методу создания нового сообщения.
И так как ничего кроме модуля и фасада не нужно, то ограничим экспорт модулей и сервисов:
export * from './lib/chat.facade';
export * from './lib/chat-state.module';
Chat UI
Добавим несколько компонентов для реализации чата:
form
— форма создания нового сообщения;messages
— список сообщений в службу поддержки вместе с ответами;pipes
— несколько пайпов для компонентов.
Chat Form
Добавим библиотеку:
nx g lib chat/ui/form
Добавим компонент:
nx g c chat-form --project=chat-ui-form
Форма сообщения содержит textarea
и кнопку отправки сообщения:
<form automation-id="form" [formGroup]="form" (ngSubmit)="onSubmit()">
<mat-form-field banshop-full-width>
<mat-label
i18n="Chat form|Write a message">
Write a message
</mat-label>
<textarea
matInput
formControlName="message"
cdkTextareaAutosize
cdkAutosizeMinRows="1"
cdkAutosizeMaxRows="5"
></textarea>
</mat-form-field>
<button
aria-label="Send"
i18n-aria-label="Chat form|Send area label"
mat-icon-button
type="button"
(click)="onSubmit()"
>
<mat-icon>send</mat-icon>
</button>
</form>
Сам компонент достаточно прост. При создании компонента идет подписка на добавление нового сообщения:
ngOnInit(): void {
this.chatFacade.createMessageFailure$
.pipe(
tap(() => {
this.submitted = false;
this.changeDetectorRef.markForCheck();
}),
takeUntil(this.destroy$)
)
.subscribe();
this.chatFacade.createMessageSuccess$
.pipe(
tap(() => {
this.submitted = false;
this.form.reset();
this.changeDetectorRef.markForCheck();
}),
takeUntil(this.destroy$)
)
.subscribe();
}
При клике на кнопку добавления сообщения проверяется валидность формы, в данном случае наличии текста. Если текст есть, то комментарий добавляется.
onSubmit(): void {
this.form.markAllAsTouched();
if (!this.submitted && this.form.value?.message?.length) {
this.submitted = true;
this.chatFacade.addMessage(this.form.value.message);
}
this.changeDetectorRef.markForCheck();
}
В случае успешного добавления комментария, форма очищается.
В результате получим:
Если добавить текст:
Chat Pipes
Добавим модуль пайпов.
nx g lib chat/ui/pipes
Добавим 3 pipe:
chat-date-can
— пайп, который проверяет возможность показ даты;chat-icon
— пайп, который возвращает иконку для данного сообщения;chat-icon-can
— пайп, который проверяет необходимость показа иконки.
Логика отображения иконки: иконку нужно показать если меняется собственник сообщения.
Логика типа иконки — у каждого участника своя иконка. В данном случае у оператора иконка в виде логотипа, а у клиента просто стоковое изображение.
Логика отображения даты — показывается каждый новый день.
Chat Messages
Добавим компонент, который будет выводить список сообщений.
Создадим библиотеку:
nx g lib chat/ui/messages
Создадим компонент:
nx g c chat-messages --project=chat-ui-messages
Компонент сообщений выводит два модуля, это сообщения и дату.
<ng-container
*ngFor="let chatMessage of chatMessages; trackBy: trackByFn">
<banshop-chat-date
[chatMessage]="chatMessage"
*ngIf="chatMessage | banshopChatDateCan | async">
</banshop-chat-date>
<banshop-chat-message
[chatMessage]="chatMessage">
</banshop-chat-message>
</ng-container>
Создадим эти два модуля:
nx g m chat-date --project=chat-ui-messages
nx g c chat-date --project=chat-ui-messages
Компонент просто оформляет вывод даты.
nx g m chat-message --project=chat-ui-messages
nx g c chat-message --project=chat-ui-messages
Если вкратце, то компонент сообщения состоит из отображения двух компонентов: иконки и блока с сообщением:
<banshop-chat-icon [chatMessage]="chatMessage"></banshop-chat-icon>
<banshop-chat-box
[chatMessage]="chatMessage"
[lastOwner]="chatMessage | banshopChatIconCan | async">
</banshop-chat-box>
Компонент с иконкой это просто image с условием отображения:
<img
alt=""
[src]="chatMessage.isOwner | banshopChatIcon"
*ngIf="chatMessage | banshopChatIconCan | async"
/>
Блок с сообщением содержит тело сообщения и дату:
<banshop-chat-body automation-id="body">
{{ chatMessage.message }}
</banshop-chat-body>
<banshop-chat-time automation-id="time">
{{ chatMessage.created | date: 'shortTime' }}
</banshop-chat-time>
В результате получим:
Chat Page
Добавим страницу чата, на которой отобразим список сообщений и форму.
Создадим библиотеку:
nx g lib chat/page
Создадим компонент:
nx g c chat-page --project=chat-page
Добавим модуль роутинга:
nx g m chat-page-routing --project=chat-page
В компоненте берется список сообщений из ChatFacade
:
export class ChatPageComponent implements OnInit {
charMessages$!: Observable<ChatMessage[]>;
constructor(private readonly chatFacade: ChatFacade) {}
ngOnInit(): void {
this.charMessages$ = this.chatFacade.chatMessages$;
}
}
В шаблоне компонента подписываемся и отображаем список сообщений:
<banshop-chat-messages
automation-id="messages"
[chatMessages]="charMessages"
*ngIf="charMessages$ | async; let charMessages"
></banshop-chat-messages>
<banshop-chat-form automation-id="form"></banshop-chat-form>
В результате получим страницу:
Ссылки
Предыдущая статья — Модуль оформления заказа.
Следующая статья — Страницы ошибок.
Все исходники находятся на github, в репозитории:
Для того, чтобы посмотреть состояние проекта на момент написания статьи, нужно выбрать соответствующий тег — article.
Подписывайтесь на блог, чтобы не пропустить новые статьи про Angular, и веб-разработку. Medium | Telegram| VK |Tw| Ln