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

Commit d46ba1b

Browse files
feat: new config option - showMaxErrors
1 parent 55fd44a commit d46ba1b

File tree

10 files changed

+237
-44
lines changed

10 files changed

+237
-44
lines changed

README.md

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ The design of this library promotes less boilerplate code, which keeps your temp
3131
- [How it works](#how-it-works)
3232
- [Installation](#installation)
3333
- [Usage](#usage)
34-
- [Advanced configuration](#configuration)
34+
- [Configuration](#configuration)
3535
- [Handling form submission](#handling-form-submission)
3636
- [Getting error details](#getting-error-details)
3737
- [Styling](#styling)
@@ -173,7 +173,9 @@ Here's the configuration object interface:
173173
```ts
174174
export interface IErrorsConfiguration {
175175
/**
176-
* Configures when to display an error for an invalid control. Available options are:
176+
* Configures when to display an error for an invalid control. Options that
177+
* are available by default are listed below. Note, custom options can be
178+
* provided using CUSTOM_ERROR_STATE_MATCHERS injection token.
177179
*
178180
* `'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.
179181
*
@@ -183,16 +185,56 @@ export interface IErrorsConfiguration {
183185
*
184186
* `'formIsSubmitted'` - shows an error when parent form was submitted.
185187
*/
186-
showErrorsWhenInput: ShowErrorWhen;
188+
showErrorsWhenInput: string;
189+
190+
/**
191+
* The maximum amount of errors to display per ngxErrors block.
192+
*/
193+
showMaxErrors?: number;
187194
}
195+
```
196+
197+
### Providing custom logic for displaying errors
188198

189-
export type ShowErrorWhen =
190-
| 'touched'
191-
| 'dirty'
192-
| 'touchedAndDirty'
193-
| 'formIsSubmitted';
199+
By default, the following error state matchers for displaying errors can be used: `'touched'`, `'dirty'`, `'touchedAndDirty'`, `'formIsSubmitted'`.
200+
201+
Custom error state matchers can be added using the `CUSTOM_ERROR_STATE_MATCHERS` injection token.
202+
203+
First, define the new error state matcher:
204+
205+
```ts
206+
@Injectable({ providedIn: 'root' })
207+
export class MyAwesomeErrorStateMatcher implements ErrorStateMatcher {
208+
isErrorState(
209+
control: AbstractControl | null,
210+
form: FormGroupDirective | NgForm | null
211+
): boolean {
212+
return !!(control && control.value && /* my awesome logic is here */);
213+
}
214+
}
215+
```
216+
217+
Second, use the new error state matcher when providing `CUSTOM_ERROR_STATE_MATCHERS` in the AppModule:
218+
219+
```ts
220+
providers: [
221+
{
222+
provide: CUSTOM_ERROR_STATE_MATCHERS,
223+
deps: [MyAwesomeErrorStateMatcher],
224+
useFactory: (myAwesomeErrorStateMatcher: MyAwesomeErrorStateMatcher) => {
225+
return {
226+
myAwesome: myAwesomeErrorStateMatcher,
227+
};
228+
},
229+
},
230+
];
194231
```
195232

233+
Now the string `'myAwesome'` can be used either in the `showErrorsWhenInput` property of the configuration object or in the `[showWhen]` inputs.
234+
235+
> 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.
236+
> 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.
237+
196238
### Overriding global config
197239

198240
You can override the configuration specified at the module level by using `[showWhen]` input on `[ngxErrors]` and on `[ngxError]` directives:

projects/ngx-errors/src/lib/error.directive.spec.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ const myAsyncValidator: AsyncValidatorFn = (c: AbstractControl) => {
4040
class TestHostComponent {
4141
validInitialVal = new FormControl('val', Validators.required);
4242
invalidInitialVal = new FormControl('', Validators.required);
43+
multipleErrors = new FormControl('123456', [
44+
Validators.minLength(10),
45+
Validators.maxLength(3),
46+
]);
4347

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

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

81-
function createDirectiveWithConfig(showErrorsWhenInput: string) {
85+
function createDirectiveWithConfig(
86+
showErrorsWhenInput: string,
87+
showMaxErrors?: number
88+
) {
8289
const config: IErrorsConfiguration = { showErrorsWhenInput };
90+
if (showMaxErrors !== undefined) {
91+
config.showMaxErrors = showMaxErrors;
92+
}
8393
spectator = createDirective(template, {
8494
providers: [
8595
{
@@ -285,6 +295,34 @@ describe('ErrorDirective', () => {
285295
});
286296
});
287297

298+
describe('TEST: limiting amount of visible ngxError', () => {
299+
let showMaxErrors: number;
300+
301+
When(() => {
302+
template = `
303+
<ng-container [ngxErrors]="multipleErrors">
304+
<div ngxError="minlength">minlength</div>
305+
<div ngxError="maxlength">maxlength</div>
306+
</ng-container>`;
307+
createDirectiveWithConfig(showWhen, showMaxErrors);
308+
spectator.hostComponent.multipleErrors.markAsTouched();
309+
spectator.hostComponent.multipleErrors.markAsDirty();
310+
});
311+
312+
describe('GIVEN: showMaxErrors is 1', () => {
313+
Given(() => {
314+
showMaxErrors = 1;
315+
showWhen = 'touched';
316+
});
317+
318+
Then(async () => {
319+
await wait(0);
320+
const errors = spectator.queryAll('[ngxerror]:not([hidden])');
321+
expect(errors.length).toBe(1);
322+
});
323+
});
324+
});
325+
288326
describe('TEST: submitting a form should display an error', () => {
289327
Given(() => {
290328
template = `

projects/ngx-errors/src/lib/error.directive.ts

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,14 @@ export class ErrorDirective implements AfterViewInit, OnDestroy {
6060

6161
ngAfterViewInit() {
6262
this.validateDirective();
63+
this.watchForEventsTriggeringVisibilityChange();
64+
}
65+
66+
ngOnDestroy() {
67+
this.subs.unsubscribe();
68+
}
6369

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

7077
const sub = this.errorsDirective.control$
7178
.pipe(
72-
filter((c): c is AbstractControl => !!c),
7379
tap((control) => {
7480
this.initConfig(control);
81+
this.watchForVisibilityChange(control);
7582
}),
7683
tap((control) => {
7784
touchedChanges$ = extractTouchedChanges(control);
@@ -110,10 +117,6 @@ export class ErrorDirective implements AfterViewInit, OnDestroy {
110117
this.subs.add(sub);
111118
}
112119

113-
ngOnDestroy() {
114-
this.subs.unsubscribe();
115-
}
116-
117120
private calcShouldDisplay(control: AbstractControl) {
118121
const hasError = control.hasError(this.errorName);
119122

@@ -128,20 +131,41 @@ export class ErrorDirective implements AfterViewInit, OnDestroy {
128131
);
129132
}
130133

131-
const couldShowError = errorStateMatcher.isErrorState(control, form);
134+
const hasErrorState = errorStateMatcher.isErrorState(control, form);
132135

133-
this.hidden = !(couldShowError && hasError);
136+
const couldBeHidden = !(hasErrorState && hasError);
134137

135-
this.overriddenShowWhen.errorVisibilityChanged(
136-
control,
138+
this.errorsDirective.visibilityChanged(
137139
this.errorName,
138140
this.showWhen,
139-
!this.hidden
141+
couldBeHidden
140142
);
143+
}
141144

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

144-
this.cdr.detectChanges();
148+
const sub = this.errorsDirective
149+
.visibilityForKey$(key)
150+
.pipe(
151+
tap((hidden) => {
152+
this.hidden = hidden;
153+
154+
this.overriddenShowWhen.errorVisibilityChanged(
155+
control,
156+
this.errorName,
157+
this.showWhen,
158+
!this.hidden
159+
);
160+
161+
this.err = control.getError(this.errorName) || {};
162+
163+
this.cdr.detectChanges();
164+
})
165+
)
166+
.subscribe();
167+
168+
this.subs.add(sub);
145169
}
146170

147171
private initConfig(control: AbstractControl) {

projects/ngx-errors/src/lib/errors-configuration.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export type ShowErrorWhen =
1414

1515
export interface IErrorsConfiguration {
1616
/**
17-
* Configures when to display an error for an invalid control. Available options are:
17+
* 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.
1818
*
1919
* `'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.
2020
*
@@ -25,9 +25,15 @@ export interface IErrorsConfiguration {
2525
* `'formIsSubmitted'` - shows an error when parent form was submitted.
2626
*/
2727
showErrorsWhenInput: string;
28+
29+
/**
30+
* The maximum amount of errors to display per ngxErrors block.
31+
*/
32+
showMaxErrors?: number;
2833
}
2934

3035
@Injectable()
3136
export class ErrorsConfiguration implements IErrorsConfiguration {
3237
showErrorsWhenInput = 'touched';
38+
showMaxErrors = undefined;
3339
}

projects/ngx-errors/src/lib/errors.directive.spec.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { Component } from '@angular/core';
2-
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
2+
import {
3+
AbstractControl,
4+
FormControl,
5+
FormGroup,
6+
ReactiveFormsModule,
7+
} from '@angular/forms';
38
import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator';
9+
import { first } from 'rxjs/operators';
410
import { ErrorsDirective } from './errors.directive';
511
import {
612
ControlNotFoundError,
@@ -55,6 +61,16 @@ describe('ErrorsDirective ', () => {
5561
});
5662

5763
describe('GIVEN: with parent form', () => {
64+
function expectControl(expectedControl: AbstractControl) {
65+
let actualControl: AbstractControl | undefined;
66+
67+
spectator.directive.control$.pipe(first()).subscribe((control) => {
68+
actualControl = control;
69+
});
70+
71+
expect(actualControl).toBe(expectedControl);
72+
}
73+
5874
describe('GIVEN: control specified as string; control exists', () => {
5975
Then('should not throw', () => {
6076
expect(() => {
@@ -68,7 +84,8 @@ describe('ErrorsDirective ', () => {
6884
const fName = spectator.hostComponent.form.get(
6985
'firstName'
7086
) as FormControl;
71-
expect(spectator.directive.control$.getValue()).toBe(fName);
87+
88+
expectControl(fName);
7289
});
7390
});
7491

@@ -98,9 +115,7 @@ describe('ErrorsDirective ', () => {
98115
});
99116

100117
Then('control should be the "street"', () => {
101-
expect(spectator.directive.control$.getValue()).toBe(
102-
spectator.hostComponent.street
103-
);
118+
expectControl(spectator.hostComponent.street);
104119
});
105120
});
106121

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

120135
Then('control should be the "street"', () => {
121-
expect(spectator.directive.control$.getValue()).toBe(
122-
spectator.hostComponent.street
123-
);
136+
expectControl(spectator.hostComponent.street);
124137
});
125138
});
126139
});

0 commit comments

Comments
 (0)