Создание пользовательского интерфейса для модуля Webhook с помощью Angular
В этой статье я опишу создание таблички отображающей данные и формы для ее заполнения, интерфейсы строятся на компонентах от https://ng.ant.design, формы создаются и управляются с помощью https://formly.dev, для стилей используется https://tailwindcss.com, стейт машины нет.
1. Создаем пустую Angular библиотеку
В этой библиотеке лежат компоненты для отображения и работы с данными сущности Webhook
.
Commands
# Create Angular library
./node_modules/.bin/nx g @nx/angular:library webhook-angular --buildable --publishable --directory=libs/feature/webhook-angular --simpleName=true --projectNameAndRootFormat=as-provided --strict=true --prefix= --standalone=true --selector= --changeDetection=OnPush --importPath=@nestjs-mod-fullstack/webhook-angular
# Change file with test options
rm -rf libs/feature/webhook-angular/src/test-setup.ts
cp apps/client/src/test-setup.ts libs/feature/webhook-angular/src/test-setup.ts
Вывод консоли
$ ./node_modules/.bin/nx g @nx/angular:library webhook-angular --buildable --publishable --directory=libs/feature/webhook-angular --simpleName=true --projectNameAndRootFormat=as-provided --strict=true --prefix= --standalone=true --selector= --changeDetection=OnPush --importPath=@nestjs-mod-fullstack/webhook-angular
NX Generating @nx/angular:library
CREATE libs/feature/webhook-angular/project.json
CREATE libs/feature/webhook-angular/README.md
CREATE libs/feature/webhook-angular/ng-package.json
CREATE libs/feature/webhook-angular/package.json
CREATE libs/feature/webhook-angular/tsconfig.json
CREATE libs/feature/webhook-angular/tsconfig.lib.json
CREATE libs/feature/webhook-angular/tsconfig.lib.prod.json
CREATE libs/feature/webhook-angular/src/index.ts
CREATE libs/feature/webhook-angular/jest.config.ts
CREATE libs/feature/webhook-angular/src/test-setup.ts
CREATE libs/feature/webhook-angular/tsconfig.spec.json
CREATE libs/feature/webhook-angular/src/lib/webhook-angular/webhook-angular.component.css
CREATE libs/feature/webhook-angular/src/lib/webhook-angular/webhook-angular.component.html
CREATE libs/feature/webhook-angular/src/lib/webhook-angular/webhook-angular.component.spec.ts
CREATE libs/feature/webhook-angular/src/lib/webhook-angular/webhook-angular.component.ts
CREATE libs/feature/webhook-angular/.eslintrc.json
UPDATE package.json
UPDATE tsconfig.base.json
> @nestjs-mod-fullstack/source@0.0.9 prepare
> npx -y husky install
install command is DEPRECATED
removed 2 packages, changed 5 packages, and audited 2726 packages in 13s
332 packages are looking for funding
run `npm fund` for details
33 vulnerabilities (4 low, 12 moderate, 17 high)
To address issues that do not require attention, run:
npm audit fix
To address all issues possible (including breaking changes), run:
npm audit fix --force
Some issues need review, and may require choosing
a different dependency.
Run `npm audit` for details.
NX 👀 View Details of webhook-angular
Run "nx show project webhook-angular" to view details about this project.
2. Создаем общую Angular библиотеку
Общая библиотека содержит функции и классы которые используются другими Angular
-библиотеками.
Commands
# Create Angular library
./node_modules/.bin/nx g @nx/angular:library common-angular --buildable --publishable --directory=libs/common-angular --simpleName=true --projectNameAndRootFormat=as-provided --strict=true --prefix= --standalone=true --selector= --changeDetection=OnPush --importPath=@nestjs-mod-fullstack/common-angular
# Change file with test options
rm -rf libs/common-angular/src/test-setup.ts
cp apps/client/src/test-setup.ts libs/common-angular/src/test-setup.ts
Вывод консоли
$ ./node_modules/.bin/nx g @nx/angular:library common-angular --buildable --publishable --directory=libs/common-angular --simpleName=true --projectNameAndRootFormat=as-provided --strict=true --prefix= --standalone=true --selector= --changeDetection=OnPush --importPath=@nestjs-mod-fullstack/common-angular
NX Generating @nx/angular:library
CREATE libs/common-angular/project.json
CREATE libs/common-angular/README.md
CREATE libs/common-angular/ng-package.json
CREATE libs/common-angular/package.json
CREATE libs/common-angular/tsconfig.json
CREATE libs/common-angular/tsconfig.lib.json
CREATE libs/common-angular/tsconfig.lib.prod.json
CREATE libs/common-angular/src/index.ts
CREATE libs/common-angular/jest.config.ts
CREATE libs/common-angular/src/test-setup.ts
CREATE libs/common-angular/tsconfig.spec.json
CREATE libs/common-angular/src/lib/common-angular/common-angular.component.css
CREATE libs/common-angular/src/lib/common-angular/common-angular.component.html
CREATE libs/common-angular/src/lib/common-angular/common-angular.component.spec.ts
CREATE libs/common-angular/src/lib/common-angular/common-angular.component.ts
CREATE libs/common-angular/.eslintrc.json
UPDATE tsconfig.base.json
NX 👀 View Details of common-angular
Run "nx show project common-angular" to view details about this project.
3. Устанавливаем дополнительные библиотеки
Устанавливаем библиотеку визуальных компонентов ng-zorro-antd
, библиотеку для работы с формами @ngx-formly/core @ngx-formly/ng-zorro-antd
, утилиту для авто-отписки @ngneat/until-destroy
и колекцию утилит lodash
.
Commands
npm install --save ng-zorro-antd @ngx-formly/core @ngx-formly/ng-zorro-antd @ngneat/until-destroy lodash
Вывод консоли
$ npm install --save ng-zorro-antd @ngx-formly/core @ngx-formly/ng-zorro-antd @ngneat/until-destroy
added 8 packages, removed 2 packages, and audited 2794 packages in 25s
343 packages are looking for funding
run `npm fund` for details
38 vulnerabilities (8 low, 12 moderate, 18 high)
To address issues that do not require attention, run:
npm audit fix
To address all issues (including breaking changes), run:
npm audit fix --force
Run `npm audit` for details.
4. Так как для работы модуля нужен идентификаторы пользователя и компании, то нужно создать интерфейсы для передачи этих данных
Создаем форму и сервис авторизации в Webhook
- модуле.
Сервис имеет методы для получения профиля пользователя по переданным xExternalUserId
и xExternalTenantId
, а также хранить их значения и данные по профилю пользователя.
Идентификатор администратора прокидывается из переменных окружения CI/CD
.
Для защиты страниц создадим специальный Guard
.
Создаем сервис libs/feature/webhook-angular/src/lib/services/webhook-auth.service.ts
import { Injectable } from '@angular/core';
import { WebhookErrorInterface, WebhookRestService, WebhookUserObjectInterface } from '@nestjs-mod-fullstack/app-angular-rest-sdk';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { BehaviorSubject, catchError, of, tap, throwError } from 'rxjs';
export type WebhookAuthCredentials = {
xExternalUserId?: string;
xExternalTenantId?: string;
};
@UntilDestroy()
@Injectable({ providedIn: 'root' })
export class WebhookAuthService {
private webhookAuthCredentials$ = new BehaviorSubject<WebhookAuthCredentials>({});
private webhookUser$ = new BehaviorSubject<WebhookUserObjectInterface | null>(null);
constructor(private readonly webhookRestService: WebhookRestService) {}
getWebhookAuthCredentials() {
return this.webhookAuthCredentials$.value;
}
getWebhookUser() {
return this.webhookUser$.value;
}
setWebhookAuthCredentials(webhookAuthCredentials: WebhookAuthCredentials) {
this.webhookAuthCredentials$.next(webhookAuthCredentials);
this.loadWebhookUser().pipe(untilDestroyed(this)).subscribe();
}
loadWebhookUser() {
return this.webhookRestService.webhookControllerProfile(this.getWebhookAuthCredentials().xExternalUserId, this.getWebhookAuthCredentials().xExternalTenantId).pipe(
tap((profile) => this.webhookUser$.next(profile)),
catchError((err: { error?: WebhookErrorInterface }) => {
if (err.error?.code === 'WEBHOOK-002') {
return of(null);
}
return throwError(() => err);
})
);
}
webhookAuthCredentialsUpdates() {
return this.webhookAuthCredentials$.asObservable();
}
webhookUserUpdates() {
return this.webhookUser$.asObservable();
}
}
Псевдо форма авторизации имеет два поля xExternalUserId
и xExternalTenantId
, построение и валидация формы происходит через библиотеку https://formly.dev.
Кроме кнопки войти, на форме также есть еще две кнопки:
- Заполнить данные пользователя - подставляет в
xExternalUserId
иxExternalTenantId
захардкоженные случайныеuuid
-идентификаторы; - Заполнить данные администратора - подставляет в
xExternalUserId
идентификатор пользователя с рольюAdmin
, бэкенд при старте создает этого пользователя, а идентификатор вставляется во фронтенд при сборке его вCI\CD
.
Создаем файл libs/feature/webhook-angular/src/lib/forms/webhook-auth-form/webhook-auth-form.component.ts
import { AsyncPipe, NgIf } from '@angular/common';
import { ChangeDetectionStrategy, Component, EventEmitter, Inject, Input, OnInit, Optional, Output } from '@angular/core';
import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzFormModule } from 'ng-zorro-antd/form';
import { NzInputModule } from 'ng-zorro-antd/input';
import { NzMessageService } from 'ng-zorro-antd/message';
import { NZ_MODAL_DATA } from 'ng-zorro-antd/modal';
import { BehaviorSubject } from 'rxjs';
import { WebhookAuthCredentials, WebhookAuthService } from '../../services/webhook-auth.service';
import { WEBHOOK_CONFIGURATION_TOKEN, WebhookConfiguration } from '../../services/webhook.configuration';
@Component({
standalone: true,
imports: [FormlyModule, NzFormModule, NzInputModule, NzButtonModule, FormsModule, ReactiveFormsModule, AsyncPipe, NgIf],
selector: 'webhook-auth-form',
templateUrl: './webhook-auth-form.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class WebhookAuthFormComponent implements OnInit {
@Input()
hideButtons?: boolean;
@Output()
afterSignIn = new EventEmitter<WebhookAuthCredentials>();
form = new UntypedFormGroup({});
formlyModel$ = new BehaviorSubject<object | null>(null);
formlyFields$ = new BehaviorSubject<FormlyFieldConfig[] | null>(null);
constructor(
@Optional()
@Inject(NZ_MODAL_DATA)
private readonly nzModalData: WebhookAuthFormComponent,
@Inject(WEBHOOK_CONFIGURATION_TOKEN)
private readonly webhookConfiguration: WebhookConfiguration,
private readonly webhookAuthService: WebhookAuthService,
private readonly nzMessageService: NzMessageService
) {}
ngOnInit(): void {
Object.assign(this, this.nzModalData);
this.setFieldsAndModel(this.webhookAuthService.getWebhookAuthCredentials());
}
setFieldsAndModel(
data: Partial<WebhookAuthCredentials> = {},
options: { xExternalTenantIdIsRequired: boolean } = {
xExternalTenantIdIsRequired: true,
}
) {
this.formlyFields$.next([
{
key: 'xExternalUserId',
type: 'input',
validation: {
show: true,
},
props: {
label: `webhook.form.xExternalUserId`,
placeholder: 'xExternalUserId',
required: true,
},
},
{
key: 'xExternalTenantId',
type: 'input',
validation: {
show: true,
},
props: {
label: `webhook.form.xExternalTenantId`,
placeholder: 'xExternalTenantId',
required: options.xExternalTenantIdIsRequired,
},
},
]);
this.formlyModel$.next(this.toModel(data));
}
submitForm(): void {
if (this.form.valid) {
const value = this.toJson(this.form.value);
this.afterSignIn.next(value);
this.webhookAuthService.setWebhookAuthCredentials(value);
this.nzMessageService.success('Success');
} else {
console.log(this.form.controls);
this.nzMessageService.warning('Validation errors');
}
}
fillUserCredentials() {
this.setFieldsAndModel({
xExternalTenantId: '2079150a-f133-405c-9e77-64d3ab8aff77',
xExternalUserId: '3072607c-8c59-4fc4-9a37-916825bc0f99',
});
}
fillAdminCredentials() {
this.setFieldsAndModel(
{
xExternalTenantId: '',
xExternalUserId: this.webhookConfiguration.webhookSuperAdminExternalUserId,
},
{ xExternalTenantIdIsRequired: false }
);
}
private toModel(data: Partial<WebhookAuthCredentials>): object | null {
return {
xExternalUserId: data['xExternalUserId'],
xExternalTenantId: data['xExternalTenantId'],
};
}
private toJson(data: Partial<WebhookAuthCredentials>) {
return {
xExternalUserId: data['xExternalUserId'],
xExternalTenantId: data['xExternalTenantId'],
};
}
}
Создаем файл libs/feature/webhook-angular/src/lib/forms/webhook-auth-form/webhook-auth-form.component.html
@if (formlyFields$ | async; as formlyFields) {
<form nz-form [formGroup]="form" (ngSubmit)="submitForm()_
<formly-form [model]="formlyModel$ | async" [fields]="formlyFields" [form]="form_ </formly-form>
@if (!hideButtons) {
<nz-form-control>
<div class="flex justify-between_
<div>
<button nz-button type="button" (click)="fillUserCredentials()_Fill user credentials</button>
<button nz-button type="button" (click)="fillAdminCredentials()_Fill admin credentials</button>
</div>
<button nz-button nzType="primary" type="submit" [disabled]="!form.valid_Sign-in</button>
</div>
</nz-form-control>
}
</form>
}
Идентификатор администратора передается через конфигурацию и переменные окружения.
Обновляем файл apps/client/src/environments/environment.prod.ts
export const serverUrl = '';
export const webhookSuperAdminExternalUserId = '___CLIENT_WEBHOOK_SUPER_ADMIN_EXTERNAL_USER_ID___';
Обновляем файл apps/client/src/app/app.config.ts
import { provideHttpClient } from '@angular/common/http';
import { ApplicationConfig, ErrorHandler, importProvidersFrom, provideZoneChangeDetection } from '@angular/core';
import { provideClientHydration } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { provideRouter } from '@angular/router';
import { RestClientApiModule, RestClientConfiguration } from '@nestjs-mod-fullstack/app-angular-rest-sdk';
import { WEBHOOK_CONFIGURATION_TOKEN, WebhookConfiguration } from '@nestjs-mod-fullstack/webhook-angular';
import { FormlyModule } from '@ngx-formly/core';
import { FormlyNgZorroAntdModule } from '@ngx-formly/ng-zorro-antd';
import { en_US, provideNzI18n } from 'ng-zorro-antd/i18n';
import { serverUrl, webhookSuperAdminExternalUserId } from '../environments/environment';
import { AppErrorHandler } from './app.error-handler';
import { appRoutes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(appRoutes),
provideHttpClient(),
provideNzI18n(en_US),
{
provide: WEBHOOK_CONFIGURATION_TOKEN,
useValue: new WebhookConfiguration({ webhookSuperAdminExternalUserId }), // <-- update
},
importProvidersFrom(
BrowserAnimationsModule,
RestClientApiModule.forRoot(
() =>
new RestClientConfiguration({
basePath: serverUrl,
})
),
FormlyModule.forRoot(),
FormlyNgZorroAntdModule
),
{ provide: ErrorHandler, useClass: AppErrorHandler },
],
};
Страницу с формой входа создаем на уровне приложения, так как у нас нет необходимости ее переиспользовать.
Создаем файл apps/client/src/app/pages/sign-in/sign-in.component.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Router } from '@angular/router';
import { WebhookAuthFormComponent } from '@nestjs-mod-fullstack/webhook-angular';
import { NzBreadCrumbModule } from 'ng-zorro-antd/breadcrumb';
@Component({
standalone: true,
selector: 'app-sign-in',
templateUrl: './sign-in.component.html',
imports: [NzBreadCrumbModule, WebhookAuthFormComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SignInComponent {
constructor(private readonly router: Router) {}
onAfterSignIn() {
this.router.navigate(['/webhook']);
}
}
Создаем файл apps/client/src/app/pages/sign-in/sign-in.component.html
<nz-breadcrumb>
<nz-breadcrumb-item>Sign-in</nz-breadcrumb-item>
</nz-breadcrumb>
<div class="inner-content_
<webhook-auth-form (afterSignIn)="onAfterSignIn()_</webhook-auth-form>
</div>
Страница авторизации должна быть доступна только когда пользователь не ввел авторизационные данные, для этого напишем Guard
и закроем им наши страницы.
Создаем файл libs/feature/webhook-angular/src/lib/services/webhook-guard.service.ts
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate } from '@angular/router';
import { WebhookRoleInterface } from '@nestjs-mod-fullstack/app-angular-rest-sdk';
import { map, of } from 'rxjs';
import { WebhookAuthService } from './webhook-auth.service';
export const WEBHOOK_GUARD_DATA_ROUTE_KEY = 'webhookGuardData';
export class WebhookGuardData {
roles?: WebhookRoleInterface[];
constructor(options?: WebhookGuardData) {
Object.assign(this, options);
}
}
@Injectable({ providedIn: 'root' })
export class WebhookGuardService implements CanActivate {
constructor(private readonly webhookAuthService: WebhookAuthService) {}
canActivate(route: ActivatedRouteSnapshot) {
if (route.data[WEBHOOK_GUARD_DATA_ROUTE_KEY] instanceof WebhookGuardData) {
const webhookGuardData = route.data[WEBHOOK_GUARD_DATA_ROUTE_KEY];
return this.webhookAuthService.loadWebhookUser().pipe(
map((webhookUser) => {
return Boolean((webhookGuardData.roles && webhookUser && webhookGuardData.roles.length > 0 && webhookGuardData.roles.includes(webhookUser.userRole)) || ((webhookGuardData.roles || []).length === 0 && !webhookUser?.userRole));
})
);
}
return of(true);
}
}
Обновляем файл apps/client/src/app/app.routes.ts
import { Route } from '@angular/router';
import { WEBHOOK_GUARD_DATA_ROUTE_KEY, WebhookGuardData, WebhookGuardService } from '@nestjs-mod-fullstack/webhook-angular';
import { HomeComponent } from './pages/home/home.component';
import { SignInComponent } from './pages/sign-in/sign-in.component';
import { WebhookComponent } from './pages/webhook/webhook.component';
import { DemoComponent } from './pages/demo/demo.component';
export const appRoutes: Route[] = [
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: 'home', component: HomeComponent },
{ path: 'demo', component: DemoComponent },
{
path: 'webhook',
component: WebhookComponent,
canActivate: [WebhookGuardService],
data: {
[WEBHOOK_GUARD_DATA_ROUTE_KEY]: new WebhookGuardData({
roles: ['Admin', 'User'],
}),
},
},
{
path: 'sign-in',
component: SignInComponent,
canActivate: [WebhookGuardService],
data: {
[WEBHOOK_GUARD_DATA_ROUTE_KEY]: new WebhookGuardData({ roles: [] }),
},
},
];
5. Описываем компоненту с формой и сервис для создания и редактирования сущности Webhook
Так как метод для работы с Webhook
-сущностью требуют авторизационные данные, мы подключаем WebhookAuthService
в сервис для работы с бэкенд сущности Webhook
.
Создаем сервис libs/feature/webhook-angular/src/lib/services/webhook.service.ts
import { Injectable } from '@angular/core';
import { CreateWebhookArgsInterface, UpdateWebhookArgsInterface, WebhookRestService } from '@nestjs-mod-fullstack/app-angular-rest-sdk';
import { RequestMeta } from '@nestjs-mod-fullstack/common-angular';
import { WebhookAuthService } from './webhook-auth.service';
@Injectable({ providedIn: 'root' })
export class WebhookService {
constructor(private readonly webhookAuthService: WebhookAuthService, private readonly webhookRestService: WebhookRestService) {}
findOne(id: string) {
return this.webhookRestService.webhookControllerFindOne(id, this.webhookAuthService.getWebhookAuthCredentials().xExternalUserId, this.webhookAuthService.getWebhookAuthCredentials().xExternalTenantId);
}
findMany({ filters, meta }: { filters: Record<string, string>; meta?: RequestMeta }) {
return this.webhookRestService.webhookControllerFindMany(
this.webhookAuthService.getWebhookAuthCredentials().xExternalUserId,
this.webhookAuthService.getWebhookAuthCredentials().xExternalTenantId,
meta?.curPage,
meta?.perPage,
filters['search'],
meta?.sort
? Object.entries(meta?.sort)
.map(([key, value]) => `${key}:${value}`)
.join(',')
: undefined
);
}
updateOne(id: string, data: UpdateWebhookArgsInterface) {
return this.webhookRestService.webhookControllerUpdateOne(id, data, this.webhookAuthService.getWebhookAuthCredentials().xExternalUserId, this.webhookAuthService.getWebhookAuthCredentials().xExternalTenantId);
}
deleteOne(id: string) {
return this.webhookRestService.webhookControllerDeleteOne(id, this.webhookAuthService.getWebhookAuthCredentials().xExternalUserId, this.webhookAuthService.getWebhookAuthCredentials().xExternalTenantId);
}
createOne(data: CreateWebhookArgsInterface) {
return this.webhookRestService.webhookControllerCreateOne(data, this.webhookAuthService.getWebhookAuthCredentials().xExternalUserId, this.webhookAuthService.getWebhookAuthCredentials().xExternalTenantId);
}
}
Цель данного поста создать простой пример CRUD
на Angular
, форма состоит из стандартных типов контролов (checkbox, input, select, textarea)
, а логика по трансформированию данных в formly
и обратно лежит в этой же компоненте.
В дальнейших статьях будут созданы дополнительные кастомные типы контролов для formly
с собственными логиками трансформации.
Создаем класс формы libs/feature/webhook-angular/src/lib/forms/webhook-form/webhook-form.component.ts
import { AsyncPipe, NgIf } from '@angular/common';
import { ChangeDetectionStrategy, Component, EventEmitter, Inject, Input, OnInit, Optional, Output } from '@angular/core';
import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { WebhookEventInterface, WebhookObjectInterface } from '@nestjs-mod-fullstack/app-angular-rest-sdk';
import { safeParseJson } from '@nestjs-mod-fullstack/common-angular';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzFormModule } from 'ng-zorro-antd/form';
import { NzInputModule } from 'ng-zorro-antd/input';
import { NzMessageService } from 'ng-zorro-antd/message';
import { NZ_MODAL_DATA } from 'ng-zorro-antd/modal';
import { BehaviorSubject, tap } from 'rxjs';
import { WebhookEventsService } from '../../services/webhook-events.service';
import { WebhookService } from '../../services/webhook.service';
@UntilDestroy()
@Component({
standalone: true,
imports: [FormlyModule, NzFormModule, NzInputModule, NzButtonModule, FormsModule, ReactiveFormsModule, AsyncPipe, NgIf],
selector: 'webhook-form',
templateUrl: './webhook-form.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class WebhookFormComponent implements OnInit {
@Input()
id?: string;
@Input()
hideButtons?: boolean;
@Output()
afterFind = new EventEmitter<WebhookObjectInterface>();
@Output()
afterCreate = new EventEmitter<WebhookObjectInterface>();
@Output()
afterUpdate = new EventEmitter<WebhookObjectInterface>();
form = new UntypedFormGroup({});
formlyModel$ = new BehaviorSubject<object | null>(null);
formlyFields$ = new BehaviorSubject<FormlyFieldConfig[] | null>(null);
events: WebhookEventInterface[] = [];
constructor(
@Optional()
@Inject(NZ_MODAL_DATA)
private readonly nzModalData: WebhookFormComponent,
private readonly webhookService: WebhookService,
private readonly webhookEventsService: WebhookEventsService,
private readonly nzMessageService: NzMessageService
) {}
ngOnInit(): void {
Object.assign(this, this.nzModalData);
this.webhookEventsService
.findMany()
.pipe(
tap((events) => {
this.events = events;
if (this.id) {
this.findOne()
.pipe(
tap((result) => this.afterFind.next(result)),
untilDestroyed(this)
)
.subscribe();
} else {
this.setFieldsAndModel();
}
}),
untilDestroyed(this)
)
.subscribe();
}
setFieldsAndModel(data: Partial<WebhookObjectInterface> = {}) {
this.formlyFields$.next([
{
key: 'enabled',
type: 'checkbox',
validation: {
show: true,
},
props: {
label: `webhook.form.enabled`,
placeholder: 'enabled',
required: true,
},
},
{
key: 'endpoint',
type: 'input',
validation: {
show: true,
},
props: {
label: `webhook.form.endpoint`,
placeholder: 'endpoint',
required: true,
},
},
{
key: 'eventName',
type: 'select',
validation: {
show: true,
},
props: {
label: `webhook.form.eventName`,
placeholder: 'eventName',
required: true,
options: this.events.map((e) => ({
value: e.eventName,
label: e.description,
})),
},
},
{
key: 'headers',
type: 'textarea',
validation: {
show: true,
},
props: {
label: `webhook.form.headers`,
placeholder: 'headers',
required: true,
},
},
{
key: 'requestTimeout',
type: 'input',
validation: {
show: true,
},
props: {
label: `webhook.form.requestTimeout`,
placeholder: 'requestTimeout',
required: false,
},
},
]);
this.formlyModel$.next(this.toModel(data));
}
submitForm(): void {
if (this.form.valid) {
if (this.id) {
this.updateOne()
.pipe(
tap((result) => {
this.nzMessageService.success('Success');
this.afterUpdate.next(result);
}),
untilDestroyed(this)
)
.subscribe();
} else {
this.createOne()
.pipe(
tap((result) => {
this.nzMessageService.success('Success');
this.afterCreate.next(result);
}),
untilDestroyed(this)
)
.subscribe();
}
} else {
console.log(this.form.controls);
this.nzMessageService.warning('Validation errors');
}
}
createOne() {
return this.webhookService.createOne(this.toJson(this.form.value));
}
updateOne() {
if (!this.id) {
throw new Error('id not set');
}
return this.webhookService.updateOne(this.id, this.toJson(this.form.value));
}
findOne() {
if (!this.id) {
throw new Error('id not set');
}
return this.webhookService.findOne(this.id).pipe(
tap((result) => {
this.setFieldsAndModel(result);
})
);
}
private toModel(data: Partial<WebhookObjectInterface>): object | null {
return {
enabled: (data['enabled'] as unknown as string) === 'true' || data['enabled'] === true,
endpoint: data['endpoint'],
eventName: data['eventName'],
headers: data['headers'] ? JSON.stringify(data['headers']) : '',
requestTimeout: data['requestTimeout'] && !isNaN(+data['requestTimeout']) ? data['requestTimeout'] : '',
};
}
private toJson(data: Partial<WebhookObjectInterface>) {
return {
enabled: data['enabled'] === true,
endpoint: data['endpoint'] || '',
eventName: data['eventName'] || '',
headers: data['headers'] ? safeParseJson(data['headers']) : null,
requestTimeout: data['requestTimeout'] && !isNaN(+data['requestTimeout']) ? +data['requestTimeout'] : undefined,
};
}
}
Разметка формы имеет возможность отображать ее в виде inline
на странице с встроенными кнопками, а также ее можно отображать в модальном окне у которого своя разметка для кнопок.
Создаем разметку формы libs/feature/webhook-angular/src/lib/forms/webhook-form/webhook-form.component.html
@if (formlyFields$ | async; as formlyFields) {
<form nz-form [formGroup]="form" (ngSubmit)="submitForm()_
<formly-form [model]="formlyModel$ | async" [fields]="formlyFields" [form]="form_ </formly-form>
@if (!hideButtons) {
<nz-form-control>
<button nzBlock nz-button nzType="primary" type="submit" [disabled]="!form.valid_{{ id ? 'Save' : 'Create' }}</button>
</nz-form-control>
}
</form>
}