Skip to content

Commit

Permalink
feat: add isValid((prop | *), [conditions]) method
Browse files Browse the repository at this point in the history
  • Loading branch information
toddmotto committed Apr 20, 2017
1 parent 212a010 commit 3f9c23d
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 37 deletions.
61 changes: 57 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ The `hasError` method informs you if your control has the given error. This can
> Example: Adds `class="required"` when "myError" has the `required` error.
```html
<div [ngClass]="{ required: myError.hasError('required') }">
<div [class.required]="myError.hasError('required')">
<input type="text" formControlName="username">
</div>

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

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

#### isValid(name: string, conditions?: string | string[]): boolean;

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.

> Example: Adds `class="valid"` when "myError" has no `required` error.
```html
<div [class.valid]="myError.isValid('required')">
<input type="text" formControlName="username">
</div>

<div ngxErrors="username" #myError="ngxErrors">
<div ngxError="required" [when]="dirty">
Field is required
</div>
</div>
```

You can optionally pass in conditions in which to evaluate alongside the property you're checking is valid.

> Example: Adds `class="valid"` when "myError" has no `required` error _and_ the states are `'dirty'` and `'touched'`.
```html
<div [class.valid]="myError.isValid('required', ['dirty', 'touched'])">
<input type="text" formControlName="username">
</div>

<div ngxErrors="username" #myError="ngxErrors">
<div ngxError="required" [when]="dirty">
Field is required
</div>
</div>
```

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.

```html
<div>
<div [ngClass]="{
valid: myError.isValid('*'),
validTouchedDirty: myError.isValid('*', ['touched', 'dirty'])
}">
</div>
<input type="text" formControlName="username">
</div>

<div ngxErrors="username" #myError="ngxErrors">
<div ngxError="required" [when]="dirty">
Field is required
</div>
</div>
```

#### hasErrors: boolean;

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.

> Example: Adds `class="hasErrors"` when "myError" has any errors.
> Example: Adds `class="errors"` when "myError" has any errors.
```html
<div [ngClass]="{ hasErrors: myError.hasErrors }">
<div [class.errors]="myError.hasErrors">
<input type="text" formControlName="username">
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,24 @@ import { FormGroup } from '@angular/forms';
<div>
<p>Errors: {{ myError.hasError('*', ['touched']) | json }}</p>
<p>No Errors: {{ !myError.hasError('*', ['touched']) | json }}</p>
<p>No Errors: {{ myError.isValid('required', ['dirty']) | json }}</p>
</div>
<input
type="text"
placeholder="Manager Code"
formControlName="code"
[class.errors]="myError.hasError('*', ['touched'])"
[class.no-errors]="!myError.hasError('*', ['touched'])">
[class.errors]="myError.hasError('*', ['dirty'])"
[class.no-errors]="myError.isValid('*', ['dirty'])">
<div ngxErrors="store.code" #myError="ngxErrors">
<div class="error" ngxError="required" [when]="['touched']">
<div class="error" ngxError="required" [when]="['dirty']">
Field is required
</div>
<div class="error" ngxError="minlength" [when]="['touched']">
<div class="error" ngxError="minlength" [when]="['dirty']">
Min-length is {{ myError.getError('minlength')?.requiredLength }}
</div>
<div class="error" ngxError="maxlength" [when]="['dirty', 'touched']">
<div class="error" ngxError="maxlength" [when]="['dirty']">
Max-length is {{ myError.getError('maxlength')?.requiredLength }}
</div>
</div>
Expand Down
63 changes: 38 additions & 25 deletions src/ngxerrors.directive.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Directive, Input, OnChanges, AfterViewInit, SimpleChanges } from '@angular/core';
import { Directive, Input, OnChanges, OnDestroy, AfterViewInit } from '@angular/core';
import { FormGroupDirective, AbstractControl } from '@angular/forms';

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

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

ready: boolean = false;

constructor(
private form: FormGroupDirective
) {}
) { }

