Skip to content

Commit 3f9c23d

Browse files
committed
feat: add isValid((prop | *), [conditions]) method
1 parent 212a010 commit 3f9c23d

File tree

4 files changed

+116
-37
lines changed

4 files changed

+116
-37
lines changed

README.md

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ The `hasError` method informs you if your control has the given error. This can
224224
> Example: Adds `class="required"` when "myError" has the `required` error.
225225
226226
```html
227-
<div [ngClass]="{ required: myError.hasError('required') }">
227+
<div [class.required]="myError.hasError('required')">
228228
<input type="text" formControlName="username">
229229
</div>
230230

@@ -240,7 +240,7 @@ You can optionally pass in conditions in which to activate the error.
240240
> Example: Adds `class="required"` when "myError" has the `required` error _and_ the states are `'dirty'` and `'touched'`.
241241
242242
```html
243-
<div [ngClass]="{ required: myError.hasError('required', ['dirty', 'touched']) }">
243+
<div [class.required]="myError.hasError('required', ['dirty', 'touched'])">
244244
<input type="text" formControlName="username">
245245
</div>
246246

@@ -270,14 +270,67 @@ You can also use the "catch-all" selector to get the state of your entire contro
270270
</div>
271271
```
272272

273+
#### isValid(name: string, conditions?: string | string[]): boolean;
274+
275+
The `isValid` method informs you if a your control is valid, or a property is valid. This can be useful for styling elsewhere in your template based off the control's validity state.
276+
277+
> Example: Adds `class="valid"` when "myError" has no `required` error.
278+
279+
```html
280+
<div [class.valid]="myError.isValid('required')">
281+
<input type="text" formControlName="username">
282+
</div>
283+
284+
<div ngxErrors="username" #myError="ngxErrors">
285+
<div ngxError="required" [when]="dirty">
286+
Field is required
287+
</div>
288+
</div>
289+
```
290+
291+
You can optionally pass in conditions in which to evaluate alongside the property you're checking is valid.
292+
293+
> Example: Adds `class="valid"` when "myError" has no `required` error _and_ the states are `'dirty'` and `'touched'`.
294+
295+
```html
296+
<div [class.valid]="myError.isValid('required', ['dirty', 'touched'])">
297+
<input type="text" formControlName="username">
298+
</div>
299+
300+
<div ngxErrors="username" #myError="ngxErrors">
301+
<div ngxError="required" [when]="dirty">
302+
Field is required
303+
</div>
304+
</div>
305+
```
306+
307+
You can also use the "catch-all" selector to check if the control is valid, with no specific error properties, alongside on an optional state collection.
308+
309+
```html
310+
<div>
311+
<div [ngClass]="{
312+
valid: myError.isValid('*'),
313+
validTouchedDirty: myError.isValid('*', ['touched', 'dirty'])
314+
}">
315+
</div>
316+
<input type="text" formControlName="username">
317+
</div>
318+
319+
<div ngxErrors="username" #myError="ngxErrors">
320+
<div ngxError="required" [when]="dirty">
321+
Field is required
322+
</div>
323+
</div>
324+
```
325+
273326
#### hasErrors: boolean;
274327

275328
The `hasErrors` property returns `true` if your control has any number of errors. This can be useful for styling elsewhere in your template on a global control level rather than individual errors.
276329

277-
> Example: Adds `class="hasErrors"` when "myError" has any errors.
330+
> Example: Adds `class="errors"` when "myError" has any errors.
278331
279332
```html
280-
<div [ngClass]="{ hasErrors: myError.hasErrors }">
333+
<div [class.errors]="myError.hasErrors">
281334
<input type="text" formControlName="username">
282335
</div>
283336

