├── .editorconfig ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── angular.json ├── package-lock.json ├── package.json ├── projects ├── angular-reactive-validation │ ├── README.md │ ├── ng-package.json │ ├── package.json │ ├── src │ │ ├── lib │ │ │ ├── form │ │ │ │ ├── form.directive.spec.ts │ │ │ │ └── form.directive.ts │ │ │ ├── get-control-path.spec.ts │ │ │ ├── get-control-path.ts │ │ │ ├── get-form-control-from-container.spec.ts │ │ │ ├── get-form-control-from-container.ts │ │ │ ├── reactive-validation-module-configuration-token.ts │ │ │ ├── reactive-validation-module-configuration.ts │ │ │ ├── reactive-validation.module.spec.ts │ │ │ ├── reactive-validation.module.ts │ │ │ ├── validation-error.spec.ts │ │ │ ├── validation-error.ts │ │ │ ├── validation-message │ │ │ │ ├── validation-message.component.html │ │ │ │ ├── validation-message.component.spec.ts │ │ │ │ └── validation-message.component.ts │ │ │ ├── validation-messages │ │ │ │ ├── validation-messages.component.html │ │ │ │ ├── validation-messages.component.spec.ts │ │ │ │ └── validation-messages.component.ts │ │ │ ├── validator-declaration.spec.ts │ │ │ ├── validator-declaration.ts │ │ │ ├── validators.spec.ts │ │ │ └── validators.ts │ │ └── public-api.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ └── tsconfig.spec.json └── test-app │ ├── src │ ├── app │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ └── app.module.ts │ ├── assets │ │ └── .gitkeep │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ └── styles.css │ ├── tsconfig.app.json │ └── tsconfig.spec.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://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 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [16.x, 18.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm run build angular-reactive-validation 31 | - run: npm run build test-app 32 | - run: npm run test:ci 33 | -------------------------------------------------------------------------------- /.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 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "pwa-chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 11.0.0 4 | 5 | * add support for Angular 17 6 | 7 | ## 10.0.0 8 | 9 | * add support for Angular 16 10 | 11 | ## 9.0.0 12 | 13 | * add support for Angular 15 14 | 15 | ## 8.0.0 16 | 17 | * add support for Angular 14 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 David Walschots 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Reactive Validation 2 | 3 | Reactive Forms validation shouldn't require the developer to write lots of HTML to show validation messages. This library makes it easy. 4 | 5 | ## Table of contents 6 | 7 | * [Installation](#installation) 8 | * [Compatibility](#compatibility) 9 | * [Basic usage](#basic-usage) 10 | * [Advanced validation declaration](#advanced-validation-declaration) 11 | * [Changing when validation messages are displayed](#changing-when-validation-messages-are-displayed) 12 | * [Declaring your own validator functions](#declaring-your-own-validator-functions) 13 | * [Edge use cases](#edge-use-cases) 14 | * [Handling custom HTML validation messages](#handling-custom-html-validation-messages) 15 | * [Using arv-validation-messages when not using `[formGroup]` or `formGroupName` attributes](#using-arv-validation-messages-when-not-using-formgroup-or-formgroupname-attributes) 16 | 17 | ## Installation 18 | 19 | To install this library, run: 20 | 21 | ```bash 22 | npm install angular-reactive-validation --save 23 | ``` 24 | 25 | ## Compatibility 26 | | Angular version | Package version | 27 | |-----------------|-----------------| 28 | | 18 | 12.x | 29 | | 17 | 11.x | 30 | | 16 | 10.x | 31 | | 15 | 9.x | 32 | | 14 | 8.x | 33 | 34 | 35 | ## Basic usage 36 | Import the `ReactiveValidationModule`: 37 | 38 | ```ts 39 | import { ReactiveValidationModule } from 'angular-reactive-validation'; 40 | 41 | @NgModule({ 42 | imports: [ 43 | ..., 44 | ReactiveValidationModule 45 | ] 46 | }) 47 | export class AppModule { } 48 | ``` 49 | 50 | Declare your validation with messages: 51 | 52 | ```ts 53 | import { Validators } from 'angular-reactive-validation'; 54 | 55 | ... 56 | 57 | form = this.fb.group({ 58 | name: this.fb.group({ 59 | firstName: ['', [Validators.required('A first name is required'), 60 | Validators.minLength(1, minLength => `The minimum length is ${minLength}`), 61 | Validators.maxLength(50, maxLength => `Maximum length is ${maxLength}`)]], 62 | middleName: ['', [Validators.maxLength(50, maxLength => `Maximum length is ${maxLength}`)]], 63 | lastName: ['', [Validators.required('A last name is required'), 64 | Validators.maxLength(50, maxLength => `Maximum length is ${maxLength}`)]] 65 | }), 66 | age: [null, [ 67 | Validators.required('An age is required'), 68 | Validators.min(0, 'You can\'t be less than zero years old.'), 69 | Validators.max(150, max => `Can't be more than ${max}`) 70 | ]] 71 | }); 72 | ``` 73 | 74 | See [Advanced validation declaration](#advanced-validation-declaration) for other ways to declare your validation. 75 | 76 | Add the component that will display the messages to your HTML: 77 | 78 | ```html 79 | 80 | 81 | 82 | 83 | 84 | ``` 85 | 86 | Make changes to the styling of the validation messages when needed by using the `invalid-feedback` class. E.g.: 87 | 88 | ```scss 89 | .invalid-feedback { 90 | width: 100%; 91 | margin-top: .25rem; 92 | font-size: 80%; 93 | color: red; 94 | } 95 | ``` 96 | 97 | ## Advanced validation declaration 98 | 99 | The library supports specifying validators in a number of ways: 100 | 101 | With a static message: 102 | 103 | ```ts 104 | Validators.minLength(1, 'The minimum length is not reached.') 105 | ``` 106 | 107 | With a dynamic message, which is passed the validation value: 108 | 109 | ```ts 110 | Validators.minLength(1, minLength => `The minimum length is ${minLength}.`) 111 | ``` 112 | 113 | With a dynamic validation value: 114 | 115 | ```ts 116 | Validators.minLength(() => this.getMinimumLength(), 'The minimum length is not reached.') 117 | ``` 118 | 119 | Or combining the two options above: 120 | 121 | ```ts 122 | Validators.minLength(() => this.getMinimumLength(), minLength => `The minimum length is ${minLength}.`) 123 | ``` 124 | 125 | ## Changing when validation messages are displayed 126 | 127 | By default validation messages are displayed when the associated control is touched, or when the form has been submitted while the `arv-validation-messages` component was instantiated (meaning any hidden elements would not show their validation until a resubmit). 128 | 129 | This implementation can be overridden by changing the module import as follows: 130 | 131 | ```ts 132 | import { ReactiveValidationModule } from 'angular-reactive-validation'; 133 | 134 | @NgModule({ 135 | imports: [ 136 | ..., 137 | ReactiveValidationModule.forRoot({ 138 | displayValidationMessageWhen: (control, formSubmitted) => { 139 | return true; // Replace with your implementation. 140 | } 141 | }) 142 | ] 143 | }) 144 | export class AppModule { } 145 | ``` 146 | 147 | Note that `formSubmitted` can be undefined when it's not known if the form is submitted, due to the form tag missing a formGroup attribute. 148 | 149 | ## Declaring your own validator functions 150 | 151 | Angular provides a limited set of validator functions. To declare your own validator functions _and_ combine it with this library use the `ValidatorDeclaration` class. It supports declaring validators with zero, one or two arguments. 152 | 153 | **Note** that if your validator doesn't return an object as the inner error result, but e.g. a `boolean` such as in the examples below, then this will be replaced by an object that can hold the validation message. Thus, in the first example below `{ 'hasvalue': true }` becomes `{ 'hasvalue': { 'message': 'validation message' } }`. 154 | 155 | ```ts 156 | const hasValueValidator = ValidatorDeclaration.wrapNoArgumentValidator(control => { 157 | return !!control.value ? null : { 'hasvalue': true }; 158 | }, 'hasvalue'); 159 | 160 | const formControl = new FormControl('', hasValueValidator('error message to show')); 161 | ``` 162 | 163 | ```ts 164 | const minimumValueValidator = ValidatorDeclaration.wrapSingleArgumentValidator((min: number) => { 165 | return function(control: AbstractControl): ValidationErrors { 166 | return control.value >= min ? null : { 'min': true }; 167 | }; 168 | }, 'min'); 169 | 170 | const formControl = new FormControl('', minimumValueValidator(5, 'error message to show')); 171 | ``` 172 | 173 | ```ts 174 | const betweenValueValidator = ValidatorDeclaration.wrapTwoArgumentValidator((min: number, max: number) => { 175 | return function(control: AbstractControl): ValidationErrors { 176 | return control.value >= min && control.value <= max ? null : { 'between': true }; 177 | }; 178 | }, 'between'); 179 | 180 | const formControl = new FormControl('', betweenValueValidator(5, 10, 'error message to show')); 181 | ``` 182 | 183 | Wrapping validator functions provided by other packages is also very simple: 184 | 185 | ```ts 186 | const minValidator = ValidatorDeclaration.wrapSingleArgumentValidator(AngularValidators.min, 'min') 187 | ``` 188 | 189 | ## Edge use cases 190 | 191 | ### Handling custom HTML validation messages 192 | 193 | Though not the purpose of this library. There might be times when you want to declare a validation message within your HTML, because it requires some custom formatting. Therefore, all the `Validators` can also be used without declaring a message: 194 | 195 | ```ts 196 | Validators.minLength(() => this.getMinimumLength()) 197 | ``` 198 | 199 | And the following HTML can be used: 200 | 201 | ```html 202 | 203 | 204 | Your custom validation message HTML for the minimum value validation. 205 | 206 | 207 | ``` 208 | 209 | If the `arv-validation-messages`'s `for` attribute specifies multiple controls, be sure to declare the `for` attribute on the `arv-validation-message` element as well: 210 | 211 | ```html 212 | 213 | 214 | Your custom validation message HTML for the required validation. 215 | 216 | 217 | Your custom validation message HTML for the minlength validation. 218 | 219 | 220 | ``` 221 | 222 | Note that unlike the default Angular validation, parameterless functions need to be called to work properly: 223 | 224 | ```ts 225 | Validators.required() 226 | Validators.requiredTrue() 227 | Validators.email() 228 | ``` 229 | 230 | ### Using arv-validation-messages when not using `[formGroup]` or `formGroupName` attributes 231 | 232 | Supplying FormControl instances instead of names is also supported: 233 | 234 | ```html 235 | 236 | 237 | ... 238 | 239 | 240 | ``` 241 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular-reactive-validation": { 7 | "projectType": "library", 8 | "root": "projects/angular-reactive-validation", 9 | "sourceRoot": "projects/angular-reactive-validation/src", 10 | "prefix": "lib", 11 | "architect": { 12 | "build": { 13 | "builder": "@angular-devkit/build-angular:ng-packagr", 14 | "options": { 15 | "project": "projects/angular-reactive-validation/ng-package.json" 16 | }, 17 | "configurations": { 18 | "production": { 19 | "tsConfig": "projects/angular-reactive-validation/tsconfig.lib.prod.json" 20 | }, 21 | "development": { 22 | "tsConfig": "projects/angular-reactive-validation/tsconfig.lib.json" 23 | } 24 | }, 25 | "defaultConfiguration": "production" 26 | }, 27 | "test": { 28 | "builder": "@angular-devkit/build-angular:karma", 29 | "options": { 30 | "tsConfig": "projects/angular-reactive-validation/tsconfig.spec.json", 31 | "polyfills": [ 32 | "zone.js", 33 | "zone.js/testing" 34 | ] 35 | } 36 | } 37 | } 38 | }, 39 | "test-app": { 40 | "projectType": "application", 41 | "schematics": {}, 42 | "root": "projects/test-app", 43 | "sourceRoot": "projects/test-app/src", 44 | "prefix": "app", 45 | "architect": { 46 | "build": { 47 | "builder": "@angular-devkit/build-angular:browser", 48 | "options": { 49 | "outputPath": "dist/test-app", 50 | "index": "projects/test-app/src/index.html", 51 | "main": "projects/test-app/src/main.ts", 52 | "polyfills": [ 53 | "zone.js" 54 | ], 55 | "tsConfig": "projects/test-app/tsconfig.app.json", 56 | "assets": [ 57 | "projects/test-app/src/favicon.ico", 58 | "projects/test-app/src/assets" 59 | ], 60 | "styles": [ 61 | "projects/test-app/src/styles.css" 62 | ], 63 | "scripts": [] 64 | }, 65 | "configurations": { 66 | "production": { 67 | "budgets": [ 68 | { 69 | "type": "initial", 70 | "maximumWarning": "500kb", 71 | "maximumError": "1mb" 72 | }, 73 | { 74 | "type": "anyComponentStyle", 75 | "maximumWarning": "2kb", 76 | "maximumError": "4kb" 77 | } 78 | ], 79 | "outputHashing": "all" 80 | }, 81 | "development": { 82 | "buildOptimizer": false, 83 | "optimization": false, 84 | "vendorChunk": true, 85 | "extractLicenses": false, 86 | "sourceMap": true, 87 | "namedChunks": true 88 | } 89 | }, 90 | "defaultConfiguration": "production" 91 | }, 92 | "serve": { 93 | "builder": "@angular-devkit/build-angular:dev-server", 94 | "configurations": { 95 | "production": { 96 | "buildTarget": "test-app:build:production" 97 | }, 98 | "development": { 99 | "buildTarget": "test-app:build:development" 100 | } 101 | }, 102 | "defaultConfiguration": "development" 103 | }, 104 | "extract-i18n": { 105 | "builder": "@angular-devkit/build-angular:extract-i18n", 106 | "options": { 107 | "buildTarget": "test-app:build" 108 | } 109 | }, 110 | "test": { 111 | "builder": "@angular-devkit/build-angular:karma", 112 | "options": { 113 | "polyfills": [ 114 | "zone.js", 115 | "zone.js/testing" 116 | ], 117 | "tsConfig": "projects/test-app/tsconfig.spec.json", 118 | "assets": [ 119 | "projects/test-app/src/favicon.ico", 120 | "projects/test-app/src/assets" 121 | ], 122 | "styles": [ 123 | "projects/test-app/src/styles.css" 124 | ], 125 | "scripts": [] 126 | } 127 | } 128 | } 129 | } 130 | }, 131 | "cli": { 132 | "analytics": false 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-reactive-validation", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build angular-reactive-validation", 8 | "watch": "ng build angular-reactive-validation --watch --configuration development", 9 | "test": "ng test", 10 | "test:ci": "ng test --browsers=ChromeHeadless --watch=false --code-coverage" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "^18.0.0", 15 | "@angular/common": "^18.0.0", 16 | "@angular/compiler": "^18.0.0", 17 | "@angular/core": "^18.0.0", 18 | "@angular/forms": "^18.0.0", 19 | "@angular/platform-browser": "^18.0.0", 20 | "@angular/platform-browser-dynamic": "^18.0.0", 21 | "@angular/router": "^18.0.0", 22 | "rxjs": "~7.8.0", 23 | "tslib": "^2.3.0", 24 | "zone.js": "~0.14.3" 25 | }, 26 | "devDependencies": { 27 | "@angular-devkit/build-angular": "^18.0.4", 28 | "@angular/cli": "~18.0.4", 29 | "@angular/compiler-cli": "^18.0.0", 30 | "@types/jasmine": "~4.3.0", 31 | "jasmine-core": "~4.5.0", 32 | "karma": "~6.4.0", 33 | "karma-chrome-launcher": "~3.1.0", 34 | "karma-coverage": "~2.2.0", 35 | "karma-jasmine": "~5.1.0", 36 | "karma-jasmine-html-reporter": "~2.0.0", 37 | "ng-packagr": "^18.0.0", 38 | "typescript": "^5.4.5" 39 | } 40 | } -------------------------------------------------------------------------------- /projects/angular-reactive-validation/README.md: -------------------------------------------------------------------------------- 1 | # AngularReactiveValidation 2 | 3 | This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 15.1.0. 4 | 5 | ## Build 6 | 7 | Run `ng build angular-reactive-validation` to build the project. The build artifacts will be stored in the `dist/` directory. 8 | 9 | ## Publishing 10 | 11 | After building your library with `ng build angular-reactive-validation`, go to the dist folder `cd dist/angular-reactive-validation`, copy the root `README.md` into it, and run `npm publish`. 12 | 13 | ## Running unit tests 14 | 15 | Run `ng test angular-reactive-validation` to execute the unit tests via [Karma](https://karma-runner.github.io). 16 | -------------------------------------------------------------------------------- /projects/angular-reactive-validation/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/angular-reactive-validation", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } -------------------------------------------------------------------------------- /projects/angular-reactive-validation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-reactive-validation", 3 | "version": "12.0.0", 4 | "description": "Reactive Forms validation shouldn't require the developer to write lots of HTML to show validation messages. This library makes it easy.", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/davidwalschots/angular-reactive-validation.git" 8 | }, 9 | "author": { 10 | "name": "David Walschots", 11 | "email": "davidwalschots.npm@gmail.com" 12 | }, 13 | "bugs": "https://github.com/davidwalschots/angular-reactive-validation/issues", 14 | "keywords": [ 15 | "angular", 16 | "validation", 17 | "reactive-forms" 18 | ], 19 | "license": "MIT", 20 | "private": false, 21 | "peerDependencies": { 22 | "@angular/common": "^18.0.0", 23 | "@angular/core": "^18.0.0", 24 | "@angular/forms": "^18.0.0", 25 | "rxjs": "^6.5.3 || ^7.4.0" 26 | }, 27 | "dependencies": { 28 | "tslib": "^2.3.0" 29 | }, 30 | "sideEffects": false 31 | } 32 | -------------------------------------------------------------------------------- /projects/angular-reactive-validation/src/lib/form/form.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { UntypedFormGroup, ReactiveFormsModule } from '@angular/forms'; 3 | import { TestBed } from '@angular/core/testing'; 4 | import { By } from '@angular/platform-browser'; 5 | 6 | import { FormDirective } from './form.directive'; 7 | 8 | describe('FormDirective', () => { 9 | it(`submitted observable emits when the form is submitted`, () => { 10 | TestBed.configureTestingModule({ 11 | imports: [ReactiveFormsModule], 12 | declarations: [FormDirective, TestHostComponent] 13 | }); 14 | 15 | const fixture = TestBed.createComponent(TestHostComponent); 16 | fixture.detectChanges(); 17 | 18 | const formDirective = fixture.debugElement 19 | .query(By.directive(FormDirective)) 20 | .injector.get(FormDirective); 21 | 22 | let called = false; 23 | formDirective.submitted.subscribe(() => { 24 | called = true; 25 | }); 26 | 27 | const button = fixture.debugElement.nativeElement.querySelector('button'); 28 | button.click(); 29 | 30 | expect(called).toEqual(true); 31 | }); 32 | 33 | @Component({ 34 | template: ` 35 |
36 | 37 |
` 38 | }) 39 | class TestHostComponent { 40 | form = new UntypedFormGroup({}); 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /projects/angular-reactive-validation/src/lib/form/form.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive } from '@angular/core'; 2 | import { FormGroupDirective } from '@angular/forms'; 3 | import { Observable } from 'rxjs'; 4 | 5 | @Directive({ 6 | // eslint-disable-next-line @angular-eslint/directive-selector 7 | selector: 'form[formGroup]' 8 | }) 9 | /** 10 | * Encapsulates properties and events of the form and makes them available for child components. 11 | */ 12 | export class FormDirective { 13 | /** 14 | * Observable which emits when the form is submitted. 15 | */ 16 | submitted: Observable; 17 | 18 | constructor(formGroupDirective: FormGroupDirective) { 19 | this.submitted = formGroupDirective.ngSubmit.asObservable(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /projects/angular-reactive-validation/src/lib/get-control-path.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | import { UntypedFormBuilder } from '@angular/forms'; 3 | 4 | import { getControlPath } from './get-control-path'; 5 | 6 | describe('getControlPath', () => { 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({ 9 | providers: [ 10 | UntypedFormBuilder 11 | ] 12 | }); 13 | }); 14 | 15 | it(`emits paths for form groups`, inject([UntypedFormBuilder], (fb: UntypedFormBuilder) => { 16 | const firstName = fb.control(''); 17 | fb.group({ 18 | name: fb.group({ 19 | firstName: firstName 20 | }) 21 | }); 22 | 23 | expect(getControlPath(firstName)).toEqual('name.firstName'); 24 | })); 25 | 26 | it(`emits numeric paths for form arrays`, inject([UntypedFormBuilder], (fb: UntypedFormBuilder) => { 27 | const firstName = fb.control(''); 28 | const firstName2 = fb.control(''); 29 | 30 | fb.group({ 31 | persons: fb.array([ 32 | fb.group({ 33 | firstName: firstName 34 | }), 35 | fb.group({ 36 | firstName: firstName2 37 | }) 38 | ]) 39 | }); 40 | 41 | expect(getControlPath(firstName)).toEqual('persons.0.firstName'); 42 | expect(getControlPath(firstName2)).toEqual('persons.1.firstName'); 43 | })); 44 | 45 | it(`emits an empty string for a control without parents`, inject([UntypedFormBuilder], (fb: UntypedFormBuilder) => { 46 | const control = fb.control(''); 47 | 48 | expect(getControlPath(control)).toEqual(''); 49 | })); 50 | 51 | it(`emits an index string for a control with only a form array as parent`, inject([UntypedFormBuilder], (fb: UntypedFormBuilder) => { 52 | const control = fb.control(''); 53 | 54 | fb.array([control]); 55 | 56 | expect(getControlPath(control)).toEqual('0'); 57 | })); 58 | 59 | it(`emits a single identifier for a control with only a single form group as parent`, inject([UntypedFormBuilder], (fb: UntypedFormBuilder) => { 60 | const control = fb.control(''); 61 | 62 | fb.group({ 63 | control: control 64 | }); 65 | 66 | expect(getControlPath(control)).toEqual('control'); 67 | })); 68 | }); 69 | -------------------------------------------------------------------------------- /projects/angular-reactive-validation/src/lib/get-control-path.ts: -------------------------------------------------------------------------------- 1 | import { AbstractControl } from '@angular/forms'; 2 | 3 | /** 4 | * Given a control, returns a string representation of the property path to 5 | * this control. Thus, for a FormControl 'firstName', that is part of a 6 | * FormGroup named 'name', this function will return: 'name.firstName'. 7 | * 8 | * Note that FormArray indexes are also put in the path, e.g.: 'person.0.name.firstName'. 9 | */ 10 | export const getControlPath = (control: AbstractControl): string => { 11 | const parentControl = control.parent; 12 | if (parentControl) { 13 | let path = getControlPath(parentControl); 14 | if (path) { 15 | path += '.'; 16 | } 17 | return path + Object.keys(parentControl.controls).find(key => { 18 | const controls = parentControl.controls; 19 | if (Array.isArray(controls)) { 20 | return controls[Number(key)] === control; 21 | } else { 22 | return controls[key] === control; 23 | } 24 | }); 25 | } 26 | 27 | return ''; 28 | }; 29 | -------------------------------------------------------------------------------- /projects/angular-reactive-validation/src/lib/get-form-control-from-container.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | import { UntypedFormBuilder } from '@angular/forms'; 3 | 4 | import { getFormControlFromContainer } from './get-form-control-from-container'; 5 | 6 | describe('getFormControlFromContainer', () => { 7 | beforeEach(() => { 8 | TestBed.configureTestingModule({ 9 | providers: [ 10 | UntypedFormBuilder 11 | ] 12 | }); 13 | }); 14 | 15 | it(`gets a FormControl from the FormGroup`, inject([UntypedFormBuilder], (fb: UntypedFormBuilder) => { 16 | const firstName = fb.control(''); 17 | const group = fb.group({ 18 | firstName: firstName 19 | }); 20 | 21 | const container: any = { 22 | control: group 23 | }; 24 | 25 | expect(getFormControlFromContainer('firstName', container)).toBe(firstName); 26 | })); 27 | 28 | it(`throws an Error when no container is provided`, inject([UntypedFormBuilder], (fb: UntypedFormBuilder) => { 29 | expect(() => getFormControlFromContainer('firstName', undefined)).toThrow(new Error( 30 | `You can't pass a string to arv-validation-messages's for attribute, when the ` + 31 | `arv-validation-messages element is not a child of an element with a formGroupName or formGroup declaration.`)); 32 | })); 33 | 34 | it(`throws an Error when there is no FormControl with the given name`, inject([UntypedFormBuilder], (fb: UntypedFormBuilder) => { 35 | const group = fb.group({}); 36 | 37 | const container: any = { 38 | control: group, 39 | path: ['the', 'path'] 40 | }; 41 | 42 | expect(() => getFormControlFromContainer('lastName', container)).toThrow(new Error( 43 | `There is no control named 'lastName' within 'the.path'.` 44 | )); 45 | })); 46 | 47 | it(`throws an Error when there is a FormGroup with the given name`, inject([UntypedFormBuilder], (fb: UntypedFormBuilder) => { 48 | const group = fb.group({ 49 | name: fb.group({}) 50 | }); 51 | 52 | const container: any = { 53 | control: group, 54 | path: ['the', 'path'] 55 | }; 56 | 57 | expect(() => getFormControlFromContainer('name', container)).toThrow(new Error( 58 | `The control named 'name' within 'the.path' is not a FormControl. Maybe you accidentally referenced a FormGroup or FormArray?` 59 | )); 60 | })); 61 | 62 | it(`throws an Error when there is a FormArray with the given name`, inject([UntypedFormBuilder], (fb: UntypedFormBuilder) => { 63 | const group = fb.group({ 64 | name: fb.array([]) 65 | }); 66 | 67 | const container: any = { 68 | control: group, 69 | path: ['the', 'path'] 70 | }; 71 | 72 | expect(() => getFormControlFromContainer('name', container)).toThrow(new Error( 73 | `The control named 'name' within 'the.path' is not a FormControl. Maybe you accidentally referenced a FormGroup or FormArray?` 74 | )); 75 | })); 76 | }); 77 | -------------------------------------------------------------------------------- /projects/angular-reactive-validation/src/lib/get-form-control-from-container.ts: -------------------------------------------------------------------------------- 1 | import { UntypedFormGroup, UntypedFormControl, ControlContainer, FormGroupDirective } from '@angular/forms'; 2 | 3 | export const getFormControlFromContainer = (name: string, controlContainer: ControlContainer | undefined): UntypedFormControl => { 4 | if (controlContainer) { 5 | const control = (controlContainer.control as UntypedFormGroup).controls[name]; 6 | if (!control) { 7 | throw new Error(`There is no control named '${name}'` + 8 | (getPath(controlContainer).length > 0 ? ` within '${getPath(controlContainer).join('.')}'` : '') + '.'); 9 | } 10 | if (!(control instanceof UntypedFormControl)) { 11 | throw new Error(`The control named '${name}' ` + 12 | (getPath(controlContainer).length > 0 ? `within '${getPath(controlContainer).join('.')}' ` : '') + 13 | `is not a FormControl. Maybe you accidentally referenced a FormGroup or FormArray?`); 14 | } 15 | 16 | return control; 17 | } else { 18 | throw new Error(`You can't pass a string to arv-validation-messages's for attribute, when the ` + 19 | `arv-validation-messages element is not a child of an element with a formGroupName or formGroup declaration.`); 20 | } 21 | }; 22 | 23 | export const isControlContainerVoidOrInitialized = (controlContainer: ControlContainer | undefined) => 24 | !!(!controlContainer || (controlContainer as FormGroupDirective).form || 25 | (controlContainer.formDirective && (controlContainer.formDirective as FormGroupDirective).form)); 26 | 27 | const getPath = (controlContainer: ControlContainer): string[] => controlContainer.path || []; 28 | -------------------------------------------------------------------------------- /projects/angular-reactive-validation/src/lib/reactive-validation-module-configuration-token.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | 3 | import { ReactiveValidationModuleConfiguration } from './reactive-validation-module-configuration'; 4 | 5 | export const REACTIVE_VALIDATION_MODULE_CONFIGURATION_TOKEN = 6 | new InjectionToken('ReactiveValidationModuleConfiguration'); 7 | -------------------------------------------------------------------------------- /projects/angular-reactive-validation/src/lib/reactive-validation-module-configuration.ts: -------------------------------------------------------------------------------- 1 | import { UntypedFormControl } from '@angular/forms'; 2 | 3 | export interface ReactiveValidationModuleConfiguration { 4 | /** 5 | * Function to override the default validation message display behaviour with a different implementation. 6 | * The default returns true when the control is touched, or the form has been submitted. 7 | * 8 | * @param control The control for which to display errors 9 | * @param formSubmitted whether the form is submitted or not. When undefined, it's not known 10 | * if the form is submitted, due to the form tag missing a formGroup. 11 | */ 12 | displayValidationMessageWhen?: (control: UntypedFormControl, formSubmitted: boolean | undefined) => boolean; 13 | } 14 | -------------------------------------------------------------------------------- /projects/angular-reactive-validation/src/lib/reactive-validation.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ReactiveValidationModule } from './reactive-validation.module'; 4 | import { REACTIVE_VALIDATION_MODULE_CONFIGURATION_TOKEN } from './reactive-validation-module-configuration-token'; 5 | 6 | describe(`ReactiveValidationModule`, () => { 7 | describe(`when not calling forRoot`, () => { 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({ 10 | imports: [ ReactiveValidationModule ] 11 | }); 12 | }); 13 | 14 | it(`should not provide configuration`, () => { 15 | expect(() => TestBed.inject(REACTIVE_VALIDATION_MODULE_CONFIGURATION_TOKEN)).toThrowError(/No provider for/); 16 | }); 17 | }); 18 | 19 | describe(`when calling forRoot`, () => { 20 | let configuration: any; 21 | 22 | beforeEach(() => { 23 | configuration = { }; 24 | TestBed.configureTestingModule({ 25 | imports: [ 26 | ReactiveValidationModule.forRoot(configuration) 27 | ] 28 | }); 29 | }); 30 | 31 | it(`should provide configuration`, () => { 32 | expect(TestBed.inject(REACTIVE_VALIDATION_MODULE_CONFIGURATION_TOKEN)).toEqual(configuration); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /projects/angular-reactive-validation/src/lib/reactive-validation.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { ValidationMessagesComponent } from './validation-messages/validation-messages.component'; 5 | import { ValidationMessageComponent } from './validation-message/validation-message.component'; 6 | import { FormDirective } from './form/form.directive'; 7 | import { ReactiveValidationModuleConfiguration } from './reactive-validation-module-configuration'; 8 | import { REACTIVE_VALIDATION_MODULE_CONFIGURATION_TOKEN } from './reactive-validation-module-configuration-token'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | CommonModule 13 | ], 14 | declarations: [ 15 | ValidationMessagesComponent, 16 | ValidationMessageComponent, 17 | FormDirective 18 | ], 19 | exports: [ 20 | ValidationMessagesComponent, 21 | ValidationMessageComponent, 22 | FormDirective 23 | ] 24 | }) 25 | export class ReactiveValidationModule { 26 | static forRoot(configuration?: ReactiveValidationModuleConfiguration): ModuleWithProviders { 27 | return { 28 | ngModule: ReactiveValidationModule, 29 | providers: [{ 30 | provide: REACTIVE_VALIDATION_MODULE_CONFIGURATION_TOKEN, useValue: configuration 31 | }] 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /projects/angular-reactive-validation/src/lib/validation-error.spec.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError } from './validation-error'; 2 | 3 | describe('ValidationError', () => { 4 | it(`fromFirstError takes the first error object from a FormControl`, () => { 5 | const requiredErrorObject = {}; 6 | const control: any = { 7 | errors: { 8 | required: requiredErrorObject, 9 | test: true 10 | } 11 | }; 12 | const error = ValidationError.fromFirstError(control); 13 | 14 | expect(error).not.toBeUndefined(); 15 | expect((error as ValidationError).control).toEqual(control); 16 | expect((error as ValidationError).key).toEqual('required'); 17 | expect((error as ValidationError).errorObject).toEqual(requiredErrorObject); 18 | }); 19 | 20 | it(`fromFirstError returns undefined when the FormControl has no errors`, () => { 21 | const control: any = { 22 | errors: null 23 | }; 24 | const error = ValidationError.fromFirstError(control); 25 | 26 | expect(error).toEqual(undefined); 27 | }); 28 | 29 | it(`hasMessage returns true when the errorObject contains a message`, () => { 30 | const control: any = { 31 | errors: { 32 | required: { 33 | message: 'This is the expected message' 34 | } 35 | } 36 | }; 37 | const error = ValidationError.fromFirstError(control); 38 | 39 | expect(error).not.toBeUndefined(); 40 | expect((error as ValidationError).hasMessage()).toEqual(true); 41 | }); 42 | 43 | it(`hasMessage returns false when the errorObject doesn't contain a message`, () => { 44 | const control: any = { 45 | errors: { 46 | required: {} 47 | } 48 | }; 49 | const error = ValidationError.fromFirstError(control); 50 | 51 | expect(error).not.toBeUndefined(); 52 | expect((error as ValidationError).hasMessage()).toEqual(false); 53 | }); 54 | 55 | it(`getMessage returns the message from the errorObject`, () => { 56 | const expected = 'This is the expected message'; 57 | const control: any = { 58 | errors: { 59 | required: { 60 | message: expected 61 | } 62 | } 63 | }; 64 | const error = ValidationError.fromFirstError(control); 65 | 66 | expect(error).not.toBeUndefined(); 67 | expect((error as ValidationError).getMessage()).toEqual(expected); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /projects/angular-reactive-validation/src/lib/validation-error.ts: -------------------------------------------------------------------------------- 1 | import { UntypedFormControl, ValidationErrors } from '@angular/forms'; 2 | 3 | export class ValidationError { 4 | control: UntypedFormControl; 5 | key: string; 6 | errorObject: ValidationErrors; 7 | 8 | constructor(control: UntypedFormControl, key: string, errorObject: ValidationErrors) { 9 | this.control = control; 10 | this.key = key; 11 | this.errorObject = errorObject; 12 | } 13 | 14 | static fromFirstError(control: UntypedFormControl): ValidationError | undefined { 15 | if (!control.errors) { 16 | return undefined; 17 | } 18 | 19 | return new ValidationError(control, Object.keys(control.errors)[0], control.errors[Object.keys(control.errors)[0]]); 20 | } 21 | 22 | hasMessage(): boolean { 23 | return !!this.getMessage(); 24 | } 25 | 26 | getMessage() { 27 | return this.errorObject['message']; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /projects/angular-reactive-validation/src/lib/validation-message/validation-message.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /projects/angular-reactive-validation/src/lib/validation-message/validation-message.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild } from '@angular/core'; 2 | import { TestBed, ComponentFixture } from '@angular/core/testing'; 3 | 4 | import { ValidationMessageComponent } from './validation-message.component'; 5 | import { ValidationError } from '../validation-error'; 6 | import { Validators } from '../validators'; 7 | import { UntypedFormControl, UntypedFormGroup, ReactiveFormsModule } from '@angular/forms'; 8 | 9 | describe('ValidationMessageComponent', () => { 10 | describe('canHandle', () => { 11 | let control: any; 12 | let component: ValidationMessageComponent; 13 | let error: ValidationError; 14 | 15 | beforeEach(() => { 16 | control = { 17 | errors: { 18 | required: true 19 | } 20 | }; 21 | 22 | component = new ValidationMessageComponent(undefined as any); 23 | error = ValidationError.fromFirstError(control) as ValidationError; 24 | }); 25 | 26 | it(`returns true when the error key and component key are equal (without for)`, () => { 27 | component.key = 'required'; 28 | 29 | const result = component.canHandle(error); 30 | 31 | expect(result).toEqual(true); 32 | }); 33 | 34 | it(`returns true when the error key and component key are equal (with for)`, () => { 35 | component.for = control; 36 | component.key = 'required'; 37 | 38 | const result = component.canHandle(error); 39 | 40 | expect(result).toEqual(true); 41 | }); 42 | 43 | it(`returns false when the component 'for' doesn't equal the error's control`, () => { 44 | component.for = {} as any; 45 | component.key = 'required'; 46 | 47 | const result = component.canHandle(error); 48 | 49 | expect(result).toEqual(false); 50 | }); 51 | 52 | it(`returns false when the error key doesn't equal the component key`, () => { 53 | component.key = 'minlength'; 54 | 55 | const result = component.canHandle(error); 56 | 57 | expect(result).toEqual(false); 58 | }); 59 | }); 60 | 61 | describe('error messages', () => { 62 | let fixture: ComponentFixture; 63 | let validationMessageComponent: ValidationMessageComponent | undefined; 64 | beforeEach(() => { 65 | TestBed.configureTestingModule({ 66 | declarations: [ValidationMessageComponent, TestHostComponent] 67 | }); 68 | 69 | fixture = TestBed.createComponent(TestHostComponent); 70 | validationMessageComponent = fixture.componentInstance.validationMessageComponent; 71 | }); 72 | 73 | it(`are displayed by the show function`, () => { 74 | const error = ValidationError.fromFirstError(TestHostComponent.getFormControl()) as ValidationError; 75 | 76 | validationMessageComponent?.show(error); 77 | 78 | expect(fixture.nativeElement.querySelector('.message')).toBeFalsy(); 79 | fixture.detectChanges(); 80 | expect(fixture.nativeElement.querySelector('.message')).not.toBeFalsy(); 81 | expect(fixture.nativeElement.querySelector('.message').textContent) 82 | .toEqual(`The message is shown. requiredLength: ${error.errorObject['requiredLength']}`); 83 | }); 84 | 85 | it(`are hidden by the reset function`, () => { 86 | const error = ValidationError.fromFirstError(TestHostComponent.getFormControl()) as ValidationError; 87 | 88 | validationMessageComponent?.show(error); 89 | fixture.detectChanges(); 90 | validationMessageComponent?.reset(); 91 | fixture.detectChanges(); 92 | expect(fixture.nativeElement.querySelector('.message')).toBeFalsy(); 93 | }); 94 | 95 | it(`and their context is set by the show function`, () => { 96 | const error = ValidationError.fromFirstError(TestHostComponent.getFormControl()) as ValidationError; 97 | 98 | validationMessageComponent?.show(error); 99 | expect(validationMessageComponent?.context).toEqual(error.errorObject); 100 | }); 101 | 102 | @Component({ 103 | template: ` 104 | 105 |

