Skip to content
This repository has been archived by the owner on May 3, 2024. It is now read-only.

Commit

Permalink
feat: new config option - showMaxErrors
Browse files Browse the repository at this point in the history
  • Loading branch information
DmitryEfimenko committed Jan 11, 2022
1 parent 55fd44a commit d46ba1b
Show file tree
Hide file tree
Showing 10 changed files with 237 additions and 44 deletions.
58 changes: 50 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ The design of this library promotes less boilerplate code, which keeps your temp
- [How it works](#how-it-works)
- [Installation](#installation)
- [Usage](#usage)
- [Advanced configuration](#configuration)
- [Configuration](#configuration)
- [Handling form submission](#handling-form-submission)
- [Getting error details](#getting-error-details)
- [Styling](#styling)
Expand Down Expand Up @@ -173,7 +173,9 @@ Here's the configuration object interface:
```ts
export interface IErrorsConfiguration {
/**
* Configures when to display an error for an invalid control. Available options are:
* Configures when to display an error for an invalid control. Options that
* are available by default are listed below. Note, custom options can be
* provided using CUSTOM_ERROR_STATE_MATCHERS injection token.
*
* `'touched'` - *[default]* shows an error when control is marked as touched. For example, user focused on the input and clicked away or tabbed through the input.
*
Expand All @@ -183,16 +185,56 @@ export interface IErrorsConfiguration {
*
* `'formIsSubmitted'` - shows an error when parent form was submitted.
*/
showErrorsWhenInput: ShowErrorWhen;
showErrorsWhenInput: string;

/**
* The maximum amount of errors to display per ngxErrors block.
*/
showMaxErrors?: number;
}
```

### Providing custom logic for displaying errors

export type ShowErrorWhen =
| 'touched'
| 'dirty'
| 'touchedAndDirty'
| 'formIsSubmitted';
By default, the following error state matchers for displaying errors can be used: `'touched'`, `'dirty'`, `'touchedAndDirty'`, `'formIsSubmitted'`.

Custom error state matchers can be added using the `CUSTOM_ERROR_STATE_MATCHERS` injection token.

First, define the new error state matcher:

```ts
@Injectable({ providedIn: 'root' })
export class MyAwesomeErrorStateMatcher implements ErrorStateMatcher {
isErrorState(
control: AbstractControl | null,
form: FormGroupDirective | NgForm | null
): boolean {
return !!(control && control.value && /* my awesome logic is here */);
}
}
```

Second, use the new error state matcher when providing `CUSTOM_ERROR_STATE_MATCHERS` in the AppModule:

```ts
providers: [
{
provide: CUSTOM_ERROR_STATE_MATCHERS,
deps: [MyAwesomeErrorStateMatcher],
useFactory: (myAwesomeErrorStateMatcher: MyAwesomeErrorStateMatcher) => {
return {
myAwesome: myAwesomeErrorStateMatcher,
};
},
},
];
```

Now the string `'myAwesome'` can be used either in the `showErrorsWhenInput` property of the configuration object or in the `[showWhen]` inputs.

> In the example above, notice the use of the `ErrorStateMatcher` class. This class actually comes from `@angular/material/core`. Under the hood, @ngspot/ngx-errors uses the `ErrorStateMatcher` class to implement all available error state matchers allowing @ngspot/ngx-errors to integrate with Angular Material inputs smoothly.
> Looking at the [documentation](https://material.angular.io/components/input/overview#changing-when-error-messages-are-shown), Angular Material inputs have their own way of setting logic for determining if the input needs to be highlighted red or not. If custom behavior is needed, a developer needs to provide appropriate configuration. @ngspot/ngx-errors configures this functionality for the developer under the hood.
### Overriding global config

You can override the configuration specified at the module level by using `[showWhen]` input on `[ngxErrors]` and on `[ngxError]` directives:
Expand Down
40 changes: 39 additions & 1 deletion projects/ngx-errors/src/lib/error.directive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ const myAsyncValidator: AsyncValidatorFn = (c: AbstractControl) => {
class TestHostComponent {
validInitialVal = new FormControl('val', Validators.required);
invalidInitialVal = new FormControl('', Validators.required);
multipleErrors = new FormControl('123456', [
Validators.minLength(10),
Validators.maxLength(3),
]);

form = new FormGroup({
validInitialVal: new FormControl('val', Validators.required),
Expand Down Expand Up @@ -78,8 +82,14 @@ describe('ErrorDirective', () => {

Given(() => (showWhen = undefined as any));

function createDirectiveWithConfig(showErrorsWhenInput: string) {
function createDirectiveWithConfig(
showErrorsWhenInput: string,
showMaxErrors?: number
) {
const config: IErrorsConfiguration = { showErrorsWhenInput };
if (showMaxErrors !== undefined) {
config.showMaxErrors = showMaxErrors;
}
spectator = createDirective(template, {
providers: [
{
Expand Down Expand Up @@ -285,6 +295,34 @@ describe('ErrorDirective', () => {
});
});

describe('TEST: limiting amount of visible ngxError', () => {
let showMaxErrors: number;

When(() => {
template = `
<ng-container [ngxErrors]="multipleErrors">
<div ngxError="minlength">minlength</div>
<div ngxError="maxlength">maxlength</div>
</ng-container>`;
createDirectiveWithConfig(showWhen, showMaxErrors);
spectator.hostComponent.multipleErrors.markAsTouched();
spectator.hostComponent.multipleErrors.markAsDirty();
});

describe('GIVEN: showMaxErrors is 1', () => {
Given(() => {
showMaxErrors = 1;
showWhen = 'touched';
});

Then(async () => {
await wait(0);
const errors = spectator.queryAll('[ngxerror]:not([hidden])');
expect(errors.length).toBe(1);
});
});
});

describe('TEST: submitting a form should display an error', () => {
Given(() => {
template = `
Expand Down
48 changes: 36 additions & 12 deletions projects/ngx-errors/src/lib/error.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,14 @@ export class ErrorDirective implements AfterViewInit, OnDestroy {

ngAfterViewInit() {
this.validateDirective();
this.watchForEventsTriggeringVisibilityChange();
}

ngOnDestroy() {
this.subs.unsubscribe();
}

private watchForEventsTriggeringVisibilityChange() {
const ngSubmit$ = this.errorsDirective.parentForm
? this.errorsDirective.parentForm.ngSubmit
: NEVER;
Expand All @@ -69,9 +76,9 @@ export class ErrorDirective implements AfterViewInit, OnDestroy {

const sub = this.errorsDirective.control$
.pipe(
filter((c): c is AbstractControl => !!c),
tap((control) => {
this.initConfig(control);
this.watchForVisibilityChange(control);
}),
tap((control) => {
touchedChanges$ = extractTouchedChanges(control);
Expand Down Expand Up @@ -110,10 +117,6 @@ export class ErrorDirective implements AfterViewInit, OnDestroy {
this.subs.add(sub);
}

ngOnDestroy() {
this.subs.unsubscribe();
}

private calcShouldDisplay(control: AbstractControl) {
const hasError = control.hasError(this.errorName);

Expand All @@ -128,20 +131,41 @@ export class ErrorDirective implements AfterViewInit, OnDestroy {
);
}

const couldShowError = errorStateMatcher.isErrorState(control, form);
const hasErrorState = errorStateMatcher.isErrorState(control, form);

this.hidden = !(couldShowError && hasError);
const couldBeHidden = !(hasErrorState && hasError);

this.overriddenShowWhen.errorVisibilityChanged(
control,
this.errorsDirective.visibilityChanged(
this.errorName,
this.showWhen,
!this.hidden
couldBeHidden
);
}

this.err = control.getError(this.errorName) || {};
private watchForVisibilityChange(control: AbstractControl) {
const key = `${this.errorName}-${this.showWhen}`;

this.cdr.detectChanges();
const sub = this.errorsDirective
.visibilityForKey$(key)
.pipe(
tap((hidden) => {
this.hidden = hidden;

this.overriddenShowWhen.errorVisibilityChanged(
control,
this.errorName,
this.showWhen,
!this.hidden
);

this.err = control.getError(this.errorName) || {};

this.cdr.detectChanges();
})
)
.subscribe();

this.subs.add(sub);
}

private initConfig(control: AbstractControl) {
Expand Down
8 changes: 7 additions & 1 deletion projects/ngx-errors/src/lib/errors-configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type ShowErrorWhen =

export interface IErrorsConfiguration {
/**
* Configures when to display an error for an invalid control. Available options are:
* Configures when to display an error for an invalid control. Options that are available by default are listed below. Note, custom options can be provided using CUSTOM_ERROR_STATE_MATCHERS injection token.
*
* `'touched'` - *[default]* shows an error when control is marked as touched. For example, user focused on the input and clicked away or tabbed through the input.
*
Expand All @@ -25,9 +25,15 @@ export interface IErrorsConfiguration {
* `'formIsSubmitted'` - shows an error when parent form was submitted.
*/
showErrorsWhenInput: string;

/**
* The maximum amount of errors to display per ngxErrors block.
*/
showMaxErrors?: number;
}

@Injectable()
export class ErrorsConfiguration implements IErrorsConfiguration {
showErrorsWhenInput = 'touched';
showMaxErrors = undefined;
}
29 changes: 21 additions & 8 deletions projects/ngx-errors/src/lib/errors.directive.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { Component } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import {
AbstractControl,
FormControl,
FormGroup,
ReactiveFormsModule,
} from '@angular/forms';
import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator';
import { first } from 'rxjs/operators';
import { ErrorsDirective } from './errors.directive';
import {
ControlNotFoundError,
Expand Down Expand Up @@ -55,6 +61,16 @@ describe('ErrorsDirective ', () => {
});

describe('GIVEN: with parent form', () => {
function expectControl(expectedControl: AbstractControl) {
let actualControl: AbstractControl | undefined;

spectator.directive.control$.pipe(first()).subscribe((control) => {
actualControl = control;
});

expect(actualControl).toBe(expectedControl);
}

describe('GIVEN: control specified as string; control exists', () => {
Then('should not throw', () => {
expect(() => {
Expand All @@ -68,7 +84,8 @@ describe('ErrorsDirective ', () => {
const fName = spectator.hostComponent.form.get(
'firstName'
) as FormControl;
expect(spectator.directive.control$.getValue()).toBe(fName);

expectControl(fName);
});
});

Expand Down Expand Up @@ -98,9 +115,7 @@ describe('ErrorsDirective ', () => {
});

Then('control should be the "street"', () => {
expect(spectator.directive.control$.getValue()).toBe(
spectator.hostComponent.street
);
expectControl(spectator.hostComponent.street);
});
});

Expand All @@ -118,9 +133,7 @@ describe('ErrorsDirective ', () => {
});

Then('control should be the "street"', () => {
expect(spectator.directive.control$.getValue()).toBe(
spectator.hostComponent.street
);
expectControl(spectator.hostComponent.street);
});
});
});
Expand Down
Loading

0 comments on commit d46ba1b

Please sign in to comment.