├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── README.md ├── angular.json ├── package-lock.json ├── package.json ├── projects ├── demo │ ├── .browserslistrc │ ├── .eslintrc.json │ ├── src │ │ ├── app │ │ │ ├── app.component.html │ │ │ ├── app.component.scss │ │ │ ├── app.component.ts │ │ │ └── async-min-length-validator.directive.ts │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── main.ts │ │ ├── polyfills.ts │ │ ├── styles.scss │ │ └── test.ts │ ├── tsconfig.app.json │ └── tsconfig.spec.json └── ngx-mat-errors │ ├── .eslintrc.json │ ├── ng-package.json │ ├── package.json │ ├── src │ ├── lib │ │ ├── locales │ │ │ ├── en.ts │ │ │ ├── hu.ts │ │ │ ├── index.ts │ │ │ └── pt-br.ts │ │ ├── ngx-mat-error-control.ts │ │ ├── ngx-mat-error-def.directive.spec.ts │ │ ├── ngx-mat-error-def.directive.ts │ │ ├── ngx-mat-errors-for-date-range-picker.directive.spec.ts │ │ ├── ngx-mat-errors-for-date-range-picker.directive.ts │ │ ├── ngx-mat-errors.component.spec.ts │ │ ├── ngx-mat-errors.component.ts │ │ ├── ngx-mat-errors.module.ts │ │ ├── types.ts │ │ └── utils │ │ │ ├── coerce-to-observable.spec.ts │ │ │ ├── coerce-to-observable.ts │ │ │ ├── distinct-until-error-changed.spec.ts │ │ │ ├── distinct-until-error-changed.ts │ │ │ ├── find-error-for-control.ts │ │ │ ├── get-abstract-controls.ts │ │ │ └── get-control-with-error.ts │ └── public-api.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.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 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": [ 4 | "projects/**/*" 5 | ], 6 | "overrides": [ 7 | { 8 | "files": [ 9 | "*.ts" 10 | ], 11 | "parserOptions": { 12 | "project": [ 13 | "tsconfig.json" 14 | ], 15 | "createDefaultProgram": true 16 | }, 17 | "extends": [ 18 | "plugin:@angular-eslint/recommended", 19 | "plugin:@angular-eslint/template/process-inline-templates" 20 | ], 21 | "rules": { 22 | "@angular-eslint/component-selector": [ 23 | "error", 24 | { 25 | "prefix": "lib", 26 | "style": "kebab-case", 27 | "type": "element" 28 | } 29 | ], 30 | "@angular-eslint/directive-selector": [ 31 | "error", 32 | { 33 | "prefix": "lib", 34 | "style": "camelCase", 35 | "type": "attribute" 36 | } 37 | ] 38 | } 39 | }, 40 | { 41 | "files": [ 42 | "*.html" 43 | ], 44 | "extends": [ 45 | "plugin:@angular-eslint/template/recommended" 46 | ], 47 | "rules": {} 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /.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 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.angular/cache 36 | /.sass-cache 37 | /connect.lock 38 | /coverage 39 | /libpeerconnection.log 40 | npm-debug.log 41 | yarn-error.log 42 | testem.log 43 | /typings 44 | 45 | # System Files 46 | .DS_Store 47 | Thumbs.db 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NgxMatErrors 2 | 3 | [![npm version](https://img.shields.io/npm/v/ngx-mat-errors.svg?style=flat-square)](https://www.npmjs.com/package/ngx-mat-errors) 4 | [![npm downloads total](https://img.shields.io/npm/dt/ngx-mat-errors.svg?style=flat-square)](https://www.npmjs.com/package/ngx-mat-errors) 5 | [![npm downloads monthly](https://img.shields.io/npm/dm/ngx-mat-errors.svg?style=flat-square)](https://www.npmjs.com/package/ngx-mat-errors) 6 | 7 | ## What Does It Do? 8 | 9 | NgxMatErrors provides an easy yet flexible solution for displaying error messages in a `MatFormField`. 10 | 11 | ## Try It 12 | 13 | See it in action on [StackBlitz](https://stackblitz.com/edit/ngx-mat-errors-angular-19?file=src%2Fapp%2Fapp.component.html). 14 | 15 | ## How to Use It 16 | 17 | Install `ngx-mat-errors` in your project: 18 | 19 | ```sh 20 | npm install ngx-mat-errors 21 | ``` 22 | 23 | Import `NgxMatErrorsModule` and provide `NGX_MAT_ERROR_CONFIG_EN` (or your custom error messages) in your `app.module.ts`. 24 | 25 | ```typescript 26 | import { NgxMatErrorsModule, NGX_MAT_ERROR_CONFIG_EN } from "ngx-mat-errors"; 27 | 28 | @NgModule({ 29 | imports: [ 30 | ..., 31 | NgxMatErrorsModule 32 | ], 33 | providers: [NGX_MAT_ERROR_CONFIG_EN], 34 | }) 35 | export class AppModule {} 36 | ``` 37 | 38 | Or you can import only `NgxMatErrors` and `NgxMatErrorDef` as they are marked standalone. 39 | 40 | ```typescript 41 | import { NgxMatErrors, NgxMatErrorDef, NGX_MAT_ERROR_CONFIG_EN } from "ngx-mat-errors"; 42 | 43 | @NgModule({ 44 | imports: [ 45 | ..., 46 | NgxMatErrors, 47 | NgxMatErrorDef 48 | ], 49 | providers: [NGX_MAT_ERROR_CONFIG_EN], 50 | }) 51 | export class AppModule {} 52 | ``` 53 | 54 | Add `[ngx-mat-errors]` to your `mat-error` in your `mat-form-field`. 55 | 56 | ```html 57 | 58 | Label 59 | 60 | 61 | 62 | ``` 63 | 64 | ### Outside a `MatFormField` or Override the Control 65 | 66 | `ngx-mat-errors` can be used as an `@Input()` to assign a control manually. 67 | 68 | #### Reactive Forms 69 | 70 | ```html 71 | 72 | Label 73 | 74 | 75 | 76 | ``` 77 | 78 | #### Template-Driven Forms 79 | 80 | ```html 81 | 82 | Label 83 | 84 | 85 | 86 | ``` 87 | 88 | ### Multiple Controls 89 | 90 | It can display errors for multiple controls, one at a time. The order of the controls is important; the first control with an error will be displayed. 91 | 92 | ```html 93 | 94 | Label 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | ``` 103 | 104 | #### `NgxMatErrorsForDateRangePicker` Directive 105 | 106 | ```typescript 107 | import { NgxMatErrorsForDateRangePicker } from "ngx-mat-errors"; 108 | ``` 109 | 110 | You can use the `[forDateRangePicker]` standalone directive to display errors for the `MatDateRangePicker` component. The directive assigns the controls used in the `MatDateRangeInput` to the `NgxMatErrors` component. 111 | 112 | ```html 113 | 114 | Label 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | ``` 123 | 124 | You can easily create directives like this to display errors in a `MatFormField` with multiple controls, look fot the implementation of `NgxMatErrorsForDateRangePicker`. 125 | 126 | ## Customize 127 | 128 | There are two ways to customize your error messages. 129 | 130 | ### Injection Token 131 | 132 | There is the `NGX_MAT_ERROR_DEFAULT_OPTIONS` injection token. You can provide it in your `app.module.ts` with `useClass` or `useFactory` and customize your error messages globally. 133 | 134 | This example changes only the `min` error message. 135 | 136 | ```typescript 137 | import { 138 | errorMessagesEnFactory, 139 | NGX_MAT_ERROR_DEFAULT_OPTIONS 140 | } from 'ngx-mat-errors'; 141 | import { FactoryProvider, LOCALE_ID } from '@angular/core'; 142 | 143 | export const NGX_MAT_ERROR_DEFAULT_CONFIG: FactoryProvider = { 144 | useFactory: (locale: string) => ({ 145 | ...errorMessagesEnFactory(locale), 146 | min: (error: MinError) => 147 | `Min value is ${error.min}, actual is ${error.actual}`, 148 | }), 149 | provide: NGX_MAT_ERROR_DEFAULT_OPTIONS, 150 | deps: [LOCALE_ID], 151 | }; 152 | 153 | @NgModule({ 154 | ... 155 | providers: [NGX_MAT_ERROR_DEFAULT_CONFIG], 156 | }) 157 | export class AppModule {} 158 | ``` 159 | 160 | You can provide an `Observable` too, which allows changes of error messages. This comes in handy when your app supports JIT localization with libraries like `@ngx-translate`. 161 | 162 | ```typescript 163 | import { 164 | NGX_MAT_ERROR_DEFAULT_OPTIONS 165 | } from 'ngx-mat-errors'; 166 | import { FactoryProvider, LOCALE_ID } from '@angular/core'; 167 | import { TranslateModule, TranslateService } from '@ngx-translate/core'; 168 | import { Observable, startWith, map } from 'rxjs'; 169 | 170 | export const NGX_MAT_ERROR_DEFAULT_CONFIG: FactoryProvider = { 171 | useFactory: ( 172 | locale: string, 173 | translateService: TranslateService 174 | ): Observable => translateService.onLangChange.pipe( 175 | startWith(null), 176 | map(() => ({ 177 | required: translateService.instant('core.validations.required'), 178 | minlength: (error: MinError) => translateService.instant('core.validations.minlength', error), 179 | ... 180 | })) 181 | ), 182 | provide: NGX_MAT_ERROR_DEFAULT_OPTIONS, 183 | deps: [LOCALE_ID, TranslateService], 184 | }; 185 | 186 | @NgModule({ 187 | ... 188 | providers: [NGX_MAT_ERROR_DEFAULT_CONFIG], 189 | }) 190 | export class AppModule {} 191 | ``` 192 | 193 | ### \*ngxMatErrorDef 194 | 195 | You can customize your error messages even more with the `*ngxMatErrorDef` directive. 196 | 197 | ```html 198 | 199 | Label 200 | 201 | 202 | Only digits are allowed, up to 12 digits. 203 | The minimum value is {{ error.min }}. 204 | 205 | 206 | ``` 207 | 208 | When used with multiple controls, you can specify the control for which the error message is intended. 209 | 210 | ```html 211 | 212 | Label 213 | 214 | 215 | 216 | 217 | 218 | 219 | Start date is required. 220 | End date is required. 221 | 222 | 223 | ``` 224 | 225 | ## Compatibility 226 | 227 | - `@angular/core: ^19.0.0 || ^20.0.0` 228 | - `@angular/material: ^19.0.0 || ^20.0.0` 229 | 230 | ### Reactive Forms 231 | 232 | #### Errors Inside a `MatFormField` 233 | 234 | ```html 235 | 236 | Label 237 | 238 | 239 | 240 | ``` 241 | 242 | #### Errors Outside a `MatFormField` 243 | 244 | ```html 245 | 246 | 247 | Label 248 | 249 | 250 | ``` 251 | 252 | #### Errors for Multiple Controls 253 | 254 | ```html 255 | 256 | Label 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | ``` 265 | 266 | ### Template-Driven Forms 267 | 268 | #### Errors Inside a `MatFormField` 269 | 270 | ```html 271 | 272 | Label 273 | 274 | 275 | 276 | ``` 277 | 278 | #### Errors Outside a `MatFormField` 279 | 280 | ```html 281 | 282 | 283 | Label 284 | 285 | 286 | ``` 287 | 288 | #### Errors for Multiple Controls 289 | 290 | ```html 291 | 292 | Label 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | ``` 301 | 302 | ## Development 303 | 304 | ### Library Build / NPM Package 305 | 306 | Run `npm run develop` to build the library and generate an NPM package. The build artifacts will be stored in the `dist/ngx-mat-errors` folder. 307 | 308 | ### Development Server 309 | 310 | Run `npm start` for a dev server. Navigate to `http://localhost:4202/`. The app will automatically reload if you change any of the source files. 311 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ngx-mat-errors": { 7 | "projectType": "library", 8 | "root": "projects/ngx-mat-errors", 9 | "sourceRoot": "projects/ngx-mat-errors/src", 10 | "prefix": "lib", 11 | "architect": { 12 | "build": { 13 | "builder": "@angular-devkit/build-angular:ng-packagr", 14 | "options": { 15 | "tsConfig": "projects/ngx-mat-errors/tsconfig.lib.json", 16 | "project": "projects/ngx-mat-errors/ng-package.json" 17 | }, 18 | "configurations": { 19 | "production": { 20 | "tsConfig": "projects/ngx-mat-errors/tsconfig.lib.prod.json" 21 | } 22 | } 23 | }, 24 | "test": { 25 | "builder": "@angular-devkit/build-angular:karma", 26 | "options": { 27 | "tsConfig": "projects/ngx-mat-errors/tsconfig.spec.json", 28 | "polyfills": ["zone.js", "zone.js/testing"] 29 | } 30 | }, 31 | "lint": { 32 | "builder": "@angular-eslint/builder:lint", 33 | "options": { 34 | "lintFilePatterns": [ 35 | "projects/ngx-mat-errors/**/*.ts", 36 | "projects/ngx-mat-errors/**/*.html" 37 | ] 38 | } 39 | } 40 | } 41 | }, 42 | "demo": { 43 | "projectType": "application", 44 | "schematics": { 45 | "@schematics/angular:component": { 46 | "style": "scss" 47 | } 48 | }, 49 | "root": "projects/demo", 50 | "sourceRoot": "projects/demo/src", 51 | "prefix": "", 52 | "architect": { 53 | "build": { 54 | "builder": "@angular-devkit/build-angular:application", 55 | "options": { 56 | "outputPath": { 57 | "base": "dist/demo" 58 | }, 59 | "index": "projects/demo/src/index.html", 60 | "browser": "projects/demo/src/main.ts", 61 | "polyfills": ["zone.js"], 62 | "tsConfig": "projects/demo/tsconfig.app.json", 63 | "inlineStyleLanguage": "scss", 64 | "assets": [ 65 | "projects/demo/src/favicon.ico", 66 | "projects/demo/src/assets" 67 | ], 68 | "styles": [ 69 | "./node_modules/@angular/material/prebuilt-themes/azure-blue.css", 70 | "projects/demo/src/styles.scss" 71 | ], 72 | "scripts": [] 73 | }, 74 | "configurations": { 75 | "production": { 76 | "fileReplacements": [ 77 | { 78 | "replace": "projects/demo/src/environments/environment.ts", 79 | "with": "projects/demo/src/environments/environment.prod.ts" 80 | } 81 | ], 82 | "outputHashing": "all", 83 | "budgets": [ 84 | { 85 | "type": "initial", 86 | "maximumWarning": "2mb", 87 | "maximumError": "5mb" 88 | }, 89 | { 90 | "type": "anyComponentStyle", 91 | "maximumWarning": "6kb", 92 | "maximumError": "10kb" 93 | } 94 | ] 95 | }, 96 | "development": { 97 | "optimization": false, 98 | "extractLicenses": false, 99 | "sourceMap": true 100 | } 101 | }, 102 | "defaultConfiguration": "production" 103 | }, 104 | "serve": { 105 | "builder": "@angular-devkit/build-angular:dev-server", 106 | "options": { 107 | "buildTarget": "demo:build" 108 | }, 109 | "configurations": { 110 | "production": { 111 | "buildTarget": "demo:build:production" 112 | }, 113 | "development": { 114 | "buildTarget": "demo:build:development" 115 | } 116 | }, 117 | "defaultConfiguration": "development" 118 | }, 119 | "test": { 120 | "builder": "@angular-devkit/build-angular:karma", 121 | "options": { 122 | "polyfills": ["zone.js", "zone.js/testing"], 123 | "tsConfig": "projects/demo/tsconfig.spec.json", 124 | "assets": [ 125 | "projects/demo/src/favicon.ico", 126 | "projects/demo/src/assets" 127 | ], 128 | "styles": [ 129 | "./node_modules/@angular/material/prebuilt-themes/azure-blue.css", 130 | "projects/demo/src/styles.scss" 131 | ], 132 | "scripts": [] 133 | } 134 | }, 135 | "lint": { 136 | "builder": "@angular-eslint/builder:lint", 137 | "options": { 138 | "lintFilePatterns": [ 139 | "projects/demo/**/*.ts", 140 | "projects/demo/**/*.html" 141 | ] 142 | } 143 | } 144 | } 145 | } 146 | }, 147 | "cli": { 148 | "analytics": "07e3ef88-7467-415e-9635-342c7918adc7", 149 | "schematicCollections": ["@angular-eslint/schematics"] 150 | }, 151 | "schematics": { 152 | "@angular-eslint/schematics:application": { 153 | "setParserOptionsProject": true 154 | }, 155 | "@angular-eslint/schematics:library": { 156 | "setParserOptionsProject": true 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-mat-errors", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve --port 4202", 7 | "build": "ng build", 8 | "build:prod": "ng build demo --configuration production ", 9 | "build:lib": "ng build ngx-mat-errors --configuration production && npm run copy-readme", 10 | "test": "ng test ngx-mat-errors", 11 | "lint": "ng lint", 12 | "develop": "ng build ngx-mat-errors --watch", 13 | "copy-readme": "copyfiles ./README.md dist/ngx-mat-errors" 14 | }, 15 | "private": true, 16 | "dependencies": { 17 | "@angular/animations": "^19.0.0", 18 | "@angular/cdk": "^19.0.0", 19 | "@angular/common": "^19.0.0", 20 | "@angular/compiler": "^19.0.0", 21 | "@angular/core": "^19.0.0", 22 | "@angular/forms": "^19.0.0", 23 | "@angular/material": "^19.0.0", 24 | "@angular/platform-browser": "^19.0.0", 25 | "@angular/platform-browser-dynamic": "^19.0.0", 26 | "@angular/router": "^19.0.0", 27 | "rxjs": "^7.8.0", 28 | "tslib": "^2.3.0", 29 | "zone.js": "~0.15.0" 30 | }, 31 | "devDependencies": { 32 | "@angular-devkit/build-angular": "^20.0.1", 33 | "@angular-devkit/core": "^19.0.0", 34 | "@angular-devkit/schematics": "^19.0.0", 35 | "@angular-eslint/builder": "^19.0.0", 36 | "@angular-eslint/eslint-plugin": "^19.0.0", 37 | "@angular-eslint/eslint-plugin-template": "^19.0.0", 38 | "@angular-eslint/schematics": "^19.0.0", 39 | "@angular-eslint/template-parser": "^19.0.0", 40 | "@angular/cli": "^19.0.0", 41 | "@angular/compiler-cli": "^19.0.0", 42 | "@angular/language-service": "^19.0.0", 43 | "@types/jasmine": "~5.1.0", 44 | "@typescript-eslint/eslint-plugin": "^7.2.0", 45 | "@typescript-eslint/parser": "^7.2.0", 46 | "copyfiles": "^2.4.1", 47 | "eslint": "^8.57.0", 48 | "jasmine-core": "~5.4.0", 49 | "karma": "~6.4.0", 50 | "karma-chrome-launcher": "~3.2.0", 51 | "karma-coverage": "~2.2.0", 52 | "karma-jasmine": "~5.1.0", 53 | "karma-jasmine-html-reporter": "~2.1.0", 54 | "ng-packagr": "^19.0.0", 55 | "typescript": "~5.6.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /projects/demo/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | last 2 Chrome versions 9 | last 1 Firefox version 10 | last 2 Edge major versions 11 | last 2 Safari major versions 12 | last 2 iOS major versions 13 | Firefox ESR 14 | -------------------------------------------------------------------------------- /projects/demo/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json", 3 | "ignorePatterns": [ 4 | "!**/*" 5 | ], 6 | "overrides": [ 7 | { 8 | "files": [ 9 | "*.ts" 10 | ], 11 | "parserOptions": { 12 | "project": [ 13 | "projects/demo/tsconfig.app.json", 14 | "projects/demo/tsconfig.spec.json" 15 | ], 16 | "createDefaultProgram": true 17 | }, 18 | "rules": { 19 | "@angular-eslint/directive-selector": [ 20 | "error", 21 | { 22 | "type": "attribute", 23 | "prefix": "app", 24 | "style": "camelCase" 25 | } 26 | ], 27 | "@angular-eslint/component-selector": [ 28 | "error", 29 | { 30 | "type": "element", 31 | "prefix": "", 32 | "style": "kebab-case" 33 | } 34 | ] 35 | } 36 | }, 37 | { 38 | "files": [ 39 | "*.html" 40 | ], 41 | "rules": {} 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /projects/demo/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | NgxMatErrors 2 |
3 |
4 | Outer NgxMatErrors control 5 | 6 | None 7 | Input 1 8 | Input 2 9 | 10 |
11 | 12 | 13 | 14 | Reactive forms 15 | 16 | 17 |

18 | A MatFormField with automatic error message and customized pattern 19 | message. 20 |

21 |

Error message outside of a mat-form-field. 22 | 23 | 24 | Only digits are allowed, up to 2 digits. Error: {{ error | json }} 25 | 26 | 27 |

28 |

You can use the setErrors method of a control to set any error.

29 | 30 | Input 1 31 | 32 | 33 | 34 | Only digits are allowed, up to 2 digits. Error: {{ error | json }} 35 | 36 | {{error}} 37 | 38 | Only digits are allowed, up to 2 digits. 39 | 40 |

Custom min and max error messages

41 | 42 | Input 2 43 | 44 | 45 | 46 | Maximum value is {{ error.max }}. Error: {{ error | json }} 47 | 48 | 49 | Min value is 10, max is 20 50 | 51 |

Async minLength validator

52 | 53 | Input 3 54 | 55 | 56 | Minimum length is 3 57 | 58 |

Multiple inputs

59 | 60 | Date range picker 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | Start date is 69 | required. 70 | End date is required. 71 | 72 | 73 | 74 | Timepicker 75 | 76 | 77 | 78 | 79 | 80 |
81 |
82 | 83 | 84 | 85 | Template driven forms 86 | 87 | 88 |

Error message outside of a mat-form-field.

89 | 90 | 91 | Only digits are allowed, up to 2 digits. Error: {{ error | json }} 92 | 93 | 94 |

95 | A MatFormField with automatic error message and customized pattern 96 | message. 97 |

98 | 99 | Input 1 100 | 102 | 103 | 104 | Only digits are allowed, up to 2 digits. Error: {{ error | json }} 105 | 106 | 107 | Only digits are allowed, up to 2 digits. 108 | 109 |

Custom min and max error messages

110 | 111 | Input 2 112 | 113 | 114 | 115 | Maximum value is {{ error.max }}. Error: {{ error | json }} 116 | 117 | 118 | Min value is 10, max is 20 119 | 120 |

Async minLength validator

121 | 122 | Input 3 123 | 124 | 125 | Minimum length is 3 126 | 127 |

Multiple controls

128 | 129 | Date range picker 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | End date is required. 138 | 139 | 140 | 141 | Timepicker 142 | 143 | 144 | 145 | 146 | 147 |
148 |
149 | 150 |
151 | -------------------------------------------------------------------------------- /projects/demo/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | p { 2 | margin-top: 0.5rem; 3 | margin-bottom: 0.5rem; 4 | } 5 | .container { 6 | max-width: 700px; 7 | margin-left: auto; 8 | margin-right: auto; 9 | overflow: auto; 10 | } 11 | 12 | fieldset { 13 | border: none; 14 | } 15 | fieldset, 16 | .mat-mdc-card { 17 | margin: 0.5rem; 18 | } 19 | 20 | .mat-mdc-form-field { 21 | width: 100%; 22 | } 23 | -------------------------------------------------------------------------------- /projects/demo/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { 3 | ChangeDetectionStrategy, 4 | Component, 5 | ViewEncapsulation, 6 | } from '@angular/core'; 7 | import { 8 | AbstractControl, 9 | AbstractControlDirective, 10 | FormControl, 11 | FormGroup, 12 | FormsModule, 13 | ReactiveFormsModule, 14 | Validators, 15 | } from '@angular/forms'; 16 | import { MatButtonModule } from '@angular/material/button'; 17 | import { MatCardModule } from '@angular/material/card'; 18 | import { MatNativeDateModule } from '@angular/material/core'; 19 | import { MatDatepickerModule } from '@angular/material/datepicker'; 20 | import { MatFormFieldModule } from '@angular/material/form-field'; 21 | import { MatInputModule } from '@angular/material/input'; 22 | import { MatRadioModule } from '@angular/material/radio'; 23 | import { MatTimepickerModule } from '@angular/material/timepicker'; 24 | import { MatToolbarModule } from '@angular/material/toolbar'; 25 | import { 26 | NgxMatErrorDef, 27 | NgxMatErrors, 28 | NgxMatErrorsForDateRangePicker, 29 | } from 'ngx-mat-errors'; 30 | import { delay, of } from 'rxjs'; 31 | import { AsyncMinLengthValidator } from './async-min-length-validator.directive'; 32 | 33 | @Component({ 34 | selector: 'body', 35 | templateUrl: './app.component.html', 36 | styleUrls: ['./app.component.scss'], 37 | changeDetection: ChangeDetectionStrategy.OnPush, 38 | encapsulation: ViewEncapsulation.None, 39 | imports: [ 40 | CommonModule, 41 | MatFormFieldModule, 42 | MatInputModule, 43 | NgxMatErrors, 44 | NgxMatErrorDef, 45 | MatCardModule, 46 | MatButtonModule, 47 | MatRadioModule, 48 | MatToolbarModule, 49 | ReactiveFormsModule, 50 | FormsModule, 51 | AsyncMinLengthValidator, 52 | MatDatepickerModule, 53 | MatNativeDateModule, 54 | MatTimepickerModule, 55 | NgxMatErrorsForDateRangePicker, 56 | ], 57 | }) 58 | export class AppComponent { 59 | readonly control1 = new FormControl('', [ 60 | Validators.required, 61 | Validators.pattern('[0-9]{0,2}'), 62 | ]); 63 | readonly control2 = new FormControl(null, [ 64 | Validators.min(10), 65 | Validators.max(20), 66 | ]); 67 | readonly control3 = new FormControl('', { 68 | asyncValidators: [ 69 | (control) => of(Validators.minLength(3)(control)).pipe(delay(250)), 70 | ], 71 | }); 72 | readonly time = new FormControl(new Date('2024-11-22 12:30')); 73 | 74 | readonly dateRange = new FormGroup({ 75 | start: new FormControl(null, [Validators.required]), 76 | end: new FormControl(null, [Validators.required]), 77 | }); 78 | 79 | readonly minDate = new Date(); 80 | 81 | value1: string | null = null; 82 | value2: number | null = null; 83 | value3: string | null = null; 84 | value4: Date | null = null; 85 | value5: Date | null = null; 86 | value6: Date | null = null; 87 | 88 | readonly outerErrorControl = new FormControl(null); 89 | getControl( 90 | control1: AbstractControl | AbstractControlDirective, 91 | control2: AbstractControl | AbstractControlDirective 92 | ) { 93 | switch (this.outerErrorControl.value) { 94 | case '1': 95 | return control1; 96 | case '2': 97 | return control2; 98 | default: 99 | return undefined; 100 | } 101 | } 102 | 103 | setError() { 104 | this.control1.setErrors({ customError: 'setErrors validation message' }); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /projects/demo/src/app/async-min-length-validator.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive } from '@angular/core'; 2 | import { 3 | NG_ASYNC_VALIDATORS, 4 | AsyncValidator, 5 | AbstractControl, 6 | ValidationErrors, 7 | MinLengthValidator, 8 | } from '@angular/forms'; 9 | import { Observable, delay, of } from 'rxjs'; 10 | 11 | @Directive({ 12 | selector: '[appAsyncMinLengthValidator]', 13 | providers: [ 14 | { 15 | provide: NG_ASYNC_VALIDATORS, 16 | useExisting: AsyncMinLengthValidator, 17 | multi: true, 18 | }, 19 | ], 20 | standalone: true, 21 | // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property 22 | inputs: ['minlength: appAsyncMinLengthValidator'], 23 | }) 24 | export class AsyncMinLengthValidator 25 | extends MinLengthValidator 26 | implements AsyncValidator 27 | { 28 | override validate( 29 | control: AbstractControl 30 | ): Observable { 31 | return of(super.validate(control)).pipe(delay(400)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /projects/demo/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Totati/ngx-mat-errors/0c3a42e89ec331ab6e34e2e3556a48596e74d952/projects/demo/src/assets/.gitkeep -------------------------------------------------------------------------------- /projects/demo/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /projects/demo/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /projects/demo/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Totati/ngx-mat-errors/0c3a42e89ec331ab6e34e2e3556a48596e74d952/projects/demo/src/favicon.ico -------------------------------------------------------------------------------- /projects/demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /projects/demo/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { bootstrapApplication } from '@angular/platform-browser'; 3 | 4 | import { provideAnimations } from '@angular/platform-browser/animations'; 5 | import { AppComponent } from './app/app.component'; 6 | import { environment } from './environments/environment'; 7 | import { NGX_MAT_ERROR_CONFIG_EN } from 'ngx-mat-errors'; 8 | 9 | if (environment.production) { 10 | enableProdMode(); 11 | } 12 | 13 | bootstrapApplication(AppComponent, { 14 | providers: [provideAnimations(), NGX_MAT_ERROR_CONFIG_EN], 15 | }).catch((err) => console.error(err)); 16 | -------------------------------------------------------------------------------- /projects/demo/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | 51 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | -------------------------------------------------------------------------------- /projects/demo/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | html, 4 | body { 5 | height: 100%; 6 | } 7 | body { 8 | margin: 0; 9 | font-family: Roboto, "Helvetica Neue", sans-serif; 10 | background: var(--mat-sys-surface); 11 | color: var(--mat-sys-on-surface); 12 | } 13 | -------------------------------------------------------------------------------- /projects/demo/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | // First, initialize the Angular testing environment. 11 | getTestBed().initTestEnvironment( 12 | BrowserDynamicTestingModule, 13 | platformBrowserDynamicTesting(), { 14 | teardown: { destroyAfterEach: false } 15 | } 16 | ); 17 | -------------------------------------------------------------------------------- /projects/demo/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ], 14 | } 15 | -------------------------------------------------------------------------------- /projects/demo/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /projects/ngx-mat-errors/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json", 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "parserOptions": { 8 | "project": [ 9 | "projects/ngx-mat-errors/tsconfig.lib.json", 10 | "projects/ngx-mat-errors/tsconfig.spec.json" 11 | ], 12 | "createDefaultProgram": true 13 | }, 14 | "rules": { 15 | "@angular-eslint/directive-selector": ["off", {}], 16 | "@angular-eslint/component-selector": ["off", {}], 17 | "@angular-eslint/directive-class-suffix": ["off", {}], 18 | "@angular-eslint/component-class-suffix": ["off", {}], 19 | "@angular-eslint/no-host-metadata-property": ["off", {}] 20 | } 21 | }, 22 | { 23 | "files": ["*.html"], 24 | "rules": {} 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /projects/ngx-mat-errors/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ngx-mat-errors", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/ngx-mat-errors/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-mat-errors", 3 | "version": "19.1.0", 4 | "author": { 5 | "name": "Totati", 6 | "url": "https://twitter.com/48Aca" 7 | }, 8 | "peerDependencies": { 9 | "@angular/common": "^19.0.0 || ^20.0.0", 10 | "@angular/core": "^19.0.0 || ^20.0.0", 11 | "@angular/material": "^19.0.0 || ^20.0.0" 12 | }, 13 | "license": "MIT", 14 | "repository": { 15 | "type": "github", 16 | "url": "https://github.com/Totati/ngx-mat-errors" 17 | }, 18 | "keywords": [ 19 | "angular", 20 | "material", 21 | "error", 22 | "error-messages", 23 | "messages", 24 | "validation", 25 | "validator", 26 | "custom", 27 | "MatFromField", 28 | "mat-form-field" 29 | ], 30 | "dependencies": { 31 | "tslib": "^2.3.0" 32 | }, 33 | "description": "NgxMatErrors provides an easy, yet flexible solution for displaying error messages in a MatFormField." 34 | } 35 | -------------------------------------------------------------------------------- /projects/ngx-mat-errors/src/lib/locales/en.ts: -------------------------------------------------------------------------------- 1 | import { formatDate } from '@angular/common'; 2 | import { type FactoryProvider, LOCALE_ID } from '@angular/core'; 3 | import type { 4 | EndDateError, 5 | ErrorMessages, 6 | LengthError, 7 | MaxError, 8 | MinError, 9 | StartDateError, 10 | ParseError, 11 | } from '../types'; 12 | import { NGX_MAT_ERROR_DEFAULT_OPTIONS } from '../ngx-mat-errors.component'; 13 | 14 | export function errorMessagesEnFactory( 15 | locale: string, 16 | dateFormat = 'shortDate', 17 | timeFormat = 'shortTime' 18 | ): ErrorMessages { 19 | return { 20 | min: (error: MinError) => 21 | `Please enter a value greater than or equal to ${error.min}.`, 22 | max: (error: MaxError) => 23 | `Please enter a value less than or equal to ${error.max}.`, 24 | required: `This field is required.`, 25 | email: `Please enter a valid email address.`, 26 | minlength: (error: LengthError) => 27 | `Please enter at least ${error.requiredLength} characters.`, 28 | maxlength: (error: LengthError) => 29 | `Please enter no more than ${error.requiredLength} characters.`, 30 | matDatepickerMin: (error: MinError) => { 31 | const formatted = formatDate(error.min, dateFormat, locale); 32 | return `Please enter a date greater than or equal to ${ 33 | formatted ?? error.min 34 | }.`; 35 | }, 36 | matDatepickerMax: (error: MaxError) => { 37 | const formatted = formatDate(error.max, dateFormat, locale); 38 | return `Please enter a date less than or equal to ${ 39 | formatted ?? error.max 40 | }.`; 41 | }, 42 | matDatepickerParse: (error: ParseError) => `Invalid date format.`, 43 | matStartDateInvalid: (error: StartDateError) => 44 | `Start date cannot be after end date.`, 45 | matEndDateInvalid: (error: EndDateError) => 46 | `End date cannot be before start date.`, 47 | matDatepickerFilter: 'This date is filtered out.', 48 | matTimepickerParse: (error: ParseError) => `Invalid time format.`, 49 | matTimepickerMin: (error: MinError) => { 50 | const formatted = formatDate(error.min, timeFormat, locale); 51 | return `Please enter a time greater than or equal to ${ 52 | formatted ?? error.min 53 | }.`; 54 | }, 55 | matTimepickerMax: (error: MaxError) => { 56 | const formatted = formatDate(error.max, timeFormat, locale); 57 | return `Please enter a time less than or equal to ${ 58 | formatted ?? error.max 59 | }.`; 60 | }, 61 | }; 62 | } 63 | 64 | export const NGX_MAT_ERROR_CONFIG_EN: FactoryProvider = { 65 | provide: NGX_MAT_ERROR_DEFAULT_OPTIONS, 66 | useFactory: errorMessagesEnFactory, 67 | deps: [LOCALE_ID], 68 | }; 69 | -------------------------------------------------------------------------------- /projects/ngx-mat-errors/src/lib/locales/hu.ts: -------------------------------------------------------------------------------- 1 | import { formatDate } from '@angular/common'; 2 | import { type FactoryProvider, LOCALE_ID } from '@angular/core'; 3 | import type { 4 | ParseError, 5 | EndDateError, 6 | ErrorMessages, 7 | LengthError, 8 | MaxError, 9 | MinError, 10 | StartDateError, 11 | } from '../types'; 12 | import { NGX_MAT_ERROR_DEFAULT_OPTIONS } from '../ngx-mat-errors.component'; 13 | 14 | export function errorMessagesHuFactory( 15 | locale: string, 16 | dateFormat = 'shortDate', 17 | timeFormat = 'shortTime' 18 | ): ErrorMessages { 19 | return { 20 | min: (error: MinError) => `Nem lehet kisebb, mint ${error.min}.`, 21 | max: (error: MaxError) => `Nem lehet nagyobb, mint ${error.max}.`, 22 | required: `Kötelező mező.`, 23 | email: `Nem érvényes e-mail cím.`, 24 | minlength: (error: LengthError) => 25 | `Legalább ${error.requiredLength} karakter hosszú lehet.`, 26 | maxlength: (error: LengthError) => 27 | `Legfeljebb ${error.requiredLength} karakter hosszú lehet.`, 28 | server: (error: string) => error, 29 | matDatepickerMin: (error: MinError) => { 30 | const formatted = formatDate(error.min, dateFormat, locale); 31 | // In Hungarian date ends with '.' 32 | return `Nem lehet korábbi dátum, mint ${formatted ?? error.min}`; 33 | }, 34 | matDatepickerMax: (error: MaxError) => { 35 | const formatted = formatDate(error.max, dateFormat, locale); 36 | // In Hungarian date ends with '.' 37 | return `Nem lehet későbbi dátum, mint ${formatted ?? error.max}`; 38 | }, 39 | matDatepickerParse: (error: ParseError) => `Érvénytelen dátum.`, 40 | matStartDateInvalid: (error: StartDateError) => 41 | `A kezdő dátum nem lehet a vég dátum után.`, 42 | matEndDateInvalid: (error: EndDateError) => 43 | `A vég dátum nem lehet a kezdő dátum előtt.`, 44 | matDatepickerFilter: 'Ez a dátum nem engedélyezett.', 45 | matTimepickerParse: (error: ParseError) => `Érvénytelen idő.`, 46 | matTimepickerMin: (error: MinError) => { 47 | const formatted = formatDate(error.min, timeFormat, locale); 48 | return `Nem lehet korábbi idő, mint ${ 49 | formatted ?? error.min 50 | }.`; 51 | }, 52 | matTimepickerMax: (error: MaxError) => { 53 | const formatted = formatDate(error.max, timeFormat, locale); 54 | return `Nem lehet későbbi idő, mint ${ 55 | formatted ?? error.max 56 | }.`; 57 | }, 58 | }; 59 | } 60 | 61 | export const NGX_MAT_ERROR_CONFIG_HU: FactoryProvider = { 62 | provide: NGX_MAT_ERROR_DEFAULT_OPTIONS, 63 | useFactory: errorMessagesHuFactory, 64 | deps: [LOCALE_ID], 65 | }; 66 | -------------------------------------------------------------------------------- /projects/ngx-mat-errors/src/lib/locales/index.ts: -------------------------------------------------------------------------------- 1 | export * from './en'; 2 | export * from './hu'; 3 | export * from './pt-br'; 4 | -------------------------------------------------------------------------------- /projects/ngx-mat-errors/src/lib/locales/pt-br.ts: -------------------------------------------------------------------------------- 1 | import { formatDate } from '@angular/common'; 2 | import { type FactoryProvider, LOCALE_ID } from '@angular/core'; 3 | import type { 4 | ParseError, 5 | EndDateError, 6 | ErrorMessages, 7 | LengthError, 8 | MaxError, 9 | MinError, 10 | StartDateError, 11 | } from '../types'; 12 | import { NGX_MAT_ERROR_DEFAULT_OPTIONS } from '../ngx-mat-errors.component'; 13 | 14 | export function errorMessagesPtBtFactory( 15 | locale: string, 16 | dateFormat = 'shortDate', 17 | timeFormat = 'shortTime' 18 | ): ErrorMessages { 19 | return { 20 | min: (error: MinError) => `Informe um valor igual ou maior a ${error.min}.`, 21 | max: (error: MaxError) => `Informe um valor igual ou menor a ${error.max}.`, 22 | required: `Campo obrigatório.`, 23 | email: `Informe um endereço de email válido.`, 24 | minlength: (error: LengthError) => 25 | `Informe pelo menos ${error.requiredLength} caracteres.`, 26 | maxlength: (error: LengthError) => 27 | `O campo não pode ter mais que ${error.requiredLength} caracteres.`, 28 | matDatepickerMin: (error: MinError) => { 29 | const formatted = formatDate(error.min, dateFormat, locale); 30 | return `Informe uma data maior ou igual a ${formatted ?? error.min}.`; 31 | }, 32 | matDatepickerMax: (error: MaxError) => { 33 | const formatted = formatDate(error.max, dateFormat, locale); 34 | return `Informe uma data menor ou igual a ${formatted ?? error.max}.`; 35 | }, 36 | matDatepickerParse: (error: ParseError) => `Formato de data inválido.`, 37 | matStartDateInvalid: (error: StartDateError) => 38 | `A data de início não pode ser posterior à data de término.`, 39 | matEndDateInvalid: (error: EndDateError) => 40 | `A data de término não pode ser anterior à data de início.`, 41 | matDatepickerFilter: 'Esta data é filtrada.', 42 | matTimepickerParse: (error: ParseError) => `Formato de hora inválido.`, 43 | matTimepickerMin: (error: MinError) => { 44 | const formatted = formatDate(error.min, timeFormat, locale); 45 | return `Insira um horário maior ou igual a ${ 46 | formatted ?? error.min 47 | }.`; 48 | }, 49 | matTimepickerMax: (error: MaxError) => { 50 | const formatted = formatDate(error.max, timeFormat, locale); 51 | return `Insira um horário menor ou igual a ${ 52 | formatted ?? error.max 53 | }.`; 54 | }, 55 | }; 56 | } 57 | 58 | export const NGX_MAT_ERROR_CONFIG_PT_BR: FactoryProvider = { 59 | provide: NGX_MAT_ERROR_DEFAULT_OPTIONS, 60 | useFactory: errorMessagesPtBtFactory, 61 | deps: [LOCALE_ID], 62 | }; 63 | -------------------------------------------------------------------------------- /projects/ngx-mat-errors/src/lib/ngx-mat-error-control.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, type Provider, inject } from '@angular/core'; 2 | import { MAT_FORM_FIELD } from '@angular/material/form-field'; 3 | import type { FormFieldControl } from './types'; 4 | 5 | /** 6 | * This class contains the logic of getting the default control of a MatFormField. 7 | * Extend it to implement a custom getter method. 8 | */ 9 | @Injectable() 10 | export class NgxMatErrorControl { 11 | protected readonly matFormField = inject(MAT_FORM_FIELD, { optional: true }); 12 | public get(): undefined | FormFieldControl | FormFieldControl[] { 13 | return this.matFormField?._control; 14 | } 15 | } 16 | 17 | /** 18 | * Provides the default control accessor of a MatFormField. 19 | */ 20 | export function provideDefaultNgxMatErrorControl(): Provider { 21 | return { 22 | provide: NgxMatErrorControl, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /projects/ngx-mat-errors/src/lib/ngx-mat-error-def.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, ViewChild } from '@angular/core'; 2 | import { TestBed } from '@angular/core/testing'; 3 | import { 4 | FormControl, 5 | FormGroup, 6 | FormsModule, 7 | ReactiveFormsModule, 8 | } from '@angular/forms'; 9 | import { NgxMatErrorDef } from './ngx-mat-error-def.directive'; 10 | 11 | describe('NgxMatErrorDef', () => { 12 | describe('withControl', () => { 13 | @Component({ 14 | template: `
15 | 18 |
`, 19 | changeDetection: ChangeDetectionStrategy.OnPush, 20 | imports: [NgxMatErrorDef, ReactiveFormsModule], 21 | }) 22 | class NgxMatErrorsWithErrorDefWithControlOfString { 23 | @ViewChild(NgxMatErrorDef, { static: true }) 24 | readonly ngxMatErrorDef!: NgxMatErrorDef; 25 | 26 | readonly fg = new FormGroup({ 27 | input: new FormControl(''), 28 | }); 29 | } 30 | 31 | it('should get the control when withControl is string', () => { 32 | const fixture = TestBed.createComponent( 33 | NgxMatErrorsWithErrorDefWithControlOfString 34 | ); 35 | fixture.detectChanges(); 36 | const { fg, ngxMatErrorDef } = fixture.componentInstance; 37 | expect(fg.controls.input).toEqual(ngxMatErrorDef.control as FormControl); 38 | }); 39 | 40 | @Component({ 41 | template: ` 42 | `, 49 | changeDetection: ChangeDetectionStrategy.OnPush, 50 | imports: [NgxMatErrorDef, FormsModule], 51 | }) 52 | class NgxMatErrorsWithErrorDefWithControlOfNgModel { 53 | @ViewChild(NgxMatErrorDef, { static: true }) 54 | readonly ngxMatErrorDef!: NgxMatErrorDef; 55 | public input = ''; 56 | } 57 | 58 | it('should get the control when withControl is NgModel', () => { 59 | const fixture = TestBed.createComponent( 60 | NgxMatErrorsWithErrorDefWithControlOfNgModel 61 | ); 62 | fixture.detectChanges(); 63 | expect(fixture.componentInstance.ngxMatErrorDef.control).toBeDefined(); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /projects/ngx-mat-errors/src/lib/ngx-mat-error-def.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, 3 | InjectionToken, 4 | Input, 5 | TemplateRef, 6 | inject, 7 | } from '@angular/core'; 8 | import { 9 | AbstractControl, 10 | ControlContainer, 11 | type AbstractControlDirective, 12 | } from '@angular/forms'; 13 | 14 | export interface INgxMatErrorDef { 15 | ngxMatErrorDefFor: string; 16 | ngxMatErrorDefWithControl?: 17 | | AbstractControlDirective 18 | | AbstractControl 19 | | string 20 | | null; 21 | template: TemplateRef; 22 | control?: AbstractControl; 23 | } 24 | 25 | /** 26 | * Lightweight injection token. When NgxMatErrorDef is not used, only this token will remain, the directive will be tree-shaken. 27 | */ 28 | export const NGX_MAT_ERROR_DEF = new InjectionToken( 29 | 'NGX_MAT_ERROR_DEF' 30 | ); 31 | 32 | @Directive({ 33 | selector: '[ngxMatErrorDef]', 34 | standalone: true, 35 | providers: [ 36 | { 37 | provide: NGX_MAT_ERROR_DEF, 38 | useExisting: NgxMatErrorDef, 39 | }, 40 | ], 41 | }) 42 | export class NgxMatErrorDef implements INgxMatErrorDef { 43 | /** 44 | * Specify the error key to be used for error matching. 45 | * @required 46 | */ 47 | @Input({ 48 | required: true, 49 | }) 50 | public ngxMatErrorDefFor!: string; 51 | 52 | /** 53 | * Specify the control to be used for error matching. 54 | * @optional 55 | */ 56 | @Input() 57 | public ngxMatErrorDefWithControl?: 58 | | AbstractControlDirective 59 | | AbstractControl 60 | | string 61 | | null = undefined; 62 | public readonly template = inject(TemplateRef); 63 | private readonly controlContainer = inject(ControlContainer, { 64 | optional: true, 65 | skipSelf: true, 66 | }); 67 | 68 | public get control(): AbstractControl | undefined { 69 | const input = this.ngxMatErrorDefWithControl; 70 | if (typeof input === 'string') { 71 | return this.controlContainer?.control?.get(input) ?? undefined; 72 | } 73 | if (input instanceof AbstractControl) { 74 | return input; 75 | } 76 | return input?.control ?? undefined; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /projects/ngx-mat-errors/src/lib/ngx-mat-errors-for-date-range-picker.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; 2 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 3 | import { TestBed } from '@angular/core/testing'; 4 | import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; 5 | import { MatNativeDateModule } from '@angular/material/core'; 6 | import { MatDatepickerModule } from '@angular/material/datepicker'; 7 | import { MatDateRangeInputHarness } from '@angular/material/datepicker/testing'; 8 | import { MatFormFieldModule } from '@angular/material/form-field'; 9 | import { MatErrorHarness } from '@angular/material/form-field/testing'; 10 | import { MatInputModule } from '@angular/material/input'; 11 | import { NoopAnimationsModule } from '@angular/platform-browser/animations'; 12 | import { NgxMatErrorDef } from './ngx-mat-error-def.directive'; 13 | import { NgxMatErrorsForDateRangePicker } from './ngx-mat-errors-for-date-range-picker.directive'; 14 | import { 15 | NGX_MAT_ERROR_DEFAULT_OPTIONS, 16 | NgxMatErrors, 17 | } from './ngx-mat-errors.component'; 18 | 19 | describe('NgxMatErrorsForDateRangePicker', () => { 20 | beforeEach(() => { 21 | TestBed.configureTestingModule({ 22 | imports: [NoopAnimationsModule], 23 | }); 24 | }); 25 | @Component({ 26 | template: ` 27 | 28 | Label 29 | 30 | 31 | 32 | 33 | 34 | 35 | start 38 | end 41 | 42 | 43 | `, 44 | changeDetection: ChangeDetectionStrategy.OnPush, 45 | imports: [ 46 | NgxMatErrors, 47 | NgxMatErrorsForDateRangePicker, 48 | NgxMatErrorDef, 49 | ReactiveFormsModule, 50 | MatFormFieldModule, 51 | MatInputModule, 52 | MatDatepickerModule, 53 | MatNativeDateModule, 54 | ], 55 | providers: [ 56 | { 57 | provide: NGX_MAT_ERROR_DEFAULT_OPTIONS, 58 | useValue: {}, 59 | }, 60 | ], 61 | }) 62 | class NgxMatErrorsForDateRangePickerComponent { 63 | readonly start = new FormControl(undefined, Validators.required); 64 | readonly end = new FormControl(undefined, Validators.required); 65 | } 66 | it('should assign controls of the MatDateRangePicker to ngx-mat-errors', async () => { 67 | const fixture = TestBed.createComponent( 68 | NgxMatErrorsForDateRangePickerComponent 69 | ); 70 | fixture.detectChanges(); 71 | const loader = TestbedHarnessEnvironment.loader(fixture); 72 | const matDateRangeInputHarness = await loader.getHarness( 73 | MatDateRangeInputHarness 74 | ); 75 | const startInput = await matDateRangeInputHarness.getStartInput(); 76 | startInput.blur(); 77 | const matError = await loader.getHarness(MatErrorHarness); 78 | 79 | expect(await matError.getText()).toBe('start'); 80 | 81 | await startInput.setValue('2023-10-15'); 82 | 83 | expect(await matError.getText()).toBe('end'); 84 | 85 | const endInput = await matDateRangeInputHarness.getEndInput(); 86 | await endInput.setValue('2023-10-15'); 87 | 88 | expect(await matError.getText()).toBe(''); 89 | 90 | await startInput.setValue(''); 91 | 92 | expect(await matError.getText()).toBe('start'); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /projects/ngx-mat-errors/src/lib/ngx-mat-errors-for-date-range-picker.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive } from '@angular/core'; 2 | import type { MatDateRangeInput } from '@angular/material/datepicker'; 3 | import { NgxMatErrorControl } from './ngx-mat-error-control'; 4 | 5 | @Directive({ 6 | selector: '[ngx-mat-errors][forDateRangePicker]', 7 | standalone: true, 8 | host: { 9 | class: 'ngx-mat-errors-for-date-range-picker', 10 | }, 11 | providers: [ 12 | { 13 | provide: NgxMatErrorControl, 14 | useExisting: NgxMatErrorsForDateRangePicker, 15 | }, 16 | ], 17 | }) 18 | export class NgxMatErrorsForDateRangePicker extends NgxMatErrorControl { 19 | /** Returns start and end controls of the date range picker. */ 20 | public override get() { 21 | const { _startInput, _endInput } = this.matFormField! 22 | ._control as MatDateRangeInput; 23 | return [_startInput, _endInput]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /projects/ngx-mat-errors/src/lib/ngx-mat-errors.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { HarnessLoader } from '@angular/cdk/testing'; 2 | import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; 3 | import { NgIf } from '@angular/common'; 4 | import { 5 | AfterContentInit, 6 | ChangeDetectionStrategy, 7 | Component, 8 | Directive, 9 | Input, 10 | inject, 11 | type Provider, 12 | } from '@angular/core'; 13 | import { 14 | ComponentFixture, 15 | TestBed, 16 | fakeAsync, 17 | tick, 18 | } from '@angular/core/testing'; 19 | import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; 20 | import { MatFormFieldModule } from '@angular/material/form-field'; 21 | import { MatErrorHarness } from '@angular/material/form-field/testing'; 22 | import { MatInputModule } from '@angular/material/input'; 23 | import { MatInputHarness } from '@angular/material/input/testing'; 24 | import { NoopAnimationsModule } from '@angular/platform-browser/animations'; 25 | import { 26 | NGX_MAT_ERROR_DEFAULT_OPTIONS, 27 | NgxMatErrors, 28 | NgxMatErrorsModule, 29 | type ErrorMessages, 30 | } from 'ngx-mat-errors'; 31 | import { delay, from, interval, map, of, take, tap, zip } from 'rxjs'; 32 | import type { LengthError } from './types'; 33 | 34 | const defaultProviders: Provider[] = [ 35 | { 36 | provide: NGX_MAT_ERROR_DEFAULT_OPTIONS, 37 | useValue: { 38 | required: 'required', 39 | minlength: (error: LengthError) => 40 | `${error.actualLength} ${error.requiredLength}`, 41 | email: 'email', 42 | }, 43 | }, 44 | ]; 45 | 46 | const defaultImports = [ 47 | ReactiveFormsModule, 48 | MatFormFieldModule, 49 | MatInputModule, 50 | NgxMatErrorsModule, 51 | ]; 52 | 53 | function createControl(value: string) { 54 | return new FormControl(value, [Validators.minLength(3), Validators.email]); 55 | } 56 | 57 | function updateControlValidators(control: FormControl) { 58 | control.setValidators([Validators.email, Validators.minLength(3)]); 59 | control.updateValueAndValidity(); 60 | } 61 | 62 | describe('NgxMatErrors', () => { 63 | let loader: HarnessLoader; 64 | 65 | beforeEach(() => { 66 | TestBed.configureTestingModule({ 67 | imports: [NoopAnimationsModule], 68 | }); 69 | }); 70 | 71 | describe('out of MatFormField', () => { 72 | @Component({ 73 | changeDetection: ChangeDetectionStrategy.OnPush, 74 | imports: [...defaultImports, NgIf], 75 | providers: [...defaultProviders], 76 | template: ``, 77 | }) 78 | class NgxMatErrorWithoutControl {} 79 | 80 | it('should not render anything when no control is connected', async () => { 81 | const fixture = TestBed.createComponent(NgxMatErrorWithoutControl); 82 | fixture.detectChanges(); 83 | loader = TestbedHarnessEnvironment.loader(fixture); 84 | 85 | const matError = await loader.getHarness(MatErrorHarness); 86 | expect(await matError.getText()).toBe(''); 87 | }); 88 | 89 | @Component({ 90 | changeDetection: ChangeDetectionStrategy.OnPush, 91 | imports: [...defaultImports], 92 | providers: [...defaultProviders], 93 | template: ` 94 | 95 | 96 | Label 97 | 98 | 99 | `, 100 | }) 101 | class NgxMatErrorWithoutDefWithControl { 102 | control = createControl('12'); 103 | } 104 | 105 | it('should render error control is connected manually', async () => { 106 | const fixture = TestBed.createComponent(NgxMatErrorWithoutDefWithControl); 107 | fixture.detectChanges(); 108 | loader = TestbedHarnessEnvironment.loader(fixture); 109 | 110 | const matError = await loader.getHarness(MatErrorHarness); 111 | expect(await matError.getText()).toBe('2 3'); 112 | }); 113 | 114 | @Component({ 115 | changeDetection: ChangeDetectionStrategy.OnPush, 116 | imports: [...defaultImports], 117 | providers: [...defaultProviders], 118 | template: ``, 119 | }) 120 | class NgxMatErrorWithControlSetError { 121 | control = new FormControl(''); 122 | } 123 | 124 | it('should update error message when setErrors is used', async () => { 125 | const fixture = TestBed.createComponent(NgxMatErrorWithControlSetError); 126 | fixture.detectChanges(); 127 | loader = TestbedHarnessEnvironment.loader(fixture); 128 | const matError = await loader.getHarness(MatErrorHarness); 129 | fixture.componentInstance.control.setErrors({ 130 | required: true, 131 | }); 132 | 133 | expect(await matError.getText()).toBe('required'); 134 | }); 135 | 136 | @Component({ 137 | changeDetection: ChangeDetectionStrategy.OnPush, 138 | imports: [...defaultImports, NgIf], 139 | providers: [...defaultProviders], 140 | template: ` 141 | 144 | 145 | Label 146 | 147 | 148 | 149 | Label 150 | 151 | 152 | `, 153 | }) 154 | class NgxMatErrorWithControlChange { 155 | control1 = createControl('12'); 156 | control2 = createControl('123'); 157 | @Input() 158 | isControlOneSelected = true; 159 | } 160 | 161 | it('should handle control change', async () => { 162 | const fixture = TestBed.createComponent(NgxMatErrorWithControlChange); 163 | fixture.detectChanges(); 164 | loader = TestbedHarnessEnvironment.loader(fixture); 165 | 166 | const matError = await loader.getHarness(MatErrorHarness); 167 | expect(await matError.getText()).toBe('2 3'); 168 | 169 | fixture.componentRef.setInput('isControlOneSelected', false); 170 | fixture.detectChanges(); 171 | 172 | expect(await matError.getText()).toBe('email'); 173 | }); 174 | 175 | @Component({ 176 | changeDetection: ChangeDetectionStrategy.OnPush, 177 | imports: [...defaultImports, NgIf], 178 | providers: [...defaultProviders], 179 | template: ` 180 | 183 | 184 | Label 185 | 186 | 187 | `, 188 | }) 189 | class NgxMatErrorWithControlRemoved { 190 | control1 = createControl('12'); 191 | @Input() 192 | isControlOneSelected = true; 193 | } 194 | 195 | it('should clear error when connected control is removed', async () => { 196 | const fixture = TestBed.createComponent(NgxMatErrorWithControlRemoved); 197 | fixture.detectChanges(); 198 | loader = TestbedHarnessEnvironment.loader(fixture); 199 | 200 | const matError = await loader.getHarness(MatErrorHarness); 201 | expect(await matError.getText()).toBe('2 3'); 202 | fixture.componentRef.setInput('isControlOneSelected', false); 203 | fixture.detectChanges(); 204 | 205 | expect(await matError.getText()).toBe(''); 206 | }); 207 | }); 208 | 209 | describe('with multiple controls', () => { 210 | @Component({ 211 | changeDetection: ChangeDetectionStrategy.OnPush, 212 | imports: [...defaultImports], 213 | providers: [...defaultProviders], 214 | template: ` 215 | 216 | 217 | `, 218 | }) 219 | class NgxMatErrorWithMultipleControls { 220 | control1 = createControl('12'); 221 | control2 = createControl('123'); 222 | } 223 | 224 | it('should render error from the first control', async () => { 225 | const fixture = TestBed.createComponent(NgxMatErrorWithMultipleControls); 226 | fixture.detectChanges(); 227 | loader = TestbedHarnessEnvironment.loader(fixture); 228 | 229 | const [matError1, matError2] = await loader.getAllHarnesses( 230 | MatErrorHarness 231 | ); 232 | expect(await matError1.getText()).toBe('2 3'); 233 | expect(await matError2.getText()).toBe('email'); 234 | 235 | const { control1, control2 } = fixture.componentInstance; 236 | 237 | control1.setValue('123'); 238 | control2.setValue('12'); 239 | 240 | expect(await matError1.getText()).toBe('email'); 241 | expect(await matError2.getText()).toBe('2 3'); 242 | 243 | control1.setValue('123@test.io'); 244 | 245 | expect(await matError1.getText()).toBe('2 3'); 246 | expect(await matError2.getText()).toBe('2 3'); 247 | 248 | control2.setValue('123@test.io'); 249 | 250 | expect(await matError1.getText()).toBe(''); 251 | expect(await matError2.getText()).toBe(''); 252 | }); 253 | }); 254 | 255 | describe('without ngxMatErrorDef', () => { 256 | @Component({ 257 | changeDetection: ChangeDetectionStrategy.OnPush, 258 | imports: [...defaultImports], 259 | providers: [...defaultProviders], 260 | template: ` 261 | 262 | Label 263 | 264 | 265 | 266 | `, 267 | }) 268 | class NgxMatErrorWithoutDef { 269 | control = createControl('12'); 270 | } 271 | 272 | let fixture: ComponentFixture; 273 | beforeEach(() => { 274 | fixture = TestBed.createComponent(NgxMatErrorWithoutDef); 275 | fixture.detectChanges(); 276 | loader = TestbedHarnessEnvironment.loader(fixture); 277 | }); 278 | 279 | it('should not display error message initially', async () => { 280 | const matError = await loader.getHarnessOrNull(MatErrorHarness); 281 | expect(matError).toBeNull(); 282 | }); 283 | 284 | it('should display only one error message when control is touched and invalid', async () => { 285 | const matInput = await loader.getHarness(MatInputHarness); 286 | await matInput.blur(); 287 | const matErrors = await loader.getAllHarnesses(MatErrorHarness); 288 | expect(matErrors.length).toBe(1); 289 | }); 290 | 291 | it('should call the error transform function', async () => { 292 | const matInput = await loader.getHarness(MatInputHarness); 293 | await matInput.blur(); 294 | const matError = await loader.getHarness(MatErrorHarness); 295 | expect(await matError.getText()).toBe('2 3'); 296 | }); 297 | 298 | it('should display errors in the order of Validators', async () => { 299 | const matInput = await loader.getHarness(MatInputHarness); 300 | await matInput.blur(); 301 | const matError = await loader.getHarness(MatErrorHarness); 302 | await matInput.setValue('asd'); 303 | expect(await matError.getText()).toBe('email'); 304 | 305 | updateControlValidators(fixture.componentInstance.control); 306 | await matInput.setValue('as'); 307 | expect(await matError.getText()).toBe('email'); 308 | }); 309 | }); 310 | 311 | describe('with ngxMatErrorDef', () => { 312 | @Component({ 313 | changeDetection: ChangeDetectionStrategy.OnPush, 314 | imports: [...defaultImports], 315 | providers: [...defaultProviders], 316 | template: ` 317 | 318 | Label 319 | 320 | 321 | {{ error.actualLength }} {{ error.requiredLength }} def 324 | email def 325 | 326 | 327 | `, 328 | }) 329 | class NgxMatErrorsWithErrorDef { 330 | control = createControl('12'); 331 | } 332 | 333 | let fixture: ComponentFixture; 334 | beforeEach(() => { 335 | fixture = TestBed.createComponent(NgxMatErrorsWithErrorDef); 336 | fixture.detectChanges(); 337 | loader = TestbedHarnessEnvironment.loader(fixture); 338 | }); 339 | it('should not display error message initially', async () => { 340 | const matError = await loader.getHarnessOrNull(MatErrorHarness); 341 | expect(matError).toBeNull(); 342 | }); 343 | 344 | it('should display only one error message when control is touched and invalid', async () => { 345 | const matInput = await loader.getHarness(MatInputHarness); 346 | await matInput.blur(); 347 | const matErrors = await loader.getAllHarnesses(MatErrorHarness); 348 | expect(matErrors.length).toBe(1); 349 | }); 350 | 351 | it('should display errors in the order of ngxMatErrorDef not the validators', async () => { 352 | const matInput = await loader.getHarness(MatInputHarness); 353 | await matInput.blur(); 354 | 355 | const matError = await loader.getHarness(MatErrorHarness); 356 | expect(await matError.getText()).toBe('2 3 def'); 357 | 358 | await matInput.setValue('asd'); 359 | expect(await matError.getText()).toBe('email def'); 360 | 361 | updateControlValidators(fixture.componentInstance.control); 362 | expect(await matError.getText()).toBe('email def'); 363 | 364 | await matInput.setValue('as'); 365 | expect(await matError.getText()).toBe('2 3 def'); 366 | }); 367 | 368 | @Component({ 369 | changeDetection: ChangeDetectionStrategy.OnPush, 370 | imports: [...defaultImports, NgIf], 371 | providers: [...defaultProviders], 372 | template: ` 373 | 374 | Label 375 | 376 | 377 | 378 | minLength 1 381 | 382 | 383 | minLength 2 386 | 387 | 388 | 389 | `, 390 | }) 391 | class NgxMatErrorsWithErrorDefChange { 392 | control = createControl('12'); 393 | @Input() 394 | isCustomMinLength1Visible = false; 395 | @Input() 396 | isCustomMinLength2Visible = false; 397 | } 398 | 399 | it('should handle ngxMatErrorDef change', async () => { 400 | const fixture = TestBed.createComponent(NgxMatErrorsWithErrorDefChange); 401 | fixture.detectChanges(); 402 | loader = TestbedHarnessEnvironment.loader(fixture); 403 | 404 | const matInput = await loader.getHarness(MatInputHarness); 405 | await matInput.blur(); 406 | 407 | const matError = await loader.getHarness(MatErrorHarness); 408 | expect(await matError.getText()).toBe('2 3'); 409 | 410 | fixture.componentRef.setInput('isCustomMinLength2Visible', true); 411 | fixture.detectChanges(); 412 | 413 | expect(await matError.getText()).toBe('minLength 2'); 414 | 415 | fixture.componentRef.setInput('isCustomMinLength1Visible', true); 416 | fixture.detectChanges(); 417 | 418 | expect(await matError.getText()).toBe('minLength 1'); 419 | 420 | fixture.componentRef.setInput('isCustomMinLength1Visible', false); 421 | fixture.detectChanges(); 422 | 423 | expect(await matError.getText()).toBe('minLength 2'); 424 | }); 425 | }); 426 | 427 | describe('with async validator', () => { 428 | @Component({ 429 | changeDetection: ChangeDetectionStrategy.OnPush, 430 | imports: [...defaultImports], 431 | providers: [...defaultProviders], 432 | template: ` 433 | 434 | Label 435 | 436 | 437 | 438 | `, 439 | }) 440 | class NgxMatErrorWithAsyncValidator { 441 | control = new FormControl('', { 442 | asyncValidators: [ 443 | (control) => 444 | of(Validators.minLength(3)(control)).pipe( 445 | delay(0), 446 | tap(console.log) 447 | ), 448 | ], 449 | }); 450 | } 451 | 452 | it('should display errors of async validators', fakeAsync(async () => { 453 | const fixture = TestBed.createComponent(NgxMatErrorWithAsyncValidator); 454 | fixture.detectChanges(); 455 | loader = TestbedHarnessEnvironment.loader(fixture); 456 | const matInput = await loader.getHarness(MatInputHarness); 457 | await matInput.blur(); 458 | await matInput.setValue('a'); 459 | tick(1); 460 | fixture.detectChanges(); 461 | await fixture.whenRenderingDone(); 462 | const matError = await loader.getHarness(MatErrorHarness); 463 | expect(await matError.getText()).toBe('1 3'); 464 | await matInput.setValue('as'); 465 | tick(1); 466 | fixture.detectChanges(); 467 | expect(await matError.getText()).toBe('2 3'); 468 | })); 469 | }); 470 | 471 | describe('with observable messages', () => { 472 | @Component({ 473 | changeDetection: ChangeDetectionStrategy.OnPush, 474 | imports: [...defaultImports], 475 | providers: [ 476 | { 477 | provide: NGX_MAT_ERROR_DEFAULT_OPTIONS, 478 | useValue: zip( 479 | from([ 480 | { 481 | minlength: 'minlength1', 482 | }, 483 | { 484 | minlength: 'minlength2', 485 | }, 486 | ] as ErrorMessages[]), 487 | interval(1) 488 | ).pipe( 489 | take(2), 490 | map(([v]) => v) 491 | ), 492 | }, 493 | ], 494 | template: ``, 495 | }) 496 | class NgxMatErrorWithObservableMessages { 497 | control = createControl('12'); 498 | } 499 | 500 | it('should change message when new messages enter the stream', fakeAsync(async () => { 501 | const fixture = TestBed.createComponent( 502 | NgxMatErrorWithObservableMessages 503 | ); 504 | fixture.detectChanges(); 505 | loader = TestbedHarnessEnvironment.loader(fixture); 506 | const matError = await loader.getHarness(MatErrorHarness); 507 | expect(await matError.getText()).toBe(''); 508 | tick(1); 509 | expect(await matError.getText()).toBe('minlength1'); 510 | tick(1); 511 | expect(await matError.getText()).toBe('minlength2'); 512 | })); 513 | }); 514 | 515 | describe('with deprecated control change', () => { 516 | @Directive({ 517 | // eslint-disable-next-line @angular-eslint/directive-selector 518 | selector: '[ngx-mat-errors][forTest]', 519 | standalone: true, 520 | }) 521 | class NgxMatErrorsForTest implements AfterContentInit { 522 | private readonly ngxMatErrors = inject(NgxMatErrors); 523 | public ngAfterContentInit() { 524 | // Remove this whole describe block when the deprecated API is removed. 525 | this.ngxMatErrors.control = new FormControl(null, [ 526 | Validators.required, 527 | ]); 528 | } 529 | } 530 | @Component({ 531 | changeDetection: ChangeDetectionStrategy.OnPush, 532 | imports: [...defaultImports, NgxMatErrorsForTest], 533 | providers: [...defaultProviders], 534 | template: ``, 535 | }) 536 | class NgxMatErrorWithDeprecatedControlSetting {} 537 | 538 | it('should be possible to set the control manually through deprecated API', async () => { 539 | const fixture = TestBed.createComponent( 540 | NgxMatErrorWithDeprecatedControlSetting 541 | ); 542 | fixture.detectChanges(); 543 | loader = TestbedHarnessEnvironment.loader(fixture); 544 | const matError = await loader.getHarness(MatErrorHarness); 545 | 546 | expect(await matError.getText()).toBe('required'); 547 | }); 548 | }); 549 | }); 550 | -------------------------------------------------------------------------------- /projects/ngx-mat-errors/src/lib/ngx-mat-errors.component.ts: -------------------------------------------------------------------------------- 1 | import { AsyncPipe, NgTemplateOutlet } from '@angular/common'; 2 | import { 3 | ChangeDetectionStrategy, 4 | Component, 5 | ContentChildren, 6 | InjectionToken, 7 | Input, 8 | ViewEncapsulation, 9 | inject, 10 | type OnDestroy, 11 | type QueryList, 12 | } from '@angular/core'; 13 | import { 14 | ReplaySubject, 15 | combineLatest, 16 | distinctUntilChanged, 17 | map, 18 | of, 19 | startWith, 20 | switchMap, 21 | type Observable, 22 | } from 'rxjs'; 23 | import { 24 | NgxMatErrorControl, 25 | provideDefaultNgxMatErrorControl, 26 | } from './ngx-mat-error-control'; 27 | import { 28 | NGX_MAT_ERROR_DEF, 29 | type INgxMatErrorDef, 30 | } from './ngx-mat-error-def.directive'; 31 | import type { 32 | ErrorMessages, 33 | ErrorTemplate, 34 | NgxMatErrorControls, 35 | } from './types'; 36 | import { coerceToObservable } from './utils/coerce-to-observable'; 37 | import { distinctUntilErrorChanged } from './utils/distinct-until-error-changed'; 38 | import { findErrorForControl } from './utils/find-error-for-control'; 39 | import { getAbstractControls } from './utils/get-abstract-controls'; 40 | import { getControlWithError } from './utils/get-control-with-error'; 41 | 42 | export const NGX_MAT_ERROR_DEFAULT_OPTIONS = new InjectionToken< 43 | ErrorMessages | Observable 44 | >('NGX_MAT_ERROR_DEFAULT_OPTIONS'); 45 | 46 | @Component({ 47 | selector: 'ngx-mat-errors, [ngx-mat-errors]', 48 | template: `{{ error }}@if( error$ | async; as error) { 50 | 54 | }`, 55 | encapsulation: ViewEncapsulation.None, 56 | changeDetection: ChangeDetectionStrategy.OnPush, 57 | imports: [AsyncPipe, NgTemplateOutlet], 58 | host: { 59 | class: 'ngx-mat-errors', 60 | }, 61 | providers: [provideDefaultNgxMatErrorControl()], 62 | }) 63 | export class NgxMatErrors implements OnDestroy { 64 | private readonly messages$ = coerceToObservable( 65 | inject(NGX_MAT_ERROR_DEFAULT_OPTIONS) 66 | ); 67 | private readonly defaultControl = inject(NgxMatErrorControl, { 68 | host: true, 69 | }); 70 | private readonly controlChangedSubject = 71 | new ReplaySubject(1); 72 | 73 | protected error$!: Observable; 74 | 75 | // ContentChildren is set before ngAfterContentInit which is before ngAfterViewInit. 76 | // Before ngAfterViewInit lifecycle hook we can modify the error$ observable without needing another change detection cycle. 77 | // This elaborates the need of rxjs defer; 78 | @ContentChildren(NGX_MAT_ERROR_DEF, { descendants: true }) 79 | protected set customErrorMessages(queryList: QueryList) { 80 | const firstControlWithError$ = this.controlChangedSubject.pipe( 81 | switchMap((_controls) => { 82 | const controls = getAbstractControls( 83 | _controls || this.defaultControl.get() 84 | ); 85 | if (!controls) { 86 | return of(null); 87 | } 88 | return getControlWithError(controls); 89 | }) 90 | ), 91 | customErrorMessages$ = ( 92 | queryList.changes as Observable> 93 | ).pipe(startWith(queryList)); 94 | this.error$ = combineLatest([ 95 | firstControlWithError$, 96 | customErrorMessages$, 97 | this.messages$, 98 | ]).pipe( 99 | map(([controlWithError, customErrorMessages, messages]) => { 100 | if (!controlWithError) { 101 | return; 102 | } 103 | const errors = controlWithError.errors!, 104 | errorOrErrorDef = findErrorForControl( 105 | controlWithError, 106 | messages, 107 | customErrorMessages.toArray() 108 | ); 109 | if (!errorOrErrorDef) { 110 | return; 111 | } 112 | if (typeof errorOrErrorDef === 'object') { 113 | return { 114 | template: errorOrErrorDef.template, 115 | $implicit: errors[errorOrErrorDef.ngxMatErrorDefFor], 116 | }; 117 | } 118 | const message = messages[errorOrErrorDef]; 119 | return { 120 | $implicit: 121 | typeof message === 'function' 122 | ? message(errors[errorOrErrorDef]) 123 | : message, 124 | }; 125 | }), 126 | distinctUntilChanged(distinctUntilErrorChanged) 127 | ); 128 | } 129 | 130 | // eslint-disable-next-line @angular-eslint/no-input-rename 131 | /** 132 | * @deprecated will be changed to a signal and it won't be possible to set the property from TS. 133 | * Instead of setting it in a directive, the directive should extend the {@link NgxMatErrorControl } class 134 | * and provide itself as it. 135 | */ 136 | @Input('ngx-mat-errors') 137 | public set control(control: NgxMatErrorControls) { 138 | this.controlChangedSubject.next(control); 139 | } 140 | 141 | /** @ignore */ 142 | public ngOnDestroy(): void { 143 | this.controlChangedSubject.complete(); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /projects/ngx-mat-errors/src/lib/ngx-mat-errors.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { NgxMatErrorDef } from './ngx-mat-error-def.directive'; 3 | import { NgxMatErrors } from './ngx-mat-errors.component'; 4 | 5 | @NgModule({ 6 | imports: [NgxMatErrors, NgxMatErrorDef], 7 | exports: [NgxMatErrors, NgxMatErrorDef], 8 | }) 9 | export class NgxMatErrorsModule {} 10 | -------------------------------------------------------------------------------- /projects/ngx-mat-errors/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { TemplateRef } from '@angular/core'; 2 | import type { 3 | AbstractControl, 4 | AbstractControlDirective, 5 | ValidationErrors, 6 | } from '@angular/forms'; 7 | import type { MatFormFieldControl } from '@angular/material/form-field'; 8 | 9 | export type ErrorTemplate = 10 | | { 11 | template: TemplateRef; 12 | $implicit: ValidationErrors; 13 | } 14 | | { 15 | template: undefined; 16 | $implicit: string; 17 | } 18 | | undefined; 19 | 20 | export type FormFieldControl = Pick, 'ngControl'>; 21 | 22 | export type NgxMatErrorControls = 23 | | FormFieldControl 24 | | AbstractControl 25 | | AbstractControlDirective 26 | | (FormFieldControl | AbstractControl | AbstractControlDirective)[] 27 | | undefined 28 | | null 29 | | ''; 30 | 31 | export type ErrorTransform = (error: any) => string; 32 | 33 | export interface ErrorMessages { 34 | [key: string]: string | ErrorTransform; 35 | } 36 | 37 | /** 38 | * For errors: 'min', 'matDatepickerMin' 39 | */ 40 | export interface MinError { 41 | min: T; 42 | actual: T; 43 | } 44 | 45 | /** 46 | * For errors: 'max', 'matDatepickerMax' 47 | */ 48 | export interface MaxError { 49 | max: T; 50 | actual: T; 51 | } 52 | 53 | /** 54 | * For errors: 'minlength', 'maxlength' 55 | */ 56 | export interface LengthError { 57 | requiredLength: number; 58 | actualLength: number; 59 | } 60 | 61 | /** 62 | * For errors: 'pattern' 63 | */ 64 | export interface PatternValidator { 65 | requiredPattern: string; 66 | actualValue: string; 67 | } 68 | 69 | /** 70 | * For errors: 'matStartDateInvalid' 71 | */ 72 | export interface StartDateError { 73 | end: D; 74 | actual: D; 75 | } 76 | 77 | /** 78 | * For errors: 'matEndDateInvalid' 79 | */ 80 | export interface EndDateError { 81 | start: D; 82 | actual: D; 83 | } 84 | 85 | /** 86 | * For errors: 'matDatepickerParse', 'matTimepickerParse' 87 | */ 88 | export interface ParseError { 89 | text: string; 90 | } 91 | /** 92 | * @deprecated to be removed in version 20. Please use ParseError instead 93 | */ 94 | export type DatepickerParseError = ParseError; 95 | -------------------------------------------------------------------------------- /projects/ngx-mat-errors/src/lib/utils/coerce-to-observable.spec.ts: -------------------------------------------------------------------------------- 1 | import { isObservable, of } from 'rxjs'; 2 | import { coerceToObservable } from './coerce-to-observable'; 3 | 4 | describe('coerceToObservable', () => { 5 | it('should return observable', () => { 6 | expect(isObservable(coerceToObservable({}))).toBeTrue(); 7 | expect(isObservable(coerceToObservable(of({})))).toBeTrue(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /projects/ngx-mat-errors/src/lib/utils/coerce-to-observable.ts: -------------------------------------------------------------------------------- 1 | import { type Observable, isObservable, of, } from 'rxjs'; 2 | import type { ErrorMessages } from '../types'; 3 | 4 | export function coerceToObservable( 5 | errorMessages: ErrorMessages | Observable 6 | ): Observable { 7 | if (isObservable(errorMessages)) { 8 | return errorMessages; 9 | } 10 | return of(errorMessages); 11 | } 12 | -------------------------------------------------------------------------------- /projects/ngx-mat-errors/src/lib/utils/distinct-until-error-changed.spec.ts: -------------------------------------------------------------------------------- 1 | import type { TemplateRef } from '@angular/core'; 2 | import type { MaxError, MinError, ErrorTemplate } from '../types'; 3 | import { distinctUntilErrorChanged } from './distinct-until-error-changed'; 4 | 5 | describe('distinctUntilErrorChanged', () => { 6 | const minError = { 7 | $implicit: 'min error', 8 | template: undefined, 9 | } as ErrorTemplate; 10 | const maxError = { 11 | $implicit: 'max error', 12 | template: undefined, 13 | } as ErrorTemplate; 14 | const minErrorTemplate = { 15 | $implicit: { 16 | actual: 2, 17 | min: 3, 18 | } satisfies MinError, 19 | template: {} as TemplateRef, 20 | } as ErrorTemplate; 21 | const maxErrorTemplate = { 22 | $implicit: { 23 | actual: 3, 24 | max: 2, 25 | } satisfies MaxError, 26 | template: {} as TemplateRef, 27 | } as ErrorTemplate; 28 | 29 | it('should return true if the value is the same', () => { 30 | expect(distinctUntilErrorChanged(undefined, undefined)).toBeTrue(); 31 | expect(distinctUntilErrorChanged(minError, minError)).toBeTrue(); 32 | expect( 33 | distinctUntilErrorChanged(minErrorTemplate, minErrorTemplate) 34 | ).toBeTrue(); 35 | }); 36 | 37 | it('should return false if undefined follows other values', () => { 38 | expect(distinctUntilErrorChanged(minError, undefined)).toBeFalse(); 39 | expect(distinctUntilErrorChanged(minErrorTemplate, undefined)).toBeFalse(); 40 | }); 41 | 42 | it('should return false if other values follow undefined', () => { 43 | expect(distinctUntilErrorChanged(undefined, minError)).toBeFalse(); 44 | expect(distinctUntilErrorChanged(undefined, minErrorTemplate)).toBeFalse(); 45 | }); 46 | 47 | it('should return false if different values follow each other', () => { 48 | expect(distinctUntilErrorChanged(minError, maxError)).toBeFalse(); 49 | expect(distinctUntilErrorChanged(maxError, maxErrorTemplate)).toBeFalse(); 50 | expect( 51 | distinctUntilErrorChanged(maxErrorTemplate, minErrorTemplate) 52 | ).toBeFalse(); 53 | expect(distinctUntilErrorChanged(minErrorTemplate, minError)).toBeFalse(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /projects/ngx-mat-errors/src/lib/utils/distinct-until-error-changed.ts: -------------------------------------------------------------------------------- 1 | import type { ErrorTemplate } from '../types'; 2 | 3 | export function distinctUntilErrorChanged

( 4 | prev: P, 5 | curr: P 6 | ) { 7 | if (prev === curr) { 8 | return true; 9 | } 10 | if (!prev || !curr) { 11 | return false; 12 | } 13 | if (prev.template !== curr.template) { 14 | return false; 15 | } 16 | return prev.$implicit === curr.$implicit; 17 | } 18 | -------------------------------------------------------------------------------- /projects/ngx-mat-errors/src/lib/utils/find-error-for-control.ts: -------------------------------------------------------------------------------- 1 | import type { AbstractControl } from '@angular/forms'; 2 | import type { INgxMatErrorDef } from '../ngx-mat-error-def.directive'; 3 | import { ErrorMessages } from '../types'; 4 | 5 | /** 6 | * Finds the error key or custom error for a control. 7 | * @returns INgxMatErrorDef | undefined 8 | */ 9 | export function findErrorForControl( 10 | control: AbstractControl, 11 | messages: ErrorMessages, 12 | customErrorMessages: readonly INgxMatErrorDef[] 13 | ) { 14 | const errorKeys = Object.keys(control.errors!); 15 | return ( 16 | customErrorMessages.find((customErrorMessage) => 17 | errorKeys.some((error) => { 18 | if (error !== customErrorMessage.ngxMatErrorDefFor) { 19 | return false; 20 | } 21 | return ( 22 | !customErrorMessage.control || customErrorMessage.control === control 23 | ); 24 | }) 25 | ) ?? errorKeys.find((key) => key in messages) 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /projects/ngx-mat-errors/src/lib/utils/get-abstract-controls.ts: -------------------------------------------------------------------------------- 1 | import { coerceArray } from '@angular/cdk/coercion'; 2 | import { AbstractControl, AbstractControlDirective } from '@angular/forms'; 3 | import type { NgxMatErrorControls } from '../types'; 4 | 5 | export function getAbstractControls( 6 | controls: NgxMatErrorControls 7 | ): AbstractControl[] | undefined { 8 | if (!controls) { 9 | return; 10 | } 11 | const _controls = coerceArray(controls) 12 | .map((control) => 13 | !control 14 | ? undefined 15 | : control instanceof AbstractControlDirective 16 | ? control.control 17 | : control instanceof AbstractControl 18 | ? control 19 | : control.ngControl?.control 20 | ) 21 | .filter((control: T): control is NonNullable => control != null); 22 | return _controls.length ? _controls : undefined; 23 | } 24 | -------------------------------------------------------------------------------- /projects/ngx-mat-errors/src/lib/utils/get-control-with-error.ts: -------------------------------------------------------------------------------- 1 | import { 2 | StatusChangeEvent, 3 | ValueChangeEvent, 4 | type AbstractControl, 5 | } from '@angular/forms'; 6 | import { combineLatest, filter, map, startWith, type Observable } from 'rxjs'; 7 | 8 | export function getControlWithError( 9 | controls: AbstractControl[] 10 | ): Observable { 11 | const controlChanges = controls.map((control) => 12 | control.events.pipe( 13 | filter( 14 | (event) => 15 | event instanceof StatusChangeEvent || 16 | event instanceof ValueChangeEvent 17 | ), 18 | startWith(null as any), 19 | map(() => control) 20 | ) 21 | ); 22 | return combineLatest(controlChanges).pipe( 23 | map((control) => control.find((control) => !!control.errors)) 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /projects/ngx-mat-errors/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of ngx-mat-errors 3 | */ 4 | 5 | export * from './lib/locales'; 6 | export * from './lib/ngx-mat-error-def.directive'; 7 | export * from './lib/ngx-mat-errors-for-date-range-picker.directive'; 8 | export * from './lib/ngx-mat-errors.component'; 9 | export * from './lib/ngx-mat-errors.module'; 10 | export type * from './lib/types'; 11 | -------------------------------------------------------------------------------- /projects/ngx-mat-errors/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "declarationMap": true, 6 | "declaration": true, 7 | "inlineSources": true, 8 | "types": [], 9 | }, 10 | "exclude": [ 11 | "src/test.ts", 12 | "**/*.spec.ts" 13 | ], 14 | } 15 | -------------------------------------------------------------------------------- /projects/ngx-mat-errors/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "declarationMap": false 5 | }, 6 | "angularCompilerOptions": { 7 | "compilationMode": "partial" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /projects/ngx-mat-errors/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": ["jasmine", "node"] 6 | }, 7 | "include": ["**/*.spec.ts", "**/*.d.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "forceConsistentCasingInFileNames": true, 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "paths": { 15 | "ngx-mat-errors": [ 16 | "dist/ngx-mat-errors/ngx-mat-errors", 17 | "dist/ngx-mat-errors" 18 | ] 19 | }, 20 | "declaration": false, 21 | "experimentalDecorators": true, 22 | "moduleResolution": "node", 23 | "importHelpers": true, 24 | "target": "ES2022", 25 | "module": "es2020", 26 | "lib": [ 27 | "es2020", 28 | "dom" 29 | ], 30 | "useDefineForClassFields": false 31 | }, 32 | "angularCompilerOptions": { 33 | "enableI18nLegacyMessageIdFormat": false, 34 | "strictInjectionParameters": true, 35 | "strictInputAccessModifiers": true, 36 | "strictTemplates": true 37 | } 38 | } 39 | --------------------------------------------------------------------------------