The message is shown. requiredLength: {{minlengthValidation.context?.requiredLength}}

106 |
` 107 | }) 108 | class TestHostComponent { 109 | @ViewChild(ValidationMessageComponent, {static: true}) validationMessageComponent: ValidationMessageComponent | undefined; 110 | 111 | static getFormControl(): any { 112 | return { 113 | errors: { 114 | minlength: { requiredLength: 10, actualLength: 5 } 115 | } 116 | }; 117 | } 118 | } 119 | }); 120 | 121 | it('can set control by name without exception being thrown due to ControlContainer not yet being initialized', () => { 122 | @Component({ 123 | template: ` 124 |
125 | 126 | 127 |
128 | ` 129 | }) 130 | class TestHostComponent { 131 | @ViewChild(ValidationMessageComponent, { static: true }) validationMessageComponent: ValidationMessageComponent | undefined; 132 | 133 | age = new UntypedFormControl(0, [ 134 | Validators.min(10, 'invalid age') 135 | ]); 136 | form = new UntypedFormGroup({ 137 | age: this.age 138 | }); 139 | } 140 | 141 | TestBed.configureTestingModule({ 142 | imports: [ReactiveFormsModule], 143 | declarations: [ValidationMessageComponent, TestHostComponent] 144 | }); 145 | 146 | expect(() => { 147 | const fixture = TestBed.createComponent(TestHostComponent); 148 | fixture.detectChanges(); 149 | expect(fixture.componentInstance.validationMessageComponent?.for).toBe(fixture.componentInstance.age); 150 | }).not.toThrow(); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /projects/angular-reactive-validation/src/lib/validation-message/validation-message.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ViewEncapsulation, Optional, OnInit } from '@angular/core'; 2 | import { UntypedFormControl, ValidationErrors, ControlContainer } from '@angular/forms'; 3 | 4 | import { ValidationError } from '../validation-error'; 5 | import { getFormControlFromContainer, isControlContainerVoidOrInitialized } from '../get-form-control-from-container'; 6 | 7 | @Component({ 8 | selector: 'arv-validation-message', 9 | templateUrl: './validation-message.component.html', 10 | encapsulation: ViewEncapsulation.None 11 | }) 12 | /** 13 | * The ValidationMessageComponent lets the developer specify a custom visual style and custom error message 14 | * for edge-cases where the standard style or message capabilities do not suffice. 15 | * 16 | * TODO: Trigger revalidation by parent whenever [for] changes. 17 | */ 18 | export class ValidationMessageComponent implements OnInit { 19 | 20 | @Input() 21 | /** 22 | * The FormControl for which a custom validation message should be shown. This is only required when the parent 23 | * ValidationMessagesComponent has multiple FormControls specified. 24 | */ 25 | set for(control: UntypedFormControl | string | undefined) { 26 | if (!isControlContainerVoidOrInitialized(this.controlContainer)) { 27 | this.initializeForOnInit = () => this.for = control; 28 | return; 29 | } 30 | this._for = typeof control === 'string' ? getFormControlFromContainer(control, this.controlContainer) : control; 31 | } 32 | get for(): UntypedFormControl | string | undefined { 33 | return this._for; 34 | } 35 | 36 | @Input() 37 | /** 38 | * The name of the returned validation object property for which the custom validation message should be shown. 39 | */ 40 | key: string | undefined; 41 | 42 | private _context: ValidationErrors | undefined; 43 | private _for: UntypedFormControl | undefined; 44 | 45 | constructor(@Optional() private controlContainer: ControlContainer) { } 46 | 47 | ngOnInit() { 48 | this.initializeForOnInit(); 49 | } 50 | 51 | canHandle(error: ValidationError) { 52 | return (!this.for || error.control === this.for) && error.key === this.key; 53 | } 54 | 55 | show(error: ValidationError) { 56 | this._context = error.errorObject; 57 | } 58 | 59 | reset() { 60 | this._context = undefined; 61 | } 62 | 63 | private initializeForOnInit = () => {}; 64 | 65 | /** 66 | * The ValidationErrors object that contains contextual information about the error, which can be used for 67 | * displaying, e.g. the minimum length within the error message. 68 | */ 69 | get context(): any { 70 | return this._context; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /projects/angular-reactive-validation/src/lib/validation-messages/validation-messages.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{message}}

4 |
5 | 6 |
7 | -------------------------------------------------------------------------------- /projects/angular-reactive-validation/src/lib/validation-messages/validation-messages.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild } from '@angular/core'; 2 | import { TestBed, ComponentFixture } from '@angular/core/testing'; 3 | import { ControlContainer, UntypedFormGroup, ReactiveFormsModule, FormGroupDirective, FormControl, FormGroup, UntypedFormControl } 4 | from '@angular/forms'; 5 | import { Subject } from 'rxjs'; 6 | 7 | import { ValidationMessagesComponent } from './validation-messages.component'; 8 | import { FormDirective } from '../form/form.directive'; 9 | import { ValidationMessageComponent } from '../validation-message/validation-message.component'; 10 | import { Validators } from '../validators'; 11 | import { ReactiveValidationModule } from '../reactive-validation.module'; 12 | 13 | const isErrorEvent = (event: Event | string): event is ErrorEvent => (event as ErrorEvent).error !== undefined; 14 | 15 | describe('ValidationMessagesComponent', () => { 16 | describe('properties and functions', () => { 17 | let component: ValidationMessagesComponent; 18 | let formGroup: UntypedFormGroup; 19 | let firstNameControl: FormControl; 20 | let middleNameControl: FormControl; 21 | let lastNameControl: FormControl; 22 | 23 | beforeEach(() => { 24 | firstNameControl = new FormControl('', [ 25 | Validators.required('A first name is required'), 26 | Validators.minLength(5, minLength => `First name needs to be at least ${minLength} characters long`) 27 | ]); 28 | middleNameControl = new FormControl('', [Validators.required('A middle name is required')]); 29 | lastNameControl = new FormControl('', [ 30 | Validators.required('A last name is required'), 31 | Validators.minLength(5, minLength => `Last name needs to be at least ${minLength} characters long`) 32 | ]); 33 | formGroup = new UntypedFormGroup({ 34 | firstName: firstNameControl, 35 | middleName: middleNameControl, 36 | lastName: lastNameControl 37 | }); 38 | 39 | const formGroupDirective: FormGroupDirective = new FormGroupDirective([], []); 40 | formGroupDirective.form = formGroup; 41 | 42 | TestBed.configureTestingModule({ 43 | declarations: [ValidationMessagesComponent], 44 | providers: [ 45 | { provide: ControlContainer, useValue: formGroupDirective } 46 | ] 47 | }); 48 | 49 | component = TestBed.createComponent(ValidationMessagesComponent).componentInstance; 50 | }); 51 | 52 | it(`for property doesn't accept an empty array`, () => { 53 | expect(() => component.for = []) 54 | .toThrowError(`arv-validation-messages doesn't allow declaring an empty array as input to the 'for' attribute.`); 55 | }); 56 | 57 | it(`for property handles a single input string`, () => { 58 | expect(() => component.for = 'firstName').not.toThrow(); 59 | }); 60 | 61 | it(`for property handles a single input FormControl`, () => { 62 | expect(() => component.for = firstNameControl).not.toThrow(); 63 | }); 64 | 65 | it(`for property handles an array with strings and FormControls`, () => { 66 | expect(() => component.for = [firstNameControl, 'middleName', lastNameControl]).not.toThrow(); 67 | }); 68 | 69 | it(`isValid returns true when there are no controls with ValidationErrors and they are touched (default configuration)`, () => { 70 | component.for = firstNameControl; 71 | firstNameControl.setValue('firstName'); 72 | firstNameControl.markAsTouched(); 73 | 74 | expect(component.isValid()).toEqual(true); 75 | }); 76 | 77 | it(`isValid returns false when there are controls with ValidationErrors and they are touched (default configuration)`, () => { 78 | component.for = [firstNameControl]; 79 | firstNameControl.markAsTouched(); 80 | 81 | expect(component.isValid()).toEqual(false); 82 | }); 83 | 84 | it(`getErrorMessages returns the first error message per touched control (default configuration)`, () => { 85 | component.for = [firstNameControl, middleNameControl, lastNameControl]; 86 | firstNameControl.markAsTouched(); 87 | // We skip middleNameControl on purpose, to ensure that it doesn't return its error. 88 | lastNameControl.markAsTouched(); 89 | lastNameControl.setValue('abc'); 90 | 91 | expect(component.getErrorMessages()).toEqual(['A first name is required', 'Last name needs to be at least 5 characters long']); 92 | }); 93 | }); 94 | 95 | describe('when in', () => { 96 | let fixture: ComponentFixture; 97 | 98 | describe('the default configuration validation is shown when', () => { 99 | let submittedSubject: Subject; 100 | 101 | beforeEach(() => { 102 | submittedSubject = new Subject(); 103 | TestBed.configureTestingModule({ 104 | imports: [ReactiveFormsModule], 105 | declarations: [ValidationMessagesComponent, ValidationMessageComponent, TestHostComponent], 106 | providers: [{ 107 | provide: FormDirective, useValue: { 108 | submitted: submittedSubject 109 | } 110 | }] 111 | }); 112 | 113 | fixture = TestBed.createComponent(TestHostComponent); 114 | fixture.detectChanges(); 115 | }); 116 | 117 | it(`the associated control is touched`, () => { 118 | fixture.componentInstance.firstNameControl.markAsTouched(); 119 | fixture.componentInstance.lastNameControl.markAsTouched(); 120 | fixture.detectChanges(); 121 | 122 | expectValidationIsShown(); 123 | }); 124 | 125 | it(`the form has been submitted`, () => { 126 | submittedSubject.next(); 127 | fixture.detectChanges(); 128 | 129 | expectValidationIsShown(); 130 | }); 131 | }); 132 | 133 | describe('an alternative configuration', () => { 134 | const configuration = { 135 | displayValidationMessageWhen: (_: UntypedFormControl, __: boolean | undefined) => true 136 | }; 137 | 138 | beforeEach(() => { 139 | spyOn(configuration, 'displayValidationMessageWhen').and.callThrough(); 140 | 141 | TestBed.configureTestingModule({ 142 | imports: [ReactiveFormsModule, ReactiveValidationModule.forRoot(configuration)], 143 | declarations: [TestHostComponent], 144 | }); 145 | 146 | fixture = TestBed.createComponent(TestHostComponent); 147 | }); 148 | 149 | it('validation is shown when displayValidationMessageWhen returns true', () => { 150 | expect(configuration.displayValidationMessageWhen).not.toHaveBeenCalled(); 151 | fixture.detectChanges(); 152 | expect(configuration.displayValidationMessageWhen).toHaveBeenCalled(); 153 | 154 | expectValidationIsShown(); 155 | }); 156 | 157 | it(`displayValidationMessageWhen's formSubmitted is undefined when a FormDirective is not provided`, () => { 158 | fixture.detectChanges(); 159 | expect(configuration.displayValidationMessageWhen).toHaveBeenCalledWith(jasmine.any(UntypedFormControl), undefined); 160 | }); 161 | }); 162 | 163 | const expectValidationIsShown = () => { 164 | expect(fixture.nativeElement.querySelector('.invalid-feedback p').textContent).toEqual('A first name is required'); 165 | expect(fixture.nativeElement.querySelector('.last-name-required').textContent).toEqual('A last name is required'); 166 | }; 167 | 168 | @Component({ 169 | template: ` 170 | 171 | 172 | 173 |

