Skip to content

Commit 8a037e2

Browse files
Sir-JSergey Rogachev
andauthored
Add new features for ControErrorDirective and fix one bug (#80)
* feat: added two properties in ConfigProvider bufix: if text unset for default error component, custom classes stay added docs: update playground: update * fix: remove console.log * fix: controlCustomClass get if not set get value from global config test: add tests Co-authored-by: Sergey Rogachev <[email protected]>
1 parent df3a3b8 commit 8a037e2

File tree

7 files changed

+113
-11
lines changed

7 files changed

+113
-11
lines changed

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,20 @@ The directive will show all errors for a form field automatically in two instanc
107107

108108
## Inputs
109109

110-
- `controlErrorsClass` - A custom classes that'll be added to the control error component, a component that is added after the form field when an error needs to be displayed:
110+
- `controlErrorsClass` - A custom classes that'll be added to the control error component and override custom classes from global config, a component that is added after the form field when an error needs to be displayed:
111111

112112
```html
113113
<input class="form-control" formControlName="city"
114114
placeholder="City" controlErrorsClass="my-class other-class" />
115+
116+
```
117+
118+
- `controlCustomClass` - A custom classes that'll be added to the control if control has error.
119+
120+
```html
121+
<input class="form-control" formControlName="city"
122+
placeholder="City" controlCustomClass="my-custom-class other-custom-class" />
123+
115124
```
116125

117126
- `controlErrorsTpl` - A custom error template to be used instead of the control error component's default view:
@@ -237,6 +246,8 @@ The library adds a `form-submitted` to the submitted form. You can use it to sty
237246
}
238247
}
239248
```
249+
- `controlErrorsClass` - Optional. A custom classes that'll be added to the control error component. Can be override if you set attribute `controlErrorsClass` on control
250+
- `controlCustomClass` - Optional. A custom classes that'll be added to the control if control has error. Can be override if you set attribute `controlCustomClass` on control
240251
- `controlErrorComponent` - Optional. Allows changing the default component that is used to render
241252
the errors. This component should implement the `ControlErrorComponent` interface. If you only need to
242253
replace the error component's template, you may derive it from the default component,

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,23 +58,44 @@ describe('ControlErrorComponent', () => {
5858
});
5959

6060
it('should set custom class on host element', () => {
61+
spectator.component.text = 'test';
6162
spectator.component.customClass = 'customClassTest';
6263

6364
expect(spectator.element).toHaveClass('customClassTest');
6465
});
6566

6667
it('should set multiply css classes on host element', () => {
68+
spectator.component.text = 'test';
6769
spectator.component.customClass = 'customClassTest1 customClassTest2';
6870

6971
expect(spectator.element).toHaveClass(['customClassTest1', 'customClassTest2']);
7072
});
7173

7274
it('should set multiply css classes as array on host element', () => {
75+
spectator.component.text = 'test';
7376
spectator.component.customClass = ['customClassTest1', 'customClassTest2'];
7477

7578
expect(spectator.element).toHaveClass(['customClassTest1', 'customClassTest2']);
7679
});
7780

81+
it('should not add custom class on host element if text unset', () => {
82+
spectator.component.customClass = 'customClassTest';
83+
84+
expect(spectator.element.classList.contains('customClassTest')).toBeFalse();
85+
});
86+
87+
it('should remove custom class on host element if text unset', () => {
88+
spectator.component.text = 'text';
89+
spectator.component.customClass = 'customClassTest';
90+
spectator.detectChanges();
91+
92+
spectator.component.text = undefined;
93+
94+
spectator.detectChanges();
95+
96+
expect(spectator.element.classList.contains('customClassTest')).toBeFalse();
97+
});
98+
7899
it('should create passed template and send its context', () => {
79100
const { component } = spectator;
80101
component.createTemplate('fakeTemplate' as any, { testError: 'test' }, 'test error');

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,21 +34,28 @@ export class DefaultControlErrorComponent implements ControlErrorComponent {
3434
errorContext: { $implicit: ValidationErrors; text: string };
3535
hideError = true;
3636

37+
private _addClasses: string[] = [];
38+
3739
createTemplate(tpl: ErrorComponentTemplate, error: ValidationErrors, text: string) {
3840
this.errorTemplate = tpl;
3941
this.errorContext = { $implicit: error, text };
4042
this.cdr.markForCheck();
4143
}
4244

4345
set customClass(classes: string | string[]) {
44-
const classesToAdd = Array.isArray(classes) ? classes : classes.split(/\s/);
45-
this.host.nativeElement.classList.add(...classesToAdd);
46+
if (!this.hideError) {
47+
this._addClasses = Array.isArray(classes) ? classes : classes.split(/\s/);
48+
this.host.nativeElement.classList.add(...this._addClasses);
49+
}
4650
}
4751

4852
set text(value: string | null) {
4953
if (value !== this.errorText) {
5054
this.errorText = value;
5155
this.hideError = !value;
56+
if (this.hideError) {
57+
this.host.nativeElement.classList.remove(...this._addClasses);
58+
}
5259
this.cdr.markForCheck();
5360
}
5461
}

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

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ function getComponentFactory<C>(component: Type<C>) {
3131
requiredone: () => 'required one error',
3232
serverError: error => error
3333
}
34-
}
34+
},
35+
controlErrorsClass: ['global', 'config']
3536
})
3637
]
3738
});
@@ -369,7 +370,12 @@ describe('ControlErrorDirective', () => {
369370
</ng-template>
370371
<input formControlName="customTemplate" placeholder="Custom template" [controlErrorsTpl]="customTpl" />
371372
372-
<input formControlName="customClass" placeholder="Custom class" controlErrorsClass="customClass" />
373+
<input
374+
formControlName="customClass"
375+
placeholder="Custom class"
376+
controlErrorsClass="customClass"
377+
controlCustomClass="customControlClass"
378+
/>
373379
374380
<ng-template controlErrorAnchor #anchor="controlErrorAnchor"></ng-template>
375381
<input formControlName="withAnchor" placeholder="With anchor" [controlErrorAnchor]="anchor" />
@@ -430,6 +436,14 @@ describe('ControlErrorDirective', () => {
430436
expect(spectator.query('.customClass')).toBeTruthy();
431437
});
432438

439+
it('should set custom class for control when it is provided', () => {
440+
const input = spectator.query<HTMLInputElement>(byPlaceholder('Custom class'));
441+
442+
typeInElementAndFocusOut(spectator, '', input);
443+
444+
expect(spectator.query('.customControlClass')).toBeTruthy();
445+
});
446+
433447
describe('when anchor is provided', () => {
434448
it('should create show error message on anchor', () => {
435449
const input = spectator.query<HTMLInputElement>(byPlaceholder('With anchor'));
@@ -499,6 +513,8 @@ describe('ControlErrorDirective', () => {
499513
required: () => 'required error'
500514
}
501515
},
516+
controlErrorsClass: ['global', 'config'],
517+
controlCustomClass: 'control custom',
502518
controlErrorComponent: CustomControlErrorComponent,
503519
controlErrorComponentAnchorFn: controlErrorComponentAnchorFn,
504520
controlErrorsOn: {
@@ -523,6 +539,22 @@ describe('ControlErrorDirective', () => {
523539
expect(spectator.query('h1')).toBeTruthy();
524540
expect(spectator.query(byText('required error'))).toBeTruthy();
525541
});
542+
543+
it('should set global custom class when it is provided', () => {
544+
const input = spectator.query<HTMLInputElement>(byPlaceholder('Name'));
545+
546+
typeInElementAndFocusOut(spectator, '', input);
547+
548+
expect(spectator.query('.global.config')).toBeTruthy();
549+
});
550+
551+
it('should set global custom class for component when it is provided', () => {
552+
const input = spectator.query<HTMLInputElement>(byPlaceholder('Name'));
553+
554+
typeInElementAndFocusOut(spectator, '', input);
555+
556+
expect(spectator.query('.control.custom')).toBeTruthy();
557+
});
526558
});
527559

528560
describe('ErrorComponentAnchorFnCallback', () => {

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

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,24 @@ import {
33
ComponentRef,
44
Directive,
55
ElementRef,
6+
EmbeddedViewRef,
67
Inject,
78
Input,
89
OnDestroy,
910
OnInit,
1011
Optional,
1112
Self,
1213
TemplateRef,
13-
ViewContainerRef,
14-
EmbeddedViewRef
14+
ViewContainerRef
1515
} from '@angular/core';
1616
import { AbstractControl, ControlContainer, NgControl, ValidationErrors } from '@angular/forms';
17-
import { DefaultControlErrorComponent, ControlErrorComponent } from './control-error.component';
18-
import { ControlErrorAnchorDirective } from './control-error-anchor.directive';
1917
import { EMPTY, fromEvent, merge, NEVER, Observable, Subject } from 'rxjs';
20-
import { ErrorTailorConfig, ErrorTailorConfigProvider, FORM_ERRORS } from './providers';
2118
import { distinctUntilChanged, mapTo, startWith, switchMap, takeUntil } from 'rxjs/operators';
19+
20+
import { ControlErrorAnchorDirective } from './control-error-anchor.directive';
21+
import { ControlErrorComponent, DefaultControlErrorComponent } from './control-error.component';
2222
import { FormActionDirective } from './form-action.directive';
23+
import { ErrorTailorConfig, ErrorTailorConfigProvider, FORM_ERRORS } from './providers';
2324
import { ErrorsMap } from './types';
2425

2526
@Directive({
@@ -30,6 +31,7 @@ import { ErrorsMap } from './types';
3031
export class ControlErrorsDirective implements OnInit, OnDestroy {
3132
@Input('controlErrors') customErrors: ErrorsMap = {};
3233
@Input() controlErrorsClass: string | string[] | undefined;
34+
@Input() controlCustomClass: string | string[] | undefined;
3335
@Input() controlErrorsTpl: TemplateRef<any> | undefined;
3436
@Input() controlErrorsOnAsync: boolean | undefined;
3537
@Input() controlErrorsOnBlur: boolean | undefined;
@@ -75,6 +77,18 @@ export class ControlErrorsDirective implements OnInit, OnDestroy {
7577
let changesOnBlur$: Observable<any> = EMPTY;
7678
let changesOnChange$: Observable<any> = EMPTY;
7779

80+
if (!this.controlErrorsClass || this.controlErrorsClass?.length === 0) {
81+
if (this.mergedConfig.controlErrorsClass && this.mergedConfig.controlErrorsClass) {
82+
this.controlErrorsClass = this.mergedConfig.controlErrorsClass;
83+
}
84+
}
85+
86+
if (!this.controlCustomClass || this.controlCustomClass?.length === 0) {
87+
if (this.mergedConfig.controlCustomClass && this.mergedConfig.controlCustomClass) {
88+
this.controlCustomClass = this.mergedConfig.controlCustomClass;
89+
}
90+
}
91+
7892
if (this.mergedConfig.controlErrorsOn.async && hasAsyncValidator) {
7993
// hasAsyncThenUponStatusChange
8094
changesOnAsync$ = statusChanges$.pipe(startWith(true));
@@ -169,6 +183,9 @@ export class ControlErrorsDirective implements OnInit, OnDestroy {
169183

170184
private valueChanges() {
171185
const controlErrors = this.control.errors;
186+
const classesAdd = Array.isArray(this.controlCustomClass)
187+
? this.controlCustomClass
188+
: this.controlCustomClass?.split(/\s/) ?? [];
172189
if (controlErrors) {
173190
const [firstKey] = Object.keys(controlErrors);
174191
const getError = this.customErrors[firstKey] || this.globalErrors[firstKey];
@@ -179,11 +196,17 @@ export class ControlErrorsDirective implements OnInit, OnDestroy {
179196
const text = typeof getError === 'function' ? getError(controlErrors[firstKey]) : getError;
180197
if (this.isInput) {
181198
this.host.nativeElement.parentElement.classList.add('error-tailor-has-error');
199+
if (this.controlCustomClass) {
200+
(this.host.nativeElement as HTMLElement).classList.add(...classesAdd);
201+
}
182202
}
183203
this.setError(text, controlErrors);
184204
} else if (this.ref) {
185205
if (this.isInput) {
186206
this.host.nativeElement.parentElement.classList.remove('error-tailor-has-error');
207+
if (this.controlCustomClass) {
208+
(this.host.nativeElement as HTMLElement).classList.remove(...classesAdd);
209+
}
187210
}
188211
this.setError(null);
189212
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export type ErrorsProvider = ErrorsUseValue | ErrorsUseFactory;
2222
export type ErrorTailorConfig = {
2323
errors?: ErrorsProvider;
2424
blurPredicate?: (element: Element) => boolean;
25+
controlErrorsClass?: string | string[] | undefined;
26+
controlCustomClass?: string | string[] | undefined;
2527
controlErrorComponent?: Type<ControlErrorComponent>;
2628
controlErrorComponentAnchorFn?: (hostElement: Element, errorElement: Element) => () => void;
2729
controlErrorsOn?: {

src/app/app.component.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,13 @@
3131

3232
<section formGroupName="address">
3333
<div class="form-group">
34-
<input class="form-control" formControlName="city" placeholder="City" controlErrorsClass="my-class" />
34+
<input
35+
class="form-control"
36+
formControlName="city"
37+
placeholder="City"
38+
controlErrorsClass="my-class"
39+
controlCustomClass="my-control-class"
40+
/>
3541
</div>
3642

3743
<div class="form-group">

0 commit comments

Comments
 (0)