get errors() {
if (!this.ready) return;
Expand All @@ -37,42 +37,55 @@ export class NgxErrorsDirective implements OnChanges, AfterViewInit {
return !!this.errors;
}

checkControlProps(props: ErrorOptions) {
return !props ? true : toArray(props).every((prop: string) => this.control[prop]);
}

hasError(name: string, conditions: ErrorOptions): boolean {
if (!this.ready) return;
const controlPropsState = this.checkControlProps(conditions);
if (name.charAt(0) === '*') {
return this.control.invalid && controlPropsState;
}
return this.control.hasError(name) && controlPropsState;
return this.checkPropState('invalid', name, conditions);
}

isValid(name: string, conditions: ErrorOptions): boolean {
return this.checkPropState('valid', name, conditions);
}

getError(name: string) {
if (!this.ready) return;
return this.control.getError(name);
}

private checkPropState(prop: string, name: string, conditions: ErrorOptions): boolean {
if (!this.ready) return;
const controlPropsState = (
!conditions || toArray(conditions).every((condition: string) => this.control[condition])
);
if (name.charAt(0) === '*') {
return this.control[prop] && controlPropsState;
}
return (
prop === 'valid' ? !this.control.hasError(name) : this.control.hasError(name) && controlPropsState
);
}

private checkStatus() {
const control = this.control;
const errors = control.errors;
this.ready = true;
if (!errors) return;
for (const errorName in errors) {
this.subject.next({ control, errorName });
}
}

ngOnChanges() {
this.control = this.form.control.get(this.controlName);
}

ngAfterViewInit() {
setTimeout(() => {
this.checkStatus();
this.control.statusChanges.subscribe(this.checkStatus.bind(this));
});
}

ngOnChanges(changes: SimpleChanges) {
this.control = this.form.control.get(changes.controlName.currentValue);
}

checkStatus() {
const errors = this.control.errors;
this.ready = true;
if (!errors) return;
for (const error in errors) {
this.subject.next({ control: this.control, errorName: error });
}
ngOnDestroy() {
this.subject.unsubscribe();
}

}
17 changes: 15 additions & 2 deletions src/test/ngxerrors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ TestBed.initTestEnvironment(
<div class="errorProps">
<div class="errorProp1">{{ prop.errors | json }}</div>
<div class="errorProp2">{{ prop.hasErrors | json }}</div>
<div class="errorProp3">{{ prop.isValid('*', ['dirty']) | json }}</div>
<div class="errorProp4">{{ prop.isValid('required', ['dirty']) | json }}</div>
</div>
<div ngxErrors="prop" #prop="ngxErrors">
<div ngxError="required" when="dirty">
Expand Down Expand Up @@ -136,6 +138,9 @@ describe('Directives: ngxErrors, ngxError, when', () => {
});

it('should provide a template ref API via ngxErrors exportAs', async (done) => {

const parse = (name) => JSON.parse(el.query(By.css(name)).nativeElement.textContent);

await fixture.whenStable();

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

component.form.patchValue({ prop: 'ngxErrors' });
component.form.get('prop').markAsDirty();
Expand All @@ -154,12 +161,16 @@ describe('Directives: ngxErrors, ngxError, when', () => {
await fixture.whenStable();
expect(component.form.get('prop').dirty).toBe(true);
expect(component.form.get('prop').touched).toBe(true);
expect(parse('.errorProp3')).toBe(true);
expect(parse('.errorProp4')).toBe(true);
expect(element.nativeElement.classList.contains('requiredVisibleAtRuntime')).toBe(false);
component.form.patchValue({ prop: '' });
fixture.detectChanges();
await fixture.whenStable();
expect(element.nativeElement.classList.contains('requiredVisibleWhenDirty')).toBe(true);
expect(element.nativeElement.classList.contains('requiredVisibleWhenDirtyTouched')).toBe(true);
expect(parse('.errorProp3')).toBe(false);
expect(parse('.errorProp4')).toBe(false);

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

component.form.patchValue({ prop: 'ngxErrors!!!!!' });
fixture.detectChanges();
Expand All @@ -184,12 +197,12 @@ describe('Directives: ngxErrors, ngxError, when', () => {
expect(component.form.get('prop').hasError('minlength')).toBe(false);
expect(component.form.get('prop').hasError('maxlength')).toBe(true);
expect(el.query(By.css('.errorMinLength')).nativeElement.textContent).toContain('10 characters maximum');

const parse = (name) => JSON.parse(el.query(By.css(name)).nativeElement.textContent);

expect(parse('.errorProp1').maxlength.requiredLength).toBe(10);
expect(parse('.errorProp1').maxlength.actualLength).toBe(14);
expect(parse('.errorProp2')).toBe(true);
expect(parse('.errorProp3')).toBe(false);
expect(parse('.errorProp4')).toBe(true);

done();

Expand Down

0 comments on commit 3f9c23d

Please sign in to comment.