Skip to content

Commit

Permalink
fix: update for correct support conditional validations
Browse files Browse the repository at this point in the history
  • Loading branch information
EndyKaufman committed Apr 14, 2020
1 parent 9c738a9 commit 1c0e744
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 37 deletions.
2 changes: 2 additions & 0 deletions apps/demo/src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export * from './panels/company-panel/company-panel.component';
export * from './panels/company-panel/company-panel.module';
export * from './panels/exp-login-panel/exp-login-panel.component';
export * from './panels/exp-login-panel/exp-login-panel.module';
export * from './panels/exp-registration-panel/exp-registration-panel.component';
export * from './panels/exp-registration-panel/exp-registration-panel.module';
export * from './panels/exp-user-panel/exp-user-panel.component';
export * from './panels/exp-user-panel/exp-user-panel.module';
export * from './panels/project-panel/project-panel-complete.component';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@
<exp-login-panel></exp-login-panel>
</div>
</ngx-docs-example>
<ngx-docs-example
title="Experimental: registration panel"
[html]="registrationSource.html"
[ts]="registrationSource.ts"
[launch]="registrationSource.launch">
<div class="body">
<exp-registration-panel></exp-registration-panel>
</div>
</ngx-docs-example>
</div>
<source-tabs
title="Other files"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ export class ExperimentalPageComponent {
}
};

registrationSource = {
html: require('!!raw-loader!./../../panels/exp-registration-panel/exp-registration-panel.component.html').default,
ts: require('!!raw-loader!./../../panels/exp-registration-panel/exp-registration-panel.component.ts').default,
launch: {
location: 'https://stackblitz.com/edit/ngx-dynamic-form-builder-experimental-login',
tooltip: `Edit in http://stackblitz.com`
}
};

