Skip to content

Commit 5892c75

Browse files
kfrederixKarel Frederix
andauthored
feat: add option to show errors always on change (#71)
Co-authored-by: Karel Frederix <[email protected]>
1 parent 2d2b41b commit 5892c75

File tree

4 files changed

+111
-20
lines changed

4 files changed

+111
-20
lines changed

README.md

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -185,16 +185,22 @@ One typical case when to use it is radio buttons in the same radio group where i
185185
<input [controlErrorsOnAsync]="false" formControlName="name" />
186186
```
187187

188-
- To modify the error display behavior and show the errors on submission alone, set the following input:
188+
- `controlErrorsOnBlur` - To modify the error display behavior to not show errors on blur, set the following input:
189+
190+
```html
191+
<input [controlErrorsOnBlur]="false" formControlName="name" />
192+
```
193+
194+
- To modify the error display behavior and show the errors on submission alone, we can disable both `controlErrorsOnBlur` and `controlErrorsOnAsync`:
189195

190196
```html
191197
<input [controlErrorsOnBlur]="false" [controlErrorsOnAsync]="false" formControlName="name" />
192198
```
193199

194-
- `controlErrorsOnBlur` - To modify the error display behavior to not show errors on blur, set the following input:
200+
- `controlErrorsOnChange` - To modify the error display behavior to show/hide the errors on every change, set the following input:
195201

196202
```html
197-
<input [controlErrorsOnBlur]="false" formControlName="name" />
203+
<input [controlErrorsOnChange]="true" formControlName="name" />
198204
```
199205

200206
## Methods
@@ -316,10 +322,16 @@ The library adds a `form-submitted` to the submitted form. You can use it to sty
316322
export class AppModule {}
317323
```
318324

319-
- `controlErrorsOnBlur` - To modify the error display behavior and show the errors on submission alone, set the following input:
325+
- `controlErrorsOn` - Optional. An object that allows the default behavior for showing the errors to be overridden. (each individual property in the object is optional, so it's possible to override only 1 setting)
320326

321-
```html
322-
<input [controlErrorsOnBlur]="false" formControlName="name" />
327+
```ts
328+
{
329+
controlErrorsOn: {
330+
async: true, // (default: true)
331+
blur: true, // (default: true)
332+
change: true, // (default: false)
333+
}
334+
}
323335
```
324336

325337
## Recipes

projects/ngneat/error-tailor/src/lib/control-error.directive.spec.ts

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ describe('ControlErrorDirective', () => {
6464
6565
<input formControlName="username" placeholder="Username" />
6666
67+
<input formControlName="onSubmitOnly" placeholder="On submit only" [controlErrorsOnBlur]="false" />
68+
<input formControlName="onEveryChange" placeholder="On every change" [controlErrorsOnChange]="true" />
69+
6770
<button type="submit">Submit</button>
6871
</form>
6972
`
@@ -75,7 +78,9 @@ describe('ControlErrorDirective', () => {
7578
ignored: ['', Validators.required],
7679
explicit: [''],
7780
names: this.builder.array([this.createName(), this.createName()], this.validator),
78-
username: ['', null, this.usernameValidator.bind(this)]
81+
username: ['', null, this.usernameValidator.bind(this)],
82+
onSubmitOnly: ['', [Validators.required]],
83+
onEveryChange: ['', [Validators.required]]
7984
});
8085

8186
@ViewChild('explicitErrorTailor', { static: true }) explicitErrorTailor: ControlErrorsDirective;
@@ -131,6 +136,11 @@ describe('ControlErrorDirective', () => {
131136
const oneNameInput = spectator.query<HTMLInputElement>(byPlaceholder('Name 0'));
132137
const oneNameInput1 = spectator.query<HTMLInputElement>(byPlaceholder('Name 1'));
133138

139+
const onSubmitOnly = spectator.query<HTMLInputElement>(byPlaceholder('On submit only'));
140+
const onEveryChange = spectator.query<HTMLInputElement>(byPlaceholder('On every change'));
141+
typeInElementAndFocusOut(spectator, 'test', onSubmitOnly);
142+
typeInElementAndFocusOut(spectator, 'test', onEveryChange);
143+
134144
spectator.click('button');
135145

136146
expect(spectator.query(byText('required one error'))).toBeTruthy();
@@ -142,6 +152,33 @@ describe('ControlErrorDirective', () => {
142152
expect(spectator.query(byText(/error/))).toBeNull();
143153
});
144154

155+
it('should show errors only on submit when controlErrorsOnBlur is disabled', () => {
156+
const onSubmitOnly = spectator.query<HTMLInputElement>(byPlaceholder('On submit only'));
157+
158+
typeInElementAndFocusOut(spectator, 'test', onSubmitOnly);
159+
160+
expect(spectator.query(byText('required error'))).toBeFalsy();
161+
162+
spectator.click('button');
163+
164+
expect(spectator.query(byText('required error'))).toBeTruthy();
165+
});
166+
167+
it('should show errors on every change when controlErrorsOnChange is enabled', () => {
168+
const onEveryChange = spectator.query<HTMLInputElement>(byPlaceholder('On every change'));
169+
170+
expect(spectator.query(byText('required error'))).toBeFalsy();
171+
172+
spectator.typeInElement('t', onEveryChange);
173+
expect(spectator.query(byText('required error'))).toBeFalsy();
174+
175+
spectator.typeInElement('', onEveryChange);
176+
expect(spectator.query(byText('required error'))).toBeTruthy();
177+
178+
spectator.typeInElement('t', onEveryChange);
179+
expect(spectator.query(byText('required error'))).toBeFalsy();
180+
});
181+
145182
it('should not show errors on interactions', () => {
146183
const ignoredInput = spectator.query<HTMLInputElement>(byPlaceholder('Ignored'));
147184

@@ -340,8 +377,6 @@ describe('ControlErrorDirective', () => {
340377
<div controlErrorAnchor>
341378
<input formControlName="withParentAnchor" placeholder="With parent anchor" />
342379
</div>
343-
344-
<input formControlName="onEveryChange" placeholder="On every change" [controlErrorsOnBlur]="false" />
345380
</form>
346381
`
347382
})
@@ -351,8 +386,7 @@ describe('ControlErrorDirective', () => {
351386
customTemplate: ['', Validators.required],
352387
customClass: ['', Validators.required],
353388
withAnchor: ['', Validators.required],
354-
withParentAnchor: ['', Validators.required],
355-
onEveryChange: ['', [Validators.required, Validators.minLength(3)]]
389+
withParentAnchor: ['', Validators.required]
356390
});
357391

358392
customErrors = {
@@ -466,7 +500,10 @@ describe('ControlErrorDirective', () => {
466500
}
467501
},
468502
controlErrorComponent: CustomControlErrorComponent,
469-
controlErrorComponentAnchorFn: controlErrorComponentAnchorFn
503+
controlErrorComponentAnchorFn: controlErrorComponentAnchorFn,
504+
controlErrorsOn: {
505+
change: true
506+
}
470507
})
471508
]
472509
});
@@ -532,5 +569,27 @@ describe('ControlErrorDirective', () => {
532569
expect(anchorFnDestroyCalled).toBeTruthy();
533570
});
534571
});
572+
573+
describe('controlErrorsOn', () => {
574+
let spectator: Spectator<CustomErrorFormGroupComponent>;
575+
const createComponent = getCustomErrorComponentFactory(CustomErrorFormGroupComponent);
576+
577+
beforeEach(() => (spectator = createComponent()));
578+
579+
it('should override default behavior for showing errors', () => {
580+
const input = spectator.query<HTMLInputElement>(byPlaceholder('Name'));
581+
582+
expect(spectator.query(byText('required error'))).toBeFalsy();
583+
584+
spectator.typeInElement('test', input);
585+
expect(spectator.query(byText('required error'))).toBeFalsy();
586+
587+
spectator.typeInElement('', input);
588+
expect(spectator.query(byText('required error'))).toBeTruthy();
589+
590+
spectator.typeInElement('t', input);
591+
expect(spectator.query(byText('required error'))).toBeFalsy();
592+
});
593+
});
535594
});
536595
});

projects/ngneat/error-tailor/src/lib/control-error.directive.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { DefaultControlErrorComponent, ControlErrorComponent } from './control-e
1818
import { ControlErrorAnchorDirective } from './control-error-anchor.directive';
1919
import { EMPTY, fromEvent, merge, NEVER, Observable, Subject } from 'rxjs';
2020
import { ErrorTailorConfig, ErrorTailorConfigProvider, FORM_ERRORS } from './providers';
21-
import { distinctUntilChanged, mapTo, startWith, switchMap, takeUntil, tap } from 'rxjs/operators';
21+
import { distinctUntilChanged, mapTo, startWith, switchMap, takeUntil } from 'rxjs/operators';
2222
import { FormActionDirective } from './form-action.directive';
2323
import { ErrorsMap } from './types';
2424

@@ -31,8 +31,9 @@ export class ControlErrorsDirective implements OnInit, OnDestroy {
3131
@Input('controlErrors') customErrors: ErrorsMap = {};
3232
@Input() controlErrorsClass: string | string[] | undefined;
3333
@Input() controlErrorsTpl: TemplateRef<any> | undefined;
34-
@Input() controlErrorsOnAsync = true;
35-
@Input() controlErrorsOnBlur = true;
34+
@Input() controlErrorsOnAsync: boolean | undefined;
35+
@Input() controlErrorsOnBlur: boolean | undefined;
36+
@Input() controlErrorsOnChange: boolean | undefined;
3637
@Input() controlErrorAnchor: ControlErrorAnchorDirective;
3738

3839
private ref: ComponentRef<ControlErrorComponent>;
@@ -58,10 +59,11 @@ export class ControlErrorsDirective implements OnInit, OnDestroy {
5859
) {
5960
this.submit$ = this.form ? this.form.submit$ : EMPTY;
6061
this.reset$ = this.form ? this.form.reset$ : EMPTY;
61-
this.mergedConfig = this.buildConfig();
6262
}
6363

6464
ngOnInit() {
65+
this.mergedConfig = this.buildConfig();
66+
6567
this.anchor = this.resolveAnchor();
6668
this.control = (this.controlContainer || this.ngControl).control;
6769
const hasAsyncValidator = !!this.control.asyncValidator;
@@ -71,13 +73,19 @@ export class ControlErrorsDirective implements OnInit, OnDestroy {
7173
const controlChanges$ = merge(statusChanges$, valueChanges$);
7274
let changesOnAsync$: Observable<any> = EMPTY;
7375
let changesOnBlur$: Observable<any> = EMPTY;
76+
let changesOnChange$: Observable<any> = EMPTY;
7477

75-
if (this.controlErrorsOnAsync && hasAsyncValidator) {
78+
if (this.mergedConfig.controlErrorsOn.async && hasAsyncValidator) {
7679
// hasAsyncThenUponStatusChange
7780
changesOnAsync$ = statusChanges$.pipe(startWith(true));
7881
}
7982

80-
if (this.controlErrorsOnBlur && this.isInput) {
83+
if (this.isInput && this.mergedConfig.controlErrorsOn.change) {
84+
// on each change
85+
changesOnChange$ = valueChanges$;
86+
}
87+
88+
if (this.isInput && this.mergedConfig.controlErrorsOn.blur) {
8189
const blur$ = fromEvent(this.host.nativeElement, 'focusout');
8290
// blurFirstThenUponChange
8391
changesOnBlur$ = blur$.pipe(switchMap(() => valueChanges$.pipe(startWith(true))));
@@ -93,7 +101,7 @@ export class ControlErrorsDirective implements OnInit, OnDestroy {
93101
// on reset, clear ComponentRef and customAnchorDestroyFn
94102
this.reset$.pipe(takeUntil(this.destroy)).subscribe(() => this.clearRefs());
95103

96-
merge(changesOnAsync$, changesOnBlur$, changesOnSubmit$, this.showError$)
104+
merge(changesOnAsync$, changesOnBlur$, changesOnChange$, changesOnSubmit$, this.showError$)
97105
.pipe(takeUntil(this.destroy))
98106
.subscribe(() => this.valueChanges());
99107
}
@@ -200,7 +208,14 @@ export class ControlErrorsDirective implements OnInit, OnDestroy {
200208
},
201209
controlErrorComponent: DefaultControlErrorComponent
202210
},
203-
...this.config
211+
212+
...this.config,
213+
214+
controlErrorsOn: {
215+
async: this.controlErrorsOnAsync ?? this.config.controlErrorsOn?.async ?? true,
216+
blur: this.controlErrorsOnAsync ?? this.config.controlErrorsOn?.blur ?? true,
217+
change: this.controlErrorsOnChange ?? this.config.controlErrorsOn?.change ?? false
218+
}
204219
};
205220
}
206221
}

projects/ngneat/error-tailor/src/lib/providers.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ export type ErrorTailorConfig = {
2424
blurPredicate?: (element: Element) => boolean;
2525
controlErrorComponent?: Type<ControlErrorComponent>;
2626
controlErrorComponentAnchorFn?: (hostElement: Element, errorElement: Element) => () => void;
27+
controlErrorsOn?: {
28+
async?: boolean;
29+
blur?: boolean;
30+
change?: boolean;
31+
};
2732
};
2833

2934
export const ErrorTailorConfigProvider = new InjectionToken<ErrorTailorConfig>('ErrorTailorConfigProvider');

0 commit comments

Comments
 (0)