example/app/stock-inventory/components/stock-branch/stock-branch.component.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,24 +26,24 @@ import { FormGroup } from '@angular/forms';
2626
2727
<div>
2828
<p>Errors: {{ myError.hasError('*', ['touched']) | json }}</p>
29-
<p>No Errors: {{ !myError.hasError('*', ['touched']) | json }}</p>
29+
<p>No Errors: {{ myError.isValid('required', ['dirty']) | json }}</p>
3030
</div>
3131
3232
<input
3333
type="text"
3434
placeholder="Manager Code"
3535
formControlName="code"
36-
[class.errors]="myError.hasError('*', ['touched'])"
37-
[class.no-errors]="!myError.hasError('*', ['touched'])">
36+
[class.errors]="myError.hasError('*', ['dirty'])"
37+
[class.no-errors]="myError.isValid('*', ['dirty'])">
3838
3939
<div ngxErrors="store.code" #myError="ngxErrors">
40-
<div class="error" ngxError="required" [when]="['touched']">
40+
<div class="error" ngxError="required" [when]="['dirty']">
4141
Field is required
4242
</div>
43-
<div class="error" ngxError="minlength" [when]="['touched']">
43+
<div class="error" ngxError="minlength" [when]="['dirty']">
4444
Min-length is {{ myError.getError('minlength')?.requiredLength }}
4545
</div>
46-
<div class="error" ngxError="maxlength" [when]="['dirty', 'touched']">
46+
<div class="error" ngxError="maxlength" [when]="['dirty']">
4747
Max-length is {{ myError.getError('maxlength')?.requiredLength }}
4848
</div>
4949
</div>

src/ngxerrors.directive.ts

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Directive, Input, OnChanges, AfterViewInit, SimpleChanges } from '@angular/core';
1+
import { Directive, Input, OnChanges, OnDestroy, AfterViewInit } from '@angular/core';
22
import { FormGroupDirective, AbstractControl } from '@angular/forms';
33

