├── .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 |
`
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------