Skip to content

Commit

Permalink
feat: add option to show errors always on change (#71)
Browse files Browse the repository at this point in the history
Co-authored-by: Karel Frederix <[email protected]>
  • Loading branch information
kfrederix and Karel Frederix committed May 24, 2022
1 parent 2d2b41b commit 5892c75
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 20 deletions.
24 changes: 18 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,16 +185,22 @@ One typical case when to use it is radio buttons in the same radio group where i
<input [controlErrorsOnAsync]="false" formControlName="name" />
```

- To modify the error display behavior and show the errors on submission alone, set the following input:
- `controlErrorsOnBlur` - To modify the error display behavior to not show errors on blur, set the following input:

```html
<input [controlErrorsOnBlur]="false" formControlName="name" />
```

- To modify the error display behavior and show the errors on submission alone, we can disable both `controlErrorsOnBlur` and `controlErrorsOnAsync`:

```html
<input [controlErrorsOnBlur]="false" [controlErrorsOnAsync]="false" formControlName="name" />
```

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

```html
<input [controlErrorsOnBlur]="false" formControlName="name" />
<input [controlErrorsOnChange]="true" formControlName="name" />
```

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

- `controlErrorsOnBlur` - To modify the error display behavior and show the errors on submission alone, set the following input:
- `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)

```html
<input [controlErrorsOnBlur]="false" formControlName="name" />
```ts
{
controlErrorsOn: {
async: true, // (default: true)
blur: true, // (default: true)
change: true, // (default: false)
}
}
```

## Recipes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ describe('ControlErrorDirective', () => {
<input formControlName="username" placeholder="Username" />
<input formControlName="onSubmitOnly" placeholder="On submit only" [controlErrorsOnBlur]="false" />
<input formControlName="onEveryChange" placeholder="On every change" [controlErrorsOnChange]="true" />
<button type="submit">Submit</button>
</form>
`
Expand All @@ -75,7 +78,9 @@ describe('ControlErrorDirective', () => {
ignored: ['', Validators.required],
explicit: [''],
names: this.builder.array([this.createName(), this.createName()], this.validator),
username: ['', null, this.usernameValidator.bind(this)]
username: ['', null, this.usernameValidator.bind(this)],
onSubmitOnly: ['', [Validators.required]],
onEveryChange: ['', [Validators.required]]
});

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

const onSubmitOnly = spectator.query<HTMLInputElement>(byPlaceholder('On submit only'));
const onEveryChange = spectator.query<HTMLInputElement>(byPlaceholder('On every change'));
typeInElementAndFocusOut(spectator, 'test', onSubmitOnly);
typeInElementAndFocusOut(spectator, 'test', onEveryChange);

spectator.click('button');

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

it('should show errors only on submit when controlErrorsOnBlur is disabled', () => {
const onSubmitOnly = spectator.query<HTMLInputElement>(byPlaceholder('On submit only'));

typeInElementAndFocusOut(spectator, 'test', onSubmitOnly);

expect(spectator.query(byText('required error'))).toBeFalsy();

spectator.click('button');

expect(spectator.query(byText('required error'))).toBeTruthy();
});

it('should show errors on every change when controlErrorsOnChange is enabled', () => {
const onEveryChange = spectator.query<HTMLInputElement>(byPlaceholder('On every change'));

expect(spectator.query(byText('required error'))).toBeFalsy();

spectator.typeInElement('t', onEveryChange);
expect(spectator.query(byText('required error'))).toBeFalsy();

spectator.typeInElement('', onEveryChange);
expect(spectator.query(byText('required error'))).toBeTruthy();

spectator.typeInElement('t', onEveryChange);
expect(spectator.query(byText('required error'))).toBeFalsy();
});