44
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
@@ -13,7 +13,7 @@ import { toArray } from './utils/toArray';
1313
selector: '[ngxErrors]',
1414
exportAs: 'ngxErrors'
1515
})
16-
export class NgxErrorsDirective implements OnChanges, AfterViewInit {
16+
export class NgxErrorsDirective implements OnChanges, OnDestroy, AfterViewInit {
1717

1818
@Input('ngxErrors')
1919
controlName: string;
@@ -23,10 +23,10 @@ export class NgxErrorsDirective implements OnChanges, AfterViewInit {
2323
control: AbstractControl;
2424

2525
ready: boolean = false;
26-
26+
2727
constructor(
2828
private form: FormGroupDirective
29-
) {}
29+
) { }
3030

3131
get errors() {
3232
if (!this.ready) return;
@@ -37,42 +37,55 @@ export class NgxErrorsDirective implements OnChanges, AfterViewInit {
3737
return !!this.errors;
3838
}
3939

40-
checkControlProps(props: ErrorOptions) {
41-
return !props ? true : toArray(props).every((prop: string) => this.control[prop]);
42-
}
43-
4440
hasError(name: string, conditions: ErrorOptions): boolean {
45-
if (!this.ready) return;
46-
const controlPropsState = this.checkControlProps(conditions);
47-
if (name.charAt(0) === '*') {
48-
return this.control.invalid && controlPropsState;
49-
}
50-
return this.control.hasError(name) && controlPropsState;
41+
return this.checkPropState('invalid', name, conditions);
42+
}
43+
44+
isValid(name: string, conditions: ErrorOptions): boolean {
45+
return this.checkPropState('valid', name, conditions);
5146
}
5247

5348
getError(name: string) {
5449
if (!this.ready) return;
5550
return this.control.getError(name);
5651
}
5752

53+
private checkPropState(prop: string, name: string, conditions: ErrorOptions): boolean {
54+
if (!this.ready) return;
55+
const controlPropsState = (
56+
!conditions || toArray(conditions).every((condition: string) => this.control[condition])
57+
);
58+
if (name.charAt(0) === '*') {
59+
return this.control[prop] && controlPropsState;
60+
}
61+
return (
62+
prop === 'valid' ? !this.control.hasError(name) : this.control.hasError(name) && controlPropsState
63+
);
64+
}
65+
66+
private checkStatus() {
67+
const control = this.control;
68+
const errors = control.errors;
69+
this.ready = true;
70+
if (!errors) return;
71+
for (const errorName in errors) {
72+
this.subject.next({ control, errorName });
73+
}
74+
}
75+
76+
ngOnChanges() {
77+
this.control = this.form.control.get(this.controlName);
78+
}
79+
5880
ngAfterViewInit() {
5981
setTimeout(() => {
6082
this.checkStatus();
6183
this.control.statusChanges.subscribe(this.checkStatus.bind(this));
6284
});
6385
}
6486

65-
ngOnChanges(changes: SimpleChanges) {
66-
this.control = this.form.control.get(changes.controlName.currentValue);
67-
}
68-
69-
checkStatus() {
70-
const errors = this.control.errors;
71-
this.ready = true;
72-
if (!errors) return;
73-
for (const error in errors) {
74-
this.subject.next({ control: this.control, errorName: error });
75-
}
87+
ngOnDestroy() {
88+
this.subject.unsubscribe();
7689
}
7790

7891
}

src/test/ngxerrors.spec.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ TestBed.initTestEnvironment(
2828
<div class="errorProps">
2929
<div class="errorProp1">{{ prop.errors | json }}</div>
3030
<div class="errorProp2">{{ prop.hasErrors | json }}</div>
31+
<div class="errorProp3">{{ prop.isValid('*', ['dirty']) | json }}</div>
32+
<div class="errorProp4">{{ prop.isValid('required', ['dirty']) | json }}</div>
3133
</div>
3234
<div ngxErrors="prop" #prop="ngxErrors">
3335
<div ngxError="required" when="dirty">
@@ -136,6 +138,9 @@ describe('Directives: ngxErrors, ngxError, when', () => {
136138
});
137139

138140
it('should provide a template ref API via ngxErrors exportAs', async (done) => {
141+
142+
const parse = (name) => JSON.parse(el.query(By.css(name)).nativeElement.textContent);
143+
139144
await fixture.whenStable();
140145

141146
fixture.changeDetectorRef.markForCheck();
@@ -146,6 +151,8 @@ describe('Directives: ngxErrors, ngxError, when', () => {
146151
expect(element.nativeElement.classList.contains('requiredVisibleAtRuntime')).toBe(true);
147152
expect(element.nativeElement.classList.contains('requiredVisibleWhenDirty')).toBe(false);
148153
expect(element.nativeElement.classList.contains('requiredVisibleWhenDirtyTouched')).toBe(false);
154+
expect(parse('.errorProp3')).toBe(false);
155+
expect(parse('.errorProp4')).toBe(false);
149156

150157
component.form.patchValue({ prop: 'ngxErrors' });
151158
component.form.get('prop').markAsDirty();
@@ -154,12 +161,16 @@ describe('Directives: ngxErrors, ngxError, when', () => {
154161
await fixture.whenStable();
155162
expect(component.form.get('prop').dirty).toBe(true);
156163
expect(component.form.get('prop').touched).toBe(true);
164+
expect(parse('.errorProp3')).toBe(true);
165+
expect(parse('.errorProp4')).toBe(true);
157166
expect(element.nativeElement.classList.contains('requiredVisibleAtRuntime')).toBe(false);
158167
component.form.patchValue({ prop: '' });
159168
fixture.detectChanges();
160169
await fixture.whenStable();
161170
expect(element.nativeElement.classList.contains('requiredVisibleWhenDirty')).toBe(true);
162171
expect(element.nativeElement.classList.contains('requiredVisibleWhenDirtyTouched')).toBe(true);
172+
expect(parse('.errorProp3')).toBe(false);
173+
expect(parse('.errorProp4')).toBe(false);
163174

164175
component.form.patchValue({ prop: 'ngx' });
165176
fixture.detectChanges();
@@ -172,6 +183,8 @@ describe('Directives: ngxErrors, ngxError, when', () => {
172183
expect(component.form.get('prop').hasError('minlength')).toBe(true);
173184
expect(component.form.get('prop').hasError('maxlength')).toBe(false);
174185
expect(el.query(By.css('.errorMinLength')).nativeElement.textContent).toContain('5 characters minimum');
186+
expect(parse('.errorProp3')).toBe(false);
187+
expect(parse('.errorProp4')).toBe(true);
175188

176189
component.form.patchValue({ prop: 'ngxErrors!!!!!' });
177190
fixture.detectChanges();
@@ -184,12 +197,12 @@ describe('Directives: ngxErrors, ngxError, when', () => {
184197
expect(component.form.get('prop').hasError('minlength')).toBe(false);
185198
expect(component.form.get('prop').hasError('maxlength')).toBe(true);
186199
expect(el.query(By.css('.errorMinLength')).nativeElement.textContent).toContain('10 characters maximum');
187-
188-
const parse = (name) => JSON.parse(el.query(By.css(name)).nativeElement.textContent);
189200

190201
expect(parse('.errorProp1').maxlength.requiredLength).toBe(10);
191202
expect(parse('.errorProp1').maxlength.actualLength).toBe(14);
192203
expect(parse('.errorProp2')).toBe(true);
204+
expect(parse('.errorProp3')).toBe(false);
205+
expect(parse('.errorProp4')).toBe(true);
193206

194207
done();
195208

0 commit comments

Comments
 (0)