A last name is required

174 |
175 |
` 176 | }) 177 | class TestHostComponent { 178 | firstNameControl: FormControl = new FormControl(null, [Validators.required('A first name is required')]); 179 | lastNameControl: FormControl = new FormControl(null, [Validators.required()]); 180 | } 181 | }); 182 | 183 | it(`a child validation message without 'for' specified while parent has multiple controls throws an error`, () => { 184 | @Component({ 185 | template: ` 186 | 187 | 188 | ` 189 | }) 190 | class TestHostComponent { 191 | firstNameControl: FormControl = new FormControl(null); 192 | lastNameControl: FormControl = new FormControl(null); 193 | } 194 | 195 | TestBed.configureTestingModule({ 196 | imports: [ReactiveFormsModule], 197 | declarations: [ValidationMessagesComponent, ValidationMessageComponent, TestHostComponent] 198 | }); 199 | 200 | const fixture = TestBed.createComponent(TestHostComponent); 201 | expect(() => fixture.detectChanges()) 202 | .toThrowError(`Specify the FormControl for which the arv-validation-message element with key 'required' should show messages.`); 203 | }); 204 | 205 | it(`a child validation message with a 'for' specified that's not in the parent throws an error`, () => { 206 | @Component({ 207 | template: ` 208 | 209 | 210 | ` 211 | }) 212 | class TestHostComponent { 213 | firstNameControl: FormControl = new FormControl(null); 214 | lastNameControl: FormControl = new FormControl(null); 215 | } 216 | 217 | TestBed.configureTestingModule({ 218 | imports: [ReactiveFormsModule], 219 | declarations: [ValidationMessagesComponent, ValidationMessageComponent, TestHostComponent] 220 | }); 221 | 222 | const fixture = TestBed.createComponent(TestHostComponent); 223 | expect(() => fixture.detectChanges()) 224 | .toThrowError(`A arv-validation-messages element with key 'required' attempts to show messages for a FormControl` + 225 | ` that is not declared in the parent arv-validation-messages element.`); 226 | }); 227 | 228 | it(`a ValidationError without a message and without a child validation message component throws an error`, () => { 229 | @Component({ 230 | template: `` 231 | }) 232 | class TestHostComponent { 233 | firstNameControl: FormControl = new FormControl(null, [Validators.required()]); 234 | } 235 | 236 | TestBed.configureTestingModule({ 237 | imports: [ReactiveFormsModule], 238 | declarations: [ValidationMessagesComponent, ValidationMessageComponent, TestHostComponent] 239 | }); 240 | 241 | const fixture = TestBed.createComponent(TestHostComponent); 242 | expect(() => fixture.detectChanges()) 243 | .toThrowError(`There is no suitable arv-validation-message element to show the 'required' error of ''`); 244 | }); 245 | 246 | it('can set control by name without exception being thrown due to ControlContainer not yet being initialized', () => { 247 | @Component({ 248 | template: ` 249 |
250 | 251 | 252 |
253 | ` 254 | }) 255 | class TestHostComponent { 256 | @ViewChild(ValidationMessagesComponent, { static: true }) validationMessagesComponent: ValidationMessagesComponent | undefined; 257 | age = new FormControl(0, [ 258 | Validators.min(10, 'invalid age') 259 | ]); 260 | form = new FormGroup({ 261 | age: this.age 262 | }); 263 | } 264 | 265 | TestBed.configureTestingModule({ 266 | imports: [ReactiveFormsModule], 267 | declarations: [ValidationMessagesComponent, ValidationMessageComponent, TestHostComponent] 268 | }); 269 | 270 | expect(() => { 271 | const fixture = TestBed.createComponent(TestHostComponent); 272 | fixture.componentInstance.age.markAsTouched(); 273 | fixture.detectChanges(); 274 | expect(fixture.componentInstance.validationMessagesComponent?.getErrorMessages()).toEqual(['invalid age']); 275 | }).not.toThrow(); 276 | }); 277 | 278 | xdescribe('', () => { 279 | let onerrorBeforeTest: OnErrorEventHandler; 280 | beforeEach(() => { 281 | onerrorBeforeTest = window.onerror; 282 | }); 283 | afterEach(() => { 284 | window.onerror = onerrorBeforeTest; 285 | }); 286 | 287 | it(`validates child validation message as they are shown or hidden through *ngIf`, (done) => { 288 | @Component({ 289 | template: ` 290 | 291 | 292 | ` 293 | }) 294 | class TestHostComponent { 295 | firstNameControl: FormControl = new FormControl(null); 296 | lastNameControl: FormControl = new FormControl(null); 297 | show = false; 298 | } 299 | 300 | TestBed.configureTestingModule({ 301 | imports: [ReactiveFormsModule], 302 | declarations: [ValidationMessagesComponent, ValidationMessageComponent, TestHostComponent] 303 | }); 304 | 305 | // We can't simply expect().toThrowError(), because in RxJS 6, any error inside of 'next' 306 | // is asynchronously thrown, instead of synchronously as before. So these errors will never reach the call stack 307 | // of the expect() function. The observables also isn't exposed, and therefore we need to resort to catching 308 | // the error through window.onerror. 309 | window.onerror = event => { 310 | if (isErrorEvent(event)) { 311 | expect(event.error.message).toEqual(`A arv-validation-messages element with key 'required' attempts to show messages ` + 312 | `for a FormControl that is not declared in the parent arv-validation-messages element.`); 313 | done(); 314 | 315 | // Though window.onerror is quirky, returning false generally works to suppress the error from reaching the console. 316 | return false; 317 | } 318 | 319 | return undefined; 320 | }; 321 | 322 | const fixture = TestBed.createComponent(TestHostComponent); 323 | fixture.detectChanges(); 324 | fixture.componentInstance.show = true; 325 | fixture.detectChanges(); 326 | }); 327 | }); 328 | }); 329 | -------------------------------------------------------------------------------- /projects/angular-reactive-validation/src/lib/validation-messages/validation-messages.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ContentChildren, QueryList, Input, ViewEncapsulation, AfterContentInit, 2 | OnDestroy, Optional, Inject, OnInit } from '@angular/core'; 3 | import { UntypedFormControl, ControlContainer } from '@angular/forms'; 4 | import { Subscription } from 'rxjs'; 5 | 6 | import { ValidationMessageComponent } from '../validation-message/validation-message.component'; 7 | import { ValidationError } from '../validation-error'; 8 | import { getFormControlFromContainer, isControlContainerVoidOrInitialized } from '../get-form-control-from-container'; 9 | import { FormDirective } from '../form/form.directive'; 10 | import { ReactiveValidationModuleConfiguration } from '../reactive-validation-module-configuration'; 11 | import { REACTIVE_VALIDATION_MODULE_CONFIGURATION_TOKEN } from '../reactive-validation-module-configuration-token'; 12 | import { getControlPath } from '../get-control-path'; 13 | 14 | @Component({ 15 | selector: 'arv-validation-messages', 16 | templateUrl: './validation-messages.component.html', 17 | encapsulation: ViewEncapsulation.None 18 | }) 19 | /** 20 | * The ValidationMessagesComponent shows validation messages for one to many FormControls. It either shows 21 | * messages specified within the reactive form model, or shows custom messages declared using the 22 | * ValidationMessageComponent. 23 | */ 24 | export class ValidationMessagesComponent implements AfterContentInit, OnDestroy, OnInit { 25 | @ContentChildren(ValidationMessageComponent) private messageComponents: QueryList | undefined; 26 | 27 | private _for: UntypedFormControl[] = []; 28 | private messageComponentsChangesSubscription = new Subscription(); 29 | private controlStatusChangesSubscription = new Subscription(); 30 | 31 | private formSubmitted: boolean | undefined = undefined; 32 | private formSubmittedSubscription = new Subscription(); 33 | 34 | constructor(@Optional() private controlContainer: ControlContainer, @Optional() formSubmitDirective: FormDirective, 35 | @Optional() @Inject(REACTIVE_VALIDATION_MODULE_CONFIGURATION_TOKEN) private configuration: ReactiveValidationModuleConfiguration) { 36 | if (formSubmitDirective) { 37 | this.formSubmitted = false; 38 | this.formSubmittedSubscription.add(formSubmitDirective.submitted.subscribe(() => { 39 | this.formSubmitted = true; 40 | })); 41 | } 42 | } 43 | 44 | ngOnInit() { 45 | this.initializeForOnInit(); 46 | } 47 | 48 | ngAfterContentInit() { 49 | this.messageComponentsChangesSubscription.add(this.messageComponents?.changes.subscribe(this.validateChildren)); 50 | this.validateChildren(); 51 | 52 | this._for.forEach(control => { 53 | this.handleControlStatusChange(control); 54 | }); 55 | } 56 | 57 | ngOnDestroy() { 58 | this.messageComponentsChangesSubscription.unsubscribe(); 59 | this.formSubmittedSubscription.unsubscribe(); 60 | this.controlStatusChangesSubscription.unsubscribe(); 61 | } 62 | 63 | isValid(): boolean { 64 | return this.getFirstErrorPerControl().length === 0; 65 | } 66 | 67 | getErrorMessages(): string[] { 68 | return this.getFirstErrorPerControl().filter(error => error.hasMessage()) 69 | .map(error => error.getMessage()); 70 | } 71 | 72 | private initializeForOnInit = () => {}; 73 | 74 | @Input() 75 | set for(controls: UntypedFormControl | (UntypedFormControl|string)[] | string) { 76 | if (!isControlContainerVoidOrInitialized(this.controlContainer)) { 77 | this.initializeForOnInit = () => this.for = controls; 78 | return; 79 | } 80 | 81 | if (!Array.isArray(controls)) { 82 | controls = controls !== undefined ? [controls] : []; 83 | } 84 | 85 | if (controls.length === 0) { 86 | throw new Error(`arv-validation-messages doesn't allow declaring an empty array as input to the 'for' attribute.`); 87 | } 88 | 89 | this._for = controls.map(control => typeof control === 'string' ? 90 | getFormControlFromContainer(control, this.controlContainer) : control); 91 | 92 | this.validateChildren(); 93 | 94 | this.controlStatusChangesSubscription.unsubscribe(); 95 | this.controlStatusChangesSubscription = new Subscription(); 96 | this._for.forEach(control => { 97 | this.controlStatusChangesSubscription.add(control.statusChanges.subscribe(() => { 98 | this.handleControlStatusChange(control); 99 | })); 100 | }); 101 | } 102 | 103 | 104 | 105 | private getFirstErrorPerControl() { 106 | return this._for.filter(control => this.configuration && this.configuration.displayValidationMessageWhen ? 107 | this.configuration.displayValidationMessageWhen(control, this.formSubmitted) : control.touched || this.formSubmitted 108 | ).map(ValidationError.fromFirstError).filter(value => value !== undefined) as ValidationError[]; 109 | } 110 | 111 | /** 112 | * Validates that the child ValidationMessageComponents declare what FormControl they specify a message for (when needed); and 113 | * that the declared FormControl is actually part of the parent ValidationMessagesComponent 'for' collection (when specified). 114 | */ 115 | private validateChildren() { 116 | if (!this.messageComponents) { 117 | return; 118 | } 119 | 120 | this.messageComponents.forEach(component => { 121 | if (this._for.length > 1 && component.for === undefined) { 122 | throw new Error(`Specify the FormControl for which the arv-validation-message element with key '${component.key}' ` + 123 | `should show messages.`); 124 | } 125 | if (component.for && this._for.indexOf(component.for as UntypedFormControl) === -1) { 126 | throw new Error(`A arv-validation-messages element with key '${component.key}' attempts to show messages ` + 127 | `for a FormControl that is not declared in the parent arv-validation-messages element.`); 128 | } 129 | }); 130 | } 131 | 132 | private handleControlStatusChange(control: UntypedFormControl) { 133 | if (!this.messageComponents) { 134 | return; 135 | } 136 | 137 | this.messageComponents.filter(component => component.for === control || component.for === undefined) 138 | .forEach(component => component.reset()); 139 | 140 | const error = ValidationError.fromFirstError(control); 141 | if (!error || error.hasMessage()) { 142 | return; 143 | } 144 | 145 | const messageComponent = this.messageComponents.find(component => component.canHandle(error)); 146 | 147 | if (messageComponent) { 148 | messageComponent.show(error); 149 | } else { 150 | throw new Error(`There is no suitable arv-validation-message element to show the '${error.key}' ` + 151 | `error of '${getControlPath(error.control)}'`); 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /projects/angular-reactive-validation/src/lib/validator-declaration.spec.ts: -------------------------------------------------------------------------------- 1 | import { UntypedFormControl, ValidationErrors, ValidatorFn, AbstractControl } from '@angular/forms'; 2 | 3 | import { ValidatorDeclaration } from './validator-declaration'; 4 | 5 | describe('ValidatorDeclaration', () => { 6 | describe('wrappers add a message when invalid', () => { 7 | const message = 'message on error'; 8 | const resultKey = 'resultKey'; 9 | const wrappedValidatorFn = (control: AbstractControl) => { 10 | const obj: any = {}; 11 | obj[resultKey] = true; 12 | return obj; 13 | }; 14 | 15 | const expectHasMessage = (validatorFn: ValidatorFn) => { 16 | const formControl = new UntypedFormControl(''); 17 | 18 | let result = validatorFn(formControl); 19 | 20 | expect(result).not.toBeNull(); 21 | result = result as ValidationErrors; 22 | expect(result[resultKey].message).toEqual(message); 23 | }; 24 | 25 | it(`wrapNoArgumentValidator`, () => { 26 | const validator = ValidatorDeclaration.wrapNoArgumentValidator(wrappedValidatorFn, resultKey); 27 | 28 | expectHasMessage(validator(message)); 29 | }); 30 | 31 | it(`wrapSingleArgumentValidator`, () => { 32 | const validator = ValidatorDeclaration.wrapSingleArgumentValidator((input: any) => wrappedValidatorFn, resultKey); 33 | 34 | expectHasMessage(validator(1, message)); 35 | }); 36 | 37 | it(`wrapTwoArgumentValidator`, () => { 38 | const validator = ValidatorDeclaration.wrapTwoArgumentValidator((input1: any, input2: any) => wrappedValidatorFn, resultKey); 39 | 40 | expectHasMessage(validator(1, 2, message)); 41 | }); 42 | }); 43 | 44 | describe('wrappers add a dynamic message when invalid', () => { 45 | const message = 'message on error'; 46 | const messageFn = () => message; 47 | const resultKey = 'resultKey'; 48 | const wrappedValidatorFn = (control: AbstractControl) => { 49 | const obj: any = {}; 50 | obj[resultKey] = true; 51 | return obj; 52 | }; 53 | 54 | const expectHasMessage = (validatorFn: ValidatorFn) => { 55 | const formControl = new UntypedFormControl(''); 56 | 57 | let result = validatorFn(formControl); 58 | 59 | expect(result).not.toBeNull(); 60 | result = result as ValidationErrors; 61 | expect(result[resultKey].message).toEqual(message); 62 | }; 63 | 64 | it(`wrapNoArgumentValidator`, () => { 65 | const validator = ValidatorDeclaration.wrapNoArgumentValidator(wrappedValidatorFn, resultKey); 66 | 67 | expectHasMessage(validator(messageFn)); 68 | }); 69 | 70 | it(`wrapSingleArgumentValidator`, () => { 71 | const validator = ValidatorDeclaration.wrapSingleArgumentValidator((input: any) => wrappedValidatorFn, resultKey); 72 | 73 | expectHasMessage(validator(1, messageFn)); 74 | }); 75 | 76 | it(`wrapTwoArgumentValidator`, () => { 77 | const validator = ValidatorDeclaration.wrapTwoArgumentValidator((input1: any, input2: any) => wrappedValidatorFn, resultKey); 78 | 79 | expectHasMessage(validator(1, 2, messageFn)); 80 | }); 81 | }); 82 | 83 | describe('wrappers get the validation value by calling the input function when specified', () => { 84 | it(`wrapSingleArgumentValidator`, () => { 85 | const spyObj = { 86 | validatorFn: (input1: number) => (control: AbstractControl) => { 87 | expect(input1).toEqual(i); 88 | return {}; 89 | } 90 | }; 91 | 92 | spyOn(spyObj, 'validatorFn').and.callThrough(); 93 | 94 | const validator = ValidatorDeclaration.wrapSingleArgumentValidator(spyObj.validatorFn, 'key'); 95 | 96 | let i = 0; 97 | const validatorFn = validator(() => i); 98 | const formControl = new UntypedFormControl(''); 99 | for (; i < 10; i++) { 100 | validatorFn(formControl); 101 | } 102 | 103 | expect(spyObj.validatorFn).toHaveBeenCalled(); 104 | }); 105 | 106 | it(`wrapTwoArgumentValidator`, () => { 107 | const spyObj = { 108 | validatorFn: (input1: number, input2: number) => (control: AbstractControl) => { 109 | expect(input1).toEqual(i); 110 | expect(input2).toEqual(j); 111 | return {}; 112 | } 113 | }; 114 | 115 | spyOn(spyObj, 'validatorFn').and.callThrough(); 116 | 117 | const validator = ValidatorDeclaration.wrapTwoArgumentValidator(spyObj.validatorFn, 'key'); 118 | 119 | let i = 0; 120 | let j = 10; 121 | const validatorFn = validator(() => i, () => j); 122 | const formControl = new UntypedFormControl(''); 123 | for (; i < 10; i++, j++) { 124 | validatorFn(formControl); 125 | } 126 | 127 | expect(spyObj.validatorFn).toHaveBeenCalled(); 128 | }); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /projects/angular-reactive-validation/src/lib/validator-declaration.ts: -------------------------------------------------------------------------------- 1 | import { ValidatorFn, ValidationErrors, AbstractControl } from '@angular/forms'; 2 | 3 | /** 4 | * @dynamic 5 | */ 6 | export class ValidatorDeclaration { 7 | /** 8 | * Wraps your own validator functions for use with the angular-reactive-validation library. 9 | * 10 | * @param validatorFn A function you want to wrap which can validate a control. 11 | * @param resultKey The error key used for indicating an error result as returned from the ValidatorFn. 12 | */ 13 | static wrapNoArgumentValidator(validatorFn: ValidatorFn, resultKey: string): 14 | (message?: string | (() => string)) => ValidatorFn { 15 | return (message?: string | (() => string)): ValidatorFn => (control: AbstractControl): ValidationErrors | null => 16 | ValidatorDeclaration.validateAndSetMessageIfInvalid(control, () => validatorFn, resultKey, message); 17 | } 18 | 19 | /** 20 | * Wraps your own validator functions for use with the angular-reactive-validation library. 21 | * 22 | * @param validatorFactoryFn A function which accepts a single argument and returns a ValidatorFn. 23 | * @param resultKey The error key used for indicating an error result as returned from the ValidatorFn. 24 | */ 25 | static wrapSingleArgumentValidator(validatorFactoryFn: ((arg1: TInput) => ValidatorFn), resultKey: string): 26 | (arg1: TInput | (() => TInput), message?: string | ((arg1: TInput) => string)) => ValidatorFn { 27 | 28 | return (arg1: TInput | (() => TInput), message?: string | ((arg1: TInput) => string)): ValidatorFn => 29 | (control: AbstractControl): ValidationErrors | null => { 30 | const unwrappedArg1 = ValidatorDeclaration.unwrapArgument(arg1); 31 | 32 | return ValidatorDeclaration.validateAndSetMessageIfInvalid(control, validatorFactoryFn, resultKey, message, unwrappedArg1); 33 | }; 34 | } 35 | 36 | /** 37 | * Wraps your own validator functions for use with the angular-reactive-validation library. 38 | * 39 | * @param validatorFactoryFn A function which accepts two arguments and returns a ValidatorFn. 40 | * @param resultKey The error key used for indicating an error result as returned from the ValidatorFn. 41 | */ 42 | static wrapTwoArgumentValidator(validatorFactoryFn: ((arg1: TInput1, arg2: TInput2) => ValidatorFn), resultKey: string): 43 | (arg1: TInput1 | (() => TInput1), arg2: TInput2 | (() => TInput2), message?: string | ((arg1: TInput1, arg2: TInput2) => string)) => 44 | ValidatorFn { 45 | 46 | return (arg1: TInput1 | (() => TInput1), arg2: TInput2 | (() => TInput2), 47 | message?: string | ((arg1: TInput1, arg2: TInput2) => string)): ValidatorFn => 48 | (control: AbstractControl): ValidationErrors | null => { 49 | const unwrappedArg1 = ValidatorDeclaration.unwrapArgument(arg1); 50 | const unwrappedArg2 = ValidatorDeclaration.unwrapArgument(arg2); 51 | 52 | return ValidatorDeclaration.validateAndSetMessageIfInvalid(control, validatorFactoryFn, resultKey, message, 53 | unwrappedArg1, unwrappedArg2); 54 | }; 55 | } 56 | 57 | private static unwrapArgument(arg: T | (() => T)): T { 58 | if (arg instanceof Function) { 59 | arg = arg(); 60 | } 61 | 62 | return arg; 63 | } 64 | 65 | private static validateAndSetMessageIfInvalid(control: AbstractControl, 66 | // eslint-disable-next-line @typescript-eslint/no-shadow 67 | validatorFactoryFn: (...args: any[]) => ValidatorFn, resultKey: string, 68 | // eslint-disable-next-line @typescript-eslint/no-shadow 69 | message?: string | ((...args: any[]) => string), ...args: any[]): ValidationErrors | null { 70 | 71 | const validationResult = ValidatorDeclaration.validate(control, validatorFactoryFn, ...args); 72 | ValidatorDeclaration.setMessageIfInvalid(control, resultKey, validationResult, message, ...args); 73 | 74 | return validationResult; 75 | } 76 | 77 | // eslint-disable-next-line @typescript-eslint/no-shadow 78 | private static validate(control: AbstractControl, validatorFactoryFn: (...args: any[]) => ValidatorFn, ...args: any[]): 79 | ValidationErrors | null { 80 | 81 | const wrappedValidatorFn = validatorFactoryFn(...args); 82 | return wrappedValidatorFn(control); 83 | } 84 | 85 | private static setMessageIfInvalid(control: AbstractControl, resultKey: string, 86 | // eslint-disable-next-line @typescript-eslint/no-shadow 87 | validationResult: ValidationErrors | null, message?: string | ((...args: any[]) => string), ...args: any[]) { 88 | if (message) { 89 | if (validationResult && validationResult[resultKey]) { 90 | if (typeof message === 'function') { 91 | message = message(...args); 92 | } 93 | 94 | // Not all validators set an object. Often they'll simply set a property to true. 95 | // Here, we replace any non-object (or array) to be an object on which we can set a message. 96 | if (!ValidatorDeclaration.isObject(validationResult[resultKey])) { 97 | validationResult[resultKey] = {}; 98 | } 99 | 100 | validationResult[resultKey]['message'] = message; 101 | } 102 | } 103 | } 104 | 105 | private static isObject(arg: any) { 106 | return arg !== null && typeof arg === 'object' && !Array.isArray(arg); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /projects/angular-reactive-validation/src/lib/validators.spec.ts: -------------------------------------------------------------------------------- 1 | import { Validators as AngularValidators, ValidationErrors } from '@angular/forms'; 2 | import { UntypedFormControl } from '@angular/forms'; 3 | 4 | import { Validators } from './validators'; 5 | 6 | describe('Validators', () => { 7 | it(`of the Angular framework don't contain a message property in the validation result`, () => { 8 | const invalidCombinations = [{ 9 | validatorFn: AngularValidators.min(0), 10 | control: new UntypedFormControl(-1) 11 | }, { 12 | validatorFn: AngularValidators.max(0), 13 | control: new UntypedFormControl(1) 14 | }, { 15 | validatorFn: AngularValidators.minLength(2), 16 | control: new UntypedFormControl('a') 17 | }, { 18 | validatorFn: AngularValidators.maxLength(2), 19 | control: new UntypedFormControl('abc') 20 | }, { 21 | validatorFn: AngularValidators.pattern(/a/), 22 | control: new UntypedFormControl('b') 23 | }, { 24 | validatorFn: AngularValidators.required, 25 | control: new UntypedFormControl(null) 26 | }, { 27 | validatorFn: AngularValidators.requiredTrue, 28 | control: new UntypedFormControl(false) 29 | }, { 30 | validatorFn: AngularValidators.email, 31 | control: new UntypedFormControl('davidwalschots@users@noreply.github.com') 32 | }]; 33 | 34 | invalidCombinations.forEach(combination => { 35 | const result = combination.validatorFn(combination.control); 36 | expect(result).not.toBeNull( 37 | `A validator deemed the control value '${combination.control.value}' to be valid. This shouldn't be the case`); 38 | expect((result as ValidationErrors)['message']).toBeUndefined( 39 | `The angular framework uses the 'message' property. This behaviour is overwritten by the library`); 40 | }); 41 | }); 42 | 43 | it(`give a validation result that is equal to the framework's validators`, () => { 44 | const combinations = [{ 45 | libraryValidatorFn: Validators.nullValidator, 46 | nativeValidatorFn: AngularValidators.nullValidator, 47 | controls: [new UntypedFormControl(1)] 48 | }, { 49 | libraryValidatorFn: Validators.min(0, 'message'), 50 | nativeValidatorFn: AngularValidators.min(0), 51 | controls: [new UntypedFormControl(-1), new UntypedFormControl(1)] 52 | }, { 53 | libraryValidatorFn: Validators.max(0, 'message'), 54 | nativeValidatorFn: AngularValidators.max(0), 55 | controls: [new UntypedFormControl(1), new UntypedFormControl(-1)] 56 | }, { 57 | libraryValidatorFn: Validators.minLength(2, 'message'), 58 | nativeValidatorFn: AngularValidators.minLength(2), 59 | controls: [new UntypedFormControl('a'), new UntypedFormControl('ab')] 60 | }, { 61 | libraryValidatorFn: Validators.maxLength(2, 'message'), 62 | nativeValidatorFn: AngularValidators.maxLength(2), 63 | controls: [new UntypedFormControl('abc'), new UntypedFormControl('ab')] 64 | }, { 65 | libraryValidatorFn: Validators.pattern(/a/, 'message'), 66 | nativeValidatorFn: AngularValidators.pattern(/a/), 67 | controls: [new UntypedFormControl('b'), new UntypedFormControl('a')] 68 | }, { 69 | libraryValidatorFn: Validators.required('message'), 70 | nativeValidatorFn: AngularValidators.required, 71 | controls: [new UntypedFormControl(null), new UntypedFormControl(123)] 72 | }, { 73 | libraryValidatorFn: Validators.requiredTrue('message'), 74 | nativeValidatorFn: AngularValidators.requiredTrue, 75 | controls: [new UntypedFormControl(false), new UntypedFormControl(true)] 76 | }, { 77 | libraryValidatorFn: Validators.email('message'), 78 | nativeValidatorFn: AngularValidators.email, 79 | controls: [new UntypedFormControl('davidwalschots@users@noreply.github.com'), new UntypedFormControl('davidwalschots@users.noreply.github.com')] 80 | }]; 81 | 82 | combinations.forEach(combination => { 83 | combination.controls.forEach(control => { 84 | const nativeResult = combination.nativeValidatorFn(control); 85 | let libraryResult = combination.libraryValidatorFn(control); 86 | 87 | // Below we perform operations to remove the message property, and replace an empty object 88 | // with true. This is not perfect for testing. But, the only way to work around Angular 89 | // sometimes using booleans and sometimes objects for specifying validation state. 90 | if (libraryResult) { 91 | for (const property in libraryResult) { 92 | if (libraryResult.hasOwnProperty(property)) { 93 | delete libraryResult[property].message; 94 | if (Object.getOwnPropertyNames(libraryResult[property]).length === 0) { 95 | libraryResult[property] = true; 96 | } 97 | } 98 | } 99 | } 100 | 101 | expect(libraryResult).toEqual(nativeResult); 102 | }); 103 | }); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /projects/angular-reactive-validation/src/lib/validators.ts: -------------------------------------------------------------------------------- 1 | import { Validators as AngularValidators, ValidatorFn } from '@angular/forms'; 2 | 3 | import { ValidatorDeclaration } from './validator-declaration'; 4 | 5 | /** 6 | * Provides a set of validators used by form controls. 7 | * 8 | * Code comments have been copied from the Angular source code. 9 | */ 10 | export class Validators { 11 | /** 12 | * No-op validator. 13 | */ 14 | static nullValidator = AngularValidators.nullValidator; 15 | static composeAsync = AngularValidators.composeAsync; 16 | 17 | private static minValidator = ValidatorDeclaration.wrapSingleArgumentValidator(AngularValidators.min, 'min'); 18 | private static maxValidator = ValidatorDeclaration.wrapSingleArgumentValidator(AngularValidators.max, 'max'); 19 | private static minLengthValidator = ValidatorDeclaration.wrapSingleArgumentValidator(AngularValidators.minLength, 'minlength'); 20 | private static maxLengthValidator = ValidatorDeclaration.wrapSingleArgumentValidator(AngularValidators.maxLength, 'maxlength'); 21 | private static patternValidator = ValidatorDeclaration.wrapSingleArgumentValidator(AngularValidators.pattern, 'pattern'); 22 | private static requiredValidator = ValidatorDeclaration.wrapNoArgumentValidator(AngularValidators.required, 'required'); 23 | private static requiredTrueValidator = ValidatorDeclaration.wrapNoArgumentValidator(AngularValidators.requiredTrue, 'required'); 24 | private static emailValidator = ValidatorDeclaration.wrapNoArgumentValidator(AngularValidators.email, 'email'); 25 | 26 | /** 27 | * Compose multiple validators into a single function that returns the union 28 | * of the individual error maps. 29 | */ 30 | static compose(validators: null): null; 31 | /** 32 | * Compose multiple validators into a single function that returns the union 33 | * of the individual error maps. 34 | */ 35 | static compose(validators: (ValidatorFn|null|undefined)[]): ValidatorFn|null; 36 | static compose(validators: (ValidatorFn|null|undefined)[]|null): ValidatorFn|null { 37 | return validators === null ? AngularValidators.compose(validators) : AngularValidators.compose(validators); 38 | } 39 | 40 | /** 41 | * Validator that requires controls to have a value greater than or equal to a number. 42 | * Note: when using this function without specifying a message, you have to declare an 43 | * arv-validation-message element in the HTML with a custom message. 44 | */ 45 | static min(min: number): ValidatorFn; 46 | /** 47 | * Validator that requires controls to have a value greater than or equal to a number. 48 | */ 49 | static min(min: number, message: string): ValidatorFn; 50 | /** 51 | * Validator that requires controls to have a value greater than or equal to a number. 52 | * Note: when using this function without specifying a message, you have to declare an 53 | * arv-validation-message element in the HTML with a custom message. 54 | */ 55 | static min(min: () => number): ValidatorFn; 56 | /** 57 | * Validator that requires controls to have a value greater than or equal to a number. 58 | */ 59 | static min(min: () => number, message: string): ValidatorFn; 60 | /** 61 | * Validator that requires controls to have a value greater than or equal to a number. 62 | */ 63 | static min(min: number, messageFunc: ((min: number) => string)): ValidatorFn; 64 | /** 65 | * Validator that requires controls to have a value greater than or equal to a number. 66 | */ 67 | static min(min: () => number, messageFunc: ((min: number) => string)): ValidatorFn; 68 | static min(min: number | (() => number), message?: string | ((min: number) => string)): ValidatorFn { 69 | return Validators.minValidator(min, message); 70 | } 71 | 72 | /** 73 | * Validator that requires controls to have a value less than or equal to a number. 74 | * Note: when using this function without specifying a message, you have to declare an 75 | * arv-validation-message element in the HTML with a custom message. 76 | */ 77 | static max(max: number): ValidatorFn; 78 | /** 79 | * Validator that requires controls to have a value less than or equal to a number. 80 | */ 81 | static max(max: number, message: string): ValidatorFn; 82 | /** 83 | * Validator that requires controls to have a value less than or equal to a number. 84 | * Note: when using this function without specifying a message, you have to declare an 85 | * arv-validation-message element in the HTML with a custom message. 86 | */ 87 | static max(max: () => number): ValidatorFn; 88 | /** 89 | * Validator that requires controls to have a value less than or equal to a number. 90 | */ 91 | static max(max: () => number, message: string): ValidatorFn; 92 | /** 93 | * Validator that requires controls to have a value less than or equal to a number. 94 | */ 95 | static max(max: number, messageFunc: ((max: number) => string)): ValidatorFn; 96 | /** 97 | * Validator that requires controls to have a value less than or equal to a number. 98 | */ 99 | static max(max: () => number, messageFunc: ((max: number) => string)): ValidatorFn; 100 | static max(max: number | (() => number), message?: string | ((max: number) => string)): ValidatorFn { 101 | return Validators.maxValidator(max, message); 102 | } 103 | 104 | /** 105 | * Validator that requires controls to have a value of a minimum length. 106 | * Note: when using this function without specifying a message, you have to declare an 107 | * arv-validation-message element in the HTML with a custom message. 108 | */ 109 | static minLength(minLength: number): ValidatorFn; 110 | /** 111 | * Validator that requires controls to have a value of a minimum length. 112 | */ 113 | static minLength(minLength: number, message: string): ValidatorFn; 114 | /** 115 | * Validator that requires controls to have a value of a minimum length. 116 | * Note: when using this function without specifying a message, you have to declare an 117 | * arv-validation-message element in the HTML with a custom message. 118 | */ 119 | static minLength(minLength: () => number): ValidatorFn; 120 | /** 121 | * Validator that requires controls to have a value of a minimum length. 122 | */ 123 | static minLength(minLength: () => number, message: string): ValidatorFn; 124 | /** 125 | * Validator that requires controls to have a value of a minimum length. 126 | */ 127 | static minLength(minLength: number, messageFunc: ((minLength: number) => string)): ValidatorFn; 128 | /** 129 | * Validator that requires controls to have a value of a minimum length. 130 | */ 131 | static minLength(minLength: () => number, messageFunc: ((minLength: number) => string)): ValidatorFn; 132 | static minLength(minLength: number | (() => number), message?: string | ((minLength: number) => string)): ValidatorFn { 133 | return Validators.minLengthValidator(minLength, message); 134 | } 135 | 136 | /** 137 | * Validator that requires controls to have a value of a maximum length. 138 | * Note: when using this function without specifying a message, you have to declare an 139 | * arv-validation-message element in the HTML with a custom message. 140 | */ 141 | static maxLength(maxLength: number): ValidatorFn; 142 | /** 143 | * Validator that requires controls to have a value of a maximum length. 144 | */ 145 | static maxLength(maxLength: number, message: string): ValidatorFn; 146 | /** 147 | * Validator that requires controls to have a value of a maximum length. 148 | * Note: when using this function without specifying a message, you have to declare an 149 | * arv-validation-message element in the HTML with a custom message. 150 | */ 151 | static maxLength(maxLength: () => number): ValidatorFn; 152 | /** 153 | * Validator that requires controls to have a value of a maximum length. 154 | */ 155 | static maxLength(maxLength: () => number, message: string): ValidatorFn; 156 | /** 157 | * Validator that requires controls to have a value of a maximum length. 158 | */ 159 | static maxLength(maxLength: number, messageFunc: ((maxLength: number) => string)): ValidatorFn; 160 | /** 161 | * Validator that requires controls to have a value of a maximum length. 162 | */ 163 | static maxLength(maxLength: () => number, messageFunc: ((maxLength: number) => string)): ValidatorFn; 164 | static maxLength(maxLength: number | (() => number), message?: string | ((maxLength: number) => string)): ValidatorFn { 165 | return Validators.maxLengthValidator(maxLength, message); 166 | } 167 | 168 | /** 169 | * Validator that requires a control to match a regex to its value. 170 | * Note: when using this function without specifying a message, you have to declare an 171 | * arv-validation-message element in the HTML with a custom message. 172 | */ 173 | static pattern(pattern: string|RegExp): ValidatorFn; 174 | /** 175 | * Validator that requires a control to match a regex to its value. 176 | */ 177 | static pattern(pattern: string|RegExp, message: string): ValidatorFn; 178 | /** 179 | * Validator that requires a control to match a regex to its value. 180 | * Note: when using this function without specifying a message, you have to declare an 181 | * arv-validation-message element in the HTML with a custom message. 182 | */ 183 | static pattern(pattern: () => string|RegExp): ValidatorFn; 184 | /** 185 | * Validator that requires a control to match a regex to its value. 186 | */ 187 | static pattern(pattern: () => string|RegExp, message: string): ValidatorFn; 188 | static pattern(pattern: (string|RegExp) | (() => string|RegExp), message?: string): ValidatorFn { 189 | return Validators.patternValidator(pattern, message); 190 | } 191 | 192 | /** 193 | * Validator that requires controls to have a non-empty value. 194 | * Note: when using this function without specifying a message, you have to declare an 195 | * arv-validation-message element in the HTML with a custom message. 196 | */ 197 | static required(): ValidatorFn; 198 | /** 199 | * Validator that requires controls to have a non-empty value. 200 | */ 201 | static required(message: string): ValidatorFn; 202 | static required(message?: string): ValidatorFn { 203 | return Validators.requiredValidator(message); 204 | } 205 | 206 | /** 207 | * Validator that requires control value to be true. 208 | * Note: when using this function without specifying a message, you have to declare an 209 | * arv-validation-message element in the HTML with a custom message. 210 | */ 211 | static requiredTrue(): ValidatorFn; 212 | /** 213 | * Validator that requires control value to be true. 214 | */ 215 | static requiredTrue(message: string): ValidatorFn; 216 | static requiredTrue(message?: string): ValidatorFn { 217 | return Validators.requiredTrueValidator(message); 218 | } 219 | 220 | /** 221 | * Validator that performs email validation. 222 | * Note: when using this function without specifying a message, you have to declare an 223 | * arv-validation-message element in the HTML with a custom message. 224 | */ 225 | static email(): ValidatorFn; 226 | /** 227 | * Validator that performs email validation. 228 | */ 229 | static email(message: string): ValidatorFn; 230 | static email(message?: string): ValidatorFn { 231 | return Validators.emailValidator(message); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /projects/angular-reactive-validation/src/public-api.ts: -------------------------------------------------------------------------------- 1 | export { FormDirective } from './lib/form/form.directive'; 2 | export { ReactiveValidationModule } from './lib/reactive-validation.module'; 3 | export { ReactiveValidationModuleConfiguration } from './lib/reactive-validation-module-configuration'; 4 | export { Validators } from './lib/validators'; 5 | export { ValidatorDeclaration } from './lib/validator-declaration'; 6 | export { ValidationMessagesComponent } from './lib/validation-messages/validation-messages.component'; 7 | export { ValidationMessageComponent } from './lib/validation-message/validation-message.component'; 8 | -------------------------------------------------------------------------------- /projects/angular-reactive-validation/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/lib", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [] 10 | }, 11 | "exclude": [ 12 | "**/*.spec.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/angular-reactive-validation/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /projects/angular-reactive-validation/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "**/*.spec.ts", 12 | "**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/test-app/src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidwalschots/angular-reactive-validation/46410a1f85982170dc1ae4994d2237e2adbe3208/projects/test-app/src/app/app.component.css -------------------------------------------------------------------------------- /projects/test-app/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 5 |
6 |
7 |
8 | 12 | 16 | 20 |
21 | 22 | 27 | minLength validation test!!! 28 | {{ minlengthValidation.context?.requiredLength }} 29 | 30 | 31 |
32 |
33 | 37 | 38 |
39 |
40 | 41 |
42 |

