Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ A lightweight library for dynamically validate Angular reactive forms using [cla
- [Creating a ClassValidatorFormGroup](#creating-a-classvalidatorformgroup)
- [Using ClassValidatorFormBuilderService](#using-classvalidatorformbuilderservice)
- [Using ClassValidatorFormGroup class](#using-classvalidatorformgroup-class)
- [Eager Validation Option]()
- [Add custom validators](#add-custom-validators)
- [Providing validators when creating the ClassValidatorFormControl](#providing-validators-when-creating-the-classvalidatorformcontrol)
- [Providing validators using `setValidators`/`setValidatorsWithDynamicValidation` methods](#providing-validators-using-setvalidatorssetvalidatorswithdynamicvalidation-methods)
Expand Down Expand Up @@ -143,6 +144,48 @@ Now, setting value to any of form controls, will perfom the validator set in the

this.profileForm.controls.email.setValue('email@email.com');
console.log(this.profileForm.controls.email) // null

#### Eager Validation Option

By default, `ngx-reactive-form-class-validator` validates form controls after the form is fully initialized (ngAfterViewInit).
If you want validation to run immediately after form initialization (for example, in ngAfterViewInit or just after you create a FormGroup), you can enable eager validation at the ClassValidatorFormGroup/FormBuilder level.

```
import { ClassValidatorFormGroup, ClassValidatorFormControl } from 'ngx-reactive-form-class-validator';

const formGroup = new ClassValidatorFormGroup({
email: new ClassValidatorFormControl(''),
password: new ClassValidatorFormControl('')
}, null, { eagerValidation: true }); // 👈 Enable eager validation here


// Or using the form builder

public constructor(
private fb: ClassValidatorFormBuilderService,
) { }

profileForm = this.fb.group(
Profile,
{
firstName: [''],
lastName: [''],
email: [''],
address: this.fb.group(Address,
{
street: [''],
city: [''],
state: [''],
zip: ['']
}
),
},
undefined,
{ eagerValidation: true } // 👈 Enable eager validation here
);

```

### Add custom validators
It is possible as well to combine dynamic validation with custom validation.
There are several ways to do it:
Expand Down
3 changes: 2 additions & 1 deletion apps/test/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,12 @@ export class AppComponent implements OnInit {
// using a FormControl will not apply dynamic validation
zip: new FormControl("")
})
});
}, undefined);

this.addressForm = this.profileForm.get(
"address"
) as ClassValidatorFormGroup;
debugger;
}

public clearValidators(): void {
Expand Down
2 changes: 1 addition & 1 deletion libs/ngx-reactive-form-class-validator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "ngx-reactive-form-class-validator",
"description": "A lightweight library for dynamically validate Angular reactive forms using class-validator library.",
"license": "MIT",
"version": "1.9.0",
"version": "1.9.1",
"keywords": [
"ng",
"angular",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ClassValidatorFormGroup } from './class-validator-form-group';
import { ClassValidatorFormControl } from './class-validator-form-control';
import { ClassValidatorFormArray } from './class-validator-form-array';
import { ClassType } from './types';
import { ClassValidatorFormGroupOptions } from './class-validator-form-group-options.interface';

// Coming from https://github.com/angular/angular/blob/3b0b7d22109c79b4dceb4ae069c3927894cf1bd6/packages/forms/src/form_builder.ts#L14
const isAbstractControlOptions = (options: AbstractControlOptions | { [key: string]: any }): options is AbstractControlOptions =>
Expand Down Expand Up @@ -44,11 +45,16 @@ export class ClassValidatorFormBuilderService {
* * `validator`: A synchronous validator function, or an array of validator functions
* * `asyncValidator`: A single async validator or array of async validator functions
*
* @param classValidatorGroupOptions Options object of type `ClassValidatorFormGroupOptions` allowing
* to define eagerValidation that validate controls immediately upon creation. Default is false (validators are executed starting from ngAfterViewInit hook)
* See https://github.com/abarghoud/ngx-reactive-form-class-validator/issues/47
*
*/
public group(
formClassType: ClassType<any>,
controlsConfig: { [p: string]: any },
options?: AbstractControlOptions | { [p: string]: any } | null
options?: AbstractControlOptions | { [p: string]: any } | null,
classValidatorGroupOptions?: ClassValidatorFormGroupOptions,
): ClassValidatorFormGroup {
// Coming from https://github.com/angular/angular/blob/3b0b7d22109c79b4dceb4ae069c3927894cf1bd6/packages/forms/src/form_builder.ts#L59
const controls = this.reduceControls(controlsConfig);
Expand All @@ -70,7 +76,7 @@ export class ClassValidatorFormBuilderService {
}
}

return new ClassValidatorFormGroup(formClassType, controls, { asyncValidators, updateOn, validators });
return new ClassValidatorFormGroup(formClassType, controls, { asyncValidators, updateOn, validators }, undefined, classValidatorGroupOptions);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,13 @@ export class ClassValidatorFormControl<T = any> extends FormControl<T | any> {
/**
* @internal
*/
public setNameAndFormGroupClassValue(name: string, value: any): void {
public setNameAndFormGroupClassValue(name: string, value: any, eagerValidation: boolean = false): void {
this.name = name;
this.formGroupClassValue = value;

if (eagerValidation) {
this.updateValueAndValidity();
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface ClassValidatorFormGroupOptions {
eagerValidation?: boolean; // default: false
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FakeUser } from './testing/fake-user-testing.model';
import { FakeThing, FakeUser } from './testing/fake-user-testing.model';
import { fakeUserFormControls } from './testing/fake-form-testing.fixture';
import { ClassValidatorFormGroup } from './class-validator-form-group';
import { ClassValidatorFormControl } from './class-validator-form-control';
Expand All @@ -20,8 +20,8 @@ describe('The ClassValidatorFormGroup class', () => {
expectedClassValue.firstName = fakeUserFormControls.firstName.value;
expectedClassValue.id = fakeUserFormControls.id.value;

expect(firstNameSetNameAndClassValueSpy).toBeCalledWith('firstName', expectedClassValue);
expect(idSetNameAndClassValueSpy).toBeCalledWith('id', expectedClassValue);
expect(firstNameSetNameAndClassValueSpy).toBeCalledWith('firstName', expectedClassValue, undefined);
expect(idSetNameAndClassValueSpy).toBeCalledWith('id', expectedClassValue, undefined);
});
});

Expand All @@ -42,7 +42,7 @@ describe('The ClassValidatorFormGroup class', () => {
const expectedClassValue = new FakeUser();
expectedClassValue.firstName = 'name';

expect(formControlSetNameAndClassValueSpy).toBeCalledWith('firstName', expectedClassValue);
expect(formControlSetNameAndClassValueSpy).toBeCalledWith('firstName', expectedClassValue, undefined);
});
});

Expand All @@ -66,8 +66,41 @@ describe('The ClassValidatorFormGroup class', () => {
expectedClassValue.id = fakeUserFormControls.id.value;
expectedClassValue.isSessionLocked = fakeUserFormControls.isSessionLocked.value;

expect(idSetNameAndClassValueSpy).toBeCalledWith('id', expectedClassValue);
expect(isSessionLockedSetNameAndClassValueSpy).toBeCalledWith('isSessionLocked', expectedClassValue);
expect(idSetNameAndClassValueSpy).toBeCalledWith('id', expectedClassValue, undefined);
expect(isSessionLockedSetNameAndClassValueSpy).toBeCalledWith('isSessionLocked', expectedClassValue, undefined);
});
});

describe('The formGroup', () => {
let formGroup: ClassValidatorFormGroup;

describe('When FormControls are created normally', () => {
beforeEach(() => {
formGroup = new ClassValidatorFormGroup(FakeThing, {
first: new ClassValidatorFormControl('notemail'),
last: new ClassValidatorFormControl('')
});
})

it('should not run validators immediately', () => {
expect(formGroup.valid).toBe(true);
expect(formGroup.controls['first'].value).toBe('notemail');
});
});

describe('When FormControls are created with eagerValidation flag', () => {
beforeEach(() => {
formGroup = new ClassValidatorFormGroup(FakeThing, {
first: new ClassValidatorFormControl('notemail'),
last: new ClassValidatorFormControl('')
}, undefined, undefined, { eagerValidation: true });
})

it('should run validators immediately', () => {
expect(formGroup.valid).toBe(false);
expect(formGroup.controls['first'].value).toBe('notemail');
expect(formGroup.controls['first'].errors.isEmail).toBeDefined();
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {

import { ClassValidatorFormControl } from './class-validator-form-control';
import { ClassType } from './types';
import { ClassValidatorFormGroupOptions } from './class-validator-form-group-options.interface';

export class ClassValidatorFormGroup<TControl extends {
[K in keyof TControl]: AbstractControl<any>;
Expand All @@ -28,12 +29,17 @@ export class ClassValidatorFormGroup<TControl extends {
*
* @param asyncValidator A single async validator or array of async validator functions
*
* @param options Options object of type `ClassValidatorFormGroupOptions` allowing
* to define eagerValidation that validate controls immediately upon creation. Default is false (validators are executed starting from ngAfterViewInit hook)
* See https://github.com/abarghoud/ngx-reactive-form-class-validator/issues/47
*
*/
public constructor(
private readonly formClassType: ClassType<any>,
controls: TControl,
validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null,
asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null,
private readonly options?: ClassValidatorFormGroupOptions,
) {
super(controls, validatorOrOpts, asyncValidator);

Expand Down Expand Up @@ -88,8 +94,7 @@ export class ClassValidatorFormGroup<TControl extends {
private setClassValidatorControlsContainerGroupClassValue(): void {
Object.entries(this.controls).forEach(([controlName, control]) => {
if (control instanceof ClassValidatorFormControl) {
(this.controls[controlName] as ClassValidatorFormControl)
.setNameAndFormGroupClassValue(controlName, this.classValue);
control.setNameAndFormGroupClassValue(controlName, this.classValue, this.options?.eagerValidation);
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ClassValidatorFormArray } from '../class-validator-form-array';
import { ClassValidatorFormGroup } from '../class-validator-form-group';
import { ClassValidatorFormControl } from '../class-validator-form-control';
import { ClassValidatorUntypedFormControl } from '../untyped/class-validator-untyped-form-control';
import { ClassValidatorUntypedFormArray, ClassValidatorUntypedFormGroup } from '../untyped';

export const fakeContactFormGroup = new ClassValidatorFormArray([
new ClassValidatorFormGroup(FakeContact, {
Expand All @@ -23,8 +24,8 @@ export const fakeUserFormControls = {
contacts: new ClassValidatorFormArray([fakeContactFormGroup]),
};

export const fakeContactUntypedFormGroup = new ClassValidatorFormArray([
new ClassValidatorFormGroup(FakeContact, {
export const fakeContactUntypedFormGroup = new ClassValidatorUntypedFormArray([
new ClassValidatorUntypedFormGroup(FakeContact, {
phoneNumber: new ClassValidatorUntypedFormControl(''),
email: new ClassValidatorUntypedFormControl(''),
type: new ClassValidatorUntypedFormControl(FakeContactType.phone),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,12 @@ export class FakeUser {
@MinLength(10)
public username: string;
}

export class FakeThing {
@IsNotEmpty()
@IsEmail()
public first: string;

@IsNotEmpty()
public last: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,13 @@ export class ClassValidatorUntypedFormControl extends UntypedFormControl {
/**
* @internal
*/
public setNameAndFormGroupClassValue(name: string, value: any): void {
public setNameAndFormGroupClassValue(name: string, value: any, eagerValidation: boolean = false): void {
this.name = name;
this.formGroupClassValue = value;

if (eagerValidation) {
this.updateValueAndValidity();
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ClassValidatorUntypedFormGroup } from './class-validator-untyped-form-group';
import { ClassValidatorUntypedFormControl } from './class-validator-untyped-form-control';
import { fakeUserUntypedFormControls } from '../testing/fake-form-testing.fixture';
import { FakeUser } from '../testing/fake-user-testing.model';
import { FakeThing, FakeUser } from '../testing/fake-user-testing.model';

describe('The ClassValidatorUntypedFormGroup class', () => {
describe('The constructor', () => {
Expand All @@ -20,8 +20,8 @@ describe('The ClassValidatorUntypedFormGroup class', () => {
expectedClassValue.firstName = fakeUserUntypedFormControls.firstName.value;
expectedClassValue.id = fakeUserUntypedFormControls.id.value;

expect(firstNameSetNameAndClassValueSpy).toBeCalledWith('firstName', expectedClassValue);
expect(idSetNameAndClassValueSpy).toBeCalledWith('id', expectedClassValue);
expect(firstNameSetNameAndClassValueSpy).toBeCalledWith('firstName', expectedClassValue, undefined);
expect(idSetNameAndClassValueSpy).toBeCalledWith('id', expectedClassValue, undefined);
});
});

Expand All @@ -42,7 +42,7 @@ describe('The ClassValidatorUntypedFormGroup class', () => {
const expectedClassValue = new FakeUser();
expectedClassValue.firstName = 'name';

expect(formControlSetNameAndClassValueSpy).toBeCalledWith('firstName', expectedClassValue);
expect(formControlSetNameAndClassValueSpy).toBeCalledWith('firstName', expectedClassValue, undefined);
});
});

Expand All @@ -66,8 +66,41 @@ describe('The ClassValidatorUntypedFormGroup class', () => {
expectedClassValue.id = fakeUserUntypedFormControls.id.value;
expectedClassValue.isSessionLocked = fakeUserUntypedFormControls.isSessionLocked.value;

expect(idSetNameAndClassValueSpy).toHaveBeenCalledWith('id', expectedClassValue);
expect(isSessionLockedSetNameAndClassValueSpy).toHaveBeenCalledWith('isSessionLocked', expectedClassValue);
expect(idSetNameAndClassValueSpy).toHaveBeenCalledWith('id', expectedClassValue, undefined);
expect(isSessionLockedSetNameAndClassValueSpy).toHaveBeenCalledWith('isSessionLocked', expectedClassValue, undefined);
});
});

describe('The formGroup', () => {
let formGroup: ClassValidatorUntypedFormGroup;

describe('When FormControls are created normally', () => {
beforeEach(() => {
formGroup = new ClassValidatorUntypedFormGroup(FakeThing, {
first: new ClassValidatorUntypedFormControl('notemail'),
last: new ClassValidatorUntypedFormControl('')
});
})

it('should not run validators immediately', () => {
expect(formGroup.valid).toBe(true);
expect(formGroup.controls['first'].value).toBe('notemail');
});
});

describe('When FormControls are created with eagerValidation flag', () => {
beforeEach(() => {
formGroup = new ClassValidatorUntypedFormGroup(FakeThing, {
first: new ClassValidatorUntypedFormControl('notemail'),
last: new ClassValidatorUntypedFormControl('')
}, undefined, undefined, { eagerValidation: true });
})

it('should run validators immediately', () => {
expect(formGroup.valid).toBe(false);
expect(formGroup.controls['first'].value).toBe('notemail');
expect(formGroup.controls['first'].errors.isEmail).toBeDefined();
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AbstractControl, AbstractControlOptions, AsyncValidatorFn, UntypedFormG

import { ClassValidatorUntypedFormControl } from './class-validator-untyped-form-control';
import { ClassType } from '../types';
import { ClassValidatorFormGroupOptions } from '../class-validator-form-group-options.interface';

export class ClassValidatorUntypedFormGroup extends UntypedFormGroup {
private classValue: any;
Expand All @@ -19,6 +20,10 @@ export class ClassValidatorUntypedFormGroup extends UntypedFormGroup {
*
* @param asyncValidator A single async validator or array of async validator functions
*
* @param options Options object of type `ClassValidatorFormGroupOptions` allowing
* to define eagerValidation that validate controls immediately upon creation. Default is false (validators are executed starting from ngAfterViewInit hook)
* See https://github.com/abarghoud/ngx-reactive-form-class-validator/issues/47
*
*/
public constructor(
private readonly formClassType: ClassType<any>,
Expand All @@ -27,6 +32,7 @@ export class ClassValidatorUntypedFormGroup extends UntypedFormGroup {
},
validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null,
asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null,
private readonly options?: ClassValidatorFormGroupOptions,
) {
super(controls, validatorOrOpts, asyncValidator);

Expand Down Expand Up @@ -85,7 +91,7 @@ export class ClassValidatorUntypedFormGroup extends UntypedFormGroup {
Object.entries(this.controls).forEach(([controlName, control]) => {
if (control instanceof ClassValidatorUntypedFormControl) {
(this.controls[controlName] as ClassValidatorUntypedFormControl)
.setNameAndFormGroupClassValue(controlName, this.classValue);
.setNameAndFormGroupClassValue(controlName, this.classValue, this.options?.eagerValidation);
}
});
}
Expand Down