Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add error formatting #178

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions projects/ngx-sub-form/src/lib/ngx-error-default-messages.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these can be overridden by passing an argument to the subFormErrorProvider helper in utils

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),
};
81 changes: 81 additions & 0 deletions projects/ngx-sub-form/src/lib/ngx-error.pipe.spec.ts
Original file line number Diff line number Diff line change
@@ -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() };
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these may need cleaned up or fixed because i didn't run them locally

});

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('');
});
});
});
});
});
35 changes: 35 additions & 0 deletions projects/ngx-sub-form/src/lib/ngx-error.pipe.ts
Original file line number Diff line number Diff line change
@@ -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 '';
}
}

2 changes: 2 additions & 0 deletions projects/ngx-sub-form/src/lib/ngx-sub-form-tokens.ts
Original file line number Diff line number Diff line change
@@ -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!
Expand All @@ -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<NgxSubFormComponent<any>>('NgxSubFormComponentToken');
export const SUB_FORM_ERRORS_TOKEN = new InjectionToken<IFormatters>('NgxSubFormErrorsToken');
14 changes: 12 additions & 2 deletions projects/ngx-sub-form/src/lib/ngx-sub-form-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = { [K in keyof T]-?: AbstractControl };

Expand Down Expand Up @@ -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<T extends string> extends Error {
Expand Down
5 changes: 4 additions & 1 deletion src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -64,6 +66,7 @@ const MATERIAL_MODULES = [
CrewMembersComponent,
CrewMemberComponent,
DisplayCrewMembersPipe,
FormatErrorPipe
],
exports: [DroidProductComponent],
imports: [
Expand All @@ -83,7 +86,7 @@ const MATERIAL_MODULES = [
{ path: '**', pathMatch: 'full', redirectTo: '/' },
]),
],
providers: [],
providers: [subFormErrorProvider()],
bootstrap: [AppComponent],
})
export class AppModule {}
8 changes: 4 additions & 4 deletions src/app/main/listing/listing-form/listing-form.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@
/>
</mat-form-field>

<mat-error data-input-image-url-error *ngIf="formGroupErrors?.imageUrl?.required">
Image url is required
<mat-error>
{{formGroupErrors?.imageUrl | formatError: 'Image Path'}}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the argument is optional. when provided, it is displayed

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only one error at a time is shown based on the order of the FormControl's validators array

</mat-error>

<mat-form-field>
Expand All @@ -75,8 +75,8 @@
/>
</mat-form-field>

<mat-error data-input-price-error *ngIf="formGroupErrors?.price?.required">
Price is required
<mat-error>
{{formGroupErrors?.price | formatError}}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

example with no argument

</mat-error>

<mat-form-field>
Expand Down
6 changes: 3 additions & 3 deletions src/app/main/listing/listing-form/listing-form.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<OneListing, OneListingForm>
export class ListingFormComponent extends NgxRootFormComponent<OneListing, OneListingForm> {
Expand All @@ -59,8 +59,8 @@ export class ListingFormComponent extends NgxRootFormComponent<OneListing, OneLi
listingType: new FormControl(null, Validators.required),
id: new FormControl(null, Validators.required),
title: new FormControl(null, Validators.required),
imageUrl: new FormControl(null, Validators.required),
price: new FormControl(null, Validators.required),
imageUrl: new FormControl(null, {validators: [Validators.required, Validators.minLength(4), Validators.maxLength(8)]}),
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these configurable Validators.* parameter values are displayed in the error messages by default

price: new FormControl(null, {validators: [Validators.required, Validators.min(0), Validators.max(5)]}),
};
}

Expand Down