Form value: {{ form.value | json }}

43 |

Form status: {{ form.status | json }}

44 |
45 | -------------------------------------------------------------------------------- /projects/test-app/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { UntypedFormBuilder } from '@angular/forms'; 2 | import { TestBed, waitForAsync } from '@angular/core/testing'; 3 | import { AppComponent } from './app.component'; 4 | import { Component } from '@angular/core'; 5 | describe('AppComponent', () => { 6 | beforeEach(waitForAsync(() => { 7 | TestBed.configureTestingModule({ 8 | declarations: [AppComponent, MockValidationMessageComponent, MockValidationMessagesComponent], 9 | providers: [UntypedFormBuilder], 10 | }).compileComponents(); 11 | })); 12 | 13 | it('should create the app', waitForAsync(() => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.debugElement.componentInstance; 16 | expect(app).toBeTruthy(); 17 | })); 18 | it(`should have as title 'app'`, waitForAsync(() => { 19 | const fixture = TestBed.createComponent(AppComponent); 20 | const app = fixture.debugElement.componentInstance; 21 | expect(app.title).toEqual('app'); 22 | })); 23 | // it('should render title in a h1 tag', waitForAsync(() => { 24 | // const fixture = TestBed.createComponent(AppComponent); 25 | // fixture.detectChanges(); 26 | // const compiled = fixture.debugElement.nativeElement; 27 | // expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!'); 28 | // })); 29 | }); 30 | 31 | @Component({ 32 | selector: 'arv-validation-message', 33 | template: '', 34 | }) 35 | class MockValidationMessageComponent { } 36 | 37 | 38 | @Component({ 39 | selector: 'arv-validation-messages', 40 | template: '', 41 | }) 42 | class MockValidationMessagesComponent { } 43 | -------------------------------------------------------------------------------- /projects/test-app/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { UntypedFormBuilder } from '@angular/forms'; 3 | import { Validators } from 'angular-reactive-validation'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | templateUrl: './app.component.html', 8 | styleUrls: ['./app.component.css'] 9 | }) 10 | export class AppComponent { 11 | 12 | title = 'app'; 13 | 14 | form = this.fb.group({ 15 | name: this.fb.group({ 16 | firstName: ['', [Validators.required('A first name is required'), 17 | Validators.minLength(5), 18 | Validators.maxLength(10, (maxLength => `Maximum length is ${maxLength}`))]], 19 | middleName: ['', [Validators.maxLength(50, (maxLength => `Maximum length is ${maxLength}`))]], 20 | lastName: ['', [Validators.required('A last name is required'), 21 | Validators.maxLength(() => 10 * 5, (maxLength => `Maximum length is ${maxLength}`))]] 22 | }), 23 | age: [null, [ 24 | Validators.required('An age is required'), 25 | Validators.min(0, `You can't be less than zero years old.`), 26 | Validators.max(150, (max => `Can't be more than ${max}`)) 27 | ]] 28 | }); 29 | 30 | constructor(private fb: UntypedFormBuilder) { 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /projects/test-app/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | import { ReactiveValidationModule } from 'angular-reactive-validation'; 5 | 6 | import { AppComponent } from './app.component'; 7 | 8 | @NgModule({ 9 | declarations: [ 10 | AppComponent 11 | ], 12 | imports: [ 13 | BrowserModule, 14 | ReactiveFormsModule, 15 | ReactiveValidationModule 16 | ], 17 | providers: [], 18 | bootstrap: [AppComponent] 19 | }) 20 | export class AppModule { } 21 | -------------------------------------------------------------------------------- /projects/test-app/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidwalschots/angular-reactive-validation/46410a1f85982170dc1ae4994d2237e2adbe3208/projects/test-app/src/assets/.gitkeep -------------------------------------------------------------------------------- /projects/test-app/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidwalschots/angular-reactive-validation/46410a1f85982170dc1ae4994d2237e2adbe3208/projects/test-app/src/favicon.ico -------------------------------------------------------------------------------- /projects/test-app/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TestApp 6 | 7 | 8 | 9 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /projects/test-app/src/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | 3 | import { AppModule } from './app/app.module'; 4 | 5 | 6 | platformBrowserDynamic().bootstrapModule(AppModule) 7 | .catch(err => console.error(err)); 8 | -------------------------------------------------------------------------------- /projects/test-app/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /projects/test-app/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/test-app/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "paths": { 6 | "angular-reactive-validation": [ 7 | "dist/angular-reactive-validation" 8 | ] 9 | }, 10 | "baseUrl": "./", 11 | "outDir": "./dist/out-tsc", 12 | "forceConsistentCasingInFileNames": true, 13 | "strict": true, 14 | "noImplicitOverride": true, 15 | "noPropertyAccessFromIndexSignature": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "sourceMap": true, 19 | "declaration": false, 20 | "downlevelIteration": true, 21 | "experimentalDecorators": true, 22 | "moduleResolution": "node", 23 | "importHelpers": true, 24 | "target": "ES2022", 25 | "module": "ES2022", 26 | "useDefineForClassFields": false, 27 | "lib": [ 28 | "ES2022", 29 | "dom" 30 | ] 31 | }, 32 | "angularCompilerOptions": { 33 | "enableI18nLegacyMessageIdFormat": false, 34 | "strictInjectionParameters": true, 35 | "strictInputAccessModifiers": true, 36 | "strictTemplates": true 37 | } 38 | } 39 | --------------------------------------------------------------------------------