├── .prettierignore ├── packages └── core │ ├── styles │ └── validator.scss │ ├── src │ ├── version.ts │ ├── testing │ │ ├── index.ts │ │ ├── dispatcher-events.ts │ │ └── events.ts │ ├── helpers.ts │ ├── strategies │ │ ├── validation-feedback-strategy.ts │ │ ├── noop-validation-feedback-strategy.ts │ │ ├── index.ts │ │ └── bootstrap-validation-feedback-strategy.ts │ ├── public-api.ts │ ├── test.ts │ ├── validator.service.spec.ts │ ├── directives │ │ ├── form-submit.directive.ts │ │ ├── validators.ts │ │ ├── form-validator.directive.ts │ │ ├── validators.spec.ts │ │ └── form-validator.directive.spec.ts │ ├── validators.ts │ ├── message-transformers.ts │ ├── validator.class.ts │ ├── module.ts │ ├── validators.spec.ts │ ├── validator-loader.service.ts │ ├── validator-loader.service.spec.ts │ ├── module.spec.ts │ └── validator.service.ts │ ├── examples │ ├── form-validator │ │ ├── examples │ │ │ ├── reactive-driven │ │ │ │ ├── reactive-driven.component.scss │ │ │ │ ├── index.md │ │ │ │ ├── reactive-driven.component.ts │ │ │ │ └── reactive-driven.component.html │ │ │ ├── template-driven │ │ │ │ ├── index.md │ │ │ │ ├── template-driven.component.scss │ │ │ │ ├── custom-select │ │ │ │ │ ├── custom-select.component.scss │ │ │ │ │ ├── custom-select.component.html │ │ │ │ │ ├── custom-select.component.spec.ts │ │ │ │ │ └── custom-select.component.ts │ │ │ │ ├── template-driven.component.ts │ │ │ │ └── template-driven.component.html │ │ │ └── module.ts │ │ ├── doc │ │ │ ├── zh-cn.md │ │ │ └── en-us.md │ │ └── api │ │ │ └── en-us.js │ └── validators │ │ ├── examples │ │ ├── max │ │ │ ├── max.component.scss │ │ │ ├── max.component.ts │ │ │ └── max.component.html │ │ ├── min │ │ │ ├── min.component.scss │ │ │ ├── min.component.ts │ │ │ └── min.component.html │ │ ├── unique-check │ │ │ ├── unique-check.component.scss │ │ │ ├── unique-check.component.ts │ │ │ └── unique-check.component.html │ │ └── module.ts │ │ └── doc │ │ ├── zh-cn.md │ │ └── en-us.md │ ├── ng-package.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ ├── tslint.json │ ├── package.json │ ├── tsconfig.lib.json │ └── karma.conf.js ├── .coveralls.yml ├── commitlint.config.js ├── .vscode └── settings.json ├── .docgeni └── public │ ├── assets │ └── favicon.ico │ ├── index.html │ ├── tsconfig.json │ └── styles.scss ├── Roadmap.md ├── .editorconfig ├── .browserslistrc ├── .prettierrc ├── .wpmrc.js ├── .github └── workflows │ └── main.yml ├── .gitignore ├── tsconfig.json ├── .circleci └── config.yml ├── LICENSE ├── .all-contributorsrc ├── angular.json ├── .docgenirc.js ├── package.json ├── README.md ├── docs ├── zh-cn │ └── index.md └── index.md ├── CHANGELOG.md └── 1.0.0-publish.md /.prettierignore: -------------------------------------------------------------------------------- 1 | *.md 2 | *.json 3 | -------------------------------------------------------------------------------- /packages/core/styles/validator.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/core/src/version.ts: -------------------------------------------------------------------------------- 1 | export const VERSION = '16.0.0'; 2 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-pro 2 | repo_token: 4dsZWc9Ld1NRky4Li80sZBPZ6fmnoa6W3 3 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /packages/core/examples/form-validator/examples/reactive-driven/reactive-driven.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/core/examples/form-validator/examples/reactive-driven/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 20 3 | --- 4 | -------------------------------------------------------------------------------- /packages/core/examples/form-validator/examples/template-driven/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 10 3 | --- 4 | -------------------------------------------------------------------------------- /packages/core/src/testing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dispatcher-events'; 2 | export * from './events'; 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Injectable", 4 | "Validators" 5 | ] 6 | } -------------------------------------------------------------------------------- /.docgeni/public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/why520crazy/ngx-validator/HEAD/.docgeni/public/assets/favicon.ico -------------------------------------------------------------------------------- /packages/core/examples/validators/examples/max/max.component.scss: -------------------------------------------------------------------------------- 1 | .btn-groups { 2 | .btn { 3 | margin-right: 15px; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/examples/validators/examples/min/min.component.scss: -------------------------------------------------------------------------------- 1 | .btn-groups { 2 | .btn { 3 | margin-right: 15px; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/helpers.ts: -------------------------------------------------------------------------------- 1 | export function isFunction(value: any) { 2 | const type = typeof value; 3 | return !!value && type === 'function'; 4 | } 5 | -------------------------------------------------------------------------------- /packages/core/examples/validators/examples/unique-check/unique-check.component.scss: -------------------------------------------------------------------------------- 1 | .btn-groups { 2 | .btn { 3 | margin-right: 15px; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/examples/form-validator/examples/template-driven/template-driven.component.scss: -------------------------------------------------------------------------------- 1 | .btn-groups { 2 | .btn { 3 | margin-right: 15px; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/core", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Roadmap.md: -------------------------------------------------------------------------------- 1 | ## ngx-validator Roadmap 2 | 3 | - Add more validators, e.g. ngx-min, ngx-max, ngx-repeat, ngx-unique-check ngx-range 4 | - Add Reactive Forms Demo and test 5 | - Add trigger condition `validateOn`, support `submit`、`blur` 6 | -------------------------------------------------------------------------------- /packages/core/src/strategies/validation-feedback-strategy.ts: -------------------------------------------------------------------------------- 1 | export interface ValidationFeedbackStrategy { 2 | showError(element: HTMLElement, errorMessages: string[]): void; 3 | removeError(element: HTMLElement): void; 4 | } 5 | -------------------------------------------------------------------------------- /packages/core/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "declarationMap": false 5 | }, 6 | "angularCompilerOptions": { 7 | "compilationMode": "partial" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/examples/form-validator/examples/template-driven/custom-select/custom-select.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex-direction: column; 4 | .item { 5 | padding: 10px 10px; 6 | display: flex; 7 | cursor: pointer; 8 | } 9 | } 10 | .active { 11 | background: #eee; 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/examples/form-validator/doc/zh-cn.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Form Validator 3 | name: validator 4 | --- 5 | 6 | `[ngxFormValidator]` 在表单上配置验证规则 7 | 8 | `(ngxFormSubmit)` 验证成功后触发提交事件 9 | 10 | ## 模板驱动 11 | 12 | 13 | ## 响应式驱动 14 | 15 | -------------------------------------------------------------------------------- /packages/core/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 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "w5c", 8 | "ngx", 9 | "camelCase" 10 | ], 11 | "component-selector": [ 12 | true, 13 | "element", 14 | "ngx", 15 | "test", 16 | "kebab-case" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.json] 12 | indent_size = 2 13 | 14 | [*.html] 15 | indent_size = 2 16 | 17 | [*.md] 18 | max_line_length = off 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.docgeni/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ngx Validator 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/core/examples/form-validator/examples/template-driven/custom-select/custom-select.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | {{ option.nativeElement.text }} 10 |
11 | -------------------------------------------------------------------------------- /packages/core/examples/validators/doc/zh-cn.md: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | `NgxValidator` 提供了如下几种验证器 5 | - `ngxMax` 数字最大值 6 | - `ngxMin` 数字最小值 7 | - `ngxUniqueCheck` 远程唯一性验证,比如:验证用户名是否存在 8 | 9 | ## Max 10 | 11 | 12 | 13 | ## Min 14 | 15 | 16 | 17 | ## Unique Check 18 | 19 | 20 | -------------------------------------------------------------------------------- /packages/core/examples/form-validator/doc/en-us.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Form Validator 3 | name: validator 4 | --- 5 | 6 | `[ngxFormValidator]` provide form validation config and validate feature. 7 | 8 | `(ngxFormSubmit)` provide submit event when validate success 9 | 10 | ## Template Driven 11 | 12 | 13 | ## Reactive Driven 14 | 15 | -------------------------------------------------------------------------------- /packages/core/src/strategies/noop-validation-feedback-strategy.ts: -------------------------------------------------------------------------------- 1 | import { ValidationFeedbackStrategy } from './validation-feedback-strategy'; 2 | 3 | export class NoopValidationFeedbackStrategy implements ValidationFeedbackStrategy { 4 | /** Does nothing, as this validation message display strategy is a no-op. */ 5 | showError(element: HTMLElement, errorMessages: string[]): void {} 6 | 7 | removeError(element: HTMLElement): void {} 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/examples/validators/doc/en-us.md: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | `NgxValidator` support some validators: 5 | - `ngxMax` number max validator 6 | - `ngxMin` number min validator 7 | - `ngxUniqueCheck` remote unique check, e.g. username 8 | 9 | ## Max 10 | 11 | 12 | 13 | ## Min 14 | 15 | 16 | 17 | ## Unique Check 18 | 19 | 20 | -------------------------------------------------------------------------------- /packages/core/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of core 3 | */ 4 | 5 | export * from './module'; 6 | export * from './validator-loader.service'; 7 | export * from './validator.class'; 8 | export * from './directives/form-validator.directive'; 9 | export * from './directives/validators'; 10 | export * from './directives/form-submit.directive'; 11 | export * from './strategies'; 12 | export { NgxValidators } from './validators'; 13 | export * from './version'; 14 | -------------------------------------------------------------------------------- /.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 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "eslintIntegration": true, 3 | "stylelintIntegration": true, 4 | "tabWidth": 4, 5 | "singleQuote": true, 6 | "semi": true, 7 | "printWidth": 120, 8 | "overrides": [ 9 | { 10 | "files": "*.json", 11 | "options": { 12 | "tabWidth": 2 13 | } 14 | }, 15 | { 16 | "files": "*.html", 17 | "options": { 18 | "tabWidth": 2 19 | } 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.wpmrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | allowBranch: ['master'], 3 | bumpFiles: [ 4 | 'package.json', 5 | './packages/core/package.json', 6 | { 7 | filename: './packages/core/src/version.ts', 8 | type: 'code' 9 | } 10 | ], 11 | skip: { 12 | changelog: true 13 | }, 14 | commitAll: true, 15 | hooks: { 16 | prepublish: 'npm run build', 17 | postreleaseBranch: 'git add .', 18 | postpublish: 'npm run pub-only' 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /packages/core/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'core-js/es7/reflect'; 4 | import 'zone.js'; 5 | import 'zone.js/testing'; 6 | import { getTestBed } from '@angular/core/testing'; 7 | import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; 8 | 9 | // First, initialize the Angular testing environment. 10 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); 11 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI NGX-VALIDATOR through Github Actions 2 | on: push 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - name: Use Node.js 16.x 9 | uses: actions/setup-node@v1 10 | with: 11 | node-version: 16.x 12 | 13 | - name: Setup 14 | run: npm ci 15 | 16 | - name: Test 17 | run: | 18 | npm test -- --no-watch --no-progress --browsers=ChromeHeadlessCI 19 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@why520crazy/ngx-validator", 3 | "version": "16.0.0", 4 | "description": "Angular7+ form validator, make error tips easy and automatic. don't need to manually write error tips templates.", 5 | "private": false, 6 | "repository": "https://github.com/why520crazy/ngx-validator", 7 | "author": "why520crazy ", 8 | "license": "MIT", 9 | "dependencies": { 10 | "tslib": "^2.0.0" 11 | }, 12 | "peerDependencies": { 13 | "@angular/common": "*", 14 | "@angular/forms": "*", 15 | "@angular/core": "*" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/src/validator.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { NgxValidatorLoader } from './validator-loader.service'; 3 | 4 | import { NgxFormValidatorService } from './validator.service'; 5 | 6 | describe('NgxFormValidatorService', () => { 7 | beforeEach(() => 8 | TestBed.configureTestingModule({ 9 | providers: [NgxFormValidatorService, NgxValidatorLoader] 10 | }) 11 | ); 12 | 13 | it('should be created', () => { 14 | const service: NgxFormValidatorService = TestBed.inject(NgxFormValidatorService); 15 | expect(service).toBeTruthy(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/core/examples/validators/examples/max/max.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { NgxValidatorConfig } from '@why520crazy/ngx-validator'; 3 | 4 | @Component({ 5 | selector: 'ngx-validators-max-example', 6 | templateUrl: './max.component.html', 7 | styleUrls: ['./max.component.scss'] 8 | }) 9 | export class NgxValidatorsMaxExampleComponent implements OnInit { 10 | validatorConfig: NgxValidatorConfig; 11 | 12 | value: number; 13 | 14 | message: string; 15 | 16 | constructor() {} 17 | 18 | ngOnInit(): void {} 19 | 20 | submit() { 21 | this.message = 'This form has submit'; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/core/examples/validators/examples/min/min.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { NgxValidatorConfig } from '@why520crazy/ngx-validator'; 3 | 4 | @Component({ 5 | selector: 'ngx-validators-min-example', 6 | templateUrl: './min.component.html', 7 | styleUrls: ['./min.component.scss'] 8 | }) 9 | export class NgxValidatorsMinExampleComponent implements OnInit { 10 | validatorConfig: NgxValidatorConfig; 11 | 12 | value: number; 13 | 14 | message: string; 15 | 16 | constructor() {} 17 | 18 | ngOnInit(): void {} 19 | 20 | submit() { 21 | this.message = 'This form has submit'; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.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 | built 8 | 9 | # dependencies 10 | /node_modules 11 | 12 | # IDEs and editors 13 | /.idea 14 | .project 15 | .classpath 16 | .c9/ 17 | *.launch 18 | .settings/ 19 | *.sublime-workspace 20 | 21 | # IDE - VSCode 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | 28 | # misc 29 | /.angular/cache 30 | /.sass-cache 31 | /connect.lock 32 | /coverage 33 | /libpeerconnection.log 34 | npm-debug.log 35 | yarn-error.log 36 | testem.log 37 | /typings 38 | 39 | # System Files 40 | .DS_Store 41 | Thumbs.db 42 | .docgeni/site -------------------------------------------------------------------------------- /packages/core/src/strategies/index.ts: -------------------------------------------------------------------------------- 1 | import { NoopValidationFeedbackStrategy } from './noop-validation-feedback-strategy'; 2 | import { BootstrapValidationFeedbackStrategy } from './bootstrap-validation-feedback-strategy'; 3 | import { ValidationFeedbackStrategy } from './validation-feedback-strategy'; 4 | 5 | export class ValidationFeedbackStrategyBuilder { 6 | static noop(): ValidationFeedbackStrategy { 7 | return new NoopValidationFeedbackStrategy(); 8 | } 9 | 10 | static bootstrap(): ValidationFeedbackStrategy { 11 | return new BootstrapValidationFeedbackStrategy(); 12 | } 13 | } 14 | 15 | export { ValidationFeedbackStrategy, NoopValidationFeedbackStrategy, BootstrapValidationFeedbackStrategy }; 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "downlevelIteration": true, 6 | "importHelpers": true, 7 | "outDir": "./dist/out-tsc", 8 | "sourceMap": true, 9 | "declaration": false, 10 | "module": "es2020", 11 | "moduleResolution": "node", 12 | "experimentalDecorators": true, 13 | "target": "ES2022", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ], 21 | "paths": { 22 | "core": [ 23 | "dist/core" 24 | ], 25 | "core/*": [ 26 | "dist/core/*" 27 | ] 28 | }, 29 | "useDefineForClassFields": false 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "declarationMap": true, 6 | "module": "es2015", 7 | "moduleResolution": "node", 8 | "declaration": true, 9 | "sourceMap": true, 10 | "inlineSources": true, 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "types": [], 14 | "lib": ["dom", "es2018"] 15 | }, 16 | "angularCompilerOptions": { 17 | "skipTemplateCodegen": true, 18 | "strictMetadataEmit": true, 19 | "fullTemplateTypeCheck": true, 20 | "strictInjectionParameters": true, 21 | "enableResourceInlining": true 22 | }, 23 | "exclude": ["src/test.ts", "**/*.spec.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | browser-tools: circleci/browser-tools@1.4.3 4 | jobs: 5 | build: 6 | working_directory: ~/ngx-validator 7 | docker: 8 | - image: cimg/node:18.16-browsers 9 | steps: 10 | - browser-tools/install-chrome 11 | - checkout 12 | - restore_cache: 13 | key: ngx-validator-{{ .Branch }}-{{ checksum "package-lock.json" }} 14 | - run: npm install 15 | - save_cache: 16 | key: ngx-validator-{{ .Branch }}-{{ checksum "package-lock.json" }} 17 | paths: 18 | - 'node_modules' 19 | - run: npm run test -- --no-watch --no-progress --browsers=ChromeHeadlessCI 20 | - run: npm run report-coverage 21 | -------------------------------------------------------------------------------- /packages/core/examples/validators/examples/unique-check/unique-check.component.ts: -------------------------------------------------------------------------------- 1 | import { NgxValidatorConfig } from '@why520crazy/ngx-validator'; 2 | import { Component, OnInit } from '@angular/core'; 3 | import { of } from 'rxjs'; 4 | 5 | @Component({ 6 | selector: 'ngx-validators-unique-check-example', 7 | templateUrl: './unique-check.component.html', 8 | styleUrls: ['./unique-check.component.scss'] 9 | }) 10 | export class NgxValidatorsUniqueCheckExampleComponent implements OnInit { 11 | validatorConfig: NgxValidatorConfig; 12 | 13 | value: number; 14 | 15 | message: string; 16 | 17 | uniqueCheck = (value: string) => { 18 | return value === 'peter' ? of(true) : of(false); 19 | }; 20 | 21 | constructor() {} 22 | 23 | ngOnInit(): void {} 24 | 25 | submit() { 26 | this.message = 'This form has submit'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/core/examples/validators/examples/module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { NgxValidatorModule } from '@why520crazy/ngx-validator'; 4 | import { ReactiveFormsModule } from '@angular/forms'; 5 | import { NgxValidatorsMinExampleComponent } from './min/min.component'; 6 | import { NgxValidatorsMaxExampleComponent } from './max/max.component'; 7 | import { NgxValidatorsUniqueCheckExampleComponent } from './unique-check/unique-check.component'; 8 | 9 | @NgModule({ 10 | declarations: [ 11 | NgxValidatorsMinExampleComponent, 12 | NgxValidatorsMaxExampleComponent, 13 | NgxValidatorsUniqueCheckExampleComponent 14 | ], 15 | imports: [CommonModule, NgxValidatorModule, ReactiveFormsModule], 16 | exports: [], 17 | providers: [] 18 | }) 19 | export class NgxValidatorsExamplesModule {} 20 | -------------------------------------------------------------------------------- /packages/core/examples/validators/examples/max/max.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 14 |
15 |
16 |
{{ message }}
17 |
18 |
19 | 20 | 21 |
22 |
23 | -------------------------------------------------------------------------------- /packages/core/examples/validators/examples/min/min.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 14 |
15 |
16 |
{{ message }}
17 |
18 |
19 | 20 | 21 |
22 |
23 | -------------------------------------------------------------------------------- /packages/core/examples/validators/examples/unique-check/unique-check.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 13 |
14 |
15 |
{{ message }}
16 |
17 |
18 | 19 | 20 |
21 |
22 | -------------------------------------------------------------------------------- /packages/core/src/directives/form-submit.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, Output, OnInit, HostBinding, HostListener, Optional, EventEmitter } from '@angular/core'; 2 | import { NgForm } from '@angular/forms'; 3 | import { NgxFormValidatorDirective } from './form-validator.directive'; 4 | 5 | @Directive({ 6 | selector: '[ngxFormSubmit],[ngx-form-submit]' 7 | }) 8 | export class NgxFormSubmitDirective implements OnInit { 9 | 10 | @Output() ngxFormSubmit = new EventEmitter(); 11 | 12 | constructor( 13 | private validatorDirective: NgxFormValidatorDirective 14 | ) { 15 | } 16 | 17 | ngOnInit(): void { 18 | this.validatorDirective.onSubmitSuccess = ($event: any) => { 19 | this.ngxFormSubmit.emit($event); 20 | }; 21 | } 22 | 23 | @HostListener('click', ['$event']) 24 | onSubmit($event: any) { 25 | this.validatorDirective.submit($event); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/examples/form-validator/examples/template-driven/custom-select/custom-select.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { CustomSelectComponent } from './custom-select.component'; 4 | 5 | describe('CustomSelectComponent', () => { 6 | let component: CustomSelectComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach( 10 | waitForAsync(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [CustomSelectComponent] 13 | }).compileComponents(); 14 | }) 15 | ); 16 | 17 | beforeEach(() => { 18 | fixture = TestBed.createComponent(CustomSelectComponent); 19 | component = fixture.componentInstance; 20 | fixture.detectChanges(); 21 | }); 22 | 23 | it('should create', () => { 24 | expect(component).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /.docgeni/public/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "outDir": "./dist/out-tsc/site", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "downlevelIteration": true, 8 | "experimentalDecorators": true, 9 | "module": "ESNext", 10 | "moduleResolution": "node", 11 | "esModuleInterop": false, 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "lib": [ 15 | "es2018", 16 | "dom" 17 | ], 18 | "types": [ 19 | "node" 20 | ], 21 | "paths":{ 22 | "@why520crazy/ngx-validator/*":["../../packages/core/src/*"], 23 | "@why520crazy/ngx-validator":["../../packages/core/src/index.ts","../../packages/core/src/public-api.ts"], 24 | } 25 | }, 26 | "files": [ 27 | "src/main.ts", 28 | "src/polyfills.ts" 29 | ], 30 | "include": [ 31 | "src/**/*.d.ts" 32 | ], 33 | "exclude": [ 34 | "src/assets", 35 | "test.ts" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /packages/core/examples/form-validator/examples/module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { NgxValidatorModule } from '@why520crazy/ngx-validator'; 4 | import { NgxValidatorTemplateDrivenExampleComponent } from './template-driven/template-driven.component'; 5 | import { NgxValidatorReactiveDrivenExampleComponent } from './reactive-driven/reactive-driven.component'; 6 | import { ReactiveFormsModule } from '@angular/forms'; 7 | import { CustomSelectComponent } from './template-driven/custom-select/custom-select.component'; 8 | 9 | @NgModule({ 10 | declarations: [ 11 | NgxValidatorTemplateDrivenExampleComponent, 12 | NgxValidatorReactiveDrivenExampleComponent, 13 | CustomSelectComponent 14 | ], 15 | imports: [CommonModule, NgxValidatorModule, ReactiveFormsModule], 16 | exports: [], 17 | providers: [] 18 | }) 19 | export class NgxValidatorExamplesModule {} 20 | -------------------------------------------------------------------------------- /packages/core/src/validators.ts: -------------------------------------------------------------------------------- 1 | import { AbstractControl, ValidationErrors } from '@angular/forms'; 2 | import { of, Observable } from 'rxjs'; 3 | import { map } from 'rxjs/operators'; 4 | 5 | export class NgxValidators { 6 | static uniqueCheckValidator( 7 | uniqueCheckFn: (value: string) => Observable 8 | ): (control: AbstractControl) => Promise | Observable { 9 | const result = ( 10 | control: AbstractControl 11 | ): Promise | Observable => { 12 | if (control.value) { 13 | return uniqueCheckFn(control.value).pipe( 14 | map(isUnique => { 15 | return isUnique ? { ngxUniqueCheck: { value: true } } : null; 16 | }) 17 | ); 18 | } else { 19 | return of(null); 20 | } 21 | }; 22 | return result; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 why520crazy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/core/src/message-transformers.ts: -------------------------------------------------------------------------------- 1 | function maxOrMinLengthTransformer(message: string, validationErrorValues: { requiredLength: number }): string { 2 | return message.replace(`{requiredLength}`, validationErrorValues.requiredLength.toString()); 3 | } 4 | 5 | function maxTransformer(message: string, validationErrorValues: { max: number; actual: number }): string { 6 | return message.replace(`{max}`, validationErrorValues.max.toString()); 7 | } 8 | 9 | function minxTransformer(message: string, validationErrorValues: { min: number; actual: number }): string { 10 | return message.replace(`{min}`, validationErrorValues.min.toString()); 11 | } 12 | 13 | const transformerMap = { 14 | minlength: maxOrMinLengthTransformer, 15 | maxlength: maxOrMinLengthTransformer, 16 | max: maxTransformer, 17 | min: minxTransformer 18 | }; 19 | 20 | export function transformMessage(validatorName: string, message: string, validationErrorValues: any) { 21 | if (transformerMap[validatorName] && validationErrorValues) { 22 | return transformerMap[validatorName](message, validationErrorValues); 23 | } 24 | return message; 25 | } 26 | -------------------------------------------------------------------------------- /packages/core/src/validator.class.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | import { ValidationFeedbackStrategy } from './strategies'; 3 | 4 | export declare type NgxValidationMessages = Record>; 5 | 6 | export declare type NgxValidateOn = 'submit' | 'blur' | 'change'; 7 | 8 | export interface NgxValidatorConfig { 9 | validationFeedbackStrategy?: ValidationFeedbackStrategy; 10 | validationMessages?: NgxValidationMessages; 11 | validateOn?: NgxValidateOn; 12 | } 13 | export interface NgxValidatorGlobalConfig extends NgxValidatorConfig { 14 | globalValidationMessages?: Record; 15 | } 16 | 17 | export const NGX_VALIDATOR_CONFIG = new InjectionToken('NGX_VALIDATION_CONFIG'); 18 | 19 | export const DEFAULT_GLOBAL_VALIDATION_MESSAGES = { 20 | required: '该选项不能为空', 21 | maxlength: '该选项输入值长度不能大于{requiredLength}', 22 | minlength: '该选项输入值长度不能小于{requiredLength}', 23 | ngxUniqueCheck: '输入值已经存在,请重新输入', 24 | email: '输入邮件的格式不正确', 25 | repeat: '两次输入不一致', 26 | pattern: '该选项输入格式不正确', 27 | number: '必须输入数字', 28 | url: '输入URL格式不正确', 29 | max: '该选项输入值不能大于{max}', 30 | min: '该选项输入值不能小于{min}' 31 | }; 32 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "@why520crazy/ngx-validator", 3 | "projectOwner": "why520carzy", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "commitConvention": "none", 12 | "contributors": [ 13 | { 14 | "login": "why520crazy", 15 | "name": "why520crazy", 16 | "avatar_url": "https://avatars2.githubusercontent.com/u/3959960?v=4", 17 | "profile": "https://www.zhihu.com/people/why520crazy/activities", 18 | "contributions": [ 19 | "question" 20 | ] 21 | }, 22 | { 23 | "login": "luxiaobei", 24 | "name": "luxiaobei", 25 | "avatar_url": "https://avatars1.githubusercontent.com/u/13583957?v=4", 26 | "profile": "https://github.com/luxiaobei", 27 | "contributions": [ 28 | "code" 29 | ] 30 | }, 31 | { 32 | "login": "walkerkay", 33 | "name": "Walker", 34 | "avatar_url": "https://avatars1.githubusercontent.com/u/15701592?v=4", 35 | "profile": "https://github.com/walkerkay", 36 | "contributions": [ 37 | "design" 38 | ] 39 | } 40 | ], 41 | "contributorsPerLine": 7 42 | } 43 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "core": { 7 | "root": "packages/core", 8 | "sourceRoot": "packages/core/src", 9 | "projectType": "library", 10 | "prefix": "ngx", 11 | "architect": { 12 | "build": { 13 | "builder": "@angular-devkit/build-angular:ng-packagr", 14 | "options": { 15 | "tsConfig": "packages/core/tsconfig.lib.json", 16 | "project": "packages/core/ng-package.json" 17 | }, 18 | "configurations": { 19 | "production": { 20 | "tsConfig": "packages/core/tsconfig.lib.prod.json" 21 | } 22 | } 23 | }, 24 | "test": { 25 | "builder": "@angular-devkit/build-angular:karma", 26 | "options": { 27 | "main": "packages/core/src/test.ts", 28 | "tsConfig": "packages/core/tsconfig.spec.json", 29 | "karmaConfig": "packages/core/karma.conf.js", 30 | "codeCoverage": true, 31 | "codeCoverageExclude": ["packages/core/src/testing/**/*"] 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/core/examples/form-validator/api/en-us.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | type: 'directive', 4 | name: '[ngxFormValidator]', 5 | description: 'Form validator', 6 | properties: [ 7 | { 8 | name: 'ngxFormValidator', 9 | type: `NgxValidatorConfig`, 10 | default: `null`, 11 | description: 'validator congif' 12 | } 13 | ] 14 | }, 15 | { 16 | type: 'interface', 17 | name: 'NgxValidatorConfig', 18 | description: 'Validator Config', 19 | properties: [ 20 | { 21 | name: 'validateOn', 22 | type: `'submit' | 'blur' | 'change'`, 23 | default: `submit`, 24 | description: 'validate trigger' 25 | }, 26 | { 27 | name: 'validationMessages', 28 | type: `Record>`, 29 | default: `null`, 30 | description: `validation messages, e.g. \n { username: { required: 'username is required'}` 31 | }, 32 | { 33 | name: 'validationFeedbackStrategy', 34 | type: `ValidationFeedbackStrategy`, 35 | default: `null`, 36 | description: `validation feedback strategy` 37 | } 38 | ] 39 | } 40 | ]; 41 | -------------------------------------------------------------------------------- /packages/core/src/module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders } from '@angular/core'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { NgxFormValidatorDirective } from './directives/form-validator.directive'; 4 | import { NgxFormSubmitDirective } from './directives/form-submit.directive'; 5 | 6 | // import { NgxUniqueCheckDirective } from './directives/form-unique-check.directive'; 7 | import { MaxValidatorDirective, MinValidatorDirective, NgxUniqueCheckDirective } from './directives/validators'; 8 | 9 | import { NgxValidatorGlobalConfig, NGX_VALIDATOR_CONFIG } from './validator.class'; 10 | import { NgxValidatorLoader } from './validator-loader.service'; 11 | 12 | const declarations = [ 13 | NgxFormValidatorDirective, 14 | NgxFormSubmitDirective, 15 | NgxUniqueCheckDirective, 16 | MaxValidatorDirective, 17 | MinValidatorDirective 18 | ]; 19 | 20 | @NgModule({ 21 | declarations: declarations, 22 | providers: [NgxValidatorLoader], 23 | imports: [FormsModule], 24 | exports: [...declarations, FormsModule] 25 | }) 26 | export class NgxValidatorModule { 27 | static forRoot(config: NgxValidatorGlobalConfig): ModuleWithProviders { 28 | return { 29 | ngModule: NgxValidatorModule, 30 | providers: [ 31 | { 32 | provide: NGX_VALIDATOR_CONFIG, 33 | useValue: config 34 | } 35 | ] 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.docgeni/public/styles.scss: -------------------------------------------------------------------------------- 1 | @import 'bootstrap/scss/bootstrap.scss'; 2 | @import '@docgeni/template/styles/index.css'; 3 | 4 | @mixin ngx-form-validation-state($state, $color) { 5 | .ngx-custom-select { 6 | .was-validated &:#{$state}, 7 | &.is-#{$state} { 8 | // border-color: $color; 9 | border: 1px solid $color; 10 | 11 | @if $enable-validation-icons { 12 | padding-right: $input-height-inner; 13 | background-repeat: no-repeat; 14 | background-position: center right calc(#{$input-height-inner} / 4); 15 | background-size: calc(#{$input-height-inner} / 2) calc(#{$input-height-inner} / 2); 16 | 17 | @if $state == 'valid' { 18 | background-image: $form-feedback-icon-valid; 19 | } @else { 20 | background-image: $form-feedback-icon-invalid; 21 | } 22 | } 23 | 24 | &:focus { 25 | border-color: $color; 26 | box-shadow: 0 0 0 $input-focus-width rgba($color, 0.25); 27 | } 28 | 29 | ~ .#{$state}-feedback, 30 | ~ .#{$state}-tooltip { 31 | display: block; 32 | } 33 | } 34 | } 35 | } 36 | 37 | @include ngx-form-validation-state('valid', $form-feedback-valid-color); 38 | @include ngx-form-validation-state('invalid', $form-feedback-invalid-color); 39 | 40 | a:hover { 41 | text-decoration: none; 42 | } 43 | -------------------------------------------------------------------------------- /packages/core/src/strategies/bootstrap-validation-feedback-strategy.ts: -------------------------------------------------------------------------------- 1 | import { ValidationFeedbackStrategy } from './validation-feedback-strategy'; 2 | 3 | const INVALID_CLASS = 'is-invalid'; 4 | const INVALID_FEEDBACK_CLASS = 'invalid-feedback'; 5 | 6 | export class BootstrapValidationFeedbackStrategy implements ValidationFeedbackStrategy { 7 | constructor() {} 8 | 9 | showError(element: HTMLElement, errorMessages: string[]): void { 10 | if (element) { 11 | element.classList.add(INVALID_CLASS); 12 | } 13 | 14 | if (element && element.parentElement) { 15 | const documentFrag = document.createDocumentFragment(); 16 | const divNode = document.createElement('DIV'); 17 | const textNode = document.createTextNode(errorMessages[0]); 18 | divNode.appendChild(textNode); 19 | divNode.setAttribute('class', INVALID_FEEDBACK_CLASS); 20 | documentFrag.appendChild(divNode); 21 | element.parentElement.append(documentFrag); 22 | } 23 | } 24 | 25 | removeError(element: HTMLElement): void { 26 | if (element) { 27 | element.classList.remove(INVALID_CLASS); 28 | } 29 | if (element && element.parentElement) { 30 | const invalidFeedback = element.parentElement.querySelector(`.${INVALID_FEEDBACK_CLASS}`); 31 | if (invalidFeedback) { 32 | element.parentElement.removeChild(invalidFeedback); 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/core/src/testing/dispatcher-events.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createFakeEvent, 3 | createKeyboardEvent, 4 | createMouseEvent, 5 | createTouchEvent 6 | } from './events'; 7 | 8 | /** Utility to dispatch any event on a Node. */ 9 | export function dispatchEvent(node: Node | Window, event: Event): Event { 10 | node.dispatchEvent(event); 11 | return event; 12 | } 13 | 14 | /** Shorthand to dispatch a fake event on a specified node. */ 15 | export function dispatchFakeEvent( 16 | node: Node | Window, 17 | type: string, 18 | canBubble?: boolean 19 | ): Event { 20 | return dispatchEvent(node, createFakeEvent(type, canBubble)); 21 | } 22 | 23 | /** Shorthand to dispatch a keyboard event with a specified key code. */ 24 | export function dispatchKeyboardEvent( 25 | node: Node, 26 | type: string, 27 | keyCode: number, 28 | target?: Element 29 | ): KeyboardEvent { 30 | return dispatchEvent( 31 | node, 32 | createKeyboardEvent(type, keyCode, target) 33 | ) as KeyboardEvent; 34 | } 35 | 36 | /** Shorthand to dispatch a mouse event on the specified coordinates. */ 37 | export function dispatchMouseEvent( 38 | node: Node, 39 | type: string, 40 | x = 0, 41 | y = 0, 42 | event = createMouseEvent(type, x, y) 43 | ): MouseEvent { 44 | return dispatchEvent(node, event) as MouseEvent; 45 | } 46 | 47 | /** Shorthand to dispatch a touch event on the specified coordinates. */ 48 | export function dispatchTouchEvent(node: Node, type: string, x = 0, y = 0) { 49 | return dispatchEvent(node, createTouchEvent(type, x, y)); 50 | } 51 | -------------------------------------------------------------------------------- /.docgenirc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@docgeni/core').DocgeniConfig} 3 | */ 4 | module.exports = { 5 | mode: 'lite', 6 | title: 'NGX-VALIDATOR', 7 | logoUrl: 'https://cdn.pingcode.com/open-sources/angular/angular.svg', 8 | repoUrl: 'https://github.com/why520crazy/ngx-validator', 9 | description: '', 10 | docsDir: 'docs', 11 | navs: [ 12 | null, 13 | { 14 | title: 'Components', 15 | path: 'components', 16 | lib: 'core', 17 | locales: { 18 | 'zh-cn': { 19 | title: '组件' 20 | } 21 | } 22 | }, 23 | { 24 | title: 'GitHub', 25 | path: 'https://github.com/why520crazy/ngx-validator', 26 | isExternal: true 27 | }, 28 | { 29 | title: 'CHANGELOG', 30 | path: 'https://github.com/why520crazy/ngx-validator/blob/master/CHANGELOG.md', 31 | isExternal: true, 32 | locales: { 33 | 'zh-cn': { 34 | title: '更新日志' 35 | } 36 | } 37 | } 38 | ], 39 | libs: [ 40 | { 41 | name: 'core', 42 | rootDir: 'packages/core', 43 | abbrName: 'ngx', 44 | include: ['src', 'examples'], 45 | categories: [] 46 | } 47 | ], 48 | locales: [ 49 | { 50 | key: 'zh-cn', 51 | name: '中文' 52 | }, 53 | { 54 | key: 'en-us', 55 | name: 'English' 56 | } 57 | ] 58 | }; 59 | -------------------------------------------------------------------------------- /packages/core/examples/form-validator/examples/template-driven/custom-select/custom-select.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ContentChildren, QueryList, ElementRef, forwardRef } from '@angular/core'; 2 | import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'; 3 | 4 | export const CUSTOM_SELECT_VALUE_ACCESSOR: any = { 5 | provide: NG_VALUE_ACCESSOR, 6 | useExisting: forwardRef(() => CustomSelectComponent), 7 | multi: true 8 | }; 9 | 10 | const noop = () => {}; 11 | 12 | @Component({ 13 | selector: 'app-custom-select', 14 | templateUrl: './custom-select.component.html', 15 | styleUrls: ['./custom-select.component.scss'], 16 | providers: [CUSTOM_SELECT_VALUE_ACCESSOR] 17 | }) 18 | export class CustomSelectComponent implements OnInit, ControlValueAccessor { 19 | selectedValue: string; 20 | 21 | private onTouchedCallback: () => void = noop; 22 | 23 | private onChangeCallback: (_: any) => void = noop; 24 | 25 | @ContentChildren('option') options: QueryList; 26 | 27 | constructor() {} 28 | 29 | ngOnInit() {} 30 | 31 | writeValue(obj: any): void { 32 | this.selectedValue = obj; 33 | } 34 | 35 | registerOnChange(fn: any): void { 36 | this.onChangeCallback = fn; 37 | } 38 | 39 | registerOnTouched(fn: any): void { 40 | this.onTouchedCallback = fn; 41 | } 42 | 43 | setDisabledState?(isDisabled: boolean): void {} 44 | 45 | selectOption(option: ElementRef) { 46 | this.selectedValue = option.nativeElement.value; 47 | this.onChangeCallback(option.nativeElement.value); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/core/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { random: false }, 17 | clearContext: false // leave Jasmine Spec Runner output visible in browser 18 | }, 19 | jasmineHtmlReporter: { 20 | suppressAll: true // removes the duplicated traces 21 | }, 22 | coverageReporter: { 23 | dir: require('path').join(__dirname, '../../coverage'), 24 | subdir: '.', 25 | reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'lcovonly' }] 26 | }, 27 | angularCli: { 28 | environment: 'dev' 29 | }, 30 | files: [], 31 | reporters: ['progress', 'kjhtml'], 32 | port: 9876, 33 | colors: true, 34 | logLevel: config.LOG_INFO, 35 | autoWatch: true, 36 | browsers: ['Chrome'], 37 | singleRun: false, 38 | customLaunchers: { 39 | ChromeHeadlessCI: { 40 | base: 'ChromeHeadless', 41 | flags: ['--no-sandbox'] 42 | } 43 | }, 44 | restartOnFileChange: true 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /packages/core/examples/form-validator/examples/template-driven/template-driven.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding } from '@angular/core'; 2 | import { NgxValidatorConfig, NgxValidateOn } from '@why520crazy/ngx-validator'; 3 | import { of } from 'rxjs'; 4 | import { delay } from 'rxjs/operators'; 5 | 6 | @Component({ 7 | selector: 'ngx-validator-template-driven-example', 8 | templateUrl: './template-driven.component.html', 9 | styleUrls: ['./template-driven.component.scss'] 10 | }) 11 | export class NgxValidatorTemplateDrivenExampleComponent { 12 | message = ''; 13 | 14 | showSex = false; 15 | 16 | validateOn: NgxValidateOn = 'change'; 17 | 18 | loadingDone = true; 19 | 20 | model = { 21 | username: '', 22 | email: '', 23 | password: '', 24 | number: '', 25 | sex: '', 26 | customSelectValue: '' 27 | }; 28 | 29 | validatorConfig: NgxValidatorConfig = { 30 | validationMessages: { 31 | username: { 32 | required: '用户名不能为空', 33 | pattern: '用户名格式不正确,以字母,数字,下划线组成,首字母不能为数字,必须是2-20个字符', 34 | ngxUniqueCheck: '输入的用户名已经存在,请重新输入' 35 | } 36 | }, 37 | validateOn: this.validateOn 38 | }; 39 | 40 | changeValidateOn() { 41 | this.loadingDone = false; 42 | this.validatorConfig.validateOn = this.validateOn; 43 | setTimeout(() => { 44 | this.loadingDone = true; 45 | }, 0); 46 | } 47 | 48 | checkUsername = (value: string) => { 49 | return value === 'peter' ? of(true).pipe(delay(200)) : of(false).pipe(delay(200)); 50 | }; 51 | 52 | setMessage(message: string) { 53 | this.message = message; 54 | } 55 | 56 | submit() { 57 | this.setMessage('This form has submit'); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/core/src/validators.spec.ts: -------------------------------------------------------------------------------- 1 | import { AbstractControl, FormControl, ValidationErrors } from '@angular/forms'; 2 | import { of, Observable } from 'rxjs'; 3 | import { NgxValidators } from './validators'; 4 | 5 | describe('NgxValidators', () => { 6 | describe('ngxUniqueCheck', () => { 7 | let ngxUniqueCheck: ( 8 | control: AbstractControl 9 | ) => Promise | Observable; 10 | 11 | const uniqueCheck = value => { 12 | return value === 'unique' ? of(false) : of(true); 13 | }; 14 | 15 | const handleUniqueCheck = (payload: Promise | Observable) => { 16 | let errorMap: ValidationErrors; 17 | if (payload instanceof Observable) { 18 | payload.subscribe(data => { 19 | errorMap = data; 20 | }); 21 | return errorMap; 22 | } 23 | }; 24 | 25 | beforeEach(() => { 26 | ngxUniqueCheck = NgxValidators.uniqueCheckValidator(uniqueCheck); 27 | }); 28 | 29 | it('should error on an empty string', () => { 30 | expect(handleUniqueCheck(ngxUniqueCheck(new FormControl('')))).toEqual(null); 31 | }); 32 | 33 | it('should error on null', () => { 34 | expect(handleUniqueCheck(ngxUniqueCheck(new FormControl(null)))).toEqual(null); 35 | }); 36 | 37 | it('should not error on undefined', () => { 38 | expect(handleUniqueCheck(ngxUniqueCheck(new FormControl(undefined)))).toEqual(null); 39 | }); 40 | 41 | it('should valid on unique value', () => { 42 | expect(handleUniqueCheck(ngxUniqueCheck(new FormControl('unique')))).toEqual(null); 43 | }); 44 | 45 | it('should invalid on a repeat value', () => { 46 | expect(handleUniqueCheck(ngxUniqueCheck(new FormControl('repeat'))).ngxUniqueCheck).toEqual({ 47 | value: true 48 | }); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /packages/core/src/directives/validators.ts: -------------------------------------------------------------------------------- 1 | import { Directive, forwardRef, Input } from '@angular/core'; 2 | import { 3 | NG_VALIDATORS, 4 | Validator, 5 | AbstractControl, 6 | Validators, 7 | ValidatorFn, 8 | NG_ASYNC_VALIDATORS, 9 | AsyncValidator, 10 | ValidationErrors 11 | } from '@angular/forms'; 12 | import { Observable, of } from 'rxjs'; 13 | import { NgxValidators } from '../validators'; 14 | 15 | @Directive({ 16 | selector: '[ngxMin][formControlName],[ngxMin][formControl],[ngxMin][ngModel],', 17 | providers: [ 18 | { 19 | provide: NG_VALIDATORS, 20 | useExisting: forwardRef(() => MinValidatorDirective), 21 | multi: true 22 | } 23 | ] 24 | }) 25 | export class MinValidatorDirective implements Validator { 26 | private validator: ValidatorFn; 27 | 28 | @Input() public set ngxMin(value: string) { 29 | this.validator = Validators.min(parseFloat(value)); 30 | } 31 | 32 | constructor() {} 33 | 34 | validate(control: AbstractControl) { 35 | return this.validator(control); 36 | } 37 | } 38 | 39 | @Directive({ 40 | selector: '[ngxMax][formControlName],[ngxMax][formControl],[ngxMax][ngModel]', 41 | providers: [ 42 | { 43 | provide: NG_VALIDATORS, 44 | useExisting: forwardRef(() => MaxValidatorDirective), 45 | multi: true 46 | } 47 | ] 48 | }) 49 | export class MaxValidatorDirective implements Validator { 50 | private validator: ValidatorFn; 51 | 52 | @Input() public set ngxMax(value: string) { 53 | this.validator = Validators.max(parseFloat(value)); 54 | } 55 | 56 | constructor() {} 57 | 58 | validate(control: AbstractControl) { 59 | return this.validator(control); 60 | } 61 | } 62 | 63 | @Directive({ 64 | selector: '[ngxUniqueCheck][formControlName],[ngxUniqueCheck][formControl],[ngxUniqueCheck][ngModel]', 65 | providers: [ 66 | { 67 | provide: NG_ASYNC_VALIDATORS, 68 | useExisting: NgxUniqueCheckDirective, 69 | multi: true 70 | } 71 | ] 72 | }) 73 | export class NgxUniqueCheckDirective implements AsyncValidator { 74 | @Input() ngxUniqueCheck: (value: any) => Observable = (value: any) => of(null); 75 | 76 | constructor() {} 77 | 78 | validate(control: AbstractControl): Promise | Observable { 79 | return NgxValidators.uniqueCheckValidator(this.ngxUniqueCheck)(control); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/core/src/validator-loader.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, Optional } from '@angular/core'; 2 | import { 3 | NgxValidatorGlobalConfig, 4 | NgxValidationMessages, 5 | NGX_VALIDATOR_CONFIG, 6 | DEFAULT_GLOBAL_VALIDATION_MESSAGES 7 | } from './validator.class'; 8 | import { ValidationFeedbackStrategy, ValidationFeedbackStrategyBuilder } from './strategies'; 9 | 10 | const defaultValidatorConfig: NgxValidatorGlobalConfig = { 11 | validationFeedbackStrategy: ValidationFeedbackStrategyBuilder.bootstrap(), 12 | validationMessages: {} 13 | }; 14 | 15 | @Injectable() 16 | export class NgxValidatorLoader { 17 | private config: NgxValidatorGlobalConfig; 18 | 19 | private getDefaultValidationMessage(key: string) { 20 | if (this.config.globalValidationMessages && this.config.globalValidationMessages[key]) { 21 | return this.config.globalValidationMessages[key]; 22 | } else { 23 | return DEFAULT_GLOBAL_VALIDATION_MESSAGES[key]; 24 | } 25 | } 26 | 27 | get validationMessages() { 28 | return this.config.validationMessages; 29 | } 30 | 31 | get validationFeedbackStrategy(): ValidationFeedbackStrategy { 32 | if (!this.config.validationFeedbackStrategy) { 33 | this.config.validationFeedbackStrategy = ValidationFeedbackStrategyBuilder.bootstrap(); 34 | } 35 | return this.config.validationFeedbackStrategy; 36 | } 37 | 38 | get validateOn() { 39 | if (!this.config.validateOn) { 40 | this.config.validateOn = 'submit'; 41 | } 42 | return this.config.validateOn; 43 | } 44 | 45 | constructor( 46 | @Optional() 47 | @Inject(NGX_VALIDATOR_CONFIG) 48 | config: NgxValidatorGlobalConfig 49 | ) { 50 | this.config = Object.assign({}, defaultValidatorConfig, config); 51 | } 52 | 53 | /** 54 | * get validation error messages 55 | * @param name formControl name, e.g. username or email 56 | * @param key validator name, e.g. required or pattern 57 | */ 58 | getErrorMessage(name: string, key: string) { 59 | let message = ''; 60 | if (this.validationMessages[name] && this.validationMessages[name][key]) { 61 | message = this.validationMessages[name][key]; 62 | } else { 63 | message = this.getDefaultValidationMessage(key); 64 | } 65 | return message; 66 | } 67 | 68 | addValidationMessages(messages: NgxValidationMessages) { 69 | Object.assign(this.config.validationMessages, messages); 70 | } 71 | 72 | setGlobalValidationMessages(validationMessages: Record) { 73 | this.config.globalValidationMessages = validationMessages; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/core/examples/form-validator/examples/reactive-driven/reactive-driven.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding } from '@angular/core'; 2 | import { NgxValidatorConfig, NgxValidators, NgxValidateOn } from '@why520crazy/ngx-validator'; 3 | import { of } from 'rxjs'; 4 | import { delay } from 'rxjs/operators'; 5 | import { FormBuilder, Validators, FormGroup } from '@angular/forms'; 6 | 7 | @Component({ 8 | selector: 'ngx-validator-reactive-driven-example', 9 | templateUrl: './reactive-driven.component.html', 10 | styleUrls: ['./reactive-driven.component.scss'] 11 | }) 12 | export class NgxValidatorReactiveDrivenExampleComponent { 13 | message = ''; 14 | 15 | formGroup: FormGroup; 16 | 17 | model = { 18 | username: '', 19 | email: '', 20 | password: '', 21 | number: '' 22 | }; 23 | 24 | validateOn: NgxValidateOn = 'change'; 25 | 26 | loadingDone = true; 27 | 28 | validatorConfig: NgxValidatorConfig = { 29 | validationMessages: { 30 | username: { 31 | required: '用户名不能为空', 32 | pattern: '用户名格式不正确,以字母,数字,下划线组成,首字母不能为数字,必须是2-20个字符', 33 | ngxUniqueCheck: '输入的用户名已经存在,请重新输入' 34 | }, 35 | street: { 36 | required: 'street不能为空' 37 | } 38 | }, 39 | validateOn: this.validateOn 40 | }; 41 | 42 | constructor(private formBuilder: FormBuilder) { 43 | this.formGroup = this.formBuilder.group({ 44 | email: ['', [Validators.required, Validators.email]], 45 | username: [ 46 | '', 47 | [Validators.required, Validators.pattern('^[A-Za-z]{1}[0-9A-Za-z_]{1,19}')], 48 | NgxValidators.uniqueCheckValidator(this.checkUsername) 49 | ], 50 | password: ['', [Validators.required, Validators.maxLength(10), Validators.minLength(6)]], 51 | number: ['', [Validators.required, Validators.max(100), Validators.min(10)]], 52 | address: this.formBuilder.group({ 53 | street: ['', Validators.required], 54 | city: this.formBuilder.group({ 55 | country: ['', Validators.required] 56 | }) 57 | }) 58 | }); 59 | } 60 | 61 | changeValidateOn() { 62 | this.loadingDone = false; 63 | this.validatorConfig.validateOn = this.validateOn; 64 | setTimeout(() => { 65 | this.loadingDone = true; 66 | }); 67 | } 68 | 69 | checkUsername = (value: string) => { 70 | return value === 'peter' ? of(true).pipe(delay(200)) : of(false).pipe(delay(200)); 71 | }; 72 | 73 | setMessage(message: string) { 74 | this.message = message; 75 | } 76 | 77 | submit() { 78 | this.setMessage('This form has submit'); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/core/src/validator-loader.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { NgxValidatorLoader } from './validator-loader.service'; 4 | import { BootstrapValidationFeedbackStrategy } from './strategies'; 5 | import { DEFAULT_GLOBAL_VALIDATION_MESSAGES } from './validator.class'; 6 | 7 | describe('NgxValidatorLoader', () => { 8 | beforeEach(() => 9 | TestBed.configureTestingModule({ 10 | providers: [NgxValidatorLoader] 11 | }) 12 | ); 13 | 14 | it('should be created NgxValidatorLoader service', () => { 15 | const service: NgxValidatorLoader = TestBed.inject(NgxValidatorLoader); 16 | expect(service).toBeTruthy(); 17 | expect(service).not.toBeNull(); 18 | expect(service).toBeDefined(); 19 | }); 20 | 21 | it(`should get empty validation messages`, () => { 22 | const service: NgxValidatorLoader = TestBed.inject(NgxValidatorLoader); 23 | expect(service.validationMessages).toEqual({}); 24 | }); 25 | 26 | it(`should get bootstrap validation feedback strategy`, () => { 27 | const service: NgxValidatorLoader = TestBed.inject(NgxValidatorLoader); 28 | expect(service.validationFeedbackStrategy instanceof BootstrapValidationFeedbackStrategy).toBe(true); 29 | }); 30 | 31 | it(`should get default global message when formControl's message is not specified`, () => { 32 | const service: NgxValidatorLoader = TestBed.inject(NgxValidatorLoader); 33 | ['required', 'email'].forEach(validatorName => { 34 | expect(service.getErrorMessage(`not_exist_form_control_name`, validatorName)).toBe( 35 | DEFAULT_GLOBAL_VALIDATION_MESSAGES[validatorName] 36 | ); 37 | }); 38 | }); 39 | 40 | it(`should get configured global message when formControl's message is not specified`, () => { 41 | const service: NgxValidatorLoader = TestBed.inject(NgxValidatorLoader); 42 | const globalValidationMessages = { 43 | required: 'this is configured required message' 44 | }; 45 | service.setGlobalValidationMessages(globalValidationMessages); 46 | expect(service.getErrorMessage(`not_exist_form_control_name`, 'required')).toBe( 47 | globalValidationMessages.required 48 | ); 49 | }); 50 | 51 | it(`should get formControl's configured message`, () => { 52 | const service: NgxValidatorLoader = TestBed.inject(NgxValidatorLoader); 53 | const validationMessages = { 54 | username: { 55 | required: `this is username configured required message` 56 | } 57 | }; 58 | service.addValidationMessages(validationMessages); 59 | expect(service.getErrorMessage(`username`, 'required')).toBe(validationMessages.username.required); 60 | expect(service.getErrorMessage(`username`, 'email')).toBe(DEFAULT_GLOBAL_VALIDATION_MESSAGES.email); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /packages/core/examples/form-validator/examples/reactive-driven/reactive-driven.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 15 |
16 | 17 |
18 |
19 | 20 | 28 |
29 |
30 | 31 | 38 |
39 |
40 | 41 | 49 |
50 | 51 |
52 | 53 | 62 |
63 | 64 |
65 | 66 |
67 | 68 | 76 |
77 |
78 |
79 | 80 | 88 |
89 |
90 |
91 | 92 |
93 |
{{ message }}
94 |
95 | 96 |
97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-validator", 3 | "version": "16.0.0", 4 | "description": "Angular7+ form validator, make error tips easy and automatic. don't need to manually write error tips templates.", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "npm run start:docs", 8 | "start:docs": "docgeni serve --port 4900", 9 | "build-docs": "docgeni serve --skip-site", 10 | "build": "ng build core --configuration production", 11 | "build:docs": "docgeni build --configuration production --base-href=/ngx-validator/", 12 | "pub-only": "npm publish ./dist/core --access=public", 13 | "pub": "wpm publish", 14 | "pull-latest": "git checkout master && git pull origin master", 15 | "release": "wpm release", 16 | "test": "ng test core", 17 | "test:demo": "ng test integration", 18 | "lint": "ng lint core", 19 | "lint:demo": "ng lint integration", 20 | "e2e": "ng e2e", 21 | "contributors:add": "all-contributors add", 22 | "contributors:generate": "all-contributors generate", 23 | "report-coverage": "cat ./coverage/lcov.info | coveralls" 24 | }, 25 | "keywords": [ 26 | "validator", 27 | "angular", 28 | "form", 29 | "validation", 30 | "validate" 31 | ], 32 | "private": false, 33 | "repository": "https://github.com/why520crazy/ngx-validator", 34 | "author": "why520crazy ", 35 | "license": "MIT", 36 | "dependencies": { 37 | "@angular/animations": "^17.0.6", 38 | "@angular/common": "^17.0.6", 39 | "@angular/compiler": "^17.0.6", 40 | "@angular/core": "^17.0.6", 41 | "@angular/forms": "^17.0.6", 42 | "@angular/platform-browser": "^17.0.6", 43 | "@angular/platform-browser-dynamic": "^17.0.6", 44 | "@angular/router": "^17.0.6", 45 | "bootstrap": "^4.2.1", 46 | "core-js": "^2.5.4", 47 | "lib": "^3.0.2", 48 | "rxjs": "~6.5.5", 49 | "tslib": "^2.0.0", 50 | "zone.js": "~0.14.2" 51 | }, 52 | "devDependencies": { 53 | "@angular-devkit/build-angular": "^17.0.6", 54 | "@angular/cli": "^17.0.6", 55 | "@angular/compiler-cli": "^17.0.6", 56 | "@angular/language-service": "^17.0.6", 57 | "@commitlint/cli": "^7.5.2", 58 | "@commitlint/config-conventional": "^7.5.0", 59 | "@docgeni/cli": "^2.0.0", 60 | "@docgeni/template": "^2.0.0", 61 | "@types/jasmine": "~3.6.0", 62 | "@types/jasminewd2": "~2.0.3", 63 | "@types/node": "^12.11.1", 64 | "@worktile/pkg-manager": "0.1.0", 65 | "all-contributors-cli": "^6.6.0", 66 | "coveralls": "^3.0.9", 67 | "highlight.js": "^10.4.1", 68 | "husky": "^2.3.0", 69 | "jasmine-core": "~4.5.0", 70 | "jasmine-spec-reporter": "~5.0.0", 71 | "karma": "~6.4.0", 72 | "karma-chrome-launcher": "~3.1.0", 73 | "karma-coverage": "~2.2.0", 74 | "karma-jasmine": "~5.1.0", 75 | "karma-jasmine-html-reporter": "~2.0.0", 76 | "ng-packagr": "^17.0.2", 77 | "prettier": "^1.17.1", 78 | "pretty-quick": "^1.10.0", 79 | "protractor": "~7.0.0", 80 | "standard-version": "^8.0.1", 81 | "ts-node": "~7.0.0", 82 | "typescript": "~5.2.2" 83 | }, 84 | "husky": { 85 | "hooks": { 86 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 87 | "pre-commit": "pretty-quick --staged" 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /packages/core/src/testing/events.ts: -------------------------------------------------------------------------------- 1 | export function createMouseEvent(type: string, x = 0, y = 0, button = 0) { 2 | const event = document.createEvent('MouseEvent'); 3 | 4 | event.initMouseEvent( 5 | type, 6 | true /* canBubble */, 7 | false /* cancelable */, 8 | window /* view */, 9 | 0 /* detail */, 10 | x /* screenX */, 11 | y /* screenY */, 12 | x /* clientX */, 13 | y /* clientY */, 14 | false /* ctrlKey */, 15 | false /* altKey */, 16 | false /* shiftKey */, 17 | false /* metaKey */, 18 | button /* button */, 19 | null /* relatedTarget */ 20 | ); 21 | 22 | // `initMouseEvent` doesn't allow us to pass the `buttons` and 23 | // defaults it to 0 which looks like a fake event. 24 | Object.defineProperty(event, 'buttons', { get: () => 1 }); 25 | 26 | return event; 27 | } 28 | 29 | /** Creates a browser TouchEvent with the specified pointer coordinates. */ 30 | export function createTouchEvent(type: string, pageX = 0, pageY = 0) { 31 | // In favor of creating events that work for most of the browsers, the event is created 32 | // as a basic UI Event. The necessary details for the event will be set manually. 33 | const event = document.createEvent('UIEvent'); 34 | const touchDetails = { pageX, pageY }; 35 | 36 | event['initUIEvent'](type, true, true, window, 0); 37 | 38 | // Most of the browsers don't have a "initTouchEvent" method that can be used to define 39 | // the touch details. 40 | Object.defineProperties(event, { 41 | touches: { value: [touchDetails] }, 42 | targetTouches: { value: [touchDetails] }, 43 | changedTouches: { value: [touchDetails] } 44 | }); 45 | 46 | return event; 47 | } 48 | 49 | /** Dispatches a keydown event from an element. */ 50 | export function createKeyboardEvent(type: string, keyCode: number, target?: Element, key?: string) { 51 | const event = document.createEvent('KeyboardEvent') as any; 52 | const originalPreventDefault = event.preventDefault; 53 | 54 | // Firefox does not support `initKeyboardEvent`, but supports `initKeyEvent`. 55 | if (event.initKeyEvent) { 56 | event.initKeyEvent(type, true, true, window, 0, 0, 0, 0, 0, keyCode); 57 | } else { 58 | event.initKeyboardEvent(type, true, true, window, 0, key, 0, '', false); 59 | } 60 | 61 | // Webkit Browsers don't set the keyCode when calling the init function. 62 | // See related bug https://bugs.webkit.org/show_bug.cgi?id=16735 63 | Object.defineProperties(event, { 64 | keyCode: { get: () => keyCode }, 65 | key: { get: () => key }, 66 | target: { get: () => target } 67 | }); 68 | 69 | // IE won't set `defaultPrevented` on synthetic events so we need to do it manually. 70 | event.preventDefault = function() { 71 | Object.defineProperty(event, 'defaultPrevented', { get: () => true }); 72 | return originalPreventDefault.apply(this, arguments); 73 | }; 74 | 75 | return event; 76 | } 77 | 78 | /** Creates a fake event object with any desired event type. */ 79 | export function createFakeEvent(type: string, canBubble = false, cancelable = true) { 80 | const event = document.createEvent('Event'); 81 | event.initEvent(type, canBubble, cancelable); 82 | return event; 83 | } 84 | -------------------------------------------------------------------------------- /packages/core/src/module.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { NgxValidatorModule } from './module'; 4 | import { NgModule } from '@angular/core'; 5 | import { NgxValidatorLoader } from './public-api'; 6 | import { ValidationFeedbackStrategyBuilder, NoopValidationFeedbackStrategy } from './strategies'; 7 | 8 | const GLOBAL_VALIDATION_MESSAGES = { 9 | required: 'This option cannot be empty', 10 | maxlength: 'The length of this option input cannot be greater than {requiredLength}', 11 | minlength: 'The length of this option input cannot be less than {requiredLength}', 12 | ngxUniqueCheck: 'The input value already exists, please re-enter', 13 | email: 'The format of the input message is incorrect', 14 | repeat: 'Inconsistent input twice', 15 | pattern: 'The option input format is incorrect', 16 | number: 'Must enter a number', 17 | url: 'The input URL format is incorrect', 18 | max: 'The input value of this option cannot be greater than {max}', 19 | min: 'The input value of this option cannot be less than {min}' 20 | }; 21 | 22 | const VALIDATION_MESSAGES = { 23 | username: { 24 | required: `this is username configured required message` 25 | } 26 | }; 27 | 28 | @NgModule({ 29 | imports: [NgxValidatorModule] 30 | }) 31 | class AppModuleDirectly {} 32 | 33 | @NgModule({ 34 | imports: [ 35 | NgxValidatorModule.forRoot({ 36 | globalValidationMessages: GLOBAL_VALIDATION_MESSAGES, 37 | validateOn: 'blur', 38 | validationMessages: VALIDATION_MESSAGES, 39 | validationFeedbackStrategy: ValidationFeedbackStrategyBuilder.noop() 40 | }) 41 | ] 42 | }) 43 | class AppModuleWithConfig {} 44 | 45 | describe('NgxValidatorModule', () => { 46 | it('should be created directly import module', () => { 47 | TestBed.configureTestingModule({ 48 | imports: [AppModuleDirectly] 49 | }); 50 | const service: NgxValidatorLoader = TestBed.inject(NgxValidatorLoader); 51 | expect(service).toBeTruthy(); 52 | }); 53 | 54 | it('should be created import module with default config ', () => { 55 | TestBed.configureTestingModule({ 56 | imports: [AppModuleWithConfig] 57 | }); 58 | const service: NgxValidatorLoader = TestBed.inject(NgxValidatorLoader); 59 | expect(service).toBeTruthy(); 60 | }); 61 | 62 | it('should be overwrite default global validation messages ', () => { 63 | TestBed.configureTestingModule({ 64 | imports: [AppModuleWithConfig] 65 | }); 66 | const service: NgxValidatorLoader = TestBed.inject(NgxValidatorLoader); 67 | expect(service['config'].globalValidationMessages).toEqual(GLOBAL_VALIDATION_MESSAGES); 68 | }); 69 | 70 | it('should be overwrite default validation messages', () => { 71 | TestBed.configureTestingModule({ 72 | imports: [AppModuleWithConfig] 73 | }); 74 | const service: NgxValidatorLoader = TestBed.inject(NgxValidatorLoader); 75 | expect(service['config'].validationMessages).toEqual(VALIDATION_MESSAGES); 76 | }); 77 | 78 | it('should be overwrite default validation feedback strategy', () => { 79 | TestBed.configureTestingModule({ 80 | imports: [AppModuleWithConfig] 81 | }); 82 | const service: NgxValidatorLoader = TestBed.inject(NgxValidatorLoader); 83 | expect(service['config'].validationFeedbackStrategy instanceof NoopValidationFeedbackStrategy).toEqual(true); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ngx-validator 2 | 3 | [![Coverage Status][coveralls-image]][coveralls-url] 4 | [![Build Status][build-status]](https://circleci.com/gh/why520crazy/ngx-validator) 5 | [![npm version](https://badge.fury.io/js/%40why520crazy%2Fngx-validator.svg)](https://www.npmjs.com/@why520crazy/ngx-validator) 6 | ![npm bundle size (scoped)](https://img.shields.io/bundlephobia/min/@why520crazy/ngx-validator) 7 | [![docgeni](https://img.shields.io/badge/docs%20by-docgeni-348fe4)](https://github.com/docgeni/docgeni) 8 | [![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors) 9 | 10 | [coveralls-image]: https://coveralls.io/repos/github/why520crazy/ngx-validator/badge.svg?branch=master 11 | [coveralls-url]: https://coveralls.io/github/why520crazy/ngx-validator 12 | [build-status]: https://circleci.com/gh/why520crazy/ngx-validator.svg?style=svg 13 | 14 | An Angular 7+ form validator library, may be the best angular validator library in the world. 15 | 16 | > handle validation messages easy and automatic, don't need to manually write error tips templates, just configure validation rules, and support extensive custom feedback strategy. 17 | 18 | ## Demo 19 | 20 | [Live Demo](https://why520crazy.github.io/ngx-validator) 21 | 22 | [Use Case](https://worktile.com/signup?utm_source=w5c-ngx-validator) 23 | 24 | ## Installation 25 | 26 | ``` 27 | npm install @why520crazy/ngx-validator --save 28 | # or 29 | yarn add @why520crazy/ngx-validator 30 | ``` 31 | 32 | ## Usage 33 | 34 | See https://why520crazy.github.io/ngx-validator/ 35 | ## Documentation 36 | 37 | - [如何优雅的使用 Angular 表单验证](https://zhuanlan.zhihu.com/p/51467181) 38 | - [Angular 表单验证类库 ngx-validator 1.0 正式发布](https://github.com/why520crazy/ngx-validator/blob/master/1.0.0-publish.md) 39 | 40 | ## Development 41 | 42 | ``` 43 | $ git clone git@github.com:why520crazy/ngx-validator.git 44 | $ cd ngx-validator 45 | $ npm install 46 | $ npm run start // http://127.0.0.1:4900 47 | $ npm run test 48 | ``` 49 | 50 | ## Building & Publish 51 | 52 | ``` 53 | $ npm run build 54 | $ npm run pub 55 | ``` 56 | 57 | ## Links 58 | 59 | - [Angular.io](https://angular.io) 60 | - [Angular.cn](https://angular.cn) 61 | - [Worktile.com](https://worktile.com?utm_source=w5c-ngx-validator) 62 | 63 | ## License 64 | 65 | [MIT License](https://github.com/why520crazy/ngx-validator/blob/master/LICENSE) 66 | 67 | ## Contributors ✨ 68 | 69 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 70 | 71 | 72 | 73 |
why520crazy
why520crazy

💬
luxiaobei
luxiaobei

💻
Walker
Walker

🎨
74 | 75 | 76 | 77 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 78 | -------------------------------------------------------------------------------- /packages/core/examples/form-validator/examples/template-driven/template-driven.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 15 |
16 | 17 |
24 |
25 | 26 | 36 |
37 |
38 | 39 | 50 |
51 |
52 | 53 | 64 |
65 | 66 |
67 | 68 | 79 |
80 |
81 | 82 | 89 | 90 | 91 | 92 |
93 | 96 |
97 | 98 | 112 |
113 |
114 |
{{ message }}
115 |
116 |
117 | 118 | 119 |
120 |
121 | -------------------------------------------------------------------------------- /docs/zh-cn/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 介绍 3 | order: 10 4 | --- 5 | 一个 Angular 7+ 的表单验证类库,可能是世界上最好用的 Angular 表单验证类库 6 | > 自动处理验证消息,不需要手动写错误提示的模板,只需要配置验证规则即可,同时支持扩展自定义验证策略。 7 | 8 | ## 安装 9 | 10 | ``` 11 | npm install @why520crazy/ngx-validator --save 12 | # or 13 | yarn add @why520crazy/ngx-validator 14 | ``` 15 | 16 | ## 快速开始 17 | 18 | 19 | ### 导入 NgxValidatorModule 20 | 在主模块或者任何特性模块中导入 NgxValidatorModule 模块 21 | ```ts 22 | import { NgxValidatorModule, ValidationFeedbackStrategyBuilder } from '@why520crazy/ngx-validator'; 23 | 24 | @NgModule({ 25 | imports: [ 26 | CommonModule, 27 | NgxValidatorModule.forRoot({ 28 | validationFeedbackStrategy: ValidationFeedbackStrategyBuilder.bootstrap(), // default is bootstrap 4 style 29 | validationMessages: { 30 | username: { 31 | required: 'Username is required.', 32 | pattern: 'Incorrect username format.' 33 | } 34 | }, 35 | validateOn: 'submit' | 'blur' // default is submit 36 | }) 37 | ] 38 | }) 39 | class AppModule {} 40 | ``` 41 | 42 | ### 在表单上添加指令 43 | 44 | 在表单元素上添加 `ngxFormValidator` 指令并在提交按钮上添加 `ngxFormSubmit` 指令处理提交事件。 45 | 46 | ```html 47 |
48 |
49 | 50 | 52 |
53 | 54 | 55 | ``` 56 | 57 | ```ts 58 | // .ts 59 | validatorConfig: NgxValidatorConfig = { 60 | validationMessages: { 61 | username: { 62 | required: '用户名不能为空', 63 | pattern: '用户名格式不正确,以字母,数字,下划线组成,首字母不能为数字,必须是2-20个字符', 64 | ngxUniqueCheck: '输入的用户名已经存在,请重新输入' 65 | } 66 | }, 67 | validateOn: 'blur' | 'submit' 68 | }; 69 | 70 | submit() { 71 | // handle submit event 72 | } 73 | ``` 74 | 75 | ## 全局配置 76 | 全局配置通过导入模块配置`NgxValidatorModule.forRoot(config)`,或者在运行时通过注入`NgxValidatorLoader`进行配置。 77 | 78 | | 名称 | 类型 | 描述 | 79 | | ------------ | ------------------- | --------------- | 80 | | validationMessages | {[controlName: string]: {[validatorErrorKey: string]: string}} | 验证规则 | 81 | | validationFeedbackStrategy | ValidationFeedbackStrategy | 验证反馈策略,主要控制错误展示和隐藏的逻辑 | 82 | | globalValidationMessages | {[validatorErrorKey: string]: string} | 全局的默认展验证规则 | 83 | | validateOn | 'submit' \| 'blur' | 验证触发条件 | 84 | 85 |
86 | 87 | 默认的全局验证规则 `globalValidationMessages` 如下 88 | 89 | ``` 90 | { 91 | required: '该选项不能为空', 92 | maxlength: '该选项输入值长度不能大于{requiredLength}', 93 | minlength: '该选项输入值长度不能小于{requiredLength}', 94 | ngxUniqueCheck: '输入值已经存在,请重新输入', 95 | email: '输入邮件的格式不正确', 96 | repeat: '两次输入不一致', 97 | pattern: '该选项输入格式不正确', 98 | number: '必须输入数字', 99 | url: '输入URL格式不正确', 100 | max: '该选项输入值不能大于{max}', 101 | min: '该选项输入值不能小于{min}' 102 | }; 103 | ``` 104 | 105 | 表单上配置的 `validationMessages` 优先级大于全局配置。 106 | 当某个元素在表单配置中没有找到,同时全局配置也没有找到,它将会使用全局默认的验证消息。 107 | 108 | ## 扩展 109 | 110 | 通过模板变量或者`ViewChild`获取到表单验证组件`ngxFormValidator` 111 | 112 | 1. 单独验证某个控件:`formValidator.validator.validateControl(name: string)` 113 | 2. 为某个控件展示服务端返回的错误提示:`formValidator.validator.markControlAsError(name: string, errorMessage: string)` 114 | 115 | 116 | ## 自定义反馈规则 117 | 118 | ```ts 119 | const CUSTOM_INVALID_CLASS = 'custom-invalid'; 120 | const CUSTOM_INVALID_FEEDBACK_CLASS = 'custom-invalid-feedback'; 121 | 122 | export class CustomValidationFeedbackStrategy implements ValidationFeedbackStrategy { 123 | showError(element: HTMLElement, errorMessages: string[]): void { 124 | element.classList.add(CUSTOM_INVALID_CLASS); 125 | // add element show error message 126 | } 127 | 128 | removeError(element: HTMLElement): void { 129 | element.classList.remove(CUSTOM_INVALID_CLASS); 130 | // remove element error message 131 | } 132 | } 133 | 134 | NgxValidatorModule.forRoot({ 135 | ... 136 | validationFeedbackStrategy: new CustomValidationFeedbackStrategy(), 137 | ... 138 | }) 139 | ``` 140 | -------------------------------------------------------------------------------- /packages/core/src/directives/form-validator.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, 3 | OnInit, 4 | NgZone, 5 | Renderer2, 6 | ElementRef, 7 | Input, 8 | OnDestroy, 9 | ContentChildren, 10 | AfterContentInit, 11 | QueryList 12 | } from '@angular/core'; 13 | import { NgxFormValidatorService } from '../validator.service'; 14 | import { NgForm, ControlContainer, NgControl } from '@angular/forms'; 15 | import { NgxValidatorConfig } from '../validator.class'; 16 | 17 | const KEY_CODES_ENTER = 13; 18 | 19 | // 1. submit 按 Enter 键提交, Textare 除外,需要按 Ctrl | Command + Enter 提交 20 | // 2. alwaysSubmit 不管是哪个元素 按 Enter 键都提交 21 | // 3. forbidSubmit Enter 键禁止提交 22 | // 默认 submit 23 | export enum NgxEnterKeyMode { 24 | submit = 'submit', 25 | alwaysSubmit = 'alwaysSubmit', 26 | forbidSubmit = 'forbidSubmit' 27 | } 28 | 29 | @Directive({ 30 | selector: 'form[ngxFormValidator],form[ngx-form-validator]', 31 | providers: [NgxFormValidatorService], 32 | exportAs: 'ngxFormValidator' 33 | }) 34 | export class NgxFormValidatorDirective implements OnInit, AfterContentInit, OnDestroy { 35 | @ContentChildren(NgControl, { 36 | descendants: true 37 | }) 38 | public controls: QueryList; 39 | 40 | private unsubscribe: () => void; 41 | 42 | onSubmitSuccess: ($event: any) => void; 43 | 44 | @Input() enterKeyMode: NgxEnterKeyMode; 45 | 46 | @Input() 47 | set ngxFormValidatorConfig(config: NgxValidatorConfig) { 48 | this.validator.setValidatorConfig(config); 49 | } 50 | 51 | @Input() 52 | set ngxFormValidator(config: NgxValidatorConfig) { 53 | this.validator.setValidatorConfig(config); 54 | } 55 | 56 | get validator() { 57 | return this._validator; 58 | } 59 | 60 | constructor( 61 | private ngZone: NgZone, 62 | private renderer: Renderer2, 63 | private elementRef: ElementRef, 64 | private _validator: NgxFormValidatorService, 65 | private ngForm: ControlContainer 66 | ) {} 67 | 68 | ngOnInit() { 69 | this.ngZone.runOutsideAngular(() => { 70 | this.unsubscribe = this.renderer.listen( 71 | this.elementRef.nativeElement, 72 | 'keydown', 73 | this.onKeydown.bind(this) 74 | ); 75 | }); 76 | 77 | this.validator.initialize(this.ngForm as NgForm, this.elementRef.nativeElement); 78 | } 79 | 80 | ngAfterContentInit() { 81 | this.validator.initializeFormControlsValidation(this.controls.toArray()); 82 | this.controls.changes.subscribe(controls => { 83 | this.validator.initializeFormControlsValidation(this.controls.toArray()); 84 | }); 85 | } 86 | 87 | submit($event: Event) { 88 | if (this.validator.validate($event) && this.onSubmitSuccess) { 89 | this.onSubmitSuccess($event); 90 | } 91 | } 92 | 93 | submitRunInZone($event: Event) { 94 | this.ngZone.run(() => { 95 | this.submit($event); 96 | }); 97 | } 98 | 99 | onKeydown($event: KeyboardEvent) { 100 | const currentInput = document.activeElement; 101 | const key = $event.which || $event.keyCode; 102 | if (key === KEY_CODES_ENTER && currentInput.tagName) { 103 | if (!this.enterKeyMode || this.enterKeyMode === NgxEnterKeyMode.submit) { 104 | // TEXTAREA Ctrl + Enter 或者 Command + Enter 阻止默认行为并提交 105 | if (currentInput.tagName === 'TEXTAREA') { 106 | if ($event.ctrlKey || $event.metaKey) { 107 | $event.preventDefault(); 108 | this.submitRunInZone($event); 109 | } 110 | } else { 111 | // 不是 TEXTAREA Enter 阻止默认行为并提交 112 | $event.preventDefault(); 113 | this.submitRunInZone($event); 114 | } 115 | } else if (this.enterKeyMode === NgxEnterKeyMode.alwaysSubmit) { 116 | $event.preventDefault(); 117 | this.submitRunInZone($event); 118 | } else { 119 | // do nothing 120 | } 121 | } 122 | } 123 | 124 | ngOnDestroy(): void { 125 | if (this.unsubscribe) { 126 | this.unsubscribe(); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Intro 3 | order: 10 4 | --- 5 | 6 | An Angular 7+ form validator library, may be the best angular validator library in the world. 7 | 8 | > handle validation messages easy and automatic, don't need to manually write error tips templates, just configure validation rules, and support extensive custom feedback strategy. 9 | 10 | ## Installation 11 | 12 | ``` 13 | npm install @why520crazy/ngx-validator --save 14 | # or 15 | yarn add @why520crazy/ngx-validator 16 | ``` 17 | 18 | ## Getting Started 19 | 20 | 21 | ### import NgxValidatorModule 22 | Loading the module in the any module (AppModule or Feature module). 23 | 24 | ```ts 25 | import { NgxValidatorModule, ValidationFeedbackStrategyBuilder } from '@why520crazy/ngx-validator'; 26 | 27 | @NgModule({ 28 | imports: [ 29 | CommonModule, 30 | NgxValidatorModule.forRoot({ 31 | validationFeedbackStrategy: ValidationFeedbackStrategyBuilder.bootstrap(), // default is bootstrap 4 style 32 | validationMessages: { 33 | username: { 34 | required: 'Username is required.', 35 | pattern: 'Incorrect username format.' 36 | } 37 | }, 38 | validateOn: 'submit' | 'blur' // default is submit 39 | }) 40 | ] 41 | }) 42 | class AppModule {} 43 | ``` 44 | 45 | ### Add directives to form elements 46 | 47 | add `ngxFormValidator` directive to form element and add `ngxFormSubmit` directive handle submit event. 48 | 49 | ```html 50 | 51 |
52 | 53 | 55 |
56 | 57 | 58 | ``` 59 | 60 | ```ts 61 | // .ts 62 | validatorConfig: NgxValidatorConfig = { 63 | validationMessages: { 64 | username: { 65 | required: '用户名不能为空', 66 | pattern: '用户名格式不正确,以字母,数字,下划线组成,首字母不能为数字,必须是2-20个字符', 67 | ngxUniqueCheck: '输入的用户名已经存在,请重新输入' 68 | } 69 | }, 70 | validateOn: 'blur' | 'submit' 71 | }; 72 | 73 | submit() { 74 | // handle submit event 75 | } 76 | ``` 77 | 78 | ## Global configuration 79 | 80 | Global configuration can be set by `NgxValidatorModule.forRoot(config)`, or by injecting `NgxValidatorLoader` service at runtime. 81 | 82 | | Name | Type | Description | 83 | | ------------ | ------------------- | --------------- | 84 | | validationMessages | {[controlName: string]: {[validatorErrorKey: string]: string}} | validation Rules | 85 | | validationFeedbackStrategy | ValidationFeedbackStrategy | validation feedback strategy which contains error show and hide | 86 | | globalValidationMessages | {[validatorErrorKey: string]: string} | validator default validation rules | 87 | | validateOn | 'submit' \| 'blur' | validate trigger | 88 | 89 |
90 | 91 | Default `globalValidationMessages` rules as below: 92 | 93 | ``` 94 | { 95 | required: '该选项不能为空', 96 | maxlength: '该选项输入值长度不能大于{requiredLength}', 97 | minlength: '该选项输入值长度不能小于{requiredLength}', 98 | ngxUniqueCheck: '输入值已经存在,请重新输入', 99 | email: '输入邮件的格式不正确', 100 | repeat: '两次输入不一致', 101 | pattern: '该选项输入格式不正确', 102 | number: '必须输入数字', 103 | url: '输入URL格式不正确', 104 | max: '该选项输入值不能大于{max}', 105 | min: '该选项输入值不能小于{min}' 106 | }; 107 | ``` 108 | 109 | The priority of ngx-form's `validationMessages` config is greater than `validationMessages`, 110 | it will use `globalValidationMessages` when an element doesn't match form config `validationMessages` or global config validationMessages 111 | 112 | ## Extensions 113 | 114 | Get `formValidator` directive by `` or `ViewChild` 115 | 116 | 1. `formValidator.validator.validateControl(name: string)` validate an control individually 117 | 2. `formValidator.validator.markControlAsError(name: string, errorMessage: string)` show error by server's error code for an control 118 | 119 | 120 | ## Custom Feedback Strategy 121 | 122 | ```ts 123 | const CUSTOM_INVALID_CLASS = 'custom-invalid'; 124 | const CUSTOM_INVALID_FEEDBACK_CLASS = 'custom-invalid-feedback'; 125 | 126 | export class CustomValidationFeedbackStrategy implements ValidationFeedbackStrategy { 127 | showError(element: HTMLElement, errorMessages: string[]): void { 128 | element.classList.add(CUSTOM_INVALID_CLASS); 129 | // add element show error message 130 | } 131 | 132 | removeError(element: HTMLElement): void { 133 | element.classList.remove(CUSTOM_INVALID_CLASS); 134 | // remove element error message 135 | } 136 | } 137 | 138 | NgxValidatorModule.forRoot({ 139 | ... 140 | validationFeedbackStrategy: new CustomValidationFeedbackStrategy(), 141 | ... 142 | }) 143 | ``` 144 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [10.0.0](https://github.com/why520crazy/ngx-validator/compare/v9.0.0...v10.0.0) (2021-07-02) 6 | 7 | ## [9.0.0](https://github.com/why520crazy/ngx-validator/compare/v1.0.0...v9.0.0) (2020-06-10) 8 | 9 | ### Build System 10 | 11 | - upgrade to 1.0.0 ([369c11a](https://github.com/why520crazy/ngx-validator/commit/369c11a)) 12 | - **deps:** bump acorn from 6.1.1 to 6.4.1 ([6645a28](https://github.com/why520crazy/ngx-validator/commit/6645a28)) 13 | - **deps:** bump fstream from 1.0.11 to 1.0.12 ([79b149b](https://github.com/why520crazy/ngx-validator/commit/79b149b)) 14 | - **deps:** bump handlebars from 4.0.12 to 4.1.2 ([4943ab7](https://github.com/why520crazy/ngx-validator/commit/4943ab7)) 15 | - **deps:** bump handlebars from 4.1.2 to 4.5.3 ([#26](https://github.com/why520crazy/ngx-validator/issues/26)) ([44f6616](https://github.com/why520crazy/ngx-validator/commit/44f6616)) 16 | - **deps:** bump https-proxy-agent from 2.2.1 to 2.2.4 ([04ab8e3](https://github.com/why520crazy/ngx-validator/commit/04ab8e3)) 17 | - **deps:** bump js-yaml from 3.12.0 to 3.13.1 ([1842157](https://github.com/why520crazy/ngx-validator/commit/1842157)) 18 | - **deps:** bump mixin-deep from 1.3.1 to 1.3.2 ([cffb2ef](https://github.com/why520crazy/ngx-validator/commit/cffb2ef)) 19 | - **deps:** bump tar from 2.2.1 to 2.2.2 ([0479d18](https://github.com/why520crazy/ngx-validator/commit/0479d18)) 20 | - **deps:** bump websocket-extensions from 0.1.3 to 0.1.4 ([23e7b17](https://github.com/why520crazy/ngx-validator/commit/23e7b17)) 21 | 22 | ### Features 23 | 24 | - upgrade angular to v9.x ([#29](https://github.com/why520crazy/ngx-validator/issues/29)) ([#31](https://github.com/why520crazy/ngx-validator/issues/31)) ([0fee817](https://github.com/why520crazy/ngx-validator/commit/0fee817)) 25 | - validateOn add type change [#8](https://github.com/why520crazy/ngx-validator/issues/8) ([#11](https://github.com/why520crazy/ngx-validator/issues/11)) ([032c541](https://github.com/why520crazy/ngx-validator/commit/032c541)) 26 | 27 | ### Tests 28 | 29 | - **ci:** add ChromeHeadlessCI for travis ci ([bebf636](https://github.com/why520crazy/ngx-validator/commit/bebf636)) 30 | - add report-coverage and badge in README ([28b0150](https://github.com/why520crazy/ngx-validator/commit/28b0150)) 31 | - add test cases for set global config and add to demo [#10](https://github.com/why520crazy/ngx-validator/issues/10) ([b420ba5](https://github.com/why520crazy/ngx-validator/commit/b420ba5)) 32 | - change ci to circleci ([6b3d977](https://github.com/why520crazy/ngx-validator/commit/6b3d977)) 33 | 34 | ## [1.0.0](https://github.com/why520crazy/ngx-validator/compare/v0.0.5...v1.0.0) (2019-05-16) 35 | 36 | ### Build System 37 | 38 | - fix build error arrow lambda not supported in static function ([e4c5e66](https://github.com/why520crazy/ngx-validator/commit/e4c5e66)) 39 | - fix pub-only 0.0.5 ([a9541dc](https://github.com/why520crazy/ngx-validator/commit/a9541dc)) 40 | 41 | ### 0.0.5 (2019-05-15) 42 | 43 | ### Bug Fixes 44 | 45 | - Maximum call stack size exceeded ([af239fe](https://github.com/why520crazy/ngx-validator/commit/af239fe)) 46 | 47 | ### Features 48 | 49 | - validateOn blur ([1bec347](https://github.com/why520crazy/ngx-validator/commit/1bec347)) 50 | 51 | ### Tests 52 | 53 | - add code coverage ([8d7e084](https://github.com/why520crazy/ngx-validator/commit/8d7e084)) 54 | - add node 10 in travis.yml ([f01112b](https://github.com/why520crazy/ngx-validator/commit/f01112b)) 55 | - add test for validator directive ([d9e84ef](https://github.com/why520crazy/ngx-validator/commit/d9e84ef)) 56 | - add travis.yml file ([c760620](https://github.com/why520crazy/ngx-validator/commit/c760620)) 57 | - add validator loader service some test cases ([6722cce](https://github.com/why520crazy/ngx-validator/commit/6722cce)) 58 | - change node version 8.11.3 ([9c031e3](https://github.com/why520crazy/ngx-validator/commit/9c031e3)) 59 | - change travis.yml config ([8fa1d0c](https://github.com/why520crazy/ngx-validator/commit/8fa1d0c)) 60 | - fix error because rename ValidationFeedbackStrategy ([906a735](https://github.com/why520crazy/ngx-validator/commit/906a735)) 61 | 62 | ## 0.0.4 63 | 64 | ### Bug Fixes 65 | 66 | - fix maxlength and minlength display incorrect error messages 67 | 68 | ### Break changes: 69 | 70 | - rename `*DisplayStrategy` to `*FeedbackStrategy` 71 | 72 | ## 0.0.3 73 | 74 | - add validation display strategy. 75 | 76 | Breaking Changes: 77 | 78 | - rename `NgxFormValidatorConfig` -> `NgxValidatorConfig` 79 | - rename `NgxFormValidatorGlobalConfig` -> `NgxValidatorGlobalConfig` 80 | - remove `NgxValidatorConfig` config 's `showError` and `removeError` options, add `validationDisplayStrategy` option replace it, support bootstrap strategy and noop strategy, default is bootstrap, you can add your own strategy through implement `IValidationDisplayStrategy` 81 | 82 | ## 0.0.2 83 | 84 | ### Features 85 | 86 | - initialize `NgxValidatorModule` module 87 | - add `ngx-form-validator`(`ngxFormValidator`)directive, extend some validation behaviors for ngForm 88 | - add `ngxFormSubmit` directive to submit form cooperate with `ngxFormValidator` 89 | -------------------------------------------------------------------------------- /1.0.0-publish.md: -------------------------------------------------------------------------------- 1 | ## 背景介绍 2 | 3 | 之前写了一篇 [《如何优雅的使用 Angular 表单验证》](https://zhuanlan.zhihu.com/p/51467181),结尾处介绍了统一验证反馈的类库 `ngx-validator` ,由于这段时间一直在新模块做微前端以及相关业务组件库,工具等开发,一直拖到现在,目前正式版 1.0 终于可以发布了。 4 | 可能有些人没有阅读过那篇博客,我这里简单介绍下 ngx-validator 主要的功能。 5 | 6 | 1. 统一验证规则和错误反馈策略,通过响应式(配置的方式)设置每个元素对应每个验证器的错误提示文案以及统一错误信息反馈,避免手动写重复的模版实现错误提示,让开发人员专心写业务即可; 7 | 1. 扩展一些 Angular 本身没有提供验证器和模版驱动的验证指令,比如 `ngxUniqueCheck`、`ngxMax`、`ngxMin`; 8 | 1. 支持模版驱动和响应式驱动表单的验证。 9 | 10 | 从上次 0.0.1 版本到 1.0.0 正式发布新增了的功能有: 11 | 12 | 1. 新增了 `validateOn` 支持 `submit` 和 `blur` 光标移走验证,之前只有点击按钮提交才会验证 13 | 1. 对响应式表单支持的完善; 14 | 1. 测试和 Demo 的完善; 15 | 1. 重构了代码,添加了自动生成 changelog 和 husky 钩子做 commit message 规范检查和自动格式化(这些和库的功能无关,与开发者有关) 16 | 17 | ## 使用方式 18 | 19 | 如果你不想听我废话,可以直接看 [示例](https://why520crazy.github.io/ngx-validator/index.html) ,其中包括模版驱动和响应式驱动表单实现验证的全部代码。 20 | 21 | ![image.gif](https://github.com/why520crazy/ngx-validator/blob/master/packages/integration/src/assets/images/ngx-validator-live-demo.gif?raw=true) 22 | 23 | #### 安装 24 | 25 | 在你的项目中执行命令 `npm install @why520crazy/ngx-validator --save` 进行模块的安装 26 | 27 | #### 引入模块 28 | 29 | 在启动模块 AppModule 中直接引入 `NgxValidatorModule`,当然引入的时候可以通过 `NgxValidatorModule.forRoot` 进行一些全局参数的配置,配置包含全局的验证错误信息,错误反馈方式,目前反馈方式支持 boostrap4 的表单错误样式和 noop(什么都不提示),当然你可以扩展自己的错误反馈策略。 30 | 31 | ``` 32 | import { NgxValidatorModule, ValidationFeedbackStrategyBuilder } from '@why520crazy/ngx-validator'; 33 | 34 | @NgModule({ 35 | imports: [ 36 | CommonModule, 37 | NgxValidatorModule.forRoot({ 38 | validationFeedbackStrategy: ValidationFeedbackStrategyBuilder.bootstrap(), 39 | validationMessages: { 40 | username: { 41 | required: 'Username is required.', 42 | pattern: 'Incorrect username format.' 43 | } 44 | } 45 | }) 46 | ] 47 | }) 48 | class AppModule {} 49 | ``` 50 | 51 | #### 模版驱动表单验证 52 | 53 | 在 form 表单元素上添加 `ngxFormValidator` 指令,配置的参数和全局配置的参数类似,此处单独配置只会对当前 Form 有效。 54 | 由于 `ngxFormValidator` 采用的验证器,以及元素是否验证通过完全读取的是 Angular 表单提供的信息,所以模版驱动表单必须遵循 Angular 表单的一些规则: 55 | 56 | 1. 表单元素必须设置一个 name,这个 name 会和 `ngForm controls` 中的对象对应; 57 | 1. 表单元素必须设置 `ngModel`,当提交表单时通过 ngModel 这只的变量获取用户输入的数据; 58 | 1. 验证器直接设置到表单元素上,比如 Angular 内置的 `required`、`email`、`pattern`、`maxlength`、`minlength` 以及 ngx-validator 类库提供的 `ngxMax`、 `ngxMin`、`ngxUniqueCheck`。 59 | 60 | 最后在提交按钮上绑定 `ngxFormSubmit` 事件,当按钮点击后会触发表单验证,验证不通过会根据每个表单元素配置的提示信息反馈错误,如果使用的默认的 bootstrap4 的反馈策略,会在表单元素上加 `is-invalid` class 样式,同时在表单元素后追加 `
{相关的错误提示信息}
` 61 | 62 | ``` 63 | 64 |
65 | 66 | 68 |
69 | 70 | 71 | ``` 72 | 73 | ![email-address-error.png](https://github.com/why520crazy/ngx-validator/blob/master/packages/integration/src/assets/images/email-address-error.png?raw=true) 74 | 75 | ![email-address-error-dom.png](https://github.com/why520crazy/ngx-validator/blob/master/packages/integration/src/assets/images/email-address-error-dom.png?raw=true) 76 | 77 | #### 响应式驱动表单验证 78 | 79 | 响应式表单验证和模版驱动类似,区别就是不需要给每个元素加 ngModel 和 验证器,直接使用 `formControlName` 指令指定名称, 然后在组件中通过 `FormBuilder` 生成 group 即可,基本没有特殊配置,参考 Angular 官网的响应式表单验证示例即可。 80 | 81 | ## APIs 82 | 83 | #### ngxFormValidator 表单配置 84 | 85 | | 属性名 | 类型 | 备注 | 86 | | -------------------------- | -------------------------------------------------------------- | -------------------------------------- | 87 | | validationMessages | {[controlName: string]: {[validatorErrorKey: string]: string}} | 表单元素验证规则 | 88 | | validationFeedbackStrategy | ValidationFeedbackStrategy | 没有配置,以全局配置的验证反馈策略为主 | 89 | | validateOn | 'submit' \| 'blur' | 没有配置,以全局配置的 validateOn 为主 | 90 | 91 | ``` 92 | validatorConfig: NgxValidatorConfig = { 93 | validationMessages: { 94 | username: { 95 | required: '用户名不能为空', 96 | pattern: '用户名格式不正确,以字母,数字,下划线组成,首字母不能为数字,必须是2-20个字符', 97 | ngxUniqueCheck: '输入的用户名已经存在,请重新输入' 98 | } 99 | }, 100 | validateOn: 'submit' 101 | }; 102 | ``` 103 | 104 | #### 全局配置 105 | 106 | 全局配置可以通过引入 `NgxValidatorModule.forRoot(config)` 进行设置,也可以在运行时注入 `NgxValidatorLoader` 服务进行配置 107 | 108 | | 属性名 | 类型 | 备注 | 109 | | -------------------------- | -------------------------------------------------------------- | -------------------------------------------- | 110 | | validationMessages | {[controlName: string]: {[validatorErrorKey: string]: string}} | 表单元素验证规则 | 111 | | validationFeedbackStrategy | ValidationFeedbackStrategy | 默认以 bootstrap 4 的表单错误提示展示 | 112 | | globalValidationMessages | {[validatorErrorKey: string]: string} | 每个验证器全局的默认验证规则 | 113 | | validateOn | 'submit' \| 'blur' | 触发验证,是提交触发验证还是光标移走触发验证 | 114 | 115 | `globalValidationMessages` 默认规则如下,当某个表单元素比如 `username` 在表单和全局的 `validationMessages` 都没有被设置,验证不通过会直接显示 `globalValidationMessages 中的 required` 提示信息 116 | 117 | ``` 118 | { 119 | required: '该选项不能为空', 120 | maxlength: '该选项输入值长度不能大于{requiredLength}', 121 | minlength: '该选项输入值长度不能小于{requiredLength}', 122 | ngxUniqueCheck: '输入值已经存在,请重新输入', 123 | email: '输入邮件的格式不正确', 124 | repeat: '两次输入不一致', 125 | pattern: '该选项输入格式不正确', 126 | number: '必须输入数字', 127 | url: '输入URL格式不正确', 128 | max: '该选项输入值不能大于{max}', 129 | min: '该选项输入值不能小于{min}' 130 | }; 131 | ``` 132 | 133 | #### 扩展方法 134 | 135 | 1. 单独验证某一个表单元素, 获取到 `NgxFormValidatorDirective` 实例 `ngxFormValidator: NgxFormValidatorDirective`,通过调用 `ngxFormValidator.validator.validateControl(name: string)` 方法单独验证; 136 | 1. 根据服务端返回的错误,设置某个表单元素错误提示信息,调用 `ngxFormValidator.validator.markControlAsError(name: string, errorMessage: string)` 137 | 138 | #### 自定义反馈策略 139 | 140 | 如果你的项目不是使用 bootstrap4,而是其他 UI 库,那么可以通过扩展自己的错误反馈策略,然后在全局设置中配置一次后所有的表单验证都会使用配置之后的策略,以下是一个自定义反馈策略的示例: 141 | 142 | ``` 143 | const CUSTOM_INVALID_CLASS = 'custom-invalid'; 144 | const CUSTOM_INVALID_FEEDBACK_CLASS = 'custom-invalid-feedback'; 145 | 146 | export class CustomValidationFeedbackStrategy implements ValidationFeedbackStrategy { 147 | showError(element: HTMLElement, errorMessages: string[]): void { 148 | element.classList.add(CUSTOM_INVALID_CLASS); 149 | // add element show error message 150 | } 151 | 152 | removeError(element: HTMLElement): void { 153 | element.classList.remove(CUSTOM_INVALID_CLASS); 154 | // remove element error message 155 | } 156 | } 157 | ``` 158 | -------------------------------------------------------------------------------- /packages/core/src/validator.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | NgForm, 4 | AbstractControl, 5 | ValidationErrors, 6 | FormGroupDirective, 7 | FormControlName, 8 | NgControl 9 | } from '@angular/forms'; 10 | import { NgxValidatorLoader } from './validator-loader.service'; 11 | import { NgxValidatorConfig, NgxValidateOn } from './validator.class'; 12 | import { transformMessage } from './message-transformers'; 13 | import { tap, debounceTime, map, distinctUntilChanged, switchMap, filter } from 'rxjs/operators'; 14 | import { of } from 'rxjs'; 15 | 16 | @Injectable() 17 | export class NgxFormValidatorService { 18 | private _ngForm: NgForm | FormGroupDirective; 19 | 20 | private _formElement: HTMLElement; 21 | 22 | private _config: NgxValidatorConfig; 23 | 24 | private _controls: NgControl[] = []; 25 | 26 | // public errors: string[]; 27 | 28 | // 记录所有元素的验证信息 29 | public validations: Record< 30 | string, 31 | { 32 | hasError?: boolean; 33 | errorMessages?: string[]; 34 | } 35 | > = {}; 36 | 37 | private _getValidationFeedbackStrategy() { 38 | const strategy = 39 | (this._config && this._config.validationFeedbackStrategy) || 40 | this.thyFormValidateLoader.validationFeedbackStrategy; 41 | if (!strategy) { 42 | throw new Error(`validation display strategy is null`); 43 | } 44 | return strategy; 45 | } 46 | 47 | private _getElement(name: string | number) { 48 | const element = this._formElement[name]; 49 | if (element) { 50 | return element; 51 | } else { 52 | return this._formElement.querySelector(`[name='${name}']`); 53 | } 54 | } 55 | 56 | private _clearElementError(name: string) { 57 | if (this.validations[name] && this.validations[name].hasError) { 58 | this.validations[name].hasError = false; 59 | this.validations[name].errorMessages = []; 60 | this._getValidationFeedbackStrategy().removeError(this._getElement(name)); 61 | } 62 | } 63 | 64 | _tryGetValidation(name: string) { 65 | if (!this.validations[name]) { 66 | this._initializeFormControlValidation(name, this._getControlByName(name)); 67 | } 68 | return this.validations[name]; 69 | } 70 | 71 | private _getValidateOn(): NgxValidateOn { 72 | return (this._config && this._config.validateOn) || this.thyFormValidateLoader.validateOn; 73 | } 74 | 75 | private _setControlValidateByBlur(control: NgControl) { 76 | const element: HTMLElement = this._getElement(control.name); 77 | if (element) { 78 | element.onblur = (event: FocusEvent) => { 79 | this.validateControl(control.name as string); 80 | }; 81 | } 82 | } 83 | 84 | private _setControlValidateByChange(control: NgControl) { 85 | control.valueChanges 86 | .pipe( 87 | debounceTime(100), 88 | distinctUntilChanged(), 89 | filter(item => { 90 | return item; 91 | }), 92 | switchMap(item => { 93 | this.validateControl(control.name as string); 94 | return of([]); 95 | }) 96 | ) 97 | .subscribe(); 98 | } 99 | 100 | private _initializeFormControlValidation(name: string, control: AbstractControl | FormControlName | NgControl) { 101 | this.validations[name] = { 102 | hasError: false, 103 | errorMessages: [] 104 | }; 105 | 106 | if (this._getValidateOn() === 'change') { 107 | this._setControlValidateByChange(control as NgControl); 108 | } else { 109 | if (this._getValidateOn() === 'blur') { 110 | this._setControlValidateByBlur(control as NgControl); 111 | } 112 | 113 | control.valueChanges.subscribe(item => { 114 | this._clearElementError(name); 115 | }); 116 | } 117 | } 118 | 119 | private _restFormControlValidation(name: string) { 120 | const validation = this.validations[name]; 121 | if (validation) { 122 | validation.hasError = false; 123 | validation.errorMessages = []; 124 | } 125 | } 126 | 127 | private _getValidationMessage(name: string, validationErrorName: string, validationErrorValues?: any) { 128 | let message = ''; 129 | if ( 130 | this._config && 131 | this._config.validationMessages && 132 | this._config.validationMessages[name] && 133 | this._config.validationMessages[name][validationErrorName] 134 | ) { 135 | message = this._config.validationMessages[name][validationErrorName]; 136 | } else { 137 | message = this.thyFormValidateLoader.getErrorMessage(name, validationErrorName); 138 | } 139 | 140 | return transformMessage(validationErrorName, message, validationErrorValues); 141 | } 142 | 143 | private _getValidationMessages(name: string, validationErrors: ValidationErrors) { 144 | const messages = []; 145 | for (const validationError in validationErrors) { 146 | if (validationErrors.hasOwnProperty(validationError)) { 147 | messages.push(this._getValidationMessage(name, validationError, validationErrors[validationError])); 148 | } 149 | } 150 | return messages; 151 | } 152 | 153 | private _setControlValidationError(name: string, errorMessages: string[]) { 154 | const validation = this._tryGetValidation(name); 155 | validation.errorMessages = errorMessages; 156 | validation.hasError = true; 157 | this._getValidationFeedbackStrategy().showError(this._getElement(name), errorMessages); 158 | } 159 | 160 | get validatorConfig() { 161 | return this._config; 162 | } 163 | 164 | constructor(private thyFormValidateLoader: NgxValidatorLoader) {} 165 | 166 | initialize(ngForm: NgForm | FormGroupDirective, formElement: HTMLElement) { 167 | this._ngForm = ngForm; 168 | this._formElement = formElement; 169 | } 170 | 171 | initializeFormControlsValidation(controls: NgControl[]) { 172 | if (this._getValidateOn() !== 'submit') { 173 | (controls || []).forEach((control: NgControl) => { 174 | if (!this._controls.find(item => item.name === control.name)) { 175 | this._initializeFormControlValidation(control.name as string, control); 176 | } 177 | }); 178 | this._controls = controls; 179 | } 180 | } 181 | 182 | setValidatorConfig(config: NgxValidatorConfig) { 183 | this._config = config; 184 | } 185 | 186 | private _getControls() { 187 | if (this._ngForm instanceof NgForm) { 188 | return (this._ngForm as NgForm).controls; 189 | } else if (this._ngForm instanceof FormGroupDirective) { 190 | const controls = {}; 191 | (this._ngForm as FormGroupDirective).directives.forEach(directive => { 192 | controls[directive.name] = directive; 193 | }); 194 | return controls; 195 | } 196 | } 197 | 198 | private _getControlByName(name: string): AbstractControl | FormControlName { 199 | const controls = this._getControls(); 200 | return controls[name]; 201 | } 202 | 203 | validateControl(name: string) { 204 | this._clearElementError(name); 205 | const control = this._getControlByName(name); 206 | if (control && control.invalid) { 207 | const errorMessages = this._getValidationMessages(name, control.errors); 208 | this._setControlValidationError(name, errorMessages); 209 | } 210 | } 211 | 212 | validateControls() { 213 | // 主要是 无法检测到 ngForm 的 controls 的变化,或者是我没有找到 214 | // 验证的时候循环 ngForm 的 controls 验证 215 | // 发现没有 validation 初始化一个,已经存在不会重新初始化,保存缓存数据 216 | const controls = this._getControls(); 217 | for (const name in controls) { 218 | if (controls.hasOwnProperty(name)) { 219 | this._tryGetValidation(name); 220 | this.validateControl(name); 221 | } 222 | } 223 | // 移除已经不存在的 validation 224 | const names = Object.keys(this.validations); 225 | names.forEach(name => { 226 | if (!controls[name]) { 227 | delete this.validations[name]; 228 | } 229 | }); 230 | } 231 | 232 | validate($event?: Event): boolean { 233 | this._ngForm.onSubmit($event); 234 | this.validateControls(); 235 | return this._ngForm.valid; 236 | } 237 | 238 | reset() { 239 | this._ngForm.reset(); 240 | for (const name in this.validations) { 241 | if (this.validations.hasOwnProperty(name)) { 242 | this._restFormControlValidation(name); 243 | this._clearElementError(name); 244 | } 245 | } 246 | } 247 | 248 | markControlAsError(name: string, message: string) { 249 | this._clearElementError(name); 250 | this._setControlValidationError(name, [message]); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /packages/core/src/directives/validators.spec.ts: -------------------------------------------------------------------------------- 1 | import { fakeAsync, tick, TestBed, ComponentFixture } from '@angular/core/testing'; 2 | import { FormControl, FormsModule } from '@angular/forms'; 3 | import { of } from 'rxjs'; 4 | import { Component, Input, DebugElement } from '@angular/core'; 5 | import { NgxUniqueCheckDirective, MinValidatorDirective, MaxValidatorDirective } from './validators'; 6 | import { By } from '@angular/platform-browser'; 7 | 8 | describe('validator Directives', () => { 9 | describe('minValidatorDirective', () => { 10 | @Component({ 11 | template: ` 12 | 13 | ` 14 | }) 15 | class TestComponent { 16 | @Input() value; 17 | 18 | @Input() min; 19 | } 20 | 21 | let fixture: ComponentFixture; 22 | let context: TestComponent; 23 | let nativeElement: HTMLElement; 24 | let directive; 25 | let input; 26 | 27 | beforeEach(() => { 28 | TestBed.configureTestingModule({ 29 | imports: [FormsModule], 30 | declarations: [TestComponent, MinValidatorDirective] 31 | }); 32 | fixture = TestBed.createComponent(TestComponent); 33 | context = fixture.componentInstance; 34 | nativeElement = fixture.nativeElement; 35 | 36 | const directives = fixture.debugElement.queryAll(By.directive(MinValidatorDirective)); 37 | directive = directives.map( 38 | (d: DebugElement) => d.injector.get(MinValidatorDirective) as MinValidatorDirective 39 | )[0]; 40 | 41 | input = nativeElement.querySelector('input') as HTMLElement; 42 | }); 43 | 44 | it('should be defined on the test component', () => { 45 | expect(directive).not.toBeUndefined(); 46 | }); 47 | 48 | it('should valid on an empty string', fakeAsync(() => { 49 | context.value = ''; 50 | context.min = 2; 51 | fixture.detectChanges(); 52 | tick(); 53 | fixture.detectChanges(); 54 | expect(input.classList).toContain('ng-valid'); 55 | })); 56 | 57 | it('should valid on null', fakeAsync(() => { 58 | context.value = null; 59 | context.min = 2; 60 | fixture.detectChanges(); 61 | tick(); 62 | fixture.detectChanges(); 63 | expect(input.classList).toContain('ng-valid'); 64 | })); 65 | 66 | it('should valid on undefined', fakeAsync(() => { 67 | context.value = undefined; 68 | context.min = 2; 69 | fixture.detectChanges(); 70 | tick(); 71 | fixture.detectChanges(); 72 | expect(input.classList).toContain('ng-valid'); 73 | })); 74 | 75 | it('should valid on NaN after parsing', fakeAsync(() => { 76 | context.value = 'a'; 77 | context.min = 2; 78 | fixture.detectChanges(); 79 | tick(); 80 | fixture.detectChanges(); 81 | expect(input.classList).toContain('ng-valid'); 82 | })); 83 | 84 | it('should invalid on small values', fakeAsync(() => { 85 | context.value = 1; 86 | context.min = 2; 87 | fixture.detectChanges(); 88 | tick(); 89 | fixture.detectChanges(); 90 | expect(input.classList).toContain('ng-invalid'); 91 | })); 92 | 93 | it('should invalid on small values converted from strings', fakeAsync(() => { 94 | context.value = '1'; 95 | context.min = 2; 96 | fixture.detectChanges(); 97 | tick(); 98 | fixture.detectChanges(); 99 | expect(input.classList).toContain('ng-invalid'); 100 | })); 101 | 102 | it('should valid on big values', fakeAsync(() => { 103 | context.value = 3; 104 | context.min = 2; 105 | fixture.detectChanges(); 106 | tick(); 107 | fixture.detectChanges(); 108 | expect(input.classList).toContain('ng-valid'); 109 | })); 110 | 111 | it('should valid on equal values', fakeAsync(() => { 112 | context.value = 2; 113 | context.min = 2; 114 | fixture.detectChanges(); 115 | tick(); 116 | fixture.detectChanges(); 117 | expect(input.classList).toContain('ng-valid'); 118 | })); 119 | 120 | it('should valid on equal values when value is string', fakeAsync(() => { 121 | context.value = '2'; 122 | context.min = 2; 123 | fixture.detectChanges(); 124 | tick(); 125 | fixture.detectChanges(); 126 | expect(input.classList).toContain('ng-valid'); 127 | })); 128 | 129 | it('should invalid as expected when min value is a string', fakeAsync(() => { 130 | context.value = 1; 131 | context.min = '2'; 132 | fixture.detectChanges(); 133 | tick(); 134 | fixture.detectChanges(); 135 | expect(input.classList).toContain('ng-invalid'); 136 | })); 137 | 138 | it('should valid if min value is undefined', fakeAsync(() => { 139 | context.value = 3; 140 | context.min = undefined; 141 | fixture.detectChanges(); 142 | tick(); 143 | fixture.detectChanges(); 144 | expect(input.classList).toContain('ng-valid'); 145 | })); 146 | 147 | it('should valid if min value is null', fakeAsync(() => { 148 | context.value = 3; 149 | context.min = null; 150 | fixture.detectChanges(); 151 | tick(); 152 | fixture.detectChanges(); 153 | expect(input.classList).toContain('ng-valid'); 154 | })); 155 | 156 | it('should invalid on small values when min value is float values', fakeAsync(() => { 157 | context.value = 2; 158 | context.min = 2.1; 159 | fixture.detectChanges(); 160 | tick(); 161 | fixture.detectChanges(); 162 | expect(input.classList).toContain('ng-invalid'); 163 | })); 164 | 165 | it('should valid on big values when min value is float values', fakeAsync(() => { 166 | context.value = 2.2; 167 | context.min = 2.1; 168 | fixture.detectChanges(); 169 | tick(); 170 | fixture.detectChanges(); 171 | expect(input.classList).toContain('ng-valid'); 172 | })); 173 | 174 | it('should valid on equal values when min value is float values', fakeAsync(() => { 175 | context.value = 2.1; 176 | context.min = 2.1; 177 | fixture.detectChanges(); 178 | tick(); 179 | fixture.detectChanges(); 180 | expect(input.classList).toContain('ng-valid'); 181 | })); 182 | }); 183 | 184 | describe('maxValidatorDirective', () => { 185 | @Component({ 186 | template: ` 187 | 188 | ` 189 | }) 190 | class TestComponent { 191 | @Input() value; 192 | 193 | @Input() max; 194 | } 195 | 196 | let fixture: ComponentFixture; 197 | let context: TestComponent; 198 | let nativeElement: HTMLElement; 199 | let directive; 200 | let input; 201 | 202 | beforeEach(() => { 203 | TestBed.configureTestingModule({ 204 | imports: [FormsModule], 205 | declarations: [TestComponent, MaxValidatorDirective] 206 | }); 207 | fixture = TestBed.createComponent(TestComponent); 208 | context = fixture.componentInstance; 209 | nativeElement = fixture.nativeElement; 210 | 211 | const directives = fixture.debugElement.queryAll(By.directive(MaxValidatorDirective)); 212 | directive = directives.map( 213 | (d: DebugElement) => d.injector.get(MaxValidatorDirective) as MaxValidatorDirective 214 | )[0]; 215 | 216 | input = nativeElement.querySelector('input') as HTMLElement; 217 | }); 218 | 219 | it('should be defined on the test component', () => { 220 | expect(directive).not.toBeUndefined(); 221 | }); 222 | 223 | it('should valid on an empty string', fakeAsync(() => { 224 | context.value = ''; 225 | context.max = 2; 226 | fixture.detectChanges(); 227 | tick(); 228 | fixture.detectChanges(); 229 | expect(input.classList).toContain('ng-valid'); 230 | })); 231 | 232 | it('should valid on null', fakeAsync(() => { 233 | context.value = null; 234 | context.max = 2; 235 | fixture.detectChanges(); 236 | tick(); 237 | fixture.detectChanges(); 238 | expect(input.classList).toContain('ng-valid'); 239 | })); 240 | 241 | it('should valid on undefined', fakeAsync(() => { 242 | context.value = undefined; 243 | context.max = 2; 244 | fixture.detectChanges(); 245 | tick(); 246 | fixture.detectChanges(); 247 | expect(input.classList).toContain('ng-valid'); 248 | })); 249 | 250 | it('should valid on NaN after parsing', fakeAsync(() => { 251 | context.value = 'a'; 252 | context.max = 2; 253 | fixture.detectChanges(); 254 | tick(); 255 | fixture.detectChanges(); 256 | expect(input.classList).toContain('ng-valid'); 257 | })); 258 | 259 | it('should invalid on big values', fakeAsync(() => { 260 | context.value = 3; 261 | context.max = 2; 262 | fixture.detectChanges(); 263 | tick(); 264 | fixture.detectChanges(); 265 | expect(input.classList).toContain('ng-invalid'); 266 | })); 267 | 268 | it('should invalid on big values converted from strings', fakeAsync(() => { 269 | context.value = '3'; 270 | context.max = 2; 271 | fixture.detectChanges(); 272 | tick(); 273 | fixture.detectChanges(); 274 | expect(input.classList).toContain('ng-invalid'); 275 | })); 276 | 277 | it('should valid on small values', fakeAsync(() => { 278 | context.value = 1; 279 | context.max = 2; 280 | fixture.detectChanges(); 281 | tick(); 282 | fixture.detectChanges(); 283 | expect(input.classList).toContain('ng-valid'); 284 | })); 285 | 286 | it('should valid on equal values', fakeAsync(() => { 287 | context.value = 2; 288 | context.max = 2; 289 | fixture.detectChanges(); 290 | tick(); 291 | fixture.detectChanges(); 292 | expect(input.classList).toContain('ng-valid'); 293 | })); 294 | 295 | it('should valid on equal values when value is string', fakeAsync(() => { 296 | context.value = '2'; 297 | context.max = 2; 298 | fixture.detectChanges(); 299 | tick(); 300 | fixture.detectChanges(); 301 | expect(input.classList).toContain('ng-valid'); 302 | })); 303 | 304 | it('should invalid as expected when max value is a string', fakeAsync(() => { 305 | context.value = 3; 306 | context.max = '2'; 307 | fixture.detectChanges(); 308 | tick(); 309 | fixture.detectChanges(); 310 | expect(input.classList).toContain('ng-invalid'); 311 | })); 312 | 313 | it('should valid if min value is undefined', fakeAsync(() => { 314 | context.value = 3; 315 | context.max = undefined; 316 | fixture.detectChanges(); 317 | tick(); 318 | fixture.detectChanges(); 319 | expect(input.classList).toContain('ng-valid'); 320 | })); 321 | 322 | it('should valid if min value is null', fakeAsync(() => { 323 | context.value = 3; 324 | context.max = null; 325 | fixture.detectChanges(); 326 | tick(); 327 | fixture.detectChanges(); 328 | expect(input.classList).toContain('ng-valid'); 329 | })); 330 | 331 | it('should invalid on big values when max value is float values', fakeAsync(() => { 332 | context.value = 3; 333 | context.max = 2.1; 334 | fixture.detectChanges(); 335 | tick(); 336 | fixture.detectChanges(); 337 | expect(input.classList).toContain('ng-invalid'); 338 | })); 339 | 340 | it('should valid on big values when min value is float values', fakeAsync(() => { 341 | context.value = 2; 342 | context.max = 2.1; 343 | fixture.detectChanges(); 344 | tick(); 345 | fixture.detectChanges(); 346 | expect(input.classList).toContain('ng-valid'); 347 | })); 348 | 349 | it('should valid on equal values when min value is float values', fakeAsync(() => { 350 | context.value = 2.1; 351 | context.max = 2.1; 352 | fixture.detectChanges(); 353 | tick(); 354 | fixture.detectChanges(); 355 | expect(input.classList).toContain('ng-valid'); 356 | })); 357 | }); 358 | 359 | describe('ngxUniqueCheckDirective', () => { 360 | @Component({ 361 | template: ` 362 | 363 | ` 364 | }) 365 | class TestComponent { 366 | @Input() email; 367 | 368 | uniqueCheck = value => { 369 | return value === 'not unique' ? of(true) : of(false); 370 | } 371 | } 372 | 373 | const uniqueCheck = value => { 374 | return value ? of(true) : of(false); 375 | }; 376 | 377 | let fixture: ComponentFixture; 378 | let context: TestComponent; 379 | let nativeElement: HTMLElement; 380 | let directive; 381 | let input; 382 | 383 | beforeEach(() => { 384 | TestBed.configureTestingModule({ 385 | imports: [FormsModule], 386 | declarations: [TestComponent, NgxUniqueCheckDirective] 387 | }); 388 | fixture = TestBed.createComponent(TestComponent); 389 | context = fixture.componentInstance; 390 | nativeElement = fixture.nativeElement; 391 | 392 | const directives = fixture.debugElement.queryAll(By.directive(NgxUniqueCheckDirective)); 393 | directive = directives.map( 394 | (d: DebugElement) => d.injector.get(NgxUniqueCheckDirective) as NgxUniqueCheckDirective 395 | )[0]; 396 | 397 | input = nativeElement.querySelector('input') as HTMLElement; 398 | }); 399 | 400 | it('should be defined on the test component', () => { 401 | expect(directive).not.toBeUndefined(); 402 | }); 403 | 404 | it('should valid on an empty string', fakeAsync(() => { 405 | context.email = ''; 406 | fixture.detectChanges(); 407 | tick(); 408 | fixture.detectChanges(); 409 | expect(input.classList).toContain('ng-valid'); 410 | })); 411 | 412 | it('should valid on null', fakeAsync(() => { 413 | context.email = ''; 414 | fixture.detectChanges(); 415 | tick(); 416 | fixture.detectChanges(); 417 | expect(input.classList).toContain('ng-valid'); 418 | })); 419 | 420 | it('should invalid on not unique value', fakeAsync(() => { 421 | context.email = 'not unique'; 422 | fixture.detectChanges(); 423 | tick(); 424 | fixture.detectChanges(); 425 | expect(input.classList).toContain('ng-invalid'); 426 | })); 427 | 428 | it('should valid on unique value', fakeAsync(() => { 429 | context.email = 'unique'; 430 | fixture.detectChanges(); 431 | tick(); 432 | fixture.detectChanges(); 433 | expect(input.classList).toContain('ng-valid'); 434 | })); 435 | 436 | // it('should run validator with the initial value', fakeAsync(() => { 437 | // const ngxUniqueCheckDirective = new NgxUniqueCheckDirective(); 438 | // ngxUniqueCheckDirective.ngxUniqueCheck = uniqueCheck; 439 | // const control = new FormControl('false', { 440 | // asyncValidators: [ngxUniqueCheckDirective.validate.bind(ngxUniqueCheckDirective)], 441 | // updateOn: 'change' 442 | // }); 443 | // tick(); 444 | // expect(control.valid).toEqual(false); 445 | // })); 446 | }); 447 | }); 448 | -------------------------------------------------------------------------------- /packages/core/src/directives/form-validator.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, tick, fakeAsync, ComponentFixture } from '@angular/core/testing'; 2 | import { Component, ViewChild } from '@angular/core'; 3 | import { NgxFormValidatorDirective, NgxEnterKeyMode } from './form-validator.directive'; 4 | import { NgxValidatorModule } from '../module'; 5 | import { NgxValidatorConfig, DEFAULT_GLOBAL_VALIDATION_MESSAGES } from '../validator.class'; 6 | import { createFakeEvent, createKeyboardEvent, dispatchEvent } from '../testing'; 7 | import { By } from '@angular/platform-browser'; 8 | import { FormsModule, NgForm, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms'; 9 | import { CommonModule } from '@angular/common'; 10 | import { ValidationFeedbackStrategy } from '../strategies'; 11 | import { NgxValidators } from '../validators'; 12 | 13 | const INVALID_CLASS = 'is-invalid'; 14 | const INVALID_FEEDBACK_CLASS = 'invalid-feedback'; 15 | 16 | const CUSTOM_INVALID_CLASS = 'custom-invalid'; 17 | const CUSTOM_INVALID_FEEDBACK_CLASS = 'custom-invalid-feedback'; 18 | 19 | describe('ngxFormValidator', () => { 20 | let fixture: ComponentFixture; 21 | 22 | function setFixtureValue( 23 | value: ComponentFixture 24 | ) { 25 | fixture = value; 26 | } 27 | 28 | function assertEmailFeedbackIsValid() { 29 | const emailInputElement = fixture.nativeElement.querySelector('#email1'); 30 | const feedbackElement = fixture.nativeElement.querySelector(`.${INVALID_FEEDBACK_CLASS}`); 31 | expect(emailInputElement.classList.contains(INVALID_CLASS)).toBe(false); 32 | expect(feedbackElement).toBeFalsy(); 33 | return { 34 | emailInputElement, 35 | feedbackElement 36 | }; 37 | } 38 | 39 | function assertEmailFeedbackError(emailErrorMessage = DEFAULT_GLOBAL_VALIDATION_MESSAGES.required) { 40 | const emailInputElement = fixture.nativeElement.querySelector('#email1'); 41 | const feedbackElement = fixture.nativeElement.querySelector(`.${INVALID_FEEDBACK_CLASS}`); 42 | expect(emailInputElement.classList.contains(INVALID_CLASS)).toBe(true); 43 | expect(feedbackElement).toBeTruthy(); 44 | expect(feedbackElement.textContent).toContain(emailErrorMessage); 45 | 46 | return { 47 | emailInputElement, 48 | feedbackElement 49 | }; 50 | } 51 | 52 | function submitFormAndAssertEmailFeedbackError(emailRequiredMessage: string) { 53 | fixture.componentInstance.ngxFormValidator.submit(createFakeEvent('click')); 54 | return assertEmailFeedbackError(emailRequiredMessage); 55 | } 56 | 57 | describe('Reactive forms', () => { 58 | beforeEach(() => { 59 | TestBed.configureTestingModule({ 60 | declarations: [SimpleReactiveFormDemoComponent], 61 | imports: [NgxValidatorModule, FormsModule, CommonModule, ReactiveFormsModule], 62 | providers: [] 63 | }); 64 | fixture = TestBed.createComponent(SimpleReactiveFormDemoComponent); 65 | setFixtureValue(fixture); 66 | }); 67 | 68 | it('should be created ngxFormValidator directive', () => { 69 | expect(fixture.componentInstance.ngxFormValidator).toBeTruthy(); 70 | }); 71 | 72 | it(`should be same input's config and ngxFormValidator 's config`, () => { 73 | const inputConfig: NgxValidatorConfig = { 74 | validationFeedbackStrategy: new CustomValidationFeedbackStrategy(), 75 | validationMessages: { 76 | email: { 77 | required: 'email is required' 78 | } 79 | } 80 | }; 81 | fixture.componentInstance.validatorConfig = inputConfig; 82 | fixture.detectChanges(); 83 | expect(fixture.componentInstance.ngxFormValidator.validator.validatorConfig).toEqual(inputConfig); 84 | }); 85 | 86 | it('should show required error feedback when submit with empty email value', fakeAsync(() => { 87 | fixture.detectChanges(); 88 | tick(); 89 | submitFormAndAssertEmailFeedbackError(DEFAULT_GLOBAL_VALIDATION_MESSAGES.required); 90 | })); 91 | 92 | it('should show config required error feedback when submit with empty email value', fakeAsync(() => { 93 | const emailRequiredMessage = 'email is required message'; 94 | fixture.componentInstance.validatorConfig = { 95 | validationMessages: { 96 | email: { 97 | required: emailRequiredMessage 98 | } 99 | } 100 | }; 101 | fixture.detectChanges(); 102 | tick(); 103 | submitFormAndAssertEmailFeedbackError(emailRequiredMessage); 104 | })); 105 | 106 | it('should remove error feedback when input value', fakeAsync(() => { 107 | fixture.detectChanges(); 108 | tick(); 109 | const { emailInputElement } = submitFormAndAssertEmailFeedbackError( 110 | DEFAULT_GLOBAL_VALIDATION_MESSAGES.required 111 | ); 112 | emailInputElement.value = 'invalid email'; 113 | dispatchEvent(emailInputElement, createFakeEvent('input')); 114 | fixture.detectChanges(); 115 | expect( 116 | (fixture as ComponentFixture).componentInstance.formGroup.value.email 117 | ).toBe(emailInputElement.value); 118 | 119 | expect(emailInputElement.classList.contains(INVALID_CLASS)).toBe(false); 120 | const feedbackElement = fixture.nativeElement.querySelector(`.${INVALID_FEEDBACK_CLASS}`); 121 | expect(feedbackElement).toBeFalsy(); 122 | 123 | submitFormAndAssertEmailFeedbackError(DEFAULT_GLOBAL_VALIDATION_MESSAGES.email); 124 | })); 125 | 126 | it('should remove error feedback when submit after input correct email', fakeAsync(() => { 127 | fixture.detectChanges(); 128 | tick(); 129 | const { emailInputElement } = submitFormAndAssertEmailFeedbackError( 130 | DEFAULT_GLOBAL_VALIDATION_MESSAGES.required 131 | ); 132 | emailInputElement.value = 'why520crazy@163.com'; 133 | dispatchEvent(emailInputElement, createFakeEvent('input')); 134 | fixture.detectChanges(); 135 | expect( 136 | (fixture as ComponentFixture).componentInstance.formGroup.value.email 137 | ).toBe(emailInputElement.value); 138 | 139 | fixture.componentInstance.ngxFormValidator.submit(createFakeEvent('click')); 140 | 141 | expect(emailInputElement.classList.contains(INVALID_CLASS)).toBe(false); 142 | const feedbackElement = fixture.nativeElement.querySelector(`.${INVALID_FEEDBACK_CLASS}`); 143 | expect(feedbackElement).toBeFalsy(); 144 | })); 145 | 146 | it('should not call submit function when ngForm is invalid', fakeAsync(() => { 147 | const spy = jasmine.createSpy('submit spy'); 148 | fixture.componentInstance.submit = spy; 149 | fixture.detectChanges(); 150 | tick(); 151 | expect(spy).not.toHaveBeenCalled(); 152 | fixture.componentInstance.ngxFormValidator.submit(createFakeEvent('click')); 153 | expect(spy).not.toHaveBeenCalled(); 154 | })); 155 | 156 | it('should call submit function when ngForm is valid', fakeAsync(() => { 157 | (fixture as ComponentFixture).componentInstance.formGroup.setValue({ 158 | email: 'why520crazy@163.com', 159 | description: '' 160 | }); 161 | const spy = jasmine.createSpy('submit spy'); 162 | fixture.componentInstance.submit = spy; 163 | fixture.detectChanges(); 164 | tick(); 165 | expect(spy).not.toHaveBeenCalled(); 166 | fixture.componentInstance.ngxFormValidator.submit(createFakeEvent('click')); 167 | expect(spy).toHaveBeenCalled(); 168 | })); 169 | 170 | it('should trigger form submit validate when press enter key and focus on input', fakeAsync(() => { 171 | fixture.detectChanges(); 172 | tick(); 173 | const emailInputElement = fixture.nativeElement.querySelector('#email1'); 174 | dispatchEvent(emailInputElement, createKeyboardEvent('keydown', 13)); 175 | fixture.detectChanges(); 176 | assertEmailFeedbackError(); 177 | })); 178 | 179 | it('should not trigger form submit validate when press enter key and focus on textarea', fakeAsync(() => { 180 | fixture.detectChanges(); 181 | tick(); 182 | const textareaInputElement = fixture.nativeElement.querySelector('#description1'); 183 | // focus textareaInputElement 184 | textareaInputElement.focus(); 185 | dispatchEvent(textareaInputElement, createKeyboardEvent('keydown', 13)); 186 | fixture.detectChanges(); 187 | tick(); 188 | assertEmailFeedbackIsValid(); 189 | })); 190 | 191 | it('should trigger form submit validate when enterKeyMode is alwaysSubmit press enter key and focus on textarea', fakeAsync(() => { 192 | fixture.componentInstance.enterKeyMode = NgxEnterKeyMode.alwaysSubmit; 193 | fixture.detectChanges(); 194 | tick(); 195 | const textareaInputElement = fixture.nativeElement.querySelector('#description1'); 196 | // focus textareaInputElement 197 | textareaInputElement.focus(); 198 | dispatchEvent(textareaInputElement, createKeyboardEvent('keydown', 13)); 199 | fixture.detectChanges(); 200 | tick(); 201 | assertEmailFeedbackError(); 202 | })); 203 | }); 204 | 205 | describe('Template Driven', () => { 206 | beforeEach(() => { 207 | TestBed.configureTestingModule({ 208 | declarations: [SimpleTemplateDrivenDemoComponent], 209 | imports: [NgxValidatorModule, FormsModule, CommonModule], 210 | providers: [] 211 | }); 212 | fixture = TestBed.createComponent(SimpleTemplateDrivenDemoComponent); 213 | setFixtureValue(fixture); 214 | }); 215 | 216 | it('should be created ngxFormValidator directive', () => { 217 | expect(fixture.componentInstance.ngxFormValidator).toBeTruthy(); 218 | }); 219 | 220 | it(`should be same input's config and ngxFormValidator 's config`, () => { 221 | const inputConfig: NgxValidatorConfig = { 222 | validationFeedbackStrategy: new CustomValidationFeedbackStrategy(), 223 | validationMessages: { 224 | hello: { 225 | required: 'hello is required' 226 | } 227 | } 228 | }; 229 | fixture.componentInstance.validatorConfig = inputConfig; 230 | fixture.detectChanges(); 231 | expect(fixture.componentInstance.ngxFormValidator.validator.validatorConfig).toEqual(inputConfig); 232 | }); 233 | 234 | it('should show required error feedback when submit with empty email value', fakeAsync(() => { 235 | fixture.detectChanges(); 236 | tick(); 237 | // const ngxFormValidator = fixture.debugElement.children[0].injector.get(NgxFormValidatorDirective); 238 | // const ngxFormValidator = fixture.debugElement.query(By.directive(NgxFormValidatorDirective)); 239 | submitFormAndAssertEmailFeedbackError(DEFAULT_GLOBAL_VALIDATION_MESSAGES.required); 240 | })); 241 | 242 | it('should show config required error feedback when submit with empty email value', fakeAsync(() => { 243 | const emailRequiredMessage = 'email is required message'; 244 | fixture.componentInstance.validatorConfig = { 245 | validationMessages: { 246 | email: { 247 | required: emailRequiredMessage 248 | } 249 | } 250 | }; 251 | fixture.detectChanges(); 252 | tick(); 253 | submitFormAndAssertEmailFeedbackError(emailRequiredMessage); 254 | })); 255 | 256 | it('should remove error feedback when input value', fakeAsync(() => { 257 | fixture.detectChanges(); 258 | tick(); 259 | const { emailInputElement } = submitFormAndAssertEmailFeedbackError( 260 | DEFAULT_GLOBAL_VALIDATION_MESSAGES.required 261 | ); 262 | emailInputElement.value = 'invalid email'; 263 | dispatchEvent(emailInputElement, createFakeEvent('input')); 264 | fixture.detectChanges(); 265 | expect(fixture.componentInstance.model.email).toBe(emailInputElement.value); 266 | 267 | expect(emailInputElement.classList.contains(INVALID_CLASS)).toBe(false); 268 | const feedbackElement = fixture.nativeElement.querySelector(`.${INVALID_FEEDBACK_CLASS}`); 269 | expect(feedbackElement).toBeFalsy(); 270 | 271 | submitFormAndAssertEmailFeedbackError(DEFAULT_GLOBAL_VALIDATION_MESSAGES.email); 272 | })); 273 | 274 | it('should remove error feedback when submit after input correct email', fakeAsync(() => { 275 | fixture.detectChanges(); 276 | tick(); 277 | const { emailInputElement } = submitFormAndAssertEmailFeedbackError( 278 | DEFAULT_GLOBAL_VALIDATION_MESSAGES.required 279 | ); 280 | emailInputElement.value = 'why520crazy@163.com'; 281 | dispatchEvent(emailInputElement, createFakeEvent('input')); 282 | fixture.detectChanges(); 283 | expect(fixture.componentInstance.model.email).toBe(emailInputElement.value); 284 | 285 | fixture.componentInstance.ngxFormValidator.submit(createFakeEvent('click')); 286 | 287 | expect(emailInputElement.classList.contains(INVALID_CLASS)).toBe(false); 288 | const feedbackElement = fixture.nativeElement.querySelector(`.${INVALID_FEEDBACK_CLASS}`); 289 | expect(feedbackElement).toBeFalsy(); 290 | })); 291 | 292 | it('should not call submit function when ngForm is invalid', fakeAsync(() => { 293 | const spy = jasmine.createSpy('submit spy'); 294 | fixture.componentInstance.submit = spy; 295 | fixture.detectChanges(); 296 | tick(); 297 | expect(spy).not.toHaveBeenCalled(); 298 | fixture.componentInstance.ngxFormValidator.submit(createFakeEvent('click')); 299 | expect(spy).not.toHaveBeenCalled(); 300 | })); 301 | 302 | it('should call submit function when ngForm is valid', fakeAsync(() => { 303 | fixture.componentInstance.model.email = 'why520crazy@163.com'; 304 | const spy = jasmine.createSpy('submit spy'); 305 | fixture.componentInstance.submit = spy; 306 | fixture.detectChanges(); 307 | tick(); 308 | expect(spy).not.toHaveBeenCalled(); 309 | fixture.componentInstance.ngxFormValidator.submit(createFakeEvent('click')); 310 | expect(spy).toHaveBeenCalled(); 311 | })); 312 | 313 | it('should trigger form submit validate when press enter key and focus on input', fakeAsync(() => { 314 | fixture.detectChanges(); 315 | tick(); 316 | const emailInputElement = fixture.nativeElement.querySelector('#email1'); 317 | dispatchEvent(emailInputElement, createKeyboardEvent('keydown', 13)); 318 | fixture.detectChanges(); 319 | assertEmailFeedbackError(); 320 | })); 321 | 322 | it('should not trigger form submit validate when press enter key and focus on textarea', fakeAsync(() => { 323 | fixture.detectChanges(); 324 | tick(); 325 | const textareaInputElement = fixture.nativeElement.querySelector('#description1'); 326 | // focus textareaInputElement 327 | textareaInputElement.focus(); 328 | dispatchEvent(textareaInputElement, createKeyboardEvent('keydown', 13)); 329 | fixture.detectChanges(); 330 | tick(); 331 | assertEmailFeedbackIsValid(); 332 | })); 333 | 334 | it('should trigger form submit validate when enterKeyMode is alwaysSubmit press enter key and focus on textarea', fakeAsync(() => { 335 | fixture.componentInstance.enterKeyMode = NgxEnterKeyMode.alwaysSubmit; 336 | fixture.detectChanges(); 337 | tick(); 338 | const textareaInputElement = fixture.nativeElement.querySelector('#description1'); 339 | // focus textareaInputElement 340 | textareaInputElement.focus(); 341 | dispatchEvent(textareaInputElement, createKeyboardEvent('keydown', 13)); 342 | fixture.detectChanges(); 343 | tick(); 344 | assertEmailFeedbackError(); 345 | })); 346 | }); 347 | }); 348 | 349 | @Component({ 350 | selector: 'test-simple-template-driven-demo', 351 | template: ` 352 | 353 |
354 | 355 | 365 |
366 |
367 | 368 | 376 |
377 |
378 | 379 | ` 380 | }) 381 | class SimpleTemplateDrivenDemoComponent { 382 | enterKeyMode: NgxEnterKeyMode; 383 | 384 | validatorConfig: NgxValidatorConfig = null; 385 | 386 | model = { 387 | email: '', 388 | description: '' 389 | }; 390 | 391 | @ViewChild(NgxFormValidatorDirective, { static: true }) ngxFormValidator: NgxFormValidatorDirective; 392 | 393 | constructor() {} 394 | 395 | submit() {} 396 | } 397 | 398 | @Component({ 399 | selector: 'test-simple-reactive-form-demo', 400 | template: ` 401 |
407 |
408 | 409 | 417 |
418 |
419 | 420 | 428 |
429 |
430 |
431 | ` 432 | }) 433 | class SimpleReactiveFormDemoComponent { 434 | enterKeyMode: NgxEnterKeyMode; 435 | 436 | validatorConfig: NgxValidatorConfig = null; 437 | 438 | formGroup: FormGroup; 439 | 440 | model = { 441 | email: '', 442 | description: '' 443 | }; 444 | 445 | @ViewChild(NgxFormValidatorDirective, { static: true }) 446 | ngxFormValidator: NgxFormValidatorDirective; 447 | 448 | constructor(private formBuilder: FormBuilder) { 449 | this.formGroup = this.formBuilder.group({ 450 | email: ['', [Validators.required, Validators.email]], 451 | description: [''] 452 | }); 453 | } 454 | 455 | submit() {} 456 | } 457 | 458 | class CustomValidationFeedbackStrategy implements ValidationFeedbackStrategy { 459 | showError(element: HTMLElement, errorMessages: string[]): void { 460 | element.classList.add(CUSTOM_INVALID_CLASS); 461 | if (element && element.parentElement) { 462 | const documentFrag = document.createDocumentFragment(); 463 | const node = document.createElement('SPAN'); 464 | const textNode = document.createTextNode(errorMessages[0]); 465 | node.appendChild(textNode); 466 | node.setAttribute('class', CUSTOM_INVALID_FEEDBACK_CLASS); 467 | documentFrag.appendChild(node); 468 | element.parentElement.append(documentFrag); 469 | } 470 | } 471 | 472 | removeError(element: HTMLElement): void { 473 | element.classList.remove(CUSTOM_INVALID_CLASS); 474 | if (element && element.parentElement) { 475 | const invalidFeedback = element.parentElement.querySelector(`.${CUSTOM_INVALID_FEEDBACK_CLASS}`); 476 | if (invalidFeedback) { 477 | element.parentElement.removeChild(invalidFeedback); 478 | } 479 | } 480 | } 481 | } 482 | --------------------------------------------------------------------------------