From cd8ddd4fb6859ccd68ef30272087971c68faf624 Mon Sep 17 00:00:00 2001 From: Lee Norris Date: Fri, 26 Jun 2020 00:18:52 -0400 Subject: [PATCH] feat: add error formatting --- .../src/lib/ngx-error-default-messages.ts | 17 ++++ .../src/lib/ngx-error.pipe.spec.ts | 81 +++++++++++++++++++ .../ngx-sub-form/src/lib/ngx-error.pipe.ts | 35 ++++++++ .../src/lib/ngx-sub-form-tokens.ts | 2 + .../src/lib/ngx-sub-form-utils.ts | 14 +++- src/app/app.module.ts | 5 +- .../listing-form/listing-form.component.html | 8 +- .../listing-form/listing-form.component.ts | 6 +- 8 files changed, 158 insertions(+), 10 deletions(-) create mode 100644 projects/ngx-sub-form/src/lib/ngx-error-default-messages.ts create mode 100644 projects/ngx-sub-form/src/lib/ngx-error.pipe.spec.ts create mode 100644 projects/ngx-sub-form/src/lib/ngx-error.pipe.ts diff --git a/projects/ngx-sub-form/src/lib/ngx-error-default-messages.ts b/projects/ngx-sub-form/src/lib/ngx-error-default-messages.ts new file mode 100644 index 00000000..0e86ba32 --- /dev/null +++ b/projects/ngx-sub-form/src/lib/ngx-error-default-messages.ts @@ -0,0 +1,17 @@ +import { IFormatters } from "./ngx-error.pipe"; + +function format(msg: string, displayName?: string) { + return displayName ? `${displayName || ''} ${(msg || '').toLowerCase()}` : msg; +} + +export const errorDefaultMessages: IFormatters = { + max: (displayName?: string, data?: any) => format(`Cannot be more than ${data?.max}`, displayName), + maxlength: (displayName?: string, data?: any) => + format(`Must be less than ${data?.requiredLength} characters`, displayName), + min: (displayName?: string, data?: any) => format(`Must be at least ${data?.min}`, displayName), + minlength: (displayName?: string, data?: any) => + format(`Must be at least ${data?.requiredLength} characters`, displayName), + required: (displayName?: string) => (displayName ? `${displayName} is required` : 'Required'), + pattern: (displayName?: string) => format('Contains invalid characters', displayName), + unique: (displayName?: string) => format('Must be unique', displayName), +}; diff --git a/projects/ngx-sub-form/src/lib/ngx-error.pipe.spec.ts b/projects/ngx-sub-form/src/lib/ngx-error.pipe.spec.ts new file mode 100644 index 00000000..61b8c6a9 --- /dev/null +++ b/projects/ngx-sub-form/src/lib/ngx-error.pipe.spec.ts @@ -0,0 +1,81 @@ +import { FormatErrorPipe } from './ngx-error.pipe'; +import { errorDefaultMessages } from './ngx-error-default-messages'; +import uuid from 'uuid'; + +describe(FormatErrorPipe.name, () => { + let pipe!: FormatErrorPipe; + let errorObject: any; + let definedErrorMessageKey: string | undefined; + let displayName: string | undefined; + const getActualValue = () => pipe.transform(errorObject, displayName); + + describe(FormatErrorPipe.prototype.transform.name, () => { + beforeAll(() => { + pipe = new FormatErrorPipe(errorDefaultMessages); + }); + describe('a successful transformation', () => { + describe('GIVEN error object has a member matching a defined error message key', () => { + beforeEach(() => { + definedErrorMessageKey = 'required'; + errorObject = { [definedErrorMessageKey]: uuid.v4() }; + }); + + describe('WHEN optional field name is falsey', () => { + let expectedErrorMessageWithoutDisplayName: any; + + beforeEach(() => { + displayName = undefined; + expectedErrorMessageWithoutDisplayName = new RegExp(definedErrorMessageKey!, 'i'); + }); + + it('returns the defined message for the defined error key', () => { + expect(expectedErrorMessageWithoutDisplayName.test(getActualValue())).toBe(true); + }); + }); + + describe('WHEN optional display name is truthy', () => { + let expectedErrorMessageWithDisplayName: any; + beforeEach(() => { + displayName = uuid.v4(); + expectedErrorMessageWithDisplayName = new RegExp(`${displayName}.*${definedErrorMessageKey}`, 'i'); + }); + it('returns the defined message with the display name', () => { + expect(expectedErrorMessageWithDisplayName.test(getActualValue())).toBe(true); + }); + }); + }); + }); + + describe('GIVEN edge cases', () => { + describe('WHEN error object is falsey but error key is one of defined messages', () => { + beforeEach(() => { + errorObject = null; + definedErrorMessageKey = 'required'; + }); + it('returns an empty string', () => { + expect(getActualValue()).toBe(''); + }); + }); + + describe('WHEN error object is falsey and error key is falsey', () => { + beforeEach(() => { + errorObject = null; + definedErrorMessageKey = undefined; + }); + it('returns an empty string', () => { + expect(getActualValue()).toBe(''); + }); + }); + + describe('WHEN error object is truthy but has no members matching a defined key', () => { + beforeEach(() => { + errorObject = { [uuid.v4()]: true }; + definedErrorMessageKey = 'required'; + }); + it('returns an empty string', () => { + expect(getActualValue()).toBe(''); + }); + }); + }); + }); +}); diff --git a/projects/ngx-sub-form/src/lib/ngx-error.pipe.ts b/projects/ngx-sub-form/src/lib/ngx-error.pipe.ts new file mode 100644 index 00000000..83dbf03f --- /dev/null +++ b/projects/ngx-sub-form/src/lib/ngx-error.pipe.ts @@ -0,0 +1,35 @@ +import { Pipe, PipeTransform, Inject } from '@angular/core'; +import { SUB_FORM_ERRORS_TOKEN } from './ngx-sub-form-tokens'; + +export interface IFormatter { + (controlDisplayName?: string, data?: any): string; +} + +export interface IFormatters { + [errorKey: string]: IFormatter; +} + +@Pipe({ name: 'formatError' }) +export class FormatErrorPipe implements PipeTransform { + + constructor(@Inject(SUB_FORM_ERRORS_TOKEN) private readonly formattedErrors: IFormatters) { } + + transform(err: any, controlName?: string) { + return this.getErrorMessage(this.formattedErrors, err, controlName); + } + + private getErrorMessage(formattedErrors: any, controlErrors: any, formControlDisplayName?: string) { + const errors = Object.keys(controlErrors || {}); + + if (errors.length) { + const validatorName: string = errors[0]; + const validationData: any = (controlErrors || {})[validatorName]; + const messager: any = (formattedErrors as any)[validatorName]; + + return messager ? messager(formControlDisplayName, validationData).trim() : ''; + } + + return ''; + } +} + diff --git a/projects/ngx-sub-form/src/lib/ngx-sub-form-tokens.ts b/projects/ngx-sub-form/src/lib/ngx-sub-form-tokens.ts index d04ff268..ac9cc5c8 100644 --- a/projects/ngx-sub-form/src/lib/ngx-sub-form-tokens.ts +++ b/projects/ngx-sub-form/src/lib/ngx-sub-form-tokens.ts @@ -1,5 +1,6 @@ import { InjectionToken } from '@angular/core'; import { NgxSubFormComponent } from './ngx-sub-form.component'; +import { IFormatters } from './ngx-error.pipe'; // ---------------------------------------------------------------------------------------- // no need to expose that token out of the lib, do not export that file from public_api.ts! @@ -9,3 +10,4 @@ import { NgxSubFormComponent } from './ngx-sub-form.component'; // this basically allows us to access the host component // from a directive without knowing the type of the component at run time export const SUB_FORM_COMPONENT_TOKEN = new InjectionToken>('NgxSubFormComponentToken'); +export const SUB_FORM_ERRORS_TOKEN = new InjectionToken('NgxSubFormErrorsToken'); diff --git a/projects/ngx-sub-form/src/lib/ngx-sub-form-utils.ts b/projects/ngx-sub-form/src/lib/ngx-sub-form-utils.ts index 8bfb5f4a..83ac5477 100644 --- a/projects/ngx-sub-form/src/lib/ngx-sub-form-utils.ts +++ b/projects/ngx-sub-form/src/lib/ngx-sub-form-utils.ts @@ -11,8 +11,10 @@ import { import { InjectionToken, Type, forwardRef, OnDestroy } from '@angular/core'; import { Observable, Subject, timer } from 'rxjs'; import { takeUntil, debounce } from 'rxjs/operators'; -import { SUB_FORM_COMPONENT_TOKEN } from './ngx-sub-form-tokens'; +import { SUB_FORM_COMPONENT_TOKEN, SUB_FORM_ERRORS_TOKEN } from './ngx-sub-form-tokens'; import { NgxSubFormComponent } from './ngx-sub-form.component'; +import { errorDefaultMessages } from './ngx-error-default-messages'; +import { IFormatters } from './ngx-error.pipe'; export type Controls = { [K in keyof T]-?: AbstractControl }; @@ -98,10 +100,18 @@ export function subformComponentProviders( { provide: SUB_FORM_COMPONENT_TOKEN, useExisting: forwardRef(() => component), - }, + } ]; } +export function subFormErrorProvider(errorFormatters?: IFormatters) { + const formatters = errorFormatters || errorDefaultMessages + return { + provide: SUB_FORM_ERRORS_TOKEN, + useValue: formatters + }; +} + const wrapAsQuote = (str: string): string => `"${str}"`; export class MissingFormControlsError extends Error { diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 6bd5dbd4..6b9bbefc 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -31,6 +31,8 @@ import { MainComponent } from './main/main.component'; import { CrewMemberComponent } from './main/listing/listing-form/vehicle-listing/crew-members/crew-member/crew-member.component'; import { DisplayCrewMembersPipe } from './main/listings/display-crew-members.pipe'; import { CrewMembersComponent } from './main/listing/listing-form/vehicle-listing/crew-members/crew-members.component'; +import { FormatErrorPipe } from 'projects/ngx-sub-form/src/lib/ngx-error.pipe'; +import { subFormErrorProvider } from 'ngx-sub-form'; const MATERIAL_MODULES = [ LayoutModule, @@ -64,6 +66,7 @@ const MATERIAL_MODULES = [ CrewMembersComponent, CrewMemberComponent, DisplayCrewMembersPipe, + FormatErrorPipe ], exports: [DroidProductComponent], imports: [ @@ -83,7 +86,7 @@ const MATERIAL_MODULES = [ { path: '**', pathMatch: 'full', redirectTo: '/' }, ]), ], - providers: [], + providers: [subFormErrorProvider()], bootstrap: [AppComponent], }) export class AppModule {} diff --git a/src/app/main/listing/listing-form/listing-form.component.html b/src/app/main/listing/listing-form/listing-form.component.html index 008d6823..aa690431 100644 --- a/src/app/main/listing/listing-form/listing-form.component.html +++ b/src/app/main/listing/listing-form/listing-form.component.html @@ -60,8 +60,8 @@ /> - - Image url is required + + {{formGroupErrors?.imageUrl | formatError: 'Image Path'}} @@ -75,8 +75,8 @@ /> - - Price is required + + {{formGroupErrors?.price | formatError}} diff --git a/src/app/main/listing/listing-form/listing-form.component.ts b/src/app/main/listing/listing-form/listing-form.component.ts index 75fecc91..5b9ad944 100644 --- a/src/app/main/listing/listing-form/listing-form.component.ts +++ b/src/app/main/listing/listing-form/listing-form.component.ts @@ -33,7 +33,7 @@ interface OneListingForm { @Component({ selector: 'app-listing-form', templateUrl: './listing-form.component.html', - styleUrls: ['./listing-form.component.scss'], + styleUrls: ['./listing-form.component.scss'] }) // export class ListingFormComponent extends NgxAutomaticRootFormComponent export class ListingFormComponent extends NgxRootFormComponent { @@ -59,8 +59,8 @@ export class ListingFormComponent extends NgxRootFormComponent