otherFiles: { name: string; language: string; content: string }[] = [
{
name: 'exp-user.ts',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { SourceTabsModule } from '../../others/source-tabs/source-tabs.module';
import { ExpUserPanelModule } from '../../panels/exp-user-panel/exp-user-panel.module';
import { ExpLoginPanelModule } from '../../panels/exp-login-panel/exp-login-panel.module';
import { DocsExampleModule } from '../../others/docs-example/docs-example.module';
import { ExpRegistrationPanelModule } from '../../panels/exp-registration-panel/exp-registration-panel.module';

@NgModule({
imports: [
Expand All @@ -17,6 +18,7 @@ import { DocsExampleModule } from '../../others/docs-example/docs-example.module
DocsExampleModule.forRoot(),
ExpUserPanelModule.forRoot(),
ExpLoginPanelModule.forRoot(),
ExpRegistrationPanelModule.forRoot(),
RouterModule.forChild(ExperimentalPageRoutes),
SourceTabsModule.forRoot()
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<form [formGroup]="form" *ngIf="form?.customValidateErrors | async as errors" novalidate>
<h3>Group form</h3>
<mat-form-field class="full-width">
<input matInput formControlName="username" placeholder="Username" />
<mat-error *ngIf="errors?.username?.length">{{ (errors?.username)[0] }}</mat-error>
</mat-form-field>
<mat-form-field class="full-width">
<input matInput type="password" formControlName="password" placeholder="Password" />
<mat-error *ngIf="errors?.password?.length">{{ (errors?.password)[0] }}</mat-error>
</mat-form-field>
<mat-form-field class="full-width">
<input matInput type="password" formControlName="rePassword" placeholder="Confirm password" />
<mat-error *ngIf="errors?.rePassword?.length">{{ (errors?.rePassword)[0] }}</mat-error>
</mat-form-field>
<div class="full-width">
<p>Form status: {{ form.status | json }}</p>
<p>Form class-validator errors: {{ errors | json }}</p>
<p>Form native errors: {{ form?.nativeValidateErrors | async | json }}</p>
<p *ngIf="savedItem">Registration user data: {{ savedItem | json }}</p>
</div>
<div class="full-width">
<button mat-raised-button (click)="onRegistrationClick()" [disabled]="!form.valid" cdkFocusInitial>
Registration
</button>
</div>
</form>
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { DynamicFormBuilder, DynamicFormGroup } from 'ngx-dynamic-form-builder';
import { ExpUser } from '../../shared/models/exp-user';

@Component({
selector: 'exp-registration-panel',
templateUrl: './exp-registration-panel.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ExpRegistrationPanelComponent implements OnInit {
form: DynamicFormGroup<ExpUser>;

fb = new DynamicFormBuilder();

savedItem?: ExpUser;

constructor() {
this.form = this.fb.group(ExpUser, {
customValidatorOptions: {
groups: ['new']
}
});
}
ngOnInit() {
this.savedItem = undefined;
this.form.object = new ExpUser();
this.form.validateAllFormFields();
}
onRegistrationClick(): void {
this.form.validateAllFormFields();
if (this.form.valid) {
this.savedItem = this.form.object;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { MatButtonModule } from '@angular/material/button';
import { ModuleWithProviders } from '@angular/core';
import { ExpRegistrationPanelComponent } from '../exp-registration-panel/exp-registration-panel.component';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { SharedModule } from '../../shared/shared.module';
import { MatInputModule } from '@angular/material/input';
import { MatCheckboxModule } from '@angular/material/checkbox';

@NgModule({
imports: [
SharedModule.forRoot(),
MatButtonModule,
MatInputModule,
MatCheckboxModule,
FormsModule,
ReactiveFormsModule,
FlexLayoutModule
],
entryComponents: [ExpRegistrationPanelComponent],
exports: [ExpRegistrationPanelComponent],
declarations: [ExpRegistrationPanelComponent]
})
export class ExpRegistrationPanelModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: ExpRegistrationPanelModule,
providers: []
};
}
}
19 changes: 16 additions & 3 deletions apps/demo/src/app/shared/models/exp-user.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
import { IsNotEmpty, IsEmail, ValidateNested, IsOptional } from 'class-validator';
import { IsEmail, IsNotEmpty, IsOptional, Validate, ValidateIf, ValidateNested } from 'class-validator';
import { EqualsTo } from '../utils/custom-validators';
import { ExpDepartment } from './exp-department';

export class ExpUser {
id: number;

@IsNotEmpty({
groups: ['user', 'guest']
groups: ['user', 'guest', 'new']
})
username: string;

@IsNotEmpty({
groups: ['guest']
groups: ['guest', 'new']
})
password: string;

@ValidateIf(o => o.password, {
groups: ['new']
})
@IsNotEmpty({
groups: ['new']
})
@Validate(EqualsTo, ['password'], {
groups: ['new']
})
rePassword: string;

@IsEmail(undefined, {
groups: ['user']
})
Expand Down Expand Up @@ -42,6 +54,7 @@ export class ExpUser {
}
this.username = data.username;
this.password = data.password;
this.rePassword = data.rePassword;
this.email = data.email;
this.isSuperuser = data.isSuperuser;
this.isStaff = data.isStaff;
Expand Down
17 changes: 17 additions & 0 deletions apps/demo/src/app/shared/utils/custom-validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,20 @@ export class ObjectMustBeNotEmpty implements ValidatorConstraintInterface {
return false;
}
}

@ValidatorConstraint({ name: 'equalsTo', async: false })
export class EqualsTo implements ValidatorConstraintInterface {
validate(value: string, validationArguments: ValidationArguments) {
return (
validationArguments.constraints.length > 0 &&
validationArguments.constraints.filter(
otherField =>
validationArguments.object.hasOwnProperty(otherField) && validationArguments.object[otherField] === value
).length > 0
);
}

defaultMessage(validationArguments: ValidationArguments) {
return `${validationArguments.constraints.join(',')} do not match to ${validationArguments.property}`;
}
}
49 changes: 15 additions & 34 deletions libs/ngx-dynamic-form-builder/src/lib/utils/dynamic-form-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
import { ValidationMetadata } from 'class-validator/metadata/ValidationMetadata';
import 'reflect-metadata';
import { BehaviorSubject, from, Observable, of, Subject, Subscription } from 'rxjs';
import { flatMap, map, mapTo, delay } from 'rxjs/operators';
import { flatMap, map, mapTo, delay, tap } from 'rxjs/operators';
import { Dictionary } from '../models/dictionary';
import { DynamicFormGroupField } from '../models/dynamic-form-group-field';
import { ErrorPropertyName } from '../models/error-property-name';
Expand Down Expand Up @@ -180,7 +180,7 @@ export class DynamicFormGroup<TModel> extends FormGroup {
...(isRoot ? this.errors : {}),
...Object.entries(control.controls).reduce((acc, [key, childControl]: [string, Dictionary]) => {
const childErrors = this.collectErrors(childControl, false);
if (childErrors && key !== 'foreverInvalid' && Object.keys(childErrors).length > 0) {
if (childErrors && Object.keys(childErrors).length > 0) {
acc = {
...acc,
[key]: {
Expand Down Expand Up @@ -626,7 +626,7 @@ export function getClassValidators<TModel>(

// Handle Custom Validation
if (isCustomValidate(validationMetadata, typeKey)) {
const customValidation = createCustomValidation(fieldName, validationMetadata, conditionalValidations);
const customValidation = createCustomValidation(fieldName, validationMetadata);
setFieldData(fieldName, fieldDefinition, customValidation);
}

Expand All @@ -653,26 +653,6 @@ export function getClassValidators<TModel>(
// Local Helper functions to help make the main code more readable
//

function checkIfConditionsMatch(control,conditionalValidations) {
if(conditionalValidations.length > 0) {
if(!control.parent) {
// during formGroup creation, the control has no parent.
// as validation concept is to opt-in for validation, the best reaction here is to let it skip.
return false;
}
let func;
for(let i = 0; i < conditionalValidations.length; i++) {
for(let i2 = 0; i2 < conditionalValidations[i].constraints.length; i2++) {
func = conditionalValidations[i].constraints[i2];
if(typeof func === 'function' && !func(control.parent.value, control.value)) {
return false;
}
}
}
}
return true;
}

function createNestedValidate(
fieldName: string,
objectToValidate: any,
Expand All @@ -682,6 +662,7 @@ export function getClassValidators<TModel>(
type: 'async',
validator: function(control: FormControl) {
return getValidateErrors(
control.parent,
fieldName,
control,
objectToValidate !== undefined ? objectToValidate : control.value,
Expand All @@ -706,18 +687,14 @@ export function getClassValidators<TModel>(
return of(null);
}

if(!checkIfConditionsMatch(control,conditionalValidations)) {
return of(null)
}

const isValid =
control.parent && control.parent.value
? validator.validateValueByMetadata(control.value, validationMetadata)
: true;
let validateState$ = of(isValid);
if (!isValid && conditionalValidations.length > 0) {
validateState$ = setObjectValueAndGetValidationErrors(fieldName, control, validatorOptions).pipe(
map(validateErrors => (validateErrors ? !!validateErrors[fieldName] : false))
map(validateErrors => (validateErrors && validateErrors[fieldName] ? false : true))
);
}

Expand All @@ -728,13 +705,10 @@ export function getClassValidators<TModel>(
};
}

function createCustomValidation(fieldName: string, validationMetadata: ValidationMetadata, conditionalValidations: ValidationMetadata[]): ValidatorFunctionType {
function createCustomValidation(fieldName: string, validationMetadata: ValidationMetadata): ValidatorFunctionType {
return {
type: 'async',
validator: function(control: FormControl) {
if(!checkIfConditionsMatch(control,conditionalValidations)) {
return of(null)
}
return setObjectValueAndGetValidationErrors(fieldName, control, validatorOptions).pipe(
map(errors => getAllErrors(errors, fieldName).length === 0),
map(validateState => getIsValidResult(validateState, validationMetadata, 'customValidation'))
Expand Down Expand Up @@ -832,6 +806,12 @@ function setObjectValueAndGetValidationErrors(
control: FormControl,
validatorOptions?: ValidatorOptions
) {
const parent =
control.parent instanceof DynamicFormGroup
? (control.parent as DynamicFormGroup<any>)
: control.parent
? control.parent
: null;
const object =
control.parent instanceof DynamicFormGroup
? (control.parent as DynamicFormGroup<any>).object
Expand All @@ -843,10 +823,11 @@ function setObjectValueAndGetValidationErrors(
object[fieldName] = control.value;
}

return getValidateErrors(fieldName, control, object, validatorOptions);
return getValidateErrors(parent, fieldName, control, object, validatorOptions);
}

function getValidateErrors(
parent: FormGroup | FormArray | null,
fieldName: string,
control: FormControl,
dataToValidate: any,
Expand All @@ -865,7 +846,7 @@ function getValidateErrors(
: of({})
)
);*/
return (control.parent && control.parent.value ? from(validate(dataToValidate, validatorOptions)) : of([])).pipe(
return (parent && parent.value ? from(validate(dataToValidate, validatorOptions)) : of([])).pipe(
map(errors => transformValidationErrors(errors))
);
}
Expand Down

0 comments on commit 1c0e744

Please sign in to comment.