├── .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 | npm version 8 |   9 | 10 | npm minified size 11 |   12 | 13 | Code coverage 14 |   15 | 16 | Build status 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 |
4 |
5 | 6 | 7 | {{ profileForm.get("firstName").errors | json }} 8 |
9 |
10 | 11 | 12 | {{ profileForm.get("lastName").errors | json }} 13 |
14 |
15 | 16 | 17 | {{ profileForm.get("email").errors | json }} 18 |
19 | 20 |
21 |

Address

22 |
23 | 24 | 25 | {{ addressForm.get("street").errors | json }} 26 |
27 |
28 | 29 | 30 | {{ addressForm.get("city").errors | json }} 31 |
32 |
33 | 34 | 35 | {{ addressForm.get("state").errors | json }} 36 |
37 |
38 | 39 | 40 | {{ addressForm.get("zip").errors | json }} 41 |
42 |
43 |
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 | --------------------------------------------------------------------------------