Skip to content

Commit fbf9304

Browse files
authored
Feature/14.0.0 (#1)
* beta.0 release * wip * readme update * update versiopn
1 parent 0ae41ce commit fbf9304

13 files changed

+280
-132
lines changed

README.md

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,23 @@
11
# NgxConditionalValidator
22

3-
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.7.
4-
5-
## Development server
6-
7-
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
3+
This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.0.
84

95
## Code scaffolding
106

11-
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
7+
Run `ng generate component component-name --project ngx-conditional-validator` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project ngx-conditional-validator`.
8+
> Note: Don't forget to add `--project ngx-conditional-validator` or else it will be added to the default project in your `angular.json` file.
129
1310
## Build
1411

15-
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
12+
Run `ng build ngx-conditional-validator` to build the project. The build artifacts will be stored in the `dist/` directory.
1613

17-
## Running unit tests
14+
## Publishing
1815

19-
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
16+
After building your library with `ng build ngx-conditional-validator`, go to the dist folder `cd dist/ngx-conditional-validator` and run `npm publish`.
2017

21-
## Running end-to-end tests
18+
## Running unit tests
2219

23-
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
20+
Run `ng test ngx-conditional-validator` to execute the unit tests via [Karma](https://karma-runner.github.io).
2421

2522
## Further help
2623

package.json

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
{
22
"name": "ngx-conditional-validator",
3-
"version": "0.0.0",
3+
"version": "14.0.0",
44
"scripts": {
55
"ng": "ng",
6-
"start": "ng serve",
7-
"build": "ng build",
8-
"watch": "ng build --watch --configuration development",
9-
"test": "ng test"
6+
"test-app": "ng serve test-app",
7+
"build-lib": "ng build ngx-conditional-validator --configuration production",
8+
"watch-lib": "ng build ngx-conditional-validator --watch --configuration development",
9+
"test": "ng test",
10+
"postbuild": "xcopy \"README.md\" \".\\dist\\ngx-conditional-validator\" /Y"
1011
},
1112
"private": true,
1213
"dependencies": {
@@ -36,4 +37,4 @@
3637
"ng-packagr": "^14.0.0",
3738
"typescript": "~4.7.2"
3839
}
39-
}
40+
}

projects/ngx-conditional-validator/README.md

Lines changed: 0 additions & 24 deletions
This file was deleted.

projects/ngx-conditional-validator/package.json

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,27 @@
11
{
2-
"name": "ngx-conditional-validator",
3-
"version": "0.0.1",
2+
"name": "@outerlimitstech/ngx-conditional-validator",
3+
"version": "14.0.0",
4+
"description": "Angular library to build reactive forms dynamic validation",
5+
"bugs": "https://github.com/OuterlimitsTech/olt-ngx-conditional-validator/issues",
6+
"homepage": "https://github.com/OuterlimitsTech/olt-ngx-conditional-validator#readme",
7+
"repository": "https://github.com/OuterlimitsTech/olt-ngx-conditional-validator.git",
8+
"license": "MIT",
9+
"author": "Chris Straw",
10+
"publishConfig": {
11+
"access": "public"
12+
},
13+
"keywords": [
14+
"angular",
15+
"conditional",
16+
"validation",
17+
"dynamic",
18+
"reactive"
19+
],
420
"peerDependencies": {
5-
"@angular/common": "^14.0.0",
6-
"@angular/core": "^14.0.0"
21+
"@angular/common": ">=14.0.0 <15.0.0",
22+
"@angular/core": ">=14.0.0 <15.0.0",
23+
"@angular/forms": ">=14.0.0 <15.0.0",
24+
"rxjs": ">=7.0.0 <8.0.0"
725
},
826
"dependencies": {
927
"tslib": "^2.3.0"
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { AsyncValidatorFn, ValidatorFn, Validators } from '@angular/forms';
2+
import { from, of } from 'rxjs';
3+
import { shareReplay, take } from 'rxjs/operators';
4+
import { ConditionalControl } from './conditional-validator';
5+
import { OltConditionalValidatorHelper } from './conditional-validator-helper';
6+
7+
export class OltConditionalValidatorBuilder<T extends OltConditionalValidatorHelper> {
8+
9+
private unionConditon: Array<(helper: T) => boolean>;
10+
11+
constructor(private helper: T, conditons?: Array<(helper: T) => boolean>) {
12+
this.unionConditon = conditons ?? [];
13+
}
14+
15+
/**
16+
* Concatenate the condition
17+
*/
18+
when(condition: (helper: T) => boolean) {
19+
return new OltConditionalValidatorBuilder(this.helper, [...this.unionConditon, condition]);
20+
}
21+
22+
/**
23+
* Run validators when condition pass
24+
*/
25+
then(validators: ValidatorFn | ValidatorFn[], otherwise?: { resetBy?: any }): ValidatorFn {
26+
return this.run(validators, otherwise);
27+
}
28+
29+
/**
30+
* Enable control and run validators when condition pass, otherwise disable the control.
31+
*/
32+
enable(validators?: ValidatorFn | ValidatorFn[]): ValidatorFn {
33+
return this.run(validators, { disable: true });
34+
}
35+
36+
private run(validators?: ValidatorFn | ValidatorFn[], otherwise?: { disable?: boolean, resetBy?: any }): ValidatorFn {
37+
return (ctrl: ConditionalControl) => {
38+
this.helper.control = ctrl;
39+
const conditionPass = this.unionConditon.every(condition => condition(this.helper));
40+
41+
// hack
42+
if (otherwise) {
43+
if (conditionPass) {
44+
ctrl.conditionDisable = false;
45+
ctrl.conditionReset = undefined;
46+
} else {
47+
ctrl.conditionDisable = otherwise.disable;
48+
ctrl.conditionReset = otherwise.resetBy;
49+
}
50+
}
51+
52+
if (conditionPass && !!validators) {
53+
if (Array.isArray(validators)) {
54+
return Validators.compose(validators)!(ctrl);
55+
} else {
56+
return validators(ctrl) || null;
57+
}
58+
} else {
59+
return null;
60+
}
61+
};
62+
}
63+
64+
/**
65+
* Run async validators when condition pass.
66+
*
67+
* Note: In current version, A control can only exist one.
68+
*/
69+
thenAsync(validators: AsyncValidatorFn | AsyncValidatorFn[]): AsyncValidatorFn {
70+
return (ctrl: ConditionalControl) => {
71+
this.helper.control = ctrl;
72+
const conditionPass = this.unionConditon.every(condition => condition(this.helper));
73+
74+
if (ctrl.lastPass === conditionPass && ctrl.lastValue === ctrl.value && ctrl.lastAsync) {
75+
return ctrl.lastAsync;
76+
}
77+
78+
ctrl.lastPass = conditionPass;
79+
ctrl.lastValue = ctrl.value;
80+
81+
if (conditionPass) {
82+
if (Array.isArray(validators)) {
83+
ctrl.lastAsync = from(Validators.composeAsync(validators)!(ctrl))
84+
} else {
85+
ctrl.lastAsync = from(validators(ctrl));
86+
}
87+
} else {
88+
ctrl.lastAsync = of(null);
89+
}
90+
ctrl.lastAsync = ctrl.lastAsync.pipe(shareReplay(1), take(1));
91+
return ctrl.lastAsync;
92+
};
93+
}
94+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { AbstractControl } from '@angular/forms';
2+
3+
export class OltConditionalValidatorHelper {
4+
5+
control: AbstractControl | null = null;
6+
7+
get value() {
8+
return this.control?.value;
9+
}
10+
11+
/**
12+
* Select the control with given path
13+
*
14+
* It will try from current to root until getting a control
15+
*/
16+
select(path: string) {
17+
if (this.control) {
18+
let result: AbstractControl | null = null;
19+
let target: AbstractControl | null = this.control;
20+
21+
do {
22+
result = target?.get(path) ?? null;
23+
target = target?.parent ?? null;
24+
} while (!result && target)
25+
26+
return result;
27+
}
28+
return null;
29+
}
30+
31+
/**
32+
* Select the control value with given path
33+
*
34+
* note: It will try from current to root until getting a control
35+
*/
36+
selectValue<T = any>(path: string): T {
37+
return this.select(path)?.value;
38+
}
39+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { EventEmitter } from "@angular/core";
2+
import { AbstractControl, FormGroup, FormArray, FormControl, ValidationErrors, ValidatorFn } from "@angular/forms";
3+
import { debounceTime, asapScheduler, Observable } from "rxjs";
4+
import { OltConditionalValidatorBuilder } from "./conditional-validator-builder";
5+
import { OltConditionalValidatorHelper } from "./conditional-validator-helper";
6+
7+
export class OltConditionalValidator {
8+
9+
static bindUpdate(control: AbstractControl) {
10+
this.updateTreeValidity(control);
11+
const subscription = control.valueChanges
12+
.pipe(
13+
// run after other valueChanges subscription, but before Angular dirty check
14+
debounceTime(0, asapScheduler)
15+
).subscribe(() => {
16+
OltConditionalValidator.updateTreeValidity(control);
17+
});
18+
19+
return subscription;
20+
}
21+
22+
static when(condition: (helper: OltConditionalValidatorHelper) => boolean) {
23+
return new OltConditionalValidatorBuilder(new OltConditionalValidatorHelper(), [condition]);
24+
}
25+
26+
static updateTreeValidity(control: AbstractControl, isRoot = true) {
27+
const oldStatus = control.status;
28+
const oldValue = control.value;
29+
const oldDisabled = control.disabled;
30+
let ctrl = control as ConditionalControl;
31+
let anyValueChange = false;
32+
let anyStatusChange = false;
33+
let isFormControl = false;
34+
35+
/* update children */
36+
if (ctrl instanceof FormGroup) {
37+
Object.keys(ctrl.controls).forEach(key => {
38+
const res = this.updateTreeValidity(ctrl.get(key)!, false);
39+
anyValueChange = anyValueChange || res.anyValueChange;
40+
anyStatusChange = anyStatusChange || res.anyStatusChange;
41+
});
42+
} else if (ctrl instanceof FormArray) {
43+
ctrl.controls.forEach(child => {
44+
const res = this.updateTreeValidity(child, false);
45+
anyValueChange = anyValueChange || res.anyValueChange;
46+
anyStatusChange = anyStatusChange || res.anyStatusChange;
47+
});
48+
} else if (ctrl instanceof FormControl) {
49+
isFormControl = true;
50+
}
51+
52+
ctrl = ctrl as ConditionalControl; // fix Typescript type assertion bug
53+
54+
/* update self */
55+
// enable if ctrl is disabled by this lib
56+
if (ctrl.disabled && ctrl.conditionDisable) {
57+
ctrl.conditionDisable = undefined;
58+
ctrl.enable({ onlySelf: true, emitEvent: false });
59+
}
60+
61+
// update ctrl
62+
ctrl.updateValueAndValidity({ onlySelf: true, emitEvent: false });
63+
64+
// reset value if ctrl is not pass condition, when use reset option
65+
if (ctrl.conditionReset !== undefined) {
66+
ctrl.setValue('', { onlySelf: true, emitEvent: false });
67+
}
68+
69+
// disable if ctrl is not pass condition, when use diable option
70+
if (ctrl.conditionDisable) {
71+
ctrl.disable({ onlySelf: true, emitEvent: false });
72+
}
73+
74+
// emit if value change
75+
anyValueChange = anyValueChange || (ctrl.disabled !== oldDisabled) || (isFormControl && ctrl.value !== oldValue);
76+
if (anyValueChange) {
77+
const valueChanges = ctrl.valueChanges as EventEmitter<any>;
78+
valueChanges.emit(ctrl.value);
79+
}
80+
81+
// emit if status change
82+
anyStatusChange = anyStatusChange || (ctrl.status !== oldStatus);
83+
if (anyStatusChange) {
84+
const statusChanges = ctrl.statusChanges as EventEmitter<any>;
85+
statusChanges.emit(ctrl.status);
86+
}
87+
88+
/* update ancestors */
89+
if (isRoot) {
90+
ctrl.parent?.updateValueAndValidity({ onlySelf: false, emitEvent: true });
91+
}
92+
93+
return { anyValueChange, anyStatusChange };
94+
}
95+
96+
static invalid(errMsg?: ValidationErrors): ValidatorFn {
97+
return () => { return errMsg ? errMsg : { conditionErr: true }; }
98+
}
99+
}
100+
101+
export interface ConditionalControl extends AbstractControl {
102+
conditionReset?: any;
103+
conditionDisable?: boolean;
104+
lastPass?: boolean;
105+
lastValue?: any;
106+
lastAsync?: Observable<any>;
107+
}

projects/ngx-conditional-validator/src/lib/ngx-conditional-validator.component.spec.ts

Lines changed: 0 additions & 23 deletions
This file was deleted.

0 commit comments

Comments
 (0)