├── .circleci
└── config.yml
├── .editorconfig
├── .env
├── .eslintrc.json
├── .github
├── release-drafter.yml
└── workflows
│ └── release-drafter.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .vscode
└── extensions.json
├── LICENSE.txt
├── README.md
├── apps
├── .gitkeep
└── test
│ ├── project.json
│ ├── src
│ ├── app
│ │ ├── app.component.html
│ │ ├── app.component.ts
│ │ └── app.config.ts
│ ├── assets
│ │ └── .gitkeep
│ ├── favicon.ico
│ ├── index.html
│ ├── main.ts
│ └── styles.css
│ ├── tsconfig.app.json
│ ├── tsconfig.editor.json
│ └── tsconfig.json
├── decorate-angular-cli.js
├── jest.config.ts
├── jest.preset.js
├── libs
├── .gitkeep
└── ngx-reactive-form-class-validator
│ ├── .eslintrc.json
│ ├── README.md
│ ├── jest.config.ts
│ ├── ng-package.json
│ ├── package.json
│ ├── project.json
│ ├── src
│ ├── index.ts
│ ├── lib
│ │ ├── class-validator-form-array.ts
│ │ ├── class-validator-form-builder.module.ts
│ │ ├── class-validator-form-builder.service.ts
│ │ ├── class-validator-form-control.spec.ts
│ │ ├── class-validator-form-control.ts
│ │ ├── class-validator-form-group-options.interface.ts
│ │ ├── class-validator-form-group.spec.ts
│ │ ├── class-validator-form-group.ts
│ │ ├── index.ts
│ │ ├── testing
│ │ │ ├── fake-form-testing.fixture.ts
│ │ │ └── fake-user-testing.model.ts
│ │ ├── types.ts
│ │ └── untyped
│ │ │ ├── class-validator-untyped-form-array.ts
│ │ │ ├── class-validator-untyped-form-builder.module.ts
│ │ │ ├── class-validator-untyped-form-builder.service.ts
│ │ │ ├── class-validator-untyped-form-control.spec.ts
│ │ │ ├── class-validator-untyped-form-control.ts
│ │ │ ├── class-validator-untyped-form-group.spec.ts
│ │ │ ├── class-validator-untyped-form-group.ts
│ │ │ └── index.ts
│ └── test-setup.ts
│ ├── tsconfig.json
│ ├── tsconfig.lib.json
│ ├── tsconfig.lib.prod.json
│ └── tsconfig.spec.json
├── nx.json
├── package.json
├── tools
├── schematics
│ └── .gitkeep
└── tsconfig.tools.json
├── tsconfig.base.json
└── yarn.lock
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 |
3 | jobs:
4 | build:
5 | docker:
6 | - image: cimg/node:22.16.0
7 | steps:
8 | - checkout
9 | - run: yarn install
10 | - run: yarn add codecov
11 | - run: yarn build:lib
12 | - run: yarn test:lib:coverage && npx codecov --token=$CODE_COV_TOKEN
13 |
14 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | # Nx 18 enables using plugins to infer targets by default
2 | # This is disabled for existing workspaces to maintain compatibility
3 | # For more info, see: https://nx.dev/concepts/inferred-tasks
4 | NX_ADD_PLUGINS=false
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "parserOptions": {
5 | "ecmaVersion": 2018,
6 | "sourceType": "module",
7 | "project": "./tsconfig.*?.json"
8 | },
9 | "ignorePatterns": ["**/*"],
10 | "plugins": ["@typescript-eslint", "@nx"],
11 | "extends": [
12 | "eslint:recommended",
13 | "plugin:@typescript-eslint/eslint-recommended",
14 | "plugin:@typescript-eslint/recommended",
15 | "prettier"
16 | ],
17 | "rules": {
18 | "@typescript-eslint/explicit-member-accessibility": "off",
19 | "@typescript-eslint/explicit-module-boundary-types": "off",
20 | "@typescript-eslint/explicit-function-return-type": "off",
21 | "@typescript-eslint/no-parameter-properties": "off",
22 | "@nx/enforce-module-boundaries": [
23 | "error",
24 | {
25 | "enforceBuildableLibDependency": true,
26 | "allow": [],
27 | "depConstraints": [
28 | {
29 | "sourceTag": "*",
30 | "onlyDependOnLibsWithTags": ["*"]
31 | }
32 | ]
33 | }
34 | ]
35 | },
36 | "overrides": [
37 | {
38 | "files": ["*.tsx"],
39 | "rules": {
40 | "@typescript-eslint/no-unused-vars": "off"
41 | }
42 | }
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/.github/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name-template: 'v$RESOLVED_VERSION'
2 | tag-template: 'v$RESOLVED_VERSION'
3 | template: |
4 | # Changes
5 | $CHANGES
6 | categories:
7 | - title: 'Breaking'
8 | label: 'type: breaking'
9 | - title: 'New'
10 | label: 'type: feature'
11 | - title: 'Bug Fixes'
12 | label: 'type: bug'
13 | - title: 'Maintenance'
14 | label: 'type: maintenance'
15 | - title: 'Documentation'
16 | label: 'type: docs'
17 | - title: 'Dependency Updates'
18 | label: 'type: dependencies'
19 |
20 | version-resolver:
21 | major:
22 | labels:
23 | - 'release: major'
24 | minor:
25 | labels:
26 | - 'release: minor'
27 | patch:
28 | labels:
29 | - 'release: patch'
30 | default: patch
31 |
--------------------------------------------------------------------------------
/.github/workflows/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name: Release Drafter
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | update_release_draft:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: release-drafter/release-drafter@v5
13 | env:
14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 |
8 | # dependencies
9 | /node_modules
10 |
11 | # IDEs and editors
12 | /.idea
13 | .project
14 | .classpath
15 | .c9/
16 | *.launch
17 | .settings/
18 | *.sublime-workspace
19 |
20 | # IDE - VSCode
21 | .vscode/*
22 | !.vscode/settings.json
23 | !.vscode/tasks.json
24 | !.vscode/launch.json
25 | !.vscode/extensions.json
26 |
27 | # misc
28 | /.angular/cache
29 | /.sass-cache
30 | /connect.lock
31 | /coverage
32 | /libpeerconnection.log
33 | npm-debug.log
34 | yarn-error.log
35 | testem.log
36 | /typings
37 |
38 | # System Files
39 | .DS_Store
40 | Thumbs.db
41 |
42 | .nx/cache
43 | .nx/workspace-data
44 | .angular
45 | .cursor/rules/nx-rules.mdc
46 | .github/instructions/nx.instructions.md
47 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Add files here to ignore them from prettier formatting
2 |
3 | /dist
4 | /coverage
5 |
6 | /.nx/cache
7 | .angular
8 |
9 | /.nx/workspace-data
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "nrwl.angular-console",
4 | "angular.ng-template",
5 | "ms-vscode.vscode-typescript-tslint-plugin",
6 | "esbenp.prettier-vscode",
7 | "firsttris.vscode-jest-runner"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Anas
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # ngx-reactive-form-class-validator
3 | A lightweight library for dynamically validate Angular reactive forms using [class-validator](https://github.com/typestack/class-validator) library.
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ## Table of contents
21 | - [Table of Contents](#table-of-contents)
22 | - [Installation](#installation)
23 | - [Peer dependencies](#peer-dependencies)
24 | - [Usage](#usage)
25 | - [Defining classes with validators](#defining-classes-with-validators)
26 | - [Untyped classes](#untyped-classes)
27 | - [Creating a ClassValidatorFormGroup](#creating-a-classvalidatorformgroup)
28 | - [Using ClassValidatorFormBuilderService](#using-classvalidatorformbuilderservice)
29 | - [Using ClassValidatorFormGroup class](#using-classvalidatorformgroup-class)
30 | - [Eager Validation Option]()
31 | - [Add custom validators](#add-custom-validators)
32 | - [Providing validators when creating the ClassValidatorFormControl](#providing-validators-when-creating-the-classvalidatorformcontrol)
33 | - [Providing validators using `setValidators`/`setValidatorsWithDynamicValidation` methods](#providing-validators-using-setvalidatorssetvalidatorswithdynamicvalidation-methods)
34 | - [Available classes](#available-classes)
35 | - [ClassValidatorFormBuilderModule](#classvalidatorformbuildermodule)
36 | - [ClassValidatorFormBuilderService](#classvalidatorformbuilderservice)
37 | - [classType parameter](#classtype-parameter)
38 | - [ClassValidatorFormGroup](#classvalidatorformgroup)
39 | - [Stackblitz example](https://stackblitz.com/edit/ngx-reactive-form-class-validator-4pbcrp)
40 | - [Developer note](#developer-note)
41 |
42 | ## Installation
43 |
44 | npm install --save ngx-reactive-form-class-validator
45 |
46 | // OR
47 |
48 | yarn add ngx-reactive-form-class-validator
49 | ### Peer dependencies
50 | "@angular/common": ">= 2.0.0 <= ^20.0.0",
51 | "@angular/core": ">= 2.0.0 <= ^20.0.0",
52 | "@angular/forms": ">= 2.0.0 <= ^20.0.0",
53 | "class-validator": ">= 0.12.0 <= ^0.14.0"
54 |
55 | ###### _While this library will function with any version of class-validator within this range, we strongly recommend using class-validator ^0.14.0 or later due to a critical [security vulnerability](https://github.com/typestack/class-validator/blob/develop/CHANGELOG.md#:~:text=forbidUnknownValues%20option%20is%20enabled%20by%20default) addressed in versions 0.14.0 and beyond. This ensures the highest level of security for your application._
56 |
57 | ## Usage
58 | ### Defining classes with validators and deserializers
59 | **Please note that properties without a class-validator decorator will not be validated, see [class-validator](https://github.com/typestack/class-validator)**
60 | profile.ts
61 |
62 | import { IsEmail, IsNotEmpty, ValidateNested } from 'class-validator';
63 |
64 | class Profile {
65 | @IsNotEmpty()
66 | public firstName: string;
67 |
68 | @IsNotEmpty()
69 | public lastName: string;
70 |
71 | @IsEmail()
72 | public email: string;
73 |
74 | @ValidateNested()
75 | public address: Address;
76 | }
77 | address.ts
78 |
79 | import { IsNotEmpty, IsOptional, ValidateNested } from 'class-validator';
80 |
81 | class Address {
82 | @IsNotEmpty()
83 | public street: string;
84 |
85 | @IsNotEmpty()
86 | public city: string;
87 |
88 | @IsOptional()
89 | public state: string;
90 |
91 | @IsNotEmpty()
92 | public zip: string;
93 | }
94 |
95 | ### Untyped classes
96 | Untyped version of ngx-class-validator form classes exist in order to be backward compatible with angular untyped form classes
97 | ### Creating a ClassValidatorFormGroup
98 | #### Using ClassValidatorFormBuilderService
99 | As described [here](#classvalidatorformbuilderservice) to be able to use the `ClassValidatorFormBuilderService`, you need to import [ClassValidatorFormBuilderModule](#classvalidatorformbuildermodule).
100 |
101 | app.module.ts
102 |
103 | imports: [
104 | ...
105 | ClassValidatorFormBuilderModule.forRoot(),
106 | ...
107 | ],
108 | Then in your component
109 | profile-form.component.ts
110 |
111 | public constructor(
112 | private fb: ClassValidatorFormBuilderService,
113 | ) { }
114 |
115 | profileForm = this.fb.group(Profile,
116 | {
117 | firstName: [''],
118 | lastName: [''],
119 | email: [''],
120 | address: this.fb.group(Address,
121 | {
122 | street: [''],
123 | city: [''],
124 | state: [''],
125 | zip: ['']
126 | }
127 | ),
128 | });
129 | #### Using ClassValidatorFormGroup class
130 | As it's possible with angular `FormGroup` class we can directly create a `ClassValidatorFormGroup` using the constructor
131 |
132 |
133 | export class ProfileFormComponent {
134 | profileForm = new ClassValidatorFormGroup({
135 | firstName: new ClassValidatorFormControl(''),
136 | lastName: new ClassValidatorFormControl(''),
137 | });
138 | }
139 |
140 | Now, setting value to any of form controls, will perfom the validator set in the corresponding class.
141 |
142 | this.profileForm.controls.email.setValue('notEmailValue');
143 | console.log(this.profileForm.controls.email) // { isEmail: 'email must be an email' }
144 |
145 | this.profileForm.controls.email.setValue('email@email.com');
146 | console.log(this.profileForm.controls.email) // null
147 |
148 | #### Eager Validation Option
149 |
150 | By default, `ngx-reactive-form-class-validator` validates form controls after the form is fully initialized (ngAfterViewInit).
151 | 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.
152 |
153 | ```
154 | import { ClassValidatorFormGroup, ClassValidatorFormControl } from 'ngx-reactive-form-class-validator';
155 |
156 | const formGroup = new ClassValidatorFormGroup({
157 | email: new ClassValidatorFormControl(''),
158 | password: new ClassValidatorFormControl('')
159 | }, null, { eagerValidation: true }); // 👈 Enable eager validation here
160 |
161 |
162 | // Or using the form builder
163 |
164 | public constructor(
165 | private fb: ClassValidatorFormBuilderService,
166 | ) { }
167 |
168 | profileForm = this.fb.group(
169 | Profile,
170 | {
171 | firstName: [''],
172 | lastName: [''],
173 | email: [''],
174 | address: this.fb.group(Address,
175 | {
176 | street: [''],
177 | city: [''],
178 | state: [''],
179 | zip: ['']
180 | }
181 | ),
182 | },
183 | undefined,
184 | { eagerValidation: true } // 👈 Enable eager validation here
185 | );
186 |
187 | ```
188 |
189 | ### Add custom validators
190 | It is possible as well to combine dynamic validation with custom validation.
191 | There are several ways to do it:
192 | #### Providing validators when creating the ClassValidatorFormControl
193 |
194 | this.fb.group (Profile, {
195 | email: ['', Validators.required],
196 | ...
197 | }
198 | )
199 |
200 | // OR
201 |
202 | new ClassValidatorFormGroup(Profile, {
203 | email: new ClassValidatorFormControl('', Validators.required)
204 | })
205 | #### Providing validators using `setValidators`/`setValidatorsWithDynamicValidation` methods
206 | Both `setValidators` and `setValidatorsWithDynamicValidation` replace validators provided in parameter, the only one difference is that `setValidatorsWithDynamicValidation` add given validators as well as re-enable dynamic validation, as the `setValidators` method replace validators with given ones without re-enabling dynamic validation.
207 |
208 | emailControl.setValidators(Validators.required); // there will be only Validators.required validator
209 |
210 | emailControl.setValidatorsWithDynamicValidation(Validators.required) // there will be Validaros.required validator as well as dynamic validator
211 |
212 |
213 |
214 | ## Available classes
215 | ### ClassValidatorFormBuilderModule
216 | An Angular module that provides [ClassValidatorFormBuilderService](#classvalidatorformbuilderservice) for dependency injection.
217 | It can either be imported `forRoot` or normally (We don't recommend importing it normally because that will create multiple instances of [ClassValidatorFormBuilderService](#classvalidatorformbuilderservice)).
218 |
219 | app.module.ts
220 |
221 | imports: [
222 | ...
223 | ClassValidatorFormBuilderModule.forRoot(),
224 | ...
225 | ],
226 | ### ClassValidatorFormBuilderService
227 | An Angular injectable service having the same methods as Angular [FormBuilder](https://angular.io/api/forms/FormBuilder) except a minor change of `group` method signature, see below:
228 |
229 | group(
230 | classType: ClassType, // The class type of the form group value.
231 | // Angular FormBuilder group method parameters
232 | controlsConfig: { [p: string]: any },
233 | options?: AbstractControlOptions | { [p: string]: any } | null,
234 | ): ClassValidatorFormGroup;
235 |
236 | #### classType parameter
237 | We've introduced a new parameter called `classType` (a class type containing [class-validator](https://github.com/typestack/class-validator) decorators) that you should provide, to enable us to perform dynamic validations.
238 | ### ClassValidatorFormGroup
239 | A typescript class extending angular [FormGroup](https://angular.io/api/forms/FormGroup) class, with a minor change of `constructor` signature, the [classType parameter](#classType-parameter).
240 |
241 | export class ClassValidatorFormGroup extends FormGroup {
242 | public constructor(
243 | private readonly classType: ClassType,
244 | // Angular FormGroup constructor parameters
245 | controls: {
246 | [key: string]: AbstractControl;
247 | },
248 | validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null,
249 | asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null,
250 | ) {
251 | ...
252 | }
253 | ### ClassValidatorFormControl
254 | A typescript class extending angular [FormControl](https://angular.io/api/forms/FormControl) class, that will use the [classType](#classtype-parameter) instance to perform validations and assign validation errors to the `ClassValidatorFormControl`.
255 |
256 | As it extends angular [FormControl](https://angular.io/api/forms/FormControl) class, it contains all `FormControl` methods, with a custom new method:
257 |
258 | setValidatorsWithDynamicValidation(newValidator: ValidatorFn | ValidatorFn[] | AbstractControlOptions | undefined): void
259 |
260 | This method has the same signature as FormControl [setValidators](https://angular.io/api/forms/AbstractControl#setValidators) method. In addition it re-enables dynamic validation when disabled.
261 |
262 | ## Developer note
263 | We are open for proposals, so please don't hesitate to create issues if you want to report any bugs/proposals/feature requests.
264 |
--------------------------------------------------------------------------------
/apps/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abarghoud/ngx-reactive-form-class-validator/d2c5e113e430f8f57a5623bcf2f7d865920c5776/apps/.gitkeep
--------------------------------------------------------------------------------
/apps/test/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test",
3 | "$schema": "../../node_modules/nx/schemas/project-schema.json",
4 | "projectType": "application",
5 | "prefix": "app",
6 | "sourceRoot": "apps/test/src",
7 | "tags": [],
8 | "targets": {
9 | "build": {
10 | "executor": "@angular/build:application",
11 | "outputs": [
12 | "{options.outputPath}"
13 | ],
14 | "options": {
15 | "outputPath": "dist/apps/test",
16 | "index": "apps/test/src/index.html",
17 | "browser": "apps/test/src/main.ts",
18 | "polyfills": [
19 | "zone.js"
20 | ],
21 | "tsConfig": "apps/test/tsconfig.app.json",
22 | "assets": [
23 | "apps/test/src/favicon.ico",
24 | "apps/test/src/assets"
25 | ],
26 | "styles": [
27 | "apps/test/src/styles.css"
28 | ],
29 | "scripts": []
30 | },
31 | "configurations": {
32 | "production": {
33 | "budgets": [
34 | {
35 | "type": "initial",
36 | "maximumWarning": "2mb",
37 | "maximumError": "5mb"
38 | },
39 | {
40 | "type": "anyComponentStyle",
41 | "maximumWarning": "6kb",
42 | "maximumError": "10kb"
43 | }
44 | ],
45 | "outputHashing": "all"
46 | },
47 | "development": {
48 | "optimization": false,
49 | "extractLicenses": false,
50 | "sourceMap": true
51 | }
52 | },
53 | "defaultConfiguration": "production"
54 | },
55 | "serve": {
56 | "executor": "@angular/build:dev-server",
57 | "configurations": {
58 | "production": {
59 | "buildTarget": "test:build:production"
60 | },
61 | "development": {
62 | "buildTarget": "test:build:development"
63 | }
64 | },
65 | "defaultConfiguration": "development",
66 | "continuous": true
67 | },
68 | "extract-i18n": {
69 | "executor": "@angular/build:extract-i18n",
70 | "options": {
71 | "buildTarget": "test:build"
72 | }
73 | }
74 | }
75 | }
--------------------------------------------------------------------------------
/apps/test/src/app/app.component.html:
--------------------------------------------------------------------------------
1 | ngx-reactive-form-class-validator example
2 |
3 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/apps/test/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
3 | import { JsonPipe } from '@angular/common';
4 |
5 | import { IsEmail, IsNotEmpty, IsOptional, ValidateNested } from 'class-validator';
6 | import { deserialize, deserializeAs } from 'cerialize';
7 | import {
8 | ClassValidatorFormBuilderModule,
9 | ClassValidatorFormBuilderService, ClassValidatorFormControl,
10 | ClassValidatorFormGroup
11 | } from 'ngx-reactive-form-class-validator';
12 |
13 |
14 | class Address {
15 | @deserialize
16 | @IsNotEmpty()
17 | public street: string;
18 |
19 | @deserialize
20 | @IsNotEmpty()
21 | public city: string;
22 |
23 | @deserialize
24 | @IsOptional()
25 | public state: string;
26 |
27 | @deserialize
28 | @IsNotEmpty()
29 | public zip: string;
30 | }
31 |
32 | class Profile {
33 | @deserialize
34 | public firstName: string;
35 |
36 | @deserialize
37 | @IsNotEmpty()
38 | public lastName: string;
39 |
40 | @deserialize
41 | @IsEmail()
42 | public email: string;
43 |
44 | @deserializeAs(Address)
45 | @ValidateNested()
46 | public address: Address;
47 | }
48 |
49 | @Component({
50 | selector: 'app-root',
51 | templateUrl: './app.component.html',
52 | imports: [
53 | ReactiveFormsModule,
54 | JsonPipe,
55 | ClassValidatorFormBuilderModule,
56 | ]
57 | })
58 | export class AppComponent implements OnInit {
59 | public profileForm: ClassValidatorFormGroup;
60 | public addressForm: ClassValidatorFormGroup;
61 |
62 | public constructor(private readonly fb: ClassValidatorFormBuilderService) {}
63 |
64 | public ngOnInit(): void {
65 | // Creating ClassValidatorFormGroup using the builder
66 | this.profileForm = this.fb.group(Profile, {
67 | firstName: ["", Validators.required], // add custom validator, firstName property in Profile class has not any class-validator
68 | lastName: [""],
69 | email: ["12", Validators.minLength(3)], // Combining class-validator with custom validator
70 | // Creating ClassValidatorFormGroup using the class
71 | address: new ClassValidatorFormGroup(Address, {
72 | street: new ClassValidatorFormControl(""),
73 | city: new ClassValidatorFormControl(""),
74 | state: new ClassValidatorFormControl(""),
75 | // using a FormControl will not apply dynamic validation
76 | zip: new FormControl("")
77 | })
78 | }, undefined);
79 |
80 | this.addressForm = this.profileForm.get(
81 | "address"
82 | ) as ClassValidatorFormGroup;
83 | debugger;
84 | }
85 |
86 | public clearValidators(): void {
87 | Object.entries(this.profileForm.controls).forEach(([name, control]) => {
88 | control.clearValidators();
89 | control.updateValueAndValidity();
90 | });
91 | Object.entries(this.addressForm.controls).forEach(([name, control]) => {
92 | control.clearValidators();
93 | control.updateValueAndValidity();
94 | });
95 | }
96 |
97 | public resetValidators(): void {
98 | Object.entries(this.profileForm.controls).forEach(([name, control]) => {
99 | if (control instanceof ClassValidatorFormControl) {
100 | if (name === "firstName") {
101 | control.setValidators(Validators.required);
102 | control.updateValueAndValidity();
103 |
104 | return;
105 | } else if (name === "email") {
106 | control.setValidatorsWithDynamicValidation(Validators.minLength(3));
107 | control.updateValueAndValidity();
108 |
109 | return;
110 | } else {
111 | control.setValidatorsWithDynamicValidation(undefined);
112 | control.updateValueAndValidity();
113 |
114 | return;
115 | }
116 | }
117 | });
118 | Object.entries(this.addressForm.controls).forEach(([name, control]) => {
119 | if (control instanceof ClassValidatorFormControl) {
120 | control.setValidatorsWithDynamicValidation(undefined);
121 | control.updateValueAndValidity();
122 | }
123 | });
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/apps/test/src/app/app.config.ts:
--------------------------------------------------------------------------------
1 | import { ApplicationConfig } from '@angular/core';
2 |
3 | export const appConfig: ApplicationConfig = {
4 | providers: [],
5 | };
6 |
--------------------------------------------------------------------------------
/apps/test/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abarghoud/ngx-reactive-form-class-validator/d2c5e113e430f8f57a5623bcf2f7d865920c5776/apps/test/src/assets/.gitkeep
--------------------------------------------------------------------------------
/apps/test/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abarghoud/ngx-reactive-form-class-validator/d2c5e113e430f8f57a5623bcf2f7d865920c5776/apps/test/src/favicon.ico
--------------------------------------------------------------------------------
/apps/test/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | test
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/apps/test/src/main.ts:
--------------------------------------------------------------------------------
1 | import { bootstrapApplication } from '@angular/platform-browser';
2 | import { appConfig } from './app/app.config';
3 | import { AppComponent } from './app/app.component';
4 |
5 | bootstrapApplication(AppComponent, appConfig).catch((err) =>
6 | console.error(err)
7 | );
8 |
--------------------------------------------------------------------------------
/apps/test/src/styles.css:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
--------------------------------------------------------------------------------
/apps/test/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "types": []
6 | },
7 | "files": ["src/main.ts"],
8 | "include": ["src/**/*.d.ts"],
9 | "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/apps/test/tsconfig.editor.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": ["src/**/*.ts"],
4 | "compilerOptions": {},
5 | "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
6 | }
7 |
--------------------------------------------------------------------------------
/apps/test/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2022",
4 | "useDefineForClassFields": false,
5 | "esModuleInterop": true
6 | },
7 | "files": [],
8 | "include": [],
9 | "references": [
10 | {
11 | "path": "./tsconfig.editor.json"
12 | },
13 | {
14 | "path": "./tsconfig.app.json"
15 | }
16 | ],
17 | "extends": "../../tsconfig.base.json"
18 | }
19 |
--------------------------------------------------------------------------------
/decorate-angular-cli.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file decorates the Angular CLI with the Nx CLI to enable features such as computation caching
3 | * and faster execution of tasks.
4 | *
5 | * It does this by:
6 | *
7 | * - Patching the Angular CLI to warn you in case you accidentally use the undecorated ng command.
8 | * - Symlinking the ng to nx command, so all commands run through the Nx CLI
9 | * - Updating the package.json postinstall script to give you control over this script
10 | *
11 | * The Nx CLI decorates the Angular CLI, so the Nx CLI is fully compatible with it.
12 | * Every command you run should work the same when using the Nx CLI, except faster.
13 | *
14 | * Because of symlinking you can still type `ng build/test/lint` in the terminal. The ng command, in this case,
15 | * will point to nx, which will perform optimizations before invoking ng. So the Angular CLI is always invoked.
16 | * The Nx CLI simply does some optimizations before invoking the Angular CLI.
17 | *
18 | * To opt out of this patch:
19 | * - Replace occurrences of nx with ng in your package.json
20 | * - Remove the script from your postinstall script in your package.json
21 | * - Delete and reinstall your node_modules
22 | */
23 |
24 | const fs = require('fs');
25 | const os = require('os');
26 | const cp = require('child_process');
27 | const isWindows = os.platform() === 'win32';
28 | let output;
29 | try {
30 | output = require('@nx/workspace').output;
31 | } catch (e) {
32 | console.warn(
33 | 'Angular CLI could not be decorated to enable computation caching. Please ensure @nx/workspace is installed.'
34 | );
35 | process.exit(0);
36 | }
37 |
38 | /**
39 | * Paths to files being patched
40 | */
41 | const angularCLIInitPath = 'node_modules/@angular/cli/lib/cli/index.js';
42 |
43 | /**
44 | * Patch index.js to warn you if you invoke the undecorated Angular CLI.
45 | */
46 | function patchAngularCLI(initPath) {
47 | const angularCLIInit = fs.readFileSync(initPath, 'utf-8').toString();
48 |
49 | if (!angularCLIInit.includes('NX_CLI_SET')) {
50 | fs.writeFileSync(
51 | initPath,
52 | `
53 | if (!process.env['NX_CLI_SET']) {
54 | const { output } = require('@nx/workspace');
55 | output.warn({ title: 'The Angular CLI was invoked instead of the Nx CLI. Use "npx ng [command]" or "nx [command]" instead.' });
56 | }
57 | ${angularCLIInit}
58 | `
59 | );
60 | }
61 | }
62 |
63 | /**
64 | * Symlink of ng to nx, so you can keep using `ng build/test/lint` and still
65 | * invoke the Nx CLI and get the benefits of computation caching.
66 | */
67 | function symlinkNgCLItoNxCLI() {
68 | try {
69 | const ngPath = './node_modules/.bin/ng';
70 | const nxPath = './node_modules/.bin/nx';
71 | if (isWindows) {
72 | /**
73 | * This is the most reliable way to create symlink-like behavior on Windows.
74 | * Such that it works in all shells and works with npx.
75 | */
76 | ['', '.cmd', '.ps1'].forEach((ext) => {
77 | if (fs.existsSync(nxPath + ext))
78 | fs.writeFileSync(ngPath + ext, fs.readFileSync(nxPath + ext));
79 | });
80 | } else {
81 | // If unix-based, symlink
82 | cp.execSync(`ln -sf ./nx ${ngPath}`);
83 | }
84 | } catch (e) {
85 | output.error({
86 | title:
87 | 'Unable to create a symlink from the Angular CLI to the Nx CLI:' +
88 | e.message,
89 | });
90 | throw e;
91 | }
92 | }
93 |
94 | try {
95 | symlinkNgCLItoNxCLI();
96 | patchAngularCLI(angularCLIInitPath);
97 | output.log({
98 | title: 'Angular CLI has been decorated to enable computation caching.',
99 | });
100 | } catch (e) {
101 | output.error({
102 | title: 'Decoration of the Angular CLI did not complete successfully',
103 | });
104 | }
105 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | const { getJestProjects } = require('@nx/jest');
2 |
3 | export default { projects: [...getJestProjects()] };
4 |
--------------------------------------------------------------------------------
/jest.preset.js:
--------------------------------------------------------------------------------
1 | const nxPreset = require('@nx/jest/preset').default;
2 |
3 | module.exports = {
4 | ...nxPreset,
5 | /* TODO: Update to latest Jest snapshotFormat
6 | * By default Nx has kept the older style of Jest Snapshot formats
7 | * to prevent breaking of any existing tests with snapshots.
8 | * It's recommend you update to the latest format.
9 | * You can do this by removing snapshotFormat property
10 | * and running tests with --update-snapshot flag.
11 | * Example: "nx affected --targets=test --update-snapshot"
12 | * More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format
13 | */
14 | snapshotFormat: { escapeString: true, printBasicPrototype: true },
15 | };
16 |
--------------------------------------------------------------------------------
/libs/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abarghoud/ngx-reactive-form-class-validator/d2c5e113e430f8f57a5623bcf2f7d865920c5776/libs/.gitkeep
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../.eslintrc.json",
3 | "ignorePatterns": ["!**/*"],
4 | "rules": {}
5 | }
6 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/README.md:
--------------------------------------------------------------------------------
1 | # ngx-reactive-form-class-validator
2 |
3 | This library was generated with [Nx](https://nx.dev).
4 |
5 | ## Running unit tests
6 |
7 | Run `nx test ngx-reactive-form-class-validator` to execute the unit tests.
8 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/jest.config.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | export default {
3 | transform: {
4 | '^.+\\.(ts|mjs|js|html)$': [
5 | 'jest-preset-angular',
6 | {
7 | stringifyContentPathRegex: '\\.(html|svg)$',
8 | tsconfig: '/tsconfig.spec.json',
9 | },
10 | ],
11 | },
12 | transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
13 | displayName: 'ngx-reactive-form-class-validator',
14 | preset: '../../jest.preset.js',
15 | setupFilesAfterEnv: ['/src/test-setup.ts'],
16 | globals: {},
17 | coverageDirectory: '../../coverage/libs/ngx-reactive-form-class-validator',
18 | coverageReporters: ['json', 'text', 'lcov'],
19 | snapshotSerializers: [
20 | 'jest-preset-angular/build/serializers/no-ng-attributes',
21 | 'jest-preset-angular/build/serializers/ng-snapshot',
22 | 'jest-preset-angular/build/serializers/html-comment',
23 | ],
24 | testPathIgnorePatterns: ['/node_modules/'],
25 | };
26 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/ng-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3 | "dest": "../../dist/libs/ngx-reactive-form-class-validator",
4 | "lib": {
5 | "entryFile": "src/index.ts"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ngx-reactive-form-class-validator",
3 | "description": "A lightweight library for dynamically validate Angular reactive forms using class-validator library.",
4 | "license": "MIT",
5 | "version": "2.0.0",
6 | "keywords": [
7 | "ng",
8 | "angular",
9 | "library",
10 | "ngx",
11 | "typescript",
12 | "reactive-forms",
13 | "class-validator",
14 | "dynamic-validator",
15 | "validation",
16 | "validator"
17 | ],
18 | "repository": {
19 | "type": "git",
20 | "url": "git+https://github.com/abarghoud/ngx-reactive-form-class-validator.git"
21 | },
22 | "bugs": {
23 | "url": "https://github.com/abarghoud/ngx-reactive-form-class-validator/issues"
24 | },
25 | "homepage": "https://github.com/abarghoud/ngx-reactive-form-class-validator#readme",
26 | "peerDependencies": {
27 | "@angular/common": ">= 2.0.0 <= ^20.0.0",
28 | "@angular/core": ">= 2.0.0 <= ^20.0.0",
29 | "@angular/forms": ">= 2.0.0 <= ^20.0.0",
30 | "class-validator": ">= 0.12.0 <= ^0.14.0"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ngx-reactive-form-class-validator",
3 | "$schema": "../../node_modules/nx/schemas/project-schema.json",
4 | "projectType": "library",
5 | "sourceRoot": "libs/ngx-reactive-form-class-validator/src",
6 | "prefix": "ngx-reactive-form-class-validator",
7 | "targets": {
8 | "build": {
9 | "executor": "@nx/angular:package",
10 | "options": {
11 | "tsConfig": "libs/ngx-reactive-form-class-validator/tsconfig.lib.json",
12 | "project": "libs/ngx-reactive-form-class-validator/ng-package.json"
13 | },
14 | "configurations": {
15 | "production": {
16 | "tsConfig": "libs/ngx-reactive-form-class-validator/tsconfig.lib.prod.json"
17 | }
18 | }
19 | },
20 | "lint": {
21 | "executor": "@nrwl/linter:eslint",
22 | "options": {
23 | "lintFilePatterns": [
24 | "libs/ngx-reactive-form-class-validator/src/**/*.ts"
25 | ]
26 | }
27 | },
28 | "test": {
29 | "executor": "@nx/jest:jest",
30 | "options": {
31 | "jestConfig": "libs/ngx-reactive-form-class-validator/jest.config.ts"
32 | }
33 | }
34 | },
35 | "generators": {
36 | "@schematics/angular:component": {
37 | "style": "scss"
38 | }
39 | },
40 | "tags": []
41 | }
42 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './lib/index';
2 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/src/lib/class-validator-form-array.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AbstractControl,
3 | AbstractControlOptions,
4 | AsyncValidatorFn,
5 | FormArray,
6 | ValidatorFn
7 | } from '@angular/forms';
8 |
9 | export class ClassValidatorFormArray extends FormArray {
10 | public constructor(
11 | controls: AbstractControl[],
12 | validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null,
13 | asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null,
14 | ) {
15 | super(
16 | controls,
17 | validatorOrOpts,
18 | asyncValidator,
19 | );
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/src/lib/class-validator-form-builder.module.ts:
--------------------------------------------------------------------------------
1 | import { ModuleWithProviders, NgModule } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 |
4 | import { ClassValidatorFormBuilderService } from './class-validator-form-builder.service';
5 |
6 | @NgModule({
7 | imports: [CommonModule],
8 | providers: [ClassValidatorFormBuilderService],
9 | })
10 | export class ClassValidatorFormBuilderModule {
11 | public static forRoot(): ModuleWithProviders {
12 | return {
13 | ngModule: ClassValidatorFormBuilderModule,
14 | providers: [
15 | ClassValidatorFormBuilderService,
16 | ],
17 | };
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/src/lib/class-validator-form-builder.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import {
3 | AbstractControl,
4 | AbstractControlOptions,
5 | AsyncValidatorFn,
6 | FormArray,
7 | FormControl,
8 | FormControlState,
9 | FormGroup,
10 | ValidatorFn
11 | } from '@angular/forms';
12 |
13 | import { ClassValidatorFormGroup } from './class-validator-form-group';
14 | import { ClassValidatorFormControl } from './class-validator-form-control';
15 | import { ClassValidatorFormArray } from './class-validator-form-array';
16 | import { ClassType } from './types';
17 | import { ClassValidatorFormGroupOptions } from './class-validator-form-group-options.interface';
18 |
19 | // Coming from https://github.com/angular/angular/blob/3b0b7d22109c79b4dceb4ae069c3927894cf1bd6/packages/forms/src/form_builder.ts#L14
20 | const isAbstractControlOptions = (options: AbstractControlOptions | { [key: string]: any }): options is AbstractControlOptions =>
21 | (options as AbstractControlOptions).asyncValidators !== undefined ||
22 | (options as AbstractControlOptions).validators !== undefined ||
23 | (options as AbstractControlOptions).updateOn !== undefined;
24 |
25 | @Injectable()
26 | export class ClassValidatorFormBuilderService {
27 | /**
28 | * @description
29 | * Construct a new `FormGroup` instance.
30 | *
31 | * @param formClassType the `classType` containing `class-validator` decorators to be used to validate form
32 | * @param controlsConfig A collection of child controls. The key for each child is the name
33 | * under which it is registered.
34 | *
35 | * @param options Configuration options object for the `FormGroup`. The object can
36 | * have two shapes:
37 | *
38 | * 1) `AbstractControlOptions` object (preferred), which consists of:
39 | * * `validators`: A synchronous validator function, or an array of validator functions
40 | * * `asyncValidators`: A single async validator or array of async validator functions
41 | * * `updateOn`: The event upon which the control should be updated (options: 'change' | 'blur' |
42 | * submit')
43 | *
44 | * 2) Legacy configuration object, which consists of:
45 | * * `validator`: A synchronous validator function, or an array of validator functions
46 | * * `asyncValidator`: A single async validator or array of async validator functions
47 | *
48 | * @param classValidatorGroupOptions Options object of type `ClassValidatorFormGroupOptions` allowing
49 | * to define eagerValidation that validate controls immediately upon creation. Default is false (validators are executed starting from ngAfterViewInit hook)
50 | * See https://github.com/abarghoud/ngx-reactive-form-class-validator/issues/47
51 | *
52 | */
53 | public group(
54 | formClassType: ClassType,
55 | controlsConfig: { [p: string]: any },
56 | options?: AbstractControlOptions | { [p: string]: any } | null,
57 | classValidatorGroupOptions?: ClassValidatorFormGroupOptions,
58 | ): ClassValidatorFormGroup {
59 | // Coming from https://github.com/angular/angular/blob/3b0b7d22109c79b4dceb4ae069c3927894cf1bd6/packages/forms/src/form_builder.ts#L59
60 | const controls = this.reduceControls(controlsConfig);
61 |
62 | let validators: ValidatorFn | ValidatorFn[] | null = null;
63 | let asyncValidators: AsyncValidatorFn | AsyncValidatorFn[] | null = null;
64 | let updateOn;
65 |
66 | if (options) {
67 | if (isAbstractControlOptions(options)) {
68 | // `options` are `AbstractControlOptions`
69 | validators = options.validators ? options.validators : null;
70 | asyncValidators = options.asyncValidators ? options.asyncValidators : null;
71 | updateOn = options.updateOn ? options.updateOn : undefined;
72 | } else {
73 | // `options` are legacy form group options
74 | validators = options['validator'] !== null ? options['validator'] : null;
75 | asyncValidators = options['asyncValidator'] !== null ? options['asyncValidator'] : null;
76 | }
77 | }
78 |
79 | return new ClassValidatorFormGroup(formClassType, controls, { asyncValidators, updateOn, validators }, undefined, classValidatorGroupOptions);
80 | }
81 |
82 | /**
83 | * Constructs a new `FormArray` from the given array of configurations,
84 | * validators and options.
85 | *
86 | * @param controlsConfig An array of child controls or control configs. Each
87 | * child control is given an index when it is registered.
88 | *
89 | * @param validatorOrOpts A synchronous validator function, or an array of
90 | * such functions, or an `AbstractControlOptions` object that contains
91 | * validation functions and a validation trigger.
92 | *
93 | * @param asyncValidator A single async validator or array of async validator
94 | * functions.
95 | */
96 | public array(
97 | controlsConfig: Array,
98 | validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null,
99 | asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null
100 | ): FormArray {
101 | const controls = controlsConfig.map(control => this.createControl(control));
102 |
103 | return new ClassValidatorFormArray(controls, validatorOrOpts, asyncValidator);
104 | }
105 |
106 |
107 | /**
108 | * @description
109 | * Construct a new `FormControl` with the given state, validators and options.
110 | *
111 | * @param formState Initializes the control with an initial state value, or
112 | * with an object that contains both a value and a disabled status.
113 | *
114 | * @param validatorOrOpts A synchronous validator function, or an array of
115 | * such functions, or an `AbstractControlOptions` object that contains
116 | * validation functions and a validation trigger.
117 | *
118 | * @param asyncValidator A single async validator or array of async validator
119 | * functions.
120 | *
121 | * @usageNotes
122 | *
123 | * ### Initialize a control as disabled
124 | *
125 | * The following example returns a control with an initial value in a disabled state.
126 | *
127 | *
128 | *
129 | */
130 | public control(
131 | formState: T|FormControlState,
132 | validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null,
133 | asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null
134 | ): ClassValidatorFormControl {
135 | return new ClassValidatorFormControl(formState, validatorOrOpts, asyncValidator);
136 | }
137 |
138 | // Coming from https://github.com/angular/angular/blob/3b0b7d22109c79b4dceb4ae069c3927894cf1bd6/packages/forms/src/form_builder.ts#L133
139 | private reduceControls(controlsConfig: { [k: string]: any }): { [key: string]: AbstractControl } {
140 | const controls: { [key: string]: AbstractControl } = {};
141 |
142 | Object.keys(controlsConfig).forEach(controlName => {
143 | controls[controlName] = this.createControl(controlsConfig[controlName]);
144 | });
145 |
146 | return controls;
147 | }
148 |
149 | private createControl(controlConfig: any): AbstractControl {
150 | if (
151 | controlConfig instanceof FormControl
152 | || controlConfig instanceof FormGroup
153 | || controlConfig instanceof FormArray
154 | ) {
155 | return controlConfig;
156 | } else if (Array.isArray(controlConfig)) {
157 | const value = controlConfig[0];
158 | const validator: ValidatorFn = controlConfig.length > 1 ? controlConfig[1] : null;
159 | const asyncValidator: AsyncValidatorFn = controlConfig.length > 2 ? controlConfig[2] : null;
160 |
161 | return this.control(value, validator, asyncValidator);
162 | } else {
163 | return this.control(controlConfig);
164 | }
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/src/lib/class-validator-form-control.spec.ts:
--------------------------------------------------------------------------------
1 | import { ValidationErrors, Validators } from '@angular/forms';
2 |
3 | import { FakeContact, FakeContactType } from './testing/fake-user-testing.model';
4 | import { ClassValidatorFormControl } from './class-validator-form-control';
5 |
6 | describe('The ClassValidatorFormControl class', () => {
7 | let phoneNumberClassValidatorFormControl: ClassValidatorFormControl;
8 |
9 | beforeEach(() => {
10 | phoneNumberClassValidatorFormControl = new ClassValidatorFormControl('');
11 | });
12 |
13 | describe('When control name and classValue are provided', () => {
14 | const contact = new FakeContact();
15 | contact.phoneNumber = '';
16 | contact.email = '';
17 | contact.type = FakeContactType.phone;
18 |
19 | beforeEach(() => {
20 | phoneNumberClassValidatorFormControl.setNameAndFormGroupClassValue('phoneNumber', contact);
21 | });
22 |
23 | describe('When dynamic class-validator', () => {
24 | const validPhoneNumber = '0634555555';
25 | const invalidPhoneNumber = '0634555555545';
26 |
27 | describe('When invalid value provided', () => {
28 | it('should has error', () => {
29 | phoneNumberClassValidatorFormControl.setValue('0634555555545');
30 |
31 | expect(phoneNumberClassValidatorFormControl.errors).toEqual({isMobilePhone: 'phoneNumber must be a phone number'});
32 | });
33 | });
34 |
35 | describe('When valid value provided', () => {
36 | it('should not have any error', () => {
37 | phoneNumberClassValidatorFormControl.setValue(validPhoneNumber);
38 |
39 | expect(phoneNumberClassValidatorFormControl.errors).toBeFalsy();
40 | });
41 | });
42 |
43 | describe('When conditional validation', () => {
44 | it('should not have any error', () => {
45 | contact.type = FakeContactType.email;
46 |
47 | phoneNumberClassValidatorFormControl.setValue(invalidPhoneNumber);
48 | expect(phoneNumberClassValidatorFormControl.errors).toBeFalsy();
49 | });
50 | });
51 | });
52 |
53 | describe('When combining dynamic class-validator and manual validation', () => {
54 | const emailClassValidatorFormControl = new ClassValidatorFormControl();
55 |
56 | beforeEach(() => {
57 | const contactForEmailValidation = Object.assign(contact, {type: FakeContactType.email});
58 |
59 | emailClassValidatorFormControl.setNameAndFormGroupClassValue('email', contactForEmailValidation);
60 | emailClassValidatorFormControl.setValidatorsWithDynamicValidation(Validators.minLength(8));
61 | });
62 |
63 | it('should consider manual validation as well as dynamic validator', () => {
64 | emailClassValidatorFormControl.setValue('123456');
65 |
66 | expect(emailClassValidatorFormControl.errors).toEqual({
67 | isEmail: 'email must be an email',
68 | minlength: {
69 | actualLength: 6,
70 | requiredLength: 8,
71 | },
72 | });
73 | });
74 |
75 | describe('When array of validators', () => {
76 | beforeEach(() => {
77 | emailClassValidatorFormControl.setValidatorsWithDynamicValidation([
78 | Validators.maxLength(8),
79 | (): ValidationErrors => ({isSecondValidatorError: true}),
80 | ],
81 | );
82 | });
83 |
84 | it('should use validators provided as well as dynamic validator', () => {
85 | emailClassValidatorFormControl.setValue('4546546546565');
86 |
87 | expect(emailClassValidatorFormControl.errors).toEqual({
88 | isEmail: 'email must be an email',
89 | maxlength: {
90 | actualLength: 13,
91 | requiredLength: 8,
92 | },
93 | isSecondValidatorError: true,
94 | });
95 | });
96 | });
97 |
98 | describe('When AbstractControlOptions validators', () => {
99 | beforeEach(() => {
100 | emailClassValidatorFormControl.setValidatorsWithDynamicValidation({
101 | validators: [
102 | Validators.maxLength(8),
103 | (): ValidationErrors => ({isSecondValidatorError: true}),
104 | ]
105 | });
106 | });
107 |
108 | it('should use validators provided as well as dynamic validator', () => {
109 | emailClassValidatorFormControl.setValue('4546546546565');
110 |
111 | expect(emailClassValidatorFormControl.errors).toEqual({
112 | isEmail: 'email must be an email',
113 | maxlength: {
114 | actualLength: 13,
115 | requiredLength: 8,
116 | },
117 | isSecondValidatorError: true,
118 | });
119 | });
120 | });
121 |
122 | describe('When `null` provided', () => {
123 | beforeEach(() => {
124 | emailClassValidatorFormControl.setValidatorsWithDynamicValidation(undefined);
125 | });
126 |
127 | it('should use only dynamic validator', () => {
128 | emailClassValidatorFormControl.setValue('notEmail');
129 |
130 | expect(emailClassValidatorFormControl.errors).toEqual({ isEmail: 'email must be an email' });
131 | });
132 | });
133 | });
134 | });
135 | });
136 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/src/lib/class-validator-form-control.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AbstractControlOptions,
3 | AsyncValidatorFn,
4 | FormControl,
5 | ValidationErrors,
6 | ValidatorFn,
7 | Validators
8 | } from '@angular/forms';
9 | import { validateSync } from 'class-validator';
10 |
11 | export class ClassValidatorFormControl extends FormControl {
12 | private formGroupClassValue: any;
13 | private name: string;
14 |
15 | /**
16 | * Creates a new `ClassValidatorFormControl` instance.
17 | *
18 | * @param formState Initializes the control with an initial value,
19 | * or an object that defines the initial value and disabled state.
20 | *
21 | * @param validatorOrOpts A synchronous validator function, or an array of
22 | * such functions, or an `AbstractControlOptions` object that contains validation functions
23 | * and a validation trigger.
24 | *
25 | * @param asyncValidator A single async validator or array of async validator functions
26 | *
27 | */
28 | public constructor(
29 | formState?: any,
30 | validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null,
31 | asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null
32 | ) {
33 | super(formState, validatorOrOpts, asyncValidator);
34 |
35 | this.setValidatorsWithDynamicValidation(validatorOrOpts);
36 | }
37 |
38 | /**
39 | * @internal
40 | */
41 | public setNameAndFormGroupClassValue(name: string, value: any, eagerValidation: boolean = false): void {
42 | this.name = name;
43 | this.formGroupClassValue = value;
44 |
45 | if (eagerValidation) {
46 | this.updateValueAndValidity();
47 | }
48 | }
49 |
50 | /**
51 | * Sets the synchronous validators that are active on this control as well as resetting the dynamic `class-validator`. Calling
52 | * this overwrites any existing sync validators.
53 | *
54 | * When you add or remove a validator at run time, you must call
55 | * `updateValueAndValidity()` for the new validation to take effect.
56 | *
57 | */
58 | public setValidatorsWithDynamicValidation(newValidator: ValidatorFn | ValidatorFn[] | AbstractControlOptions | undefined): void {
59 | this.setValidators(
60 | newValidator
61 | ? [this.composeValidators(newValidator), this.dynamicValidator]
62 | : this.dynamicValidator);
63 | }
64 |
65 | private composeValidators(validator: ValidatorFn | ValidatorFn[] | AbstractControlOptions): ValidatorFn {
66 | if (validator instanceof Array) {
67 | return Validators.compose(validator);
68 | }
69 |
70 | if ((validator as AbstractControlOptions).validators) {
71 | return this.composeValidators((validator as AbstractControlOptions).validators);
72 | }
73 |
74 | return validator as ValidatorFn;
75 | }
76 |
77 | private readonly dynamicValidator = (control: ClassValidatorFormControl): ValidationErrors => {
78 | this.formGroupClassValue[this.name] = control.value;
79 |
80 | const validationErrors = validateSync(this.formGroupClassValue)
81 | .find(error => error.property === this.name);
82 |
83 | return validationErrors ? validationErrors.constraints : undefined;
84 | };
85 | }
86 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/src/lib/class-validator-form-group-options.interface.ts:
--------------------------------------------------------------------------------
1 | export interface ClassValidatorFormGroupOptions {
2 | eagerValidation?: boolean; // default: false
3 | }
4 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/src/lib/class-validator-form-group.spec.ts:
--------------------------------------------------------------------------------
1 | import { FakeThing, FakeUser } from './testing/fake-user-testing.model';
2 | import { fakeUserFormControls } from './testing/fake-form-testing.fixture';
3 | import { ClassValidatorFormGroup } from './class-validator-form-group';
4 | import { ClassValidatorFormControl } from './class-validator-form-control';
5 |
6 | describe('The ClassValidatorFormGroup class', () => {
7 | describe('The constructor', () => {
8 | const firstNameSetNameAndClassValueSpy = jest.spyOn(fakeUserFormControls.firstName, 'setNameAndFormGroupClassValue');
9 | const idSetNameAndClassValueSpy = jest.spyOn(fakeUserFormControls.id, 'setNameAndFormGroupClassValue');
10 |
11 | beforeEach(() => {
12 | new ClassValidatorFormGroup(FakeUser, {
13 | firstName: fakeUserFormControls.firstName,
14 | id: fakeUserFormControls.id,
15 | });
16 | });
17 |
18 | it('should update class validator form controls name and class value', () => {
19 | const expectedClassValue = new FakeUser();
20 | expectedClassValue.firstName = fakeUserFormControls.firstName.value;
21 | expectedClassValue.id = fakeUserFormControls.id.value;
22 |
23 | expect(firstNameSetNameAndClassValueSpy).toBeCalledWith('firstName', expectedClassValue, undefined);
24 | expect(idSetNameAndClassValueSpy).toBeCalledWith('id', expectedClassValue, undefined);
25 | });
26 | });
27 |
28 | describe('The addControl method', () => {
29 | let fakeEmptyUserFormGroup: ClassValidatorFormGroup;
30 | let formControlSetNameAndClassValueSpy;
31 |
32 | beforeEach(() => {
33 | const fakeFormControlToAdd = new ClassValidatorFormControl('name');
34 |
35 | fakeEmptyUserFormGroup = new ClassValidatorFormGroup(FakeUser, {});
36 | formControlSetNameAndClassValueSpy = jest.spyOn(fakeFormControlToAdd, 'setNameAndFormGroupClassValue');
37 |
38 | fakeEmptyUserFormGroup.addControl('firstName', fakeFormControlToAdd);
39 | });
40 |
41 | it('should set classValidatorFormControl name and class value', () => {
42 | const expectedClassValue = new FakeUser();
43 | expectedClassValue.firstName = 'name';
44 |
45 | expect(formControlSetNameAndClassValueSpy).toBeCalledWith('firstName', expectedClassValue, undefined);
46 | });
47 | });
48 |
49 | describe('The removeControl method', () => {
50 | let partialFakeClassValidatorFormGroup;
51 | const idSetNameAndClassValueSpy = jest.spyOn(fakeUserFormControls.id, 'setNameAndFormGroupClassValue');
52 | const isSessionLockedSetNameAndClassValueSpy = jest.spyOn(fakeUserFormControls.isSessionLocked, 'setNameAndFormGroupClassValue');
53 |
54 | beforeEach(() => {
55 | partialFakeClassValidatorFormGroup = new ClassValidatorFormGroup(FakeUser, {
56 | firstName: fakeUserFormControls.firstName,
57 | id: fakeUserFormControls.id,
58 | isSessionLocked: fakeUserFormControls.isSessionLocked,
59 | });
60 |
61 | partialFakeClassValidatorFormGroup.removeControl('firstName');
62 | });
63 |
64 | it('should update ClassValidatorControls name and class value', () => {
65 | const expectedClassValue = new FakeUser();
66 | expectedClassValue.id = fakeUserFormControls.id.value;
67 | expectedClassValue.isSessionLocked = fakeUserFormControls.isSessionLocked.value;
68 |
69 | expect(idSetNameAndClassValueSpy).toBeCalledWith('id', expectedClassValue, undefined);
70 | expect(isSessionLockedSetNameAndClassValueSpy).toBeCalledWith('isSessionLocked', expectedClassValue, undefined);
71 | });
72 | });
73 |
74 | describe('The formGroup', () => {
75 | let formGroup: ClassValidatorFormGroup;
76 |
77 | describe('When FormControls are created normally', () => {
78 | beforeEach(() => {
79 | formGroup = new ClassValidatorFormGroup(FakeThing, {
80 | first: new ClassValidatorFormControl('notemail'),
81 | last: new ClassValidatorFormControl('')
82 | });
83 | })
84 |
85 | it('should not run validators immediately', () => {
86 | expect(formGroup.valid).toBe(true);
87 | expect(formGroup.controls['first'].value).toBe('notemail');
88 | });
89 | });
90 |
91 | describe('When FormControls are created with eagerValidation flag', () => {
92 | beforeEach(() => {
93 | formGroup = new ClassValidatorFormGroup(FakeThing, {
94 | first: new ClassValidatorFormControl('notemail'),
95 | last: new ClassValidatorFormControl('')
96 | }, undefined, undefined, { eagerValidation: true });
97 | })
98 |
99 | it('should run validators immediately', () => {
100 | expect(formGroup.valid).toBe(false);
101 | expect(formGroup.controls['first'].value).toBe('notemail');
102 | expect(formGroup.controls['first'].errors.isEmail).toBeDefined();
103 | });
104 | });
105 | });
106 | });
107 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/src/lib/class-validator-form-group.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AbstractControl,
3 | AbstractControlOptions,
4 | AsyncValidatorFn,
5 | FormGroup,
6 | ValidatorFn,
7 | ɵOptionalKeys
8 | } from '@angular/forms';
9 |
10 | import { ClassValidatorFormControl } from './class-validator-form-control';
11 | import { ClassType } from './types';
12 | import { ClassValidatorFormGroupOptions } from './class-validator-form-group-options.interface';
13 |
14 | export class ClassValidatorFormGroup;
16 | } = any> extends FormGroup {
17 | private classValue: any;
18 |
19 | /**
20 | * Creates a new `ClassValidatorFormGroup` instance.
21 | *
22 | * @param formClassType the `classType` containing `class-validator` decorators to be used to validate form
23 | * @param controls A collection of child controls. The key for each child is the name
24 | * under which it is registered.
25 | *
26 | * @param validatorOrOpts A synchronous validator function, or an array of
27 | * such functions, or an `AbstractControlOptions` object that contains validation functions
28 | * and a validation trigger.
29 | *
30 | * @param asyncValidator A single async validator or array of async validator functions
31 | *
32 | * @param options Options object of type `ClassValidatorFormGroupOptions` allowing
33 | * to define eagerValidation that validate controls immediately upon creation. Default is false (validators are executed starting from ngAfterViewInit hook)
34 | * See https://github.com/abarghoud/ngx-reactive-form-class-validator/issues/47
35 | *
36 | */
37 | public constructor(
38 | private readonly formClassType: ClassType,
39 | controls: TControl,
40 | validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null,
41 | asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null,
42 | private readonly options?: ClassValidatorFormGroupOptions,
43 | ) {
44 | super(controls, validatorOrOpts, asyncValidator);
45 |
46 | this.assignFormValueToClassValue();
47 | this.setClassValidatorControlsContainerGroupClassValue();
48 | }
49 |
50 | /**
51 | * Add a control to this group.
52 | *
53 | * This method also updates the value and validity of the control.
54 | *
55 | * @param name The control name to add to the collection
56 | * @param control Provides the control for the given name
57 | * @param options Specifies whether this FormGroup instance should emit events after a new
58 | * control is added.
59 | * * `emitEvent`: When true or not supplied (the default), both the `statusChanges` and
60 | * `valueChanges` observables emit events with the latest status and value when the control is
61 | * added. When false, no events are emitted.
62 | *
63 | */
64 | public addControl(
65 | name: K,
66 | control: Required[K],
67 | options?: {
68 | emitEvent?: boolean;
69 | },
70 | ): void {
71 | super.addControl(name, control, options);
72 | this.assignFormValueToClassValue();
73 | this.setClassValidatorControlsContainerGroupClassValue();
74 | }
75 |
76 | /**
77 | * Remove a control from this group.
78 | *
79 | * @param name The control name to remove from the collection
80 | * @param options Specifies whether this FormGroup instance should emit events after a
81 | * control is removed.
82 | * * `emitEvent`: When true or not supplied (the default), both the `statusChanges` and
83 | * `valueChanges` observables emit events with the latest status and value when the control is
84 | * removed. When false, no events are emitted.
85 | */
86 | public removeControl(
87 | name: ɵOptionalKeys&S, options: {emitEvent?: boolean;} = {}
88 | ): void {
89 | super.removeControl(name, options);
90 | this.assignFormValueToClassValue();
91 | this.setClassValidatorControlsContainerGroupClassValue();
92 | }
93 |
94 | private setClassValidatorControlsContainerGroupClassValue(): void {
95 | Object.entries(this.controls).forEach(([controlName, control]) => {
96 | if (control instanceof ClassValidatorFormControl) {
97 | control.setNameAndFormGroupClassValue(controlName, this.classValue, this.options?.eagerValidation);
98 | }
99 | });
100 | }
101 |
102 | private assignFormValueToClassValue(): void {
103 | this.classValue = Object.assign(new this.formClassType(), this.value);
104 | }
105 | }
106 |
107 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/src/lib/index.ts:
--------------------------------------------------------------------------------
1 | export * from './class-validator-form-array';
2 | export * from './class-validator-form-builder.service';
3 | export * from './class-validator-form-control';
4 | export * from './class-validator-form-group';
5 | export * from './class-validator-form-builder.module';
6 | export * from './types';
7 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/src/lib/testing/fake-form-testing.fixture.ts:
--------------------------------------------------------------------------------
1 | import { Validators } from '@angular/forms';
2 |
3 | import { FakeContactType, FakeContact } from './fake-user-testing.model';
4 | import { ClassValidatorFormArray } from '../class-validator-form-array';
5 | import { ClassValidatorFormGroup } from '../class-validator-form-group';
6 | import { ClassValidatorFormControl } from '../class-validator-form-control';
7 | import { ClassValidatorUntypedFormControl } from '../untyped/class-validator-untyped-form-control';
8 | import { ClassValidatorUntypedFormArray, ClassValidatorUntypedFormGroup } from '../untyped';
9 |
10 | export const fakeContactFormGroup = new ClassValidatorFormArray([
11 | new ClassValidatorFormGroup(FakeContact, {
12 | phoneNumber: new ClassValidatorFormControl(''),
13 | email: new ClassValidatorFormControl(''),
14 | type: new ClassValidatorFormControl(FakeContactType.phone),
15 | }),
16 | ],
17 | );
18 |
19 | export const fakeUserFormControls = {
20 | firstName: new ClassValidatorFormControl('anas'),
21 | id: new ClassValidatorFormControl('123456', [Validators.minLength(10)]),
22 | isSessionLocked: new ClassValidatorFormControl(true),
23 | lastActive: new ClassValidatorFormControl(''),
24 | contacts: new ClassValidatorFormArray([fakeContactFormGroup]),
25 | };
26 |
27 | export const fakeContactUntypedFormGroup = new ClassValidatorUntypedFormArray([
28 | new ClassValidatorUntypedFormGroup(FakeContact, {
29 | phoneNumber: new ClassValidatorUntypedFormControl(''),
30 | email: new ClassValidatorUntypedFormControl(''),
31 | type: new ClassValidatorUntypedFormControl(FakeContactType.phone),
32 | }),
33 | ],
34 | );
35 |
36 | export const fakeUserUntypedFormControls = {
37 | firstName: new ClassValidatorUntypedFormControl('anas'),
38 | id: new ClassValidatorUntypedFormControl('123456', [Validators.minLength(10)]),
39 | isSessionLocked: new ClassValidatorUntypedFormControl(true),
40 | lastActive: new ClassValidatorUntypedFormControl(''),
41 | contacts: new ClassValidatorUntypedFormControl([fakeContactUntypedFormGroup]),
42 | };
43 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/src/lib/testing/fake-user-testing.model.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IsBoolean,
3 | IsDate,
4 | IsEmail,
5 | IsEnum,
6 | IsMobilePhone,
7 | IsNotEmpty,
8 | IsOptional,
9 | MinLength,
10 | ValidateIf,
11 | ValidateNested
12 | } from 'class-validator';
13 |
14 | export enum FakeContactType {
15 | email = 'Email',
16 | phone = 'Phone',
17 | }
18 |
19 | export class FakeContact {
20 | @ValidateIf(contact => contact.type === FakeContactType.phone)
21 | @IsMobilePhone('fr-FR')
22 | public phoneNumber: string;
23 |
24 | @ValidateIf(contact => contact.type === FakeContactType.email)
25 | @IsEmail()
26 | public email: string;
27 |
28 | @IsEnum(FakeContactType)
29 | public type: FakeContactType;
30 | }
31 |
32 | export class FakeUser {
33 | @IsNotEmpty()
34 | public firstName: string;
35 |
36 | public id: string;
37 |
38 | @IsBoolean()
39 | public isSessionLocked: boolean;
40 |
41 | @IsOptional()
42 | @IsDate()
43 | public lastActive?: Date;
44 |
45 | @ValidateNested()
46 | public contacts: FakeContact[];
47 |
48 | @MinLength(10)
49 | public username: string;
50 | }
51 |
52 | export class FakeThing {
53 | @IsNotEmpty()
54 | @IsEmail()
55 | public first: string;
56 |
57 | @IsNotEmpty()
58 | public last: string;
59 | }
60 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/src/lib/types.ts:
--------------------------------------------------------------------------------
1 | export declare type ClassType = {
2 | new (...args: any[]): T;
3 | };
4 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/src/lib/untyped/class-validator-untyped-form-array.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AbstractControl,
3 | AbstractControlOptions,
4 | AsyncValidatorFn,
5 | UntypedFormArray,
6 | ValidatorFn
7 | } from '@angular/forms';
8 |
9 | export class ClassValidatorUntypedFormArray extends UntypedFormArray {
10 | public constructor(
11 | controls: AbstractControl[],
12 | validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null,
13 | asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null,
14 | ) {
15 | super(
16 | controls,
17 | validatorOrOpts,
18 | asyncValidator,
19 | );
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/src/lib/untyped/class-validator-untyped-form-builder.module.ts:
--------------------------------------------------------------------------------
1 | import { ModuleWithProviders, NgModule } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 |
4 | import { ClassValidatorUntypedFormBuilderService } from './class-validator-untyped-form-builder.service';
5 |
6 | @NgModule({
7 | imports: [CommonModule],
8 | providers: [ClassValidatorUntypedFormBuilderService],
9 | })
10 | export class ClassValidatorUntypedFormBuilderModule {
11 | public static forRoot(): ModuleWithProviders {
12 | return {
13 | ngModule: ClassValidatorUntypedFormBuilderModule,
14 | providers: [
15 | ClassValidatorUntypedFormBuilderService,
16 | ],
17 | };
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/src/lib/untyped/class-validator-untyped-form-builder.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import {
3 | AbstractControl,
4 | AbstractControlOptions,
5 | AsyncValidatorFn,
6 | FormArray,
7 | FormControl,
8 | FormGroup,
9 | ValidatorFn
10 | } from '@angular/forms';
11 |
12 | import { ClassValidatorUntypedFormGroup } from './class-validator-untyped-form-group';
13 | import { ClassValidatorUntypedFormControl } from './class-validator-untyped-form-control';
14 | import { ClassValidatorUntypedFormArray } from './class-validator-untyped-form-array';
15 | import { ClassType } from 'ngx-reactive-form-class-validator';
16 |
17 | // Coming from https://github.com/angular/angular/blob/3b0b7d22109c79b4dceb4ae069c3927894cf1bd6/packages/forms/src/form_builder.ts#L14
18 | const isAbstractControlOptions = (options: AbstractControlOptions | { [key: string]: any }): options is AbstractControlOptions =>
19 | (options as AbstractControlOptions).asyncValidators !== undefined ||
20 | (options as AbstractControlOptions).validators !== undefined ||
21 | (options as AbstractControlOptions).updateOn !== undefined;
22 |
23 | @Injectable()
24 | export class ClassValidatorUntypedFormBuilderService {
25 | /**
26 | * @description
27 | * Construct a new `FormGroup` instance.
28 | *
29 | * @param formClassType the `classType` containing `class-validator` decorators to be used to validate form
30 | * @param controlsConfig A collection of child controls. The key for each child is the name
31 | * under which it is registered.
32 | *
33 | * @param options Configuration options object for the `FormGroup`. The object can
34 | * have two shapes:
35 | *
36 | * 1) `AbstractControlOptions` object (preferred), which consists of:
37 | * * `validators`: A synchronous validator function, or an array of validator functions
38 | * * `asyncValidators`: A single async validator or array of async validator functions
39 | * * `updateOn`: The event upon which the control should be updated (options: 'change' | 'blur' |
40 | * submit')
41 | *
42 | * 2) Legacy configuration object, which consists of:
43 | * * `validator`: A synchronous validator function, or an array of validator functions
44 | * * `asyncValidator`: A single async validator or array of async validator functions
45 | *
46 | */
47 | public group(
48 | formClassType: ClassType,
49 | controlsConfig: { [p: string]: any },
50 | options?: AbstractControlOptions | { [p: string]: any } | null
51 | ): ClassValidatorUntypedFormGroup {
52 | // Coming from https://github.com/angular/angular/blob/3b0b7d22109c79b4dceb4ae069c3927894cf1bd6/packages/forms/src/form_builder.ts#L59
53 | const controls = this.reduceControls(controlsConfig);
54 |
55 | let validators: ValidatorFn | ValidatorFn[] | null = null;
56 | let asyncValidators: AsyncValidatorFn | AsyncValidatorFn[] | null = null;
57 | let updateOn;
58 |
59 | if (options) {
60 | if (isAbstractControlOptions(options)) {
61 | // `options` are `AbstractControlOptions`
62 | validators = options.validators ? options.validators : null;
63 | asyncValidators = options.asyncValidators ? options.asyncValidators : null;
64 | updateOn = options.updateOn ? options.updateOn : undefined;
65 | } else {
66 | // `options` are legacy form group options
67 | validators = options['validator'] !== null ? options['validator'] : null;
68 | asyncValidators = options['asyncValidator'] !== null ? options['asyncValidator'] : null;
69 | }
70 | }
71 |
72 | return new ClassValidatorUntypedFormGroup(formClassType, controls, { asyncValidators, updateOn, validators });
73 | }
74 |
75 | /**
76 | * Constructs a new `FormArray` from the given array of configurations,
77 | * validators and options.
78 | *
79 | * @param controlsConfig An array of child controls or control configs. Each
80 | * child control is given an index when it is registered.
81 | *
82 | * @param validatorOrOpts A synchronous validator function, or an array of
83 | * such functions, or an `AbstractControlOptions` object that contains
84 | * validation functions and a validation trigger.
85 | *
86 | * @param asyncValidator A single async validator or array of async validator
87 | * functions.
88 | */
89 | public array(
90 | controlsConfig: any[],
91 | validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null,
92 | asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null
93 | ): FormArray {
94 | const controls = controlsConfig.map(control => this.createControl(control));
95 |
96 | return new ClassValidatorUntypedFormArray(controls, validatorOrOpts, asyncValidator);
97 | }
98 |
99 |
100 | /**
101 | * @description
102 | * Construct a new `FormControl` with the given state, validators and options.
103 | *
104 | * @param formState Initializes the control with an initial state value, or
105 | * with an object that contains both a value and a disabled status.
106 | *
107 | * @param validatorOrOpts A synchronous validator function, or an array of
108 | * such functions, or an `AbstractControlOptions` object that contains
109 | * validation functions and a validation trigger.
110 | *
111 | * @param asyncValidator A single async validator or array of async validator
112 | * functions.
113 | *
114 | * @usageNotes
115 | *
116 | * ### Initialize a control as disabled
117 | *
118 | * The following example returns a control with an initial value in a disabled state.
119 | *
120 | *
121 | *
122 | */
123 | public control(
124 | formState: any,
125 | validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null,
126 | asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null
127 | ): ClassValidatorUntypedFormControl {
128 | return new ClassValidatorUntypedFormControl(formState, validatorOrOpts, asyncValidator);
129 | }
130 |
131 | // Coming from https://github.com/angular/angular/blob/3b0b7d22109c79b4dceb4ae069c3927894cf1bd6/packages/forms/src/form_builder.ts#L133
132 | private reduceControls(controlsConfig: { [k: string]: any }): { [key: string]: AbstractControl } {
133 | const controls: { [key: string]: AbstractControl } = {};
134 |
135 | Object.keys(controlsConfig).forEach(controlName => {
136 | controls[controlName] = this.createControl(controlsConfig[controlName]);
137 | });
138 |
139 | return controls;
140 | }
141 |
142 | private createControl(controlConfig: any): AbstractControl {
143 | if (
144 | controlConfig instanceof FormControl
145 | || controlConfig instanceof FormGroup
146 | || controlConfig instanceof FormArray
147 | ) {
148 | return controlConfig;
149 | } else if (Array.isArray(controlConfig)) {
150 | const value = controlConfig[0];
151 | const validator: ValidatorFn = controlConfig.length > 1 ? controlConfig[1] : null;
152 | const asyncValidator: AsyncValidatorFn = controlConfig.length > 2 ? controlConfig[2] : null;
153 |
154 | return this.control(value, validator, asyncValidator);
155 | } else {
156 | return this.control(controlConfig);
157 | }
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/src/lib/untyped/class-validator-untyped-form-control.spec.ts:
--------------------------------------------------------------------------------
1 | import { ValidationErrors, Validators } from '@angular/forms';
2 |
3 | import { ClassValidatorUntypedFormControl } from './class-validator-untyped-form-control';
4 | import { FakeContact, FakeContactType } from '../testing/fake-user-testing.model';
5 |
6 | describe('The ClassValidatorFormControl class', () => {
7 | let phoneNumberClassValidatorFormControl: ClassValidatorUntypedFormControl;
8 |
9 | beforeEach(() => {
10 | phoneNumberClassValidatorFormControl = new ClassValidatorUntypedFormControl('');
11 | });
12 |
13 | describe('When control name and classValue are provided', () => {
14 | const contact = new FakeContact();
15 | contact.phoneNumber = '';
16 | contact.email = '';
17 | contact.type = FakeContactType.phone;
18 |
19 | beforeEach(() => {
20 | phoneNumberClassValidatorFormControl.setNameAndFormGroupClassValue('phoneNumber', contact);
21 | });
22 |
23 | describe('When dynamic class-validator', () => {
24 | const validPhoneNumber = '0634555555';
25 | const invalidPhoneNumber = '0634555555545';
26 |
27 | describe('When invalid value provided', () => {
28 | it('should has error', () => {
29 | phoneNumberClassValidatorFormControl.setValue('0634555555545');
30 |
31 | expect(phoneNumberClassValidatorFormControl.errors).toEqual({isMobilePhone: 'phoneNumber must be a phone number'});
32 | });
33 | });
34 |
35 | describe('When valid value provided', () => {
36 | it('should not have any error', () => {
37 | phoneNumberClassValidatorFormControl.setValue(validPhoneNumber);
38 |
39 | expect(phoneNumberClassValidatorFormControl.errors).toBeFalsy();
40 | });
41 | });
42 |
43 | describe('When conditional validation', () => {
44 | it('should not have any error', () => {
45 | contact.type = FakeContactType.email;
46 |
47 | phoneNumberClassValidatorFormControl.setValue(invalidPhoneNumber);
48 | expect(phoneNumberClassValidatorFormControl.errors).toBeFalsy();
49 | });
50 | });
51 | });
52 |
53 | describe('When combining dynamic class-validator and manual validation', () => {
54 | const emailClassValidatorFormControl = new ClassValidatorUntypedFormControl();
55 |
56 | beforeEach(() => {
57 | const contactForEmailValidation = Object.assign(contact, {type: FakeContactType.email});
58 |
59 | emailClassValidatorFormControl.setNameAndFormGroupClassValue('email', contactForEmailValidation);
60 | emailClassValidatorFormControl.setValidatorsWithDynamicValidation(Validators.minLength(8));
61 | });
62 |
63 | it('should consider manual validation as well as dynamic validator', () => {
64 | emailClassValidatorFormControl.setValue('123456');
65 |
66 | expect(emailClassValidatorFormControl.errors).toEqual({
67 | isEmail: 'email must be an email',
68 | minlength: {
69 | actualLength: 6,
70 | requiredLength: 8,
71 | },
72 | });
73 | });
74 |
75 | describe('When array of validators', () => {
76 | beforeEach(() => {
77 | emailClassValidatorFormControl.setValidatorsWithDynamicValidation([
78 | Validators.maxLength(8),
79 | (): ValidationErrors => ({isSecondValidatorError: true}),
80 | ],
81 | );
82 | });
83 |
84 | it('should use validators provided as well as dynamic validator', () => {
85 | emailClassValidatorFormControl.setValue('4546546546565');
86 |
87 | expect(emailClassValidatorFormControl.errors).toEqual({
88 | isEmail: 'email must be an email',
89 | maxlength: {
90 | actualLength: 13,
91 | requiredLength: 8,
92 | },
93 | isSecondValidatorError: true,
94 | });
95 | });
96 | });
97 |
98 | describe('When AbstractControlOptions validators', () => {
99 | beforeEach(() => {
100 | emailClassValidatorFormControl.setValidatorsWithDynamicValidation({
101 | validators: [
102 | Validators.maxLength(8),
103 | (): ValidationErrors => ({isSecondValidatorError: true}),
104 | ]
105 | });
106 | });
107 |
108 | it('should use validators provided as well as dynamic validator', () => {
109 | emailClassValidatorFormControl.setValue('4546546546565');
110 |
111 | expect(emailClassValidatorFormControl.errors).toEqual({
112 | isEmail: 'email must be an email',
113 | maxlength: {
114 | actualLength: 13,
115 | requiredLength: 8,
116 | },
117 | isSecondValidatorError: true,
118 | });
119 | });
120 | });
121 |
122 | describe('When `null` provided', () => {
123 | beforeEach(() => {
124 | emailClassValidatorFormControl.setValidatorsWithDynamicValidation(undefined);
125 | });
126 |
127 | it('should use only dynamic validator', () => {
128 | emailClassValidatorFormControl.setValue('notEmail');
129 |
130 | expect(emailClassValidatorFormControl.errors).toEqual({ isEmail: 'email must be an email' });
131 | });
132 | });
133 | });
134 | });
135 | });
136 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/src/lib/untyped/class-validator-untyped-form-control.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AbstractControlOptions,
3 | AsyncValidatorFn,
4 | UntypedFormControl,
5 | ValidationErrors,
6 | ValidatorFn,
7 | Validators
8 | } from '@angular/forms';
9 | import { validateSync } from 'class-validator';
10 |
11 | export class ClassValidatorUntypedFormControl extends UntypedFormControl {
12 | private formGroupClassValue: any;
13 | private name: string;
14 |
15 | /**
16 | * Creates a new `ClassValidatorFormControl` instance.
17 | *
18 | * @param formState Initializes the control with an initial value,
19 | * or an object that defines the initial value and disabled state.
20 | *
21 | * @param validatorOrOpts A synchronous validator function, or an array of
22 | * such functions, or an `AbstractControlOptions` object that contains validation functions
23 | * and a validation trigger.
24 | *
25 | * @param asyncValidator A single async validator or array of async validator functions
26 | *
27 | */
28 | public constructor(
29 | formState?: any,
30 | validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null,
31 | asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null
32 | ) {
33 | super(formState, validatorOrOpts, asyncValidator);
34 |
35 | this.setValidatorsWithDynamicValidation(validatorOrOpts);
36 | }
37 |
38 | /**
39 | * @internal
40 | */
41 | public setNameAndFormGroupClassValue(name: string, value: any, eagerValidation: boolean = false): void {
42 | this.name = name;
43 | this.formGroupClassValue = value;
44 |
45 | if (eagerValidation) {
46 | this.updateValueAndValidity();
47 | }
48 | }
49 |
50 | /**
51 | * Sets the synchronous validators that are active on this control as well as resetting the dynamic `class-validator`. Calling
52 | * this overwrites any existing sync validators.
53 | *
54 | * When you add or remove a validator at run time, you must call
55 | * `updateValueAndValidity()` for the new validation to take effect.
56 | *
57 | */
58 | public setValidatorsWithDynamicValidation(newValidator: ValidatorFn | ValidatorFn[] | AbstractControlOptions | undefined): void {
59 | this.setValidators(
60 | newValidator
61 | ? [this.composeValidators(newValidator), this.dynamicValidator]
62 | : this.dynamicValidator);
63 | }
64 |
65 | private composeValidators(validator: ValidatorFn | ValidatorFn[] | AbstractControlOptions): ValidatorFn {
66 | if (validator instanceof Array) {
67 | return Validators.compose(validator);
68 | }
69 |
70 | if ((validator as AbstractControlOptions).validators) {
71 | return this.composeValidators((validator as AbstractControlOptions).validators);
72 | }
73 |
74 | return validator as ValidatorFn;
75 | }
76 |
77 | private readonly dynamicValidator = (control: ClassValidatorUntypedFormControl): ValidationErrors => {
78 | this.formGroupClassValue[this.name] = control.value;
79 |
80 | const validationErrors = validateSync(this.formGroupClassValue)
81 | .find(error => error.property === this.name);
82 |
83 | return validationErrors ? validationErrors.constraints : undefined;
84 | };
85 | }
86 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/src/lib/untyped/class-validator-untyped-form-group.spec.ts:
--------------------------------------------------------------------------------
1 | import { ClassValidatorUntypedFormGroup } from './class-validator-untyped-form-group';
2 | import { ClassValidatorUntypedFormControl } from './class-validator-untyped-form-control';
3 | import { fakeUserUntypedFormControls } from '../testing/fake-form-testing.fixture';
4 | import { FakeThing, FakeUser } from '../testing/fake-user-testing.model';
5 |
6 | describe('The ClassValidatorUntypedFormGroup class', () => {
7 | describe('The constructor', () => {
8 | const firstNameSetNameAndClassValueSpy = jest.spyOn(fakeUserUntypedFormControls.firstName, 'setNameAndFormGroupClassValue');
9 | const idSetNameAndClassValueSpy = jest.spyOn(fakeUserUntypedFormControls.id, 'setNameAndFormGroupClassValue');
10 |
11 | beforeEach(() => {
12 | new ClassValidatorUntypedFormGroup(FakeUser, {
13 | firstName: fakeUserUntypedFormControls.firstName,
14 | id: fakeUserUntypedFormControls.id,
15 | });
16 | });
17 |
18 | it('should update class validator untyped form controls name and class value', () => {
19 | const expectedClassValue = new FakeUser();
20 | expectedClassValue.firstName = fakeUserUntypedFormControls.firstName.value;
21 | expectedClassValue.id = fakeUserUntypedFormControls.id.value;
22 |
23 | expect(firstNameSetNameAndClassValueSpy).toBeCalledWith('firstName', expectedClassValue, undefined);
24 | expect(idSetNameAndClassValueSpy).toBeCalledWith('id', expectedClassValue, undefined);
25 | });
26 | });
27 |
28 | describe('The addControl method', () => {
29 | let fakeEmptyUserFormGroup: ClassValidatorUntypedFormGroup;
30 | let formControlSetNameAndClassValueSpy;
31 |
32 | beforeEach(() => {
33 | const fakeFormControlToAdd = new ClassValidatorUntypedFormControl('name');
34 |
35 | fakeEmptyUserFormGroup = new ClassValidatorUntypedFormGroup(FakeUser, {});
36 | formControlSetNameAndClassValueSpy = jest.spyOn(fakeFormControlToAdd, 'setNameAndFormGroupClassValue');
37 |
38 | fakeEmptyUserFormGroup.addControl('firstName', fakeFormControlToAdd);
39 | });
40 |
41 | it('should set classValidatorFormControl name and class value', () => {
42 | const expectedClassValue = new FakeUser();
43 | expectedClassValue.firstName = 'name';
44 |
45 | expect(formControlSetNameAndClassValueSpy).toBeCalledWith('firstName', expectedClassValue, undefined);
46 | });
47 | });
48 |
49 | describe('The removeControl method', () => {
50 | let partialFakeClassValidatorFormGroup;
51 | const idSetNameAndClassValueSpy = jest.spyOn(fakeUserUntypedFormControls.id, 'setNameAndFormGroupClassValue');
52 | const isSessionLockedSetNameAndClassValueSpy = jest.spyOn(fakeUserUntypedFormControls.isSessionLocked, 'setNameAndFormGroupClassValue');
53 |
54 | beforeEach(() => {
55 | partialFakeClassValidatorFormGroup = new ClassValidatorUntypedFormGroup(FakeUser, {
56 | firstName: fakeUserUntypedFormControls.firstName,
57 | id: fakeUserUntypedFormControls.id,
58 | isSessionLocked: fakeUserUntypedFormControls.isSessionLocked,
59 | });
60 |
61 | partialFakeClassValidatorFormGroup.removeControl('firstName');
62 | });
63 |
64 | it('should update ClassValidatorControls name and class value', () => {
65 | const expectedClassValue = new FakeUser();
66 | expectedClassValue.id = fakeUserUntypedFormControls.id.value;
67 | expectedClassValue.isSessionLocked = fakeUserUntypedFormControls.isSessionLocked.value;
68 |
69 | expect(idSetNameAndClassValueSpy).toHaveBeenCalledWith('id', expectedClassValue, undefined);
70 | expect(isSessionLockedSetNameAndClassValueSpy).toHaveBeenCalledWith('isSessionLocked', expectedClassValue, undefined);
71 | });
72 | });
73 |
74 | describe('The formGroup', () => {
75 | let formGroup: ClassValidatorUntypedFormGroup;
76 |
77 | describe('When FormControls are created normally', () => {
78 | beforeEach(() => {
79 | formGroup = new ClassValidatorUntypedFormGroup(FakeThing, {
80 | first: new ClassValidatorUntypedFormControl('notemail'),
81 | last: new ClassValidatorUntypedFormControl('')
82 | });
83 | })
84 |
85 | it('should not run validators immediately', () => {
86 | expect(formGroup.valid).toBe(true);
87 | expect(formGroup.controls['first'].value).toBe('notemail');
88 | });
89 | });
90 |
91 | describe('When FormControls are created with eagerValidation flag', () => {
92 | beforeEach(() => {
93 | formGroup = new ClassValidatorUntypedFormGroup(FakeThing, {
94 | first: new ClassValidatorUntypedFormControl('notemail'),
95 | last: new ClassValidatorUntypedFormControl('')
96 | }, undefined, undefined, { eagerValidation: true });
97 | })
98 |
99 | it('should run validators immediately', () => {
100 | expect(formGroup.valid).toBe(false);
101 | expect(formGroup.controls['first'].value).toBe('notemail');
102 | expect(formGroup.controls['first'].errors.isEmail).toBeDefined();
103 | });
104 | });
105 | });
106 | });
107 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/src/lib/untyped/class-validator-untyped-form-group.ts:
--------------------------------------------------------------------------------
1 | import { AbstractControl, AbstractControlOptions, AsyncValidatorFn, UntypedFormGroup, ValidatorFn } from '@angular/forms';
2 |
3 | import { ClassValidatorUntypedFormControl } from './class-validator-untyped-form-control';
4 | import { ClassType } from '../types';
5 | import { ClassValidatorFormGroupOptions } from '../class-validator-form-group-options.interface';
6 |
7 | export class ClassValidatorUntypedFormGroup extends UntypedFormGroup {
8 | private classValue: any;
9 |
10 | /**
11 | * Creates a new `ClassValidatorFormGroup` instance.
12 | *
13 | * @param formClassType the `classType` containing `class-validator` decorators to be used to validate form
14 | * @param controls A collection of child controls. The key for each child is the name
15 | * under which it is registered.
16 | *
17 | * @param validatorOrOpts A synchronous validator function, or an array of
18 | * such functions, or an `AbstractControlOptions` object that contains validation functions
19 | * and a validation trigger.
20 | *
21 | * @param asyncValidator A single async validator or array of async validator functions
22 | *
23 | * @param options Options object of type `ClassValidatorFormGroupOptions` allowing
24 | * to define eagerValidation that validate controls immediately upon creation. Default is false (validators are executed starting from ngAfterViewInit hook)
25 | * See https://github.com/abarghoud/ngx-reactive-form-class-validator/issues/47
26 | *
27 | */
28 | public constructor(
29 | private readonly formClassType: ClassType,
30 | controls: {
31 | [key: string]: AbstractControl;
32 | },
33 | validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null,
34 | asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null,
35 | private readonly options?: ClassValidatorFormGroupOptions,
36 | ) {
37 | super(controls, validatorOrOpts, asyncValidator);
38 |
39 | this.assignFormValueToClassValue();
40 | this.setClassValidatorControlsContainerGroupClassValue();
41 | }
42 |
43 | /**
44 | * Add a control to this group.
45 | *
46 | * This method also updates the value and validity of the control.
47 | *
48 | * @param name The control name to add to the collection
49 | * @param control Provides the control for the given name
50 | * @param options Specifies whether this FormGroup instance should emit events after a new
51 | * control is added.
52 | * * `emitEvent`: When true or not supplied (the default), both the `statusChanges` and
53 | * `valueChanges` observables emit events with the latest status and value when the control is
54 | * added. When false, no events are emitted.
55 | *
56 | */
57 | public addControl(
58 | name: string,
59 | control: AbstractControl,
60 | options?: {
61 | emitEvent?: boolean;
62 | },
63 | ): void {
64 | super.addControl(name, control, options);
65 | this.assignFormValueToClassValue();
66 | this.setClassValidatorControlsContainerGroupClassValue();
67 | }
68 |
69 | /**
70 | * Remove a control from this group.
71 | *
72 | * @param name The control name to remove from the collection
73 | * @param options Specifies whether this FormGroup instance should emit events after a
74 | * control is removed.
75 | * * `emitEvent`: When true or not supplied (the default), both the `statusChanges` and
76 | * `valueChanges` observables emit events with the latest status and value when the control is
77 | * removed. When false, no events are emitted.
78 | */
79 | public removeControl(
80 | name: string,
81 | options?: {
82 | emitEvent?: boolean;
83 | },
84 | ): void {
85 | super.removeControl(name, options);
86 | this.assignFormValueToClassValue();
87 | this.setClassValidatorControlsContainerGroupClassValue();
88 | }
89 |
90 | private setClassValidatorControlsContainerGroupClassValue(): void {
91 | Object.entries(this.controls).forEach(([controlName, control]) => {
92 | if (control instanceof ClassValidatorUntypedFormControl) {
93 | (this.controls[controlName] as ClassValidatorUntypedFormControl)
94 | .setNameAndFormGroupClassValue(controlName, this.classValue, this.options?.eagerValidation);
95 | }
96 | });
97 | }
98 |
99 | private assignFormValueToClassValue(): void {
100 | this.classValue = Object.assign(new this.formClassType(), this.value);
101 | }
102 | }
103 |
104 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/src/lib/untyped/index.ts:
--------------------------------------------------------------------------------
1 | export * from './class-validator-untyped-form-array';
2 | export * from './class-validator-untyped-form-builder.service';
3 | export * from './class-validator-untyped-form-control';
4 | export * from './class-validator-untyped-form-group';
5 | export * from './class-validator-untyped-form-builder.module';
6 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/src/test-setup.ts:
--------------------------------------------------------------------------------
1 | import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
2 |
3 | setupZoneTestEnv();
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "files": [],
4 | "include": [],
5 | "references": [
6 | {
7 | "path": "./tsconfig.lib.json"
8 | },
9 | {
10 | "path": "./tsconfig.lib.prod.json"
11 | },
12 | {
13 | "path": "./tsconfig.spec.json"
14 | }
15 | ],
16 | "compilerOptions": {
17 | "target": "es2020"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "target": "ES2022",
6 | "declaration": true,
7 | "declarationMap": true,
8 | "inlineSources": true,
9 | "types": [],
10 | "lib": ["dom", "es2018"],
11 | "useDefineForClassFields": false
12 | },
13 | "angularCompilerOptions": {
14 | "compilationMode": "partial",
15 | "skipTemplateCodegen": true,
16 | "strictMetadataEmit": true,
17 | "enableResourceInlining": true
18 | },
19 | "exclude": [
20 | "src/test-setup.ts",
21 | "**/*.spec.ts",
22 | "**/*.test.ts",
23 | "jest.config.ts"
24 | ],
25 | "include": ["**/*.ts"]
26 | }
27 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/tsconfig.lib.prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.lib.json",
3 | "compilerOptions": {
4 | "declarationMap": false,
5 | "target": "ES2022",
6 | "useDefineForClassFields": false
7 | },
8 | "angularCompilerOptions": {
9 | "compilationMode": "partial"
10 | },
11 | "exclude": ["jest.config.ts"]
12 | }
13 |
--------------------------------------------------------------------------------
/libs/ngx-reactive-form-class-validator/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": ["jest", "node"]
7 | },
8 | "files": ["src/test-setup.ts"],
9 | "include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts", "jest.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/nx.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultProject": "ngx-reactive-form-class-validator",
3 | "generators": {
4 | "@nx/angular:application": {
5 | "e2eTestRunner": "cypress",
6 | "linter": "none",
7 | "style": "css",
8 | "unitTestRunner": "jest",
9 | "strict": false
10 | },
11 | "@nx/angular:library": {
12 | "unitTestRunner": "jest"
13 | },
14 | "@nx/angular": {
15 | "application": {
16 | "linter": "eslint"
17 | },
18 | "library": {
19 | "linter": "eslint"
20 | },
21 | "storybook-configuration": {
22 | "linter": "eslint"
23 | }
24 | }
25 | },
26 | "$schema": "./node_modules/nx/schemas/nx-schema.json",
27 | "targetDefaults": {
28 | "build": {
29 | "dependsOn": ["^build"],
30 | "inputs": ["production", "^production"],
31 | "cache": true
32 | },
33 | "lint": {
34 | "cache": true
35 | },
36 | "@nx/jest:jest": {
37 | "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"],
38 | "cache": true,
39 | "options": {
40 | "passWithNoTests": true
41 | },
42 | "configurations": {
43 | "ci": {
44 | "ci": true,
45 | "codeCoverage": true
46 | }
47 | }
48 | },
49 | "@angular-devkit/build-angular:application": {
50 | "cache": true,
51 | "dependsOn": ["^build"],
52 | "inputs": ["production", "^production"]
53 | }
54 | },
55 | "namedInputs": {
56 | "default": ["{projectRoot}/**/*", "sharedGlobals"],
57 | "sharedGlobals": [
58 | "{workspaceRoot}/angular.json",
59 | "{workspaceRoot}/tsconfig.base.json",
60 | "{workspaceRoot}/tslint.json",
61 | "{workspaceRoot}/nx.json"
62 | ],
63 | "production": [
64 | "default",
65 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
66 | "!{projectRoot}/tsconfig.spec.json",
67 | "!{projectRoot}/jest.config.[jt]s",
68 | "!{projectRoot}/src/test-setup.[jt]s"
69 | ]
70 | },
71 | "parallel": 1,
72 | "defaultBase": "master"
73 | }
74 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ngx-reactive-form-class-validator",
3 | "description": "A lightweight library for dynamically validate Angular reactive forms using class-validator library.",
4 | "version": "0.1.0",
5 | "license": "MIT",
6 | "keywords": [
7 | "ng",
8 | "angular",
9 | "library",
10 | "ngx",
11 | "typescript",
12 | "reactive-forms",
13 | "class-validator",
14 | "dynamic-validator",
15 | "validation",
16 | "validator"
17 | ],
18 | "repository": {
19 | "type": "git",
20 | "url": "git+https://github.com/abarghoud/ngx-reactive-form-class-validator.git"
21 | },
22 | "bugs": {
23 | "url": "https://github.com/abarghoud/ngx-reactive-form-class-validator/issues"
24 | },
25 | "scripts": {
26 | "ng": "nx",
27 | "nx": "nx",
28 | "start": "ng serve",
29 | "build": "ng build",
30 | "test": "ng test ngx-reactive-form-class-validator",
31 | "lint": "nx workspace-lint && ng lint",
32 | "e2e": "ng e2e",
33 | "affected:apps": "nx affected:apps",
34 | "affected:libs": "nx affected:libs",
35 | "affected:build": "nx affected:build",
36 | "affected:e2e": "nx affected:e2e",
37 | "affected:test": "nx affected:test",
38 | "affected:lint": "nx affected:lint",
39 | "affected:dep-graph": "nx affected:dep-graph",
40 | "affected": "nx affected",
41 | "format": "nx format:write",
42 | "format:write": "nx format:write",
43 | "format:check": "nx format:check",
44 | "update": "ng update @nx/workspace",
45 | "workspace-schematic": "nx workspace-schematic",
46 | "dep-graph": "nx dep-graph",
47 | "help": "nx help",
48 | "test:lib:coverage": "nx test ngx-reactive-form-class-validator --coverage",
49 | "build:lib": "nx build ngx-reactive-form-class-validator",
50 | "build:lib-pack": "npm run build:lib && npm run copy-files && cd dist/libs/ngx-reactive-form-class-validator && npm pack",
51 | "copy-readme": "cp ./README.md ./dist/libs/ngx-reactive-form-class-validator",
52 | "copy-license": "cp ./LICENSE.txt ./dist/libs/ngx-reactive-form-class-validator",
53 | "copy-changelog": "cp ./Changelog.md ./dist/libs/ngx-reactive-form-class-validator",
54 | "copy-files": "run-s copy-readme copy-license",
55 | "bump-version:major": "cd libs/ngx-reactive-form-class-validator && npm version major --force",
56 | "bump-version:minor": "cd libs/ngx-reactive-form-class-validator && npm version minor --force",
57 | "bump-version:patch": "cd libs/ngx-reactive-form-class-validator && npm version patch --force",
58 | "pre-publish:minor": "run-s bump-version:minor build:lib-pack",
59 | "pre-publish:major": "run-s bump-version:major build:lib-pack",
60 | "pre-publish:patch": "run-s bump-version:patch build:lib-pack"
61 | },
62 | "private": true,
63 | "dependencies": {
64 | "@angular/animations": "20.0.2",
65 | "@angular/common": "20.0.2",
66 | "@angular/compiler": "20.0.2",
67 | "@angular/core": "20.0.2",
68 | "@angular/forms": "20.0.2",
69 | "@angular/platform-browser": "20.0.2",
70 | "@angular/platform-browser-dynamic": "20.0.2",
71 | "@angular/router": "20.0.2",
72 | "@nx/angular": "21.1.3",
73 | "class-validator": "^0.14.0",
74 | "cerialize": "^0.1.18",
75 | "rxjs": "7.8.1",
76 | "tslib": "^2.8.1",
77 | "zone.js": "0.15.0"
78 | },
79 | "devDependencies": {
80 | "@angular-devkit/core": "20.0.1",
81 | "@angular-devkit/schematics": "20.0.1",
82 | "@angular/build": "^20.0.1",
83 | "@angular/cli": "20.0.1",
84 | "@angular/compiler-cli": "20.0.2",
85 | "@angular/language-service": "20.0.2",
86 | "@nx/cypress": "21.1.3",
87 | "@nx/eslint-plugin": "21.1.3",
88 | "@nx/jest": "21.1.3",
89 | "@nx/js": "21.1.3",
90 | "@nx/workspace": "21.1.3",
91 | "@schematics/angular": "19.2.9",
92 | "@swc-node/register": "1.9.2",
93 | "@swc/core": "1.5.7",
94 | "@swc/helpers": "0.5.15",
95 | "@types/jest": "29.5.14",
96 | "@types/node": "18.16.9",
97 | "@typescript-eslint/eslint-plugin": "4.19.0",
98 | "@typescript-eslint/parser": "4.19.0",
99 | "cypress": "^4.1.0",
100 | "dotenv": "10.0.0",
101 | "eslint": "7.22.0",
102 | "eslint-config-prettier": "8.1.0",
103 | "eslint-plugin-cypress": "2.15.1",
104 | "jest": "29.7.0",
105 | "jest-environment-jsdom": "29.7.0",
106 | "jest-preset-angular": "14.4.2",
107 | "ng-packagr": "19.2.2",
108 | "npm-run-all": "^4.1.5",
109 | "nx": "21.1.3",
110 | "postcss": "^8.5.1",
111 | "postcss-import": "16.1.0",
112 | "postcss-preset-env": "10.1.3",
113 | "postcss-url": "10.1.3",
114 | "prettier": "3.4.2",
115 | "ts-jest": "29.2.5",
116 | "ts-node": "10.9.2",
117 | "typescript": "5.8.3"
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/tools/schematics/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abarghoud/ngx-reactive-form-class-validator/d2c5e113e430f8f57a5623bcf2f7d865920c5776/tools/schematics/.gitkeep
--------------------------------------------------------------------------------
/tools/tsconfig.tools.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "../dist/out-tsc/tools",
5 | "rootDir": ".",
6 | "module": "commonjs",
7 | "target": "es5",
8 | "types": ["node"]
9 | },
10 | "include": ["**/*.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "rootDir": ".",
5 | "sourceMap": true,
6 | "declaration": false,
7 | "moduleResolution": "node",
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "importHelpers": true,
11 | "target": "es2015",
12 | "module": "esnext",
13 | "typeRoots": ["node_modules/@types"],
14 | "lib": ["es2017", "dom"],
15 | "skipLibCheck": true,
16 | "skipDefaultLibCheck": true,
17 | "baseUrl": ".",
18 | "paths": {
19 | "ngx-reactive-form-class-validator": [
20 | "libs/ngx-reactive-form-class-validator/src/index.ts"
21 | ]
22 | }
23 | },
24 | "exclude": ["node_modules", "tmp"]
25 | }
26 |
--------------------------------------------------------------------------------