it('should not show errors on interactions', () => {
const ignoredInput = spectator.query<HTMLInputElement>(byPlaceholder('Ignored'));

Expand Down Expand Up @@ -340,8 +377,6 @@ describe('ControlErrorDirective', () => {
<div controlErrorAnchor>
<input formControlName="withParentAnchor" placeholder="With parent anchor" />
</div>
<input formControlName="onEveryChange" placeholder="On every change" [controlErrorsOnBlur]="false" />
</form>
`
})
Expand All @@ -351,8 +386,7 @@ describe('ControlErrorDirective', () => {
customTemplate: ['', Validators.required],
customClass: ['', Validators.required],
withAnchor: ['', Validators.required],
withParentAnchor: ['', Validators.required],
onEveryChange: ['', [Validators.required, Validators.minLength(3)]]
withParentAnchor: ['', Validators.required]
});

customErrors = {
Expand Down Expand Up @@ -466,7 +500,10 @@ describe('ControlErrorDirective', () => {
}
},
controlErrorComponent: CustomControlErrorComponent,
controlErrorComponentAnchorFn: controlErrorComponentAnchorFn
controlErrorComponentAnchorFn: controlErrorComponentAnchorFn,
controlErrorsOn: {
change: true
}
})
]
});
Expand Down Expand Up @@ -532,5 +569,27 @@ describe('ControlErrorDirective', () => {
expect(anchorFnDestroyCalled).toBeTruthy();
});
});

describe('controlErrorsOn', () => {
let spectator: Spectator<CustomErrorFormGroupComponent>;
const createComponent = getCustomErrorComponentFactory(CustomErrorFormGroupComponent);

beforeEach(() => (spectator = createComponent()));

it('should override default behavior for showing errors', () => {
const input = spectator.query<HTMLInputElement>(byPlaceholder('Name'));

expect(spectator.query(byText('required error'))).toBeFalsy();

spectator.typeInElement('test', input);
expect(spectator.query(byText('required error'))).toBeFalsy();

spectator.typeInElement('', input);
expect(spectator.query(byText('required error'))).toBeTruthy();

spectator.typeInElement('t', input);
expect(spectator.query(byText('required error'))).toBeFalsy();
});
});
});
});
31 changes: 23 additions & 8 deletions projects/ngneat/error-tailor/src/lib/control-error.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { DefaultControlErrorComponent, ControlErrorComponent } from './control-e
import { ControlErrorAnchorDirective } from './control-error-anchor.directive';
import { EMPTY, fromEvent, merge, NEVER, Observable, Subject } from 'rxjs';
import { ErrorTailorConfig, ErrorTailorConfigProvider, FORM_ERRORS } from './providers';
import { distinctUntilChanged, mapTo, startWith, switchMap, takeUntil, tap } from 'rxjs/operators';
import { distinctUntilChanged, mapTo, startWith, switchMap, takeUntil } from 'rxjs/operators';
import { FormActionDirective } from './form-action.directive';
import { ErrorsMap } from './types';

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

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

ngOnInit() {
this.mergedConfig = this.buildConfig();

this.anchor = this.resolveAnchor();
this.control = (this.controlContainer || this.ngControl).control;
const hasAsyncValidator = !!this.control.asyncValidator;
Expand All @@ -71,13 +73,19 @@ export class ControlErrorsDirective implements OnInit, OnDestroy {
const controlChanges$ = merge(statusChanges$, valueChanges$);
let changesOnAsync$: Observable<any> = EMPTY;
let changesOnBlur$: Observable<any> = EMPTY;
let changesOnChange$: Observable<any> = EMPTY;

if (this.controlErrorsOnAsync && hasAsyncValidator) {
if (this.mergedConfig.controlErrorsOn.async && hasAsyncValidator) {
// hasAsyncThenUponStatusChange
changesOnAsync$ = statusChanges$.pipe(startWith(true));
}

if (this.controlErrorsOnBlur && this.isInput) {
if (this.isInput && this.mergedConfig.controlErrorsOn.change) {
// on each change
changesOnChange$ = valueChanges$;
}

if (this.isInput && this.mergedConfig.controlErrorsOn.blur) {
const blur$ = fromEvent(this.host.nativeElement, 'focusout');
// blurFirstThenUponChange
changesOnBlur$ = blur$.pipe(switchMap(() => valueChanges$.pipe(startWith(true))));
Expand All @@ -93,7 +101,7 @@ export class ControlErrorsDirective implements OnInit, OnDestroy {
// on reset, clear ComponentRef and customAnchorDestroyFn
this.reset$.pipe(takeUntil(this.destroy)).subscribe(() => this.clearRefs());

merge(changesOnAsync$, changesOnBlur$, changesOnSubmit$, this.showError$)
merge(changesOnAsync$, changesOnBlur$, changesOnChange$, changesOnSubmit$, this.showError$)
.pipe(takeUntil(this.destroy))
.subscribe(() => this.valueChanges());
}
Expand Down Expand Up @@ -200,7 +208,14 @@ export class ControlErrorsDirective implements OnInit, OnDestroy {
},
controlErrorComponent: DefaultControlErrorComponent
},
...this.config

...this.config,

controlErrorsOn: {
async: this.controlErrorsOnAsync ?? this.config.controlErrorsOn?.async ?? true,
blur: this.controlErrorsOnAsync ?? this.config.controlErrorsOn?.blur ?? true,
change: this.controlErrorsOnChange ?? this.config.controlErrorsOn?.change ?? false
}
};
}
}
5 changes: 5 additions & 0 deletions projects/ngneat/error-tailor/src/lib/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ export type ErrorTailorConfig = {
blurPredicate?: (element: Element) => boolean;
controlErrorComponent?: Type<ControlErrorComponent>;
controlErrorComponentAnchorFn?: (hostElement: Element, errorElement: Element) => () => void;
controlErrorsOn?: {
async?: boolean;
blur?: boolean;
change?: boolean;
};
};

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

0 comments on commit 5892c75

Please sign in to comment.