├── .angular-cli.json ├── .codeclimate.yaml ├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── .yo-rc.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md ├── LICENSE.txt ├── README.md ├── examples └── angular4 │ ├── .angular-cli.json │ ├── .editorconfig │ ├── .gitignore │ ├── README.md │ ├── karma.conf.js │ ├── package.json │ ├── protractor.conf.js │ ├── src │ ├── app │ │ ├── app-routing.module.ts │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── core │ │ │ ├── core.module.ts │ │ │ ├── custom-asyncvalidators │ │ │ │ └── is-blacklisted-name.validator.ts │ │ │ └── custom-validators │ │ │ │ └── name.validator.ts │ │ └── page │ │ │ └── basic-usage │ │ │ ├── basic-usage.component.html │ │ │ ├── basic-usage.component.scss │ │ │ ├── basic-usage.component.ts │ │ │ └── control-state │ │ │ ├── control-state.component.html │ │ │ └── control-state.component.ts │ ├── assets │ │ └── .gitkeep │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.scss │ ├── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ └── typings.d.ts │ ├── tsconfig.json │ └── tslint.json ├── gulpfile.js ├── karma.conf.js ├── package.json ├── protractor.conf.js ├── resources └── demo.gif ├── scripts ├── check-remote.ps1 ├── create-changelog.ps1 ├── detect-project-settings.ps1 ├── github-release.ps1 ├── install.ps1 ├── release.ps1 ├── setup-and-test.ps1 └── version-bump.ps1 ├── src ├── alternative-validation.directive.spec.ts ├── alternative-validation.directive.ts ├── index.ts ├── main.ts ├── package.json ├── polyfills.ts ├── struct │ ├── alternative-validation-config.ts │ └── validator-config.ts ├── test.ts ├── tsconfig.es5.json ├── tsconfig.spec.json ├── validation-collector.service.ts └── validation-collector.spec.ts ├── tools └── gulp │ └── inline-resources.js ├── tsconfig.json └── tslint.json /.angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "project": { 4 | "name": "a-f-p" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "dist", 10 | "assets": [ 11 | "assets", 12 | "favicon.ico" 13 | ], 14 | "index": "index.html", 15 | "main": "main.ts", 16 | "polyfills": "polyfills.ts", 17 | "test": "test.ts", 18 | "tsconfig": "tsconfig.app.json", 19 | "testTsconfig": "tsconfig.spec.json", 20 | "prefix": "", 21 | "styles": [ 22 | "styles.css" 23 | ], 24 | "scripts": [], 25 | "environmentSource": "environments/environment.ts", 26 | "environments": { 27 | "dev": "environments/environment.ts", 28 | "prod": "environments/environment.prod.ts" 29 | } 30 | } 31 | ], 32 | "e2e": { 33 | "protractor": { 34 | "config": "./protractor.conf.js" 35 | } 36 | }, 37 | "lint": [ 38 | { 39 | "project": "src/tsconfig.app.json" 40 | }, 41 | { 42 | "project": "src/tsconfig.spec.json" 43 | }, 44 | { 45 | "project": "e2e/tsconfig.e2e.json" 46 | } 47 | ], 48 | "test": { 49 | "karma": { 50 | "config": "./karma.conf.js" 51 | }, 52 | "codeCoverage": { 53 | "exclude": ["./src/static-vendors/**/*"] 54 | } 55 | }, 56 | "defaults": { 57 | "styleExt": "css", 58 | "component": {} 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.codeclimate.yaml: -------------------------------------------------------------------------------- 1 | engines: 2 | duplication: 3 | enabled: true 4 | config: 5 | languages: 6 | - javascript 7 | eslint: 8 | enabled: true 9 | fixme: 10 | enabled: true 11 | ratings: 12 | paths: 13 | - "**.css" 14 | - "**.ts" 15 | - "**.js" 16 | exclude_paths: [ 17 | "examples/**/*", 18 | "./examples/**/*", 19 | "examples/", 20 | "./examples/" 21 | ] 22 | 23 | parserOptions: 24 | sourceType: module 25 | 26 | env: 27 | amd: true 28 | browser: true 29 | es6: true 30 | 31 | # http://eslint.org/docs/rules/ 32 | rules: 33 | # Possible Errors 34 | no-await-in-loop: off 35 | no-cond-assign: error 36 | no-console: off 37 | no-constant-condition: error 38 | no-control-regex: error 39 | no-debugger: error 40 | no-dupe-args: error 41 | no-dupe-keys: error 42 | no-duplicate-case: error 43 | no-empty-character-class: error 44 | no-empty: error 45 | no-ex-assign: error 46 | no-extra-boolean-cast: error 47 | no-extra-parens: off 48 | no-extra-semi: error 49 | no-func-assign: error 50 | no-inner-declarations: 51 | - error 52 | - functions 53 | no-invalid-regexp: error 54 | no-irregular-whitespace: error 55 | no-negated-in-lhs: error 56 | no-obj-calls: error 57 | no-prototype-builtins: off 58 | no-regex-spaces: error 59 | no-sparse-arrays: error 60 | no-template-curly-in-string: off 61 | no-unexpected-multiline: error 62 | no-unreachable: error 63 | no-unsafe-finally: off 64 | no-unsafe-negation: off 65 | use-isnan: error 66 | valid-jsdoc: off 67 | valid-typeof: error 68 | 69 | # Best Practices 70 | accessor-pairs: error 71 | array-callback-return: off 72 | block-scoped-var: off 73 | class-methods-use-this: off 74 | complexity: 75 | - error 76 | - 6 77 | consistent-return: off 78 | curly: off 79 | default-case: off 80 | dot-location: off 81 | dot-notation: off 82 | eqeqeq: error 83 | guard-for-in: error 84 | no-alert: error 85 | no-caller: error 86 | no-case-declarations: error 87 | no-div-regex: error 88 | no-else-return: off 89 | no-empty-function: off 90 | no-empty-pattern: error 91 | no-eq-null: error 92 | no-eval: error 93 | no-extend-native: error 94 | no-extra-bind: error 95 | no-extra-label: off 96 | no-fallthrough: error 97 | no-floating-decimal: off 98 | no-global-assign: off 99 | no-implicit-coercion: off 100 | no-implied-eval: error 101 | no-invalid-this: off 102 | no-iterator: error 103 | no-labels: 104 | - error 105 | - allowLoop: true 106 | allowSwitch: true 107 | no-lone-blocks: error 108 | no-loop-func: error 109 | no-magic-number: off 110 | no-multi-spaces: off 111 | no-multi-str: off 112 | no-native-reassign: error 113 | no-new-func: error 114 | no-new-wrappers: error 115 | no-new: error 116 | no-octal-escape: error 117 | no-octal: error 118 | no-param-reassign: off 119 | no-proto: error 120 | no-redeclare: error 121 | no-restricted-properties: off 122 | no-return-assign: error 123 | no-return-await: off 124 | no-script-url: error 125 | no-self-assign: off 126 | no-self-compare: error 127 | no-sequences: off 128 | no-throw-literal: off 129 | no-unmodified-loop-condition: off 130 | no-unused-expressions: error 131 | no-unused-labels: off 132 | no-useless-call: error 133 | no-useless-concat: error 134 | no-useless-escape: off 135 | no-useless-return: off 136 | no-void: error 137 | no-warning-comments: off 138 | no-with: error 139 | prefer-promise-reject-errors: off 140 | radix: error 141 | require-await: off 142 | vars-on-top: off 143 | wrap-iife: error 144 | yoda: off 145 | 146 | # Strict 147 | strict: off 148 | 149 | # Variables 150 | init-declarations: off 151 | no-catch-shadow: error 152 | no-delete-var: error 153 | no-label-var: error 154 | no-restricted-globals: off 155 | no-shadow-restricted-names: error 156 | no-shadow: off 157 | no-undef-init: error 158 | no-undef: off 159 | no-undefined: off 160 | no-unused-vars: off 161 | no-use-before-define: off 162 | 163 | # Node.js and CommonJS 164 | callback-return: error 165 | global-require: error 166 | handle-callback-err: error 167 | no-mixed-requires: off 168 | no-new-require: off 169 | no-path-concat: error 170 | no-process-env: off 171 | no-process-exit: error 172 | no-restricted-modules: off 173 | no-sync: off 174 | 175 | # Stylistic Issues 176 | array-bracket-spacing: off 177 | block-spacing: off 178 | brace-style: off 179 | camelcase: off 180 | capitalized-comments: off 181 | comma-dangle: 182 | - error 183 | - never 184 | comma-spacing: off 185 | comma-style: off 186 | computed-property-spacing: off 187 | consistent-this: off 188 | eol-last: off 189 | func-call-spacing: off 190 | func-name-matching: off 191 | func-names: off 192 | func-style: off 193 | id-length: off 194 | id-match: off 195 | indent: off 196 | jsx-quotes: off 197 | key-spacing: off 198 | keyword-spacing: off 199 | line-comment-position: off 200 | linebreak-style: off 201 | lines-around-comment: off 202 | lines-around-directive: off 203 | max-depth: off 204 | max-len: off 205 | max-nested-callbacks: off 206 | max-params: off 207 | max-statements-per-line: off 208 | max-statements: 209 | - error 210 | - 30 211 | multiline-ternary: off 212 | new-cap: off 213 | new-parens: off 214 | newline-after-var: off 215 | newline-before-return: off 216 | newline-per-chained-call: off 217 | no-array-constructor: off 218 | no-bitwise: off 219 | no-continue: off 220 | no-inline-comments: off 221 | no-lonely-if: off 222 | no-mixed-operators: off 223 | no-mixed-spaces-and-tabs: off 224 | no-multi-assign: off 225 | no-multiple-empty-lines: off 226 | no-negated-condition: off 227 | no-nested-ternary: off 228 | no-new-object: off 229 | no-plusplus: off 230 | no-restricted-syntax: off 231 | no-spaced-func: off 232 | no-tabs: off 233 | no-ternary: off 234 | no-trailing-spaces: off 235 | no-underscore-dangle: off 236 | no-unneeded-ternary: off 237 | object-curly-newline: off 238 | object-curly-spacing: off 239 | object-property-newline: off 240 | one-var-declaration-per-line: off 241 | one-var: off 242 | operator-assignment: off 243 | operator-linebreak: off 244 | padded-blocks: off 245 | quote-props: off 246 | quotes: off 247 | require-jsdoc: off 248 | semi-spacing: off 249 | semi: off 250 | sort-keys: off 251 | sort-vars: off 252 | space-before-blocks: off 253 | space-before-function-paren: off 254 | space-in-parens: off 255 | space-infix-ops: off 256 | space-unary-ops: off 257 | spaced-comment: off 258 | template-tag-spacing: off 259 | unicode-bom: off 260 | wrap-regex: off 261 | 262 | # ECMAScript 6 263 | arrow-body-style: off 264 | arrow-parens: off 265 | arrow-spacing: off 266 | constructor-super: off 267 | generator-star-spacing: off 268 | no-class-assign: off 269 | no-confusing-arrow: off 270 | no-const-assign: off 271 | no-dupe-class-members: off 272 | no-duplicate-imports: off 273 | no-new-symbol: off 274 | no-restricted-imports: off 275 | no-this-before-super: off 276 | no-useless-computed-key: off 277 | no-useless-constructor: off 278 | no-useless-rename: off 279 | no-var: off 280 | object-shorthand: off 281 | prefer-arrow-callback: off 282 | prefer-const: off 283 | prefer-destructuring: off 284 | prefer-numeric-literals: off 285 | prefer-rest-params: off 286 | prefer-reflect: off 287 | prefer-spread: off 288 | prefer-template: off 289 | require-yield: off 290 | rest-spread-spacing: off 291 | sort-imports: off 292 | symbol-description: off 293 | template-curly-spacing: off 294 | yield-star-spacing: off 295 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules/* 3 | npm-debug.log 4 | 5 | # TypeScript 6 | src/*.js 7 | src/*.map 8 | src/*.d.ts 9 | 10 | # JetBrains 11 | .idea 12 | .project 13 | .settings 14 | .idea/* 15 | *.iml 16 | 17 | # VS Code 18 | .vscode/* 19 | 20 | # Windows 21 | Thumbs.db 22 | Desktop.ini 23 | 24 | # Mac 25 | .DS_Store 26 | **/.DS_Store 27 | 28 | # Ngc generated files 29 | **/*.ngfactory.ts 30 | 31 | # Build files 32 | dist/* 33 | 34 | # Karma transform 35 | init-test-bed.spec.js 36 | src/**/*.js 37 | 38 | # coverage 39 | coverage 40 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Node 2 | node_modules/* 3 | npm-debug.log 4 | docs/* 5 | # DO NOT IGNORE TYPESCRIPT FILES FOR NPM 6 | # TypeScript 7 | # *.js 8 | # *.map 9 | # *.d.ts 10 | 11 | # JetBrains 12 | .idea 13 | .project 14 | .settings 15 | .idea/* 16 | *.iml 17 | 18 | # VS Code 19 | .vscode/* 20 | 21 | # Windows 22 | Thumbs.db 23 | Desktop.ini 24 | 25 | # Mac 26 | .DS_Store 27 | **/.DS_Store 28 | 29 | # Ngc generated files 30 | **/*.ngfactory.ts 31 | 32 | # Library files 33 | src/* 34 | build/* 35 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - '6.11.1' 5 | 6 | cache: 7 | directories: 8 | - ./node_modules 9 | 10 | install: 11 | - npm i 12 | 13 | before_script: 14 | 15 | script: 16 | - npm run coverage 17 | 18 | after_success: 19 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 20 | - cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js 21 | - rm -rf ./coverage 22 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-angular2-library": { 3 | "promptValues": { 4 | "gitRepositoryUrl": "https://github.com/username/repo" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [0.3.6](https://github.com/BioPhoton/angular-alternative-validation/compare/1.0.5...0.3.6) (2017-08-08) 3 | 4 | 5 | 6 | 7 | ## [0.3.6](https://github.com/BioPhoton/angular-alternative-validation/compare/1.0.4...0.3.6) (2017-08-07) 8 | 9 | 10 | 11 | 12 | ## [0.3.6](https://github.com/BioPhoton/angular-alternative-validation/compare/1.0.3...0.3.6) (2017-08-07) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * **cleanup code:** cleanup for imports, params, and formatting ([5027f80](https://github.com/BioPhoton/angular-alternative-validation/commit/5027f80)) 18 | * **linting:** changed jslint.json, fixed new linting errors ([cf3ce28](https://github.com/BioPhoton/angular-alternative-validation/commit/cf3ce28)) 19 | 20 | 21 | 22 | 23 | ## [0.3.6](https://github.com/BioPhoton/angular-alternative-validation/compare/1.0.2...0.3.6) (2017-08-05) 24 | 25 | 26 | 27 | 28 | ## [0.3.6](https://github.com/BioPhoton/angular-alternative-validation/compare/1.0.1...0.3.6) (2017-08-05) 29 | 30 | 31 | 32 | 33 | ## [0.3.6](https://github.com/BioPhoton/angular-alternative-validation/compare/1.0.0...0.3.6) (2017-08-05) 34 | 35 | 36 | 37 | 38 | ## [0.3.6](https://github.com/BioPhoton/angular-alternative-validation/compare/0.3.10...0.3.6) (2017-08-05) 39 | 40 | 41 | ### Bug Fixes 42 | 43 | * **disabled state:** implement disabled state and observable ([cf31e75](https://github.com/BioPhoton/angular-alternative-validation/commit/cf31e75)) 44 | 45 | 46 | ### Features 47 | 48 | * **first feature complete state:** first state i consider feature completeness ([cda0553](https://github.com/BioPhoton/angular-alternative-validation/commit/cda0553)) 49 | 50 | 51 | ### BREAKING CHANGES 52 | 53 | * **first feature complete state:** -) 54 | 55 | 56 | 57 | 58 | ## [0.3.6](https://github.com/BioPhoton/angular-alternative-validation/compare/0.3.9...0.3.6) (2017-08-04) 59 | 60 | 61 | ### Bug Fixes 62 | 63 | * **implement AbstractControlDirective:** implemented AbstractControlDirective and updated example ([44e711c](https://github.com/BioPhoton/angular-alternative-validation/commit/44e711c)) 64 | 65 | 66 | 67 | 68 | ## [0.3.6](https://github.com/BioPhoton/angular-alternative-validation/compare/0.3.9...0.3.6) (2017-08-04) 69 | 70 | 71 | ### Bug Fixes 72 | 73 | * **implement AbstractControlDirective:** implemented AbstractControlDirective and updated example ([44e711c](https://github.com/BioPhoton/angular-alternative-validation/commit/44e711c)) 74 | 75 | 76 | 77 | 78 | ## [0.3.8](https://github.com/BioPhoton/angular-alternative-validation/compare/0.3.7...0.3.8) (2017-07-31) 79 | 80 | 81 | 82 | 83 | ## [0.3.7](https://github.com/BioPhoton/angular-alternative-validation/compare/434b965...0.3.7) (2017-07-31) 84 | 85 | 86 | ### Bug Fixes 87 | 88 | * **changelog:** implement changelog file and autometed building ([434b965](https://github.com/BioPhoton/angular-alternative-validation/commit/434b965)), closes [#7](https://github.com/BioPhoton/angular-alternative-validation/issues/7) 89 | * **example:** setup basic example ([57b302f](https://github.com/BioPhoton/angular-alternative-validation/commit/57b302f)), closes [#5](https://github.com/BioPhoton/angular-alternative-validation/issues/5) 90 | * **test-coverage:** setup tests coverage on coveralls and codecov ([50ecd2b](https://github.com/BioPhoton/angular-alternative-validation/commit/50ecd2b)) 91 | * **unit-test:** setup unit-tests on local and on travis ([67d84e6](https://github.com/BioPhoton/angular-alternative-validation/commit/67d84e6)) 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at michael@hladky.at. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## I'm submitting a... 2 | 3 |

 4 | [ ] Bug report  
 5 | [ ] Feature request
 6 | 
7 | 8 | ## Current behavior 9 | 10 | 11 | 12 | ## Expected behavior 13 | 14 | 15 | 16 | ## Minimal reproduction of the problem with instructions 17 | 20 | 21 | 22 | ## Environment 23 | 24 |

25 | Library version: X.Y.Z
26 | Angular version: X.Y.Z
27 | 
28 | 
29 | Browser:
30 | 
31 | - [ ] Chrome (desktop) version XX
32 | 
33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Michael Hladky 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angular-alternative-validation 2 | 3 | #### Angular Alternative Validation - The smoothest way to implement validation hints/warnings for your forms 4 | 5 | ![License](https://img.shields.io/npm/l/angular-alternative-validation.svg) 6 | [![NPM Version](https://img.shields.io/npm/v/angular-alternative-validation.svg)](https://www.npmjs.com/package/angular-alternative-validation) 7 | [![Build Status](https://travis-ci.org/BioPhoton/angular-alternative-validation.svg?branch=master)](https://travis-ci.org/BioPhoton/angular-alternative-validation) 8 | [![Coverage Status](https://coveralls.io/repos/github/BioPhoton/angular-alternative-validation/badge.svg?branch=master)](https://coveralls.io/github/BioPhoton/angular-alternative-validation?branch=master) 9 | 10 | ## Demo 11 | 12 | - [x] [angular4 demo with ng-cli](https://github.com/BioPhoton/angular-alternative-validation/tree/master/examples/angular4) 13 | - [x] [plunkr demo](https://embed.plnkr.co/e3GOAFENPumfy78IWXAw/) 14 | 15 | 16 | ![Angular-Alternative-Validation](https://raw.githubusercontent.com/BioPhoton/angular-alternative-validation/master/resources/demo.gif) 17 | 18 | ## Quick code example: 19 | ``` typescript 20 | // app.component.ts 21 | ... 22 | import { IAlternativeValidationConfig } from 'angular-alternative-validation/struct/alternative-validation-config'; 23 | 24 | @Component({ 25 | selector: 'app-basic-usage', 26 | template: ` 30 | 31 | {{fg.get('name').valid}} vs {{aV.valid}}` 32 | }) 33 | export class BasicUsageComponent { 34 | ... 35 | } 36 | 37 | ``` 38 | 39 | 40 | ## Basic Usage: 41 | 42 | #### Implement Library 43 | 44 | ``` bash 45 | $ npm install angular-alternative-validation --save 46 | ``` 47 | 48 | ``` typescript 49 | // app.module.ts 50 | ... 51 | // IMPORT YOUR LIBRARY 52 | import { AlternativeValidationModule } from 'angular-alternative-validation'; 53 | 54 | @NgModule({ 55 | imports: [ 56 | ... 57 | AlternativeValidationModule.forRoot(); 58 | ] 59 | ... 60 | }) 61 | export class AppModule { } 62 | 63 | ``` 64 | 65 | #### Create alternative validation config object 66 | 67 | ``` typescript 68 | // app.component.ts 69 | ... 70 | import { IAlternativeValidationConfig } from 'angular-alternative-validation/struct/alternative-validation-config'; 71 | 72 | @Component({ 73 | selector: 'app-basic-usage', 74 | template: ` 75 |
76 | 77 | Value: {{formGroup.get('name').value}} , Valid: {{formGroup.get('name').valid}} 78 |
79 | ` 80 | }) 81 | export class BasicUsageComponent { 82 | 83 | aVConfig: IAlternativeValidationConfig = { 84 | validator: [ 85 | {name: 'required'}, 86 | {name: 'minLength', params: [3] } 87 | ] 88 | } 89 | 90 | formGroup: FormGroup; 91 | 92 | constructor(private fb: FormBuilder) { 93 | this.basicFormGroup = this.fb.group({ name: [] }); 94 | } 95 | 96 | } 97 | 98 | ``` 99 | 100 | #### Template reference to the directive 101 | 102 | ``` html 103 | // app.component.html 104 | ... 105 | 106 | {{aV.errors | json}} {{aV.valid}} 107 | ``` 108 | 109 | #### A Reference to the directive in the class 110 | 111 | ``` typescript 112 | // app.component.ts 113 | ... 114 | @ViewChild(AlternativeValidationDirective) 115 | alternativeValidationRef 116 | ... 117 | ngAfterViewInit() { 118 | console.log('Directive referenc: ', this.alternativeValidationRef); 119 | } 120 | ... 121 | ``` 122 | 123 | ## Use custom validations 124 | 125 | #### Create custom function 126 | 127 | ``` typescript 128 | // app.module.ts 129 | export function myValidation(param1, param2): ValidatorFn { 130 | 131 | } 132 | 133 | ... 134 | @NgModule({ 135 | ... 136 | providers: [ 137 | { provide: NG_VALIDATORS, useValue: myValidation, multi: true } 138 | ] 139 | ... 140 | }) 141 | export class AppModule { 142 | 143 | } 144 | 145 | ``` 146 | 147 | #### Use custom transform function in config object 148 | 149 | ``` typescript 150 | // app.component.ts 151 | ... 152 | export class BasicUsageComponent { 153 | 154 | fPConfig: IAlternativeValidationConfig = { 155 | alternativeValidation:[ 156 | { name: 'myValidation', params: [param1, param2] } 157 | ] 158 | } 159 | 160 | } 161 | 162 | ``` 163 | 164 | # What it is 165 | 166 | There are many ways to build a alternative validation state. 167 | Many of them can't reuse existing validators and all of them do not provide a separate state of validation. 168 | 169 | What this library do is it provides an alternative state of the host control. 170 | You can use it like the normal form control validation 171 | but it is not effecting the actual validation of the form. 172 | 173 | It's a mix of `FormControlName`, `AbstractControlDirective`, `ControlValueAccessor`, `NG_VALIDATORS` and a little bit of `magic-glue$`. 174 | 175 | In this way you can: 176 | - reuse default validators and async validators 177 | - treat the alternative control stated independent from the real one that effects formGroup and formControl states 178 | - display user hints/information separated from the error messages 179 | - use other libraries working with formControl logic 180 | 181 | # License 182 | 183 | MIT © [Michael Hladky](mailto:michael@hladky.at) 184 | 185 | Copyright 2017 [Michael Hladky](mailto:michael@hladky.at). All Rights Reserved. 186 | Use of this source code is governed by an MIT-style license that 187 | can be found in the LICENSE file at [angular-alternative-validation](https://github.com/BioPhoton/angular-alternative-validation/blob/master/LICENSE.txt) 188 | -------------------------------------------------------------------------------- /examples/angular4/.angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "project": { 4 | "name": "test-angular-alternative-validation" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "dist", 10 | "assets": [ 11 | "assets", 12 | "favicon.ico" 13 | ], 14 | "index": "index.html", 15 | "main": "main.ts", 16 | "polyfills": "polyfills.ts", 17 | "test": "test.ts", 18 | "tsconfig": "tsconfig.app.json", 19 | "testTsconfig": "tsconfig.spec.json", 20 | "prefix": "app", 21 | "styles": [ 22 | "styles.scss" 23 | ], 24 | "scripts": [], 25 | "environmentSource": "environments/environment.ts", 26 | "environments": { 27 | "dev": "environments/environment.ts", 28 | "prod": "environments/environment.prod.ts" 29 | } 30 | } 31 | ], 32 | "e2e": { 33 | "protractor": { 34 | "config": "./protractor.conf.js" 35 | } 36 | }, 37 | "lint": [ 38 | { 39 | "project": "src/tsconfig.app.json" 40 | }, 41 | { 42 | "project": "src/tsconfig.spec.json" 43 | }, 44 | { 45 | "project": "e2e/tsconfig.e2e.json" 46 | } 47 | ], 48 | "test": { 49 | "karma": { 50 | "config": "./karma.conf.js" 51 | } 52 | }, 53 | "defaults": { 54 | "styleExt": "scss", 55 | "component": { 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/angular4/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /examples/angular4/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | testem.log 34 | /typings 35 | 36 | # e2e 37 | /e2e/*.js 38 | /e2e/*.map 39 | 40 | # System Files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /examples/angular4/README.md: -------------------------------------------------------------------------------- 1 | # TestAngularFormatterParser 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.1.0. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | Before running the tests make sure you are serving the app via `ng serve`. 25 | 26 | ## Further help 27 | 28 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 29 | -------------------------------------------------------------------------------- /examples/angular4/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular/cli'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular/cli/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | reports: [ 'html', 'lcovonly' ], 20 | fixWebpackSourcePaths: true 21 | }, 22 | angularCli: { 23 | environment: 'dev' 24 | }, 25 | reporters: ['progress', 'kjhtml'], 26 | port: 9876, 27 | colors: true, 28 | logLevel: config.LOG_INFO, 29 | autoWatch: true, 30 | browsers: ['Chrome'], 31 | singleRun: false 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /examples/angular4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-angular-alternative-validation", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "build": "ng build", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "^4.0.0", 16 | "@angular/common": "^4.0.0", 17 | "@angular/compiler": "^4.0.0", 18 | "@angular/core": "^4.0.0", 19 | "@angular/forms": "^4.0.0", 20 | "@angular/http": "^4.0.0", 21 | "@angular/platform-browser": "^4.0.0", 22 | "@angular/platform-browser-dynamic": "^4.0.0", 23 | "@angular/router": "^4.0.0", 24 | "angular-alternative-validation": "^0.2.0", 25 | "bootstrap": "^4.0.0-alpha.6", 26 | "core-js": "^2.4.1", 27 | "font-awesome": "^4.7.0", 28 | "rxjs": "^5.1.0", 29 | "zone.js": "^0.8.4" 30 | }, 31 | "devDependencies": { 32 | "@angular/cli": "1.1.0", 33 | "@angular/compiler-cli": "^4.0.0", 34 | "@angular/language-service": "^4.0.0", 35 | "@types/jasmine": "2.5.45", 36 | "@types/node": "~6.0.60", 37 | "codelyzer": "~3.0.1", 38 | "jasmine-core": "~2.6.2", 39 | "jasmine-spec-reporter": "~4.1.0", 40 | "karma": "~1.7.0", 41 | "karma-chrome-launcher": "~2.1.1", 42 | "karma-cli": "~1.0.1", 43 | "karma-jasmine": "~1.1.0", 44 | "karma-jasmine-html-reporter": "^0.2.2", 45 | "karma-coverage-istanbul-reporter": "^1.2.1", 46 | "protractor": "~5.1.2", 47 | "ts-node": "~3.0.4", 48 | "tslint": "~5.3.2", 49 | "typescript": "~2.3.3" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/angular4/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /examples/angular4/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { BasicUsageComponent } from './page/basic-usage/basic-usage.component'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | redirectTo: 'basic-usage', 9 | pathMatch: 'full' 10 | }, 11 | { 12 | path: 'basic-usage', 13 | component: BasicUsageComponent 14 | }, 15 | { 16 | path: '**', 17 | redirectTo: 'basic-usage', 18 | pathMatch: 'full' 19 | } 20 | ]; 21 | 22 | @NgModule({ 23 | imports: [RouterModule.forRoot(routes)], 24 | exports: [RouterModule] 25 | }) 26 | export class AppRoutingModule { 27 | } 28 | -------------------------------------------------------------------------------- /examples/angular4/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 |
11 | 12 |
13 | -------------------------------------------------------------------------------- /examples/angular4/src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BioPhoton/angular-alternative-validation/a72f551819bc716c870ce086c7f6c404be244384/examples/angular4/src/app/app.component.scss -------------------------------------------------------------------------------- /examples/angular4/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | declarations: [ 9 | AppComponent 10 | ], 11 | }).compileComponents(); 12 | })); 13 | 14 | it('should create the app', async(() => { 15 | const fixture = TestBed.createComponent(AppComponent); 16 | const app = fixture.debugElement.componentInstance; 17 | expect(app).toBeTruthy(); 18 | })); 19 | 20 | it(`should have as title 'app'`, async(() => { 21 | const fixture = TestBed.createComponent(AppComponent); 22 | const app = fixture.debugElement.componentInstance; 23 | expect(app.title).toEqual('app'); 24 | })); 25 | 26 | it('should render title in a h1 tag', async(() => { 27 | const fixture = TestBed.createComponent(AppComponent); 28 | fixture.detectChanges(); 29 | const compiled = fixture.debugElement.nativeElement; 30 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!!'); 31 | })); 32 | }); 33 | -------------------------------------------------------------------------------- /examples/angular4/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent { 9 | 10 | constructor() { 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/angular4/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core' 2 | import {ReactiveFormsModule} from '@angular/forms' 3 | import {BrowserModule} from '@angular/platform-browser' 4 | import {AppRoutingModule} from './app-routing.module' 5 | 6 | // Import your library 7 | import {AlternativeValidationModule} from 'angular-alternative-validation' 8 | 9 | // Project specific imports 10 | import {CoreModule} from './core/core.module' 11 | import {AppComponent} from './app.component' 12 | import {BasicUsageComponent} from './page/basic-usage/basic-usage.component' 13 | import {ControlStateComponent} from './page/basic-usage/control-state/control-state.component' 14 | @NgModule({ 15 | declarations: [ 16 | AppComponent, 17 | BasicUsageComponent, 18 | ControlStateComponent 19 | ], 20 | imports: [ 21 | BrowserModule, 22 | AppRoutingModule, 23 | ReactiveFormsModule, 24 | AlternativeValidationModule.forRoot(), 25 | CoreModule 26 | ], 27 | bootstrap: [AppComponent] 28 | }) 29 | export class AppModule { 30 | } 31 | -------------------------------------------------------------------------------- /examples/angular4/src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule, Optional, SkipSelf } from '@angular/core'; 3 | import {validName} from './custom-validators/name.validator' 4 | import {NG_VALIDATORS} from '@angular/forms' 5 | 6 | @NgModule({ 7 | imports: [ 8 | CommonModule 9 | ], 10 | exports: [ 11 | CommonModule 12 | ], 13 | declarations: [], 14 | providers: [ 15 | {provide: NG_VALIDATORS, useValue: validName, multi: true} 16 | ] 17 | }) 18 | export class CoreModule { 19 | constructor(@Optional() @SkipSelf() parentModule: CoreModule) { 20 | console.log('CoreModlue: ', parentModule); 21 | if (parentModule) { 22 | throw new Error( 23 | 'CoreModule is already loaded. Import it in the AppModule only'); 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /examples/angular4/src/app/core/custom-asyncvalidators/is-blacklisted-name.validator.ts: -------------------------------------------------------------------------------- 1 | import {AbstractControl, ValidationErrors} from '@angular/forms' 2 | 3 | import {Observable} from 'rxjs/Observable' 4 | import 'rxjs/add/operator/debounceTime' 5 | import 'rxjs/add/operator/distinctUntilChanged' 6 | import 'rxjs/add/operator/delay' 7 | import 'rxjs/add/operator/first' 8 | 9 | export function isBlacklistedName(c: AbstractControl): Observable { 10 | 11 | const validatorName = 'isBlacklistedName'; 12 | 13 | const routValidation$ = new Observable(observer => { 14 | 15 | if (c.value && typeof c.value === 'string' && 16 | 'abcde'.indexOf(c.value.toString().toLowerCase()) !== -1) { 17 | observer.next({ 18 | [this.validationName]: { 19 | actual: c.value, 20 | mandatoryChars: 'abcde' 21 | } 22 | }); 23 | } else { 24 | observer.next(null); 25 | } 26 | }); 27 | 28 | return routValidation$.debounceTime(500).distinctUntilChanged().delay(2000).first(); 29 | 30 | 31 | } 32 | -------------------------------------------------------------------------------- /examples/angular4/src/app/core/custom-validators/name.validator.ts: -------------------------------------------------------------------------------- 1 | import {AbstractControl, ValidationErrors} from '@angular/forms' 2 | 3 | export function validName(c: AbstractControl): ValidationErrors | null { 4 | const validNames = ['Aretha', 'Ella', 'Etta', 'Nina']; 5 | 6 | const isValid = validNames 7 | .map(n => c.value && c.value.indexOf(n) !== -1) 8 | .filter(v => v) 9 | .reduce((prev, curr) => true, false); 10 | 11 | if (!isValid) { 12 | return { 13 | validName: { 14 | valid: true, 15 | validNames 16 | } 17 | } 18 | } 19 | 20 | return null 21 | } 22 | 23 | -------------------------------------------------------------------------------- /examples/angular4/src/app/page/basic-usage/basic-usage.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | The singer survey 7 |
8 |
9 |
11 | 12 | 15 | 16 | 22 | 23 | 27 | 31 | 32 | 38 | 39 | 43 | 44 | 45 | Please enter a female singer that pops up first in your mind. 46 | 47 | 48 |
49 |
50 | 66 |
67 |
68 |
69 | 70 |
71 | 72 | 73 | Angular control states 74 | 75 | 76 |
77 | 78 |
79 | 80 | 81 | Alternative control states 82 | 83 | 84 |
85 |
86 |
87 |
88 | -------------------------------------------------------------------------------- /examples/angular4/src/app/page/basic-usage/basic-usage.component.scss: -------------------------------------------------------------------------------- 1 | ul { 2 | padding: 0; 3 | } 4 | 5 | -------------------------------------------------------------------------------- /examples/angular4/src/app/page/basic-usage/basic-usage.component.ts: -------------------------------------------------------------------------------- 1 | import {AfterViewInit, Component, OnInit, ViewChild} from '@angular/core' 2 | import { 3 | AbstractControl, 4 | FormBuilder, 5 | FormControl, 6 | FormGroup, 7 | Validators 8 | } from '@angular/forms' 9 | import {AlternativeValidationDirective} from 'angular-alternative-validation' 10 | import {IAlternativeValidationConfig} from 'angular-alternative-validation/struct/alternative-validation-config' 11 | 12 | @Component({ 13 | selector: 'basic-usage', 14 | templateUrl: './basic-usage.component.html', 15 | styleUrls: ['./basic-usage.component.scss'] 16 | }) 17 | export class BasicUsageComponent implements OnInit, AfterViewInit { 18 | 19 | 20 | basicFormGroup: FormGroup; 21 | altInput: FormControl; 22 | avNameConfig: IAlternativeValidationConfig; 23 | 24 | @ViewChild(AlternativeValidationDirective) 25 | ref 26 | 27 | constructor(private fb: FormBuilder) { 28 | this.basicFormGroup = this.fb.group( 29 | { 30 | alt: ['initial', [Validators.required, Validators.minLength(3)]] 31 | } 32 | ); 33 | 34 | this.avNameConfig = { 35 | validator: [ 36 | {name: 'validName'} 37 | ] 38 | }; 39 | } 40 | 41 | ngOnInit() { 42 | this.altInput = this.basicFormGroup.get('alt') as FormControl 43 | } 44 | 45 | ngAfterViewInit() { 46 | console.log('Reference to the directive', this.ref); 47 | } 48 | 49 | getControlFeedbackName(formControl: AbstractControl, altControl: AbstractControl): string { 50 | if (formControl.disabled) { 51 | return ''; 52 | } 53 | if (formControl.invalid) { 54 | return 'danger'; 55 | } else if (altControl.invalid) { 56 | return 'warning'; 57 | } else { 58 | return 'success'; 59 | } 60 | } 61 | 62 | toggleDisabled(control: AbstractControl) { 63 | if (!control.disabled) { 64 | control.disable(); 65 | } else { 66 | control.enable(); 67 | } 68 | } 69 | 70 | resetWithValue(value) { 71 | this.basicFormGroup.reset({ 72 | alt: 'reset' 73 | }) 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /examples/angular4/src/app/page/basic-usage/control-state/control-state.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | 5 |

6 |
7 | 8 | 14 | 15 |
16 |
17 |
value
18 |
20 | {{control.value}} 21 |
22 | 23 |
valid
24 |
26 | {{control.valid }} 27 |
28 | 29 |
invalid
30 |
32 | {{control.invalid }} 33 |
34 | 35 |
status
36 |
{{control.status}} 41 |
42 | 43 |
pending
44 |
46 | {{control.pending}} 47 |
48 | 49 |
pristine
50 |
52 | {{control.pristine}} 53 |
54 |
dirty
55 |
57 | {{control.dirty}} 58 |
59 | 60 |
touched
61 |
63 | {{control.touched}} 64 |
65 | 66 |
untouched
67 |
69 | {{control.untouched}} 70 |
71 | 72 |
focus
73 |
75 | {{control.focus}} 76 |
77 | 78 |
disabled
79 |
81 | {{control.disabled}} 82 |
83 | 84 |
errors
85 |
87 |
{{control.errors | json}}
88 |
89 | 90 |
91 | 92 |
93 |
94 | -------------------------------------------------------------------------------- /examples/angular4/src/app/page/basic-usage/control-state/control-state.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, OnChanges, OnInit, SimpleChanges} from '@angular/core' 2 | import {FormControl} from '@angular/forms' 3 | 4 | @Component({ 5 | selector: 'control-state', 6 | templateUrl: './control-state.component.html', 7 | styles: [` 8 | ul { 9 | list-style: none; 10 | margin-bottom:0; 11 | } 12 | `] 13 | }) 14 | export class ControlStateComponent implements OnChanges, OnInit { 15 | 16 | @Input() 17 | control: FormControl; 18 | 19 | @Input() 20 | errorColor = 'danger'; 21 | 22 | ngOnChanges(changes: SimpleChanges): void { 23 | 24 | console.log(changes); 25 | 26 | if ('errorColor' in changes) { 27 | this.errorColor = changes.errorColor.currentValue || 'danger' 28 | } 29 | } 30 | 31 | ngOnInit() { 32 | 33 | } 34 | 35 | getErrors(): any[] { 36 | return Object.keys(this.control.errors) 37 | .map((key) => { 38 | return { 39 | name: key, 40 | value: this.control.errors[key] 41 | } 42 | }) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /examples/angular4/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BioPhoton/angular-alternative-validation/a72f551819bc716c870ce086c7f6c404be244384/examples/angular4/src/assets/.gitkeep -------------------------------------------------------------------------------- /examples/angular4/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /examples/angular4/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /examples/angular4/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BioPhoton/angular-alternative-validation/a72f551819bc716c870ce086c7f6c404be244384/examples/angular4/src/favicon.ico -------------------------------------------------------------------------------- /examples/angular4/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TestAngularFormatterParser 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/angular4/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule); 12 | -------------------------------------------------------------------------------- /examples/angular4/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following to support `@angular/animation`. */ 41 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | import 'core-js/es6/reflect'; 46 | import 'core-js/es7/reflect'; 47 | 48 | 49 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/ 50 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 51 | 52 | 53 | 54 | /*************************************************************************************************** 55 | * Zone JS is required by Angular itself. 56 | */ 57 | import 'zone.js/dist/zone'; // Included with Angular CLI. 58 | 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | 65 | /** 66 | * Date, currency, decimal and percent pipes. 67 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 68 | */ 69 | // import 'intl'; // Run `npm install --save intl`. 70 | /** 71 | * Need to import at least one locale-data with intl. 72 | */ 73 | // import 'intl/locale-data/jsonp/en'; 74 | -------------------------------------------------------------------------------- /examples/angular4/src/styles.scss: -------------------------------------------------------------------------------- 1 | 2 | // FONT AWESOME ---------------------------------------------------------------- 3 | $fa-font-path : '../node_modules/font-awesome/fonts'; 4 | 5 | @import "~font-awesome/scss/variables"; 6 | @import "~font-awesome/scss/mixins"; 7 | @import "~font-awesome/scss/path"; 8 | 9 | @import '~font-awesome/scss/path'; 10 | @import '~font-awesome/scss/core'; 11 | @import '~font-awesome/scss/larger'; 12 | @import '~font-awesome/scss/fixed-width'; 13 | @import '~font-awesome/scss/list'; 14 | @import '~font-awesome/scss/bordered-pulled'; 15 | @import '~font-awesome/scss/animated'; 16 | @import '~font-awesome/scss/rotated-flipped'; 17 | @import '~font-awesome/scss/stacked'; 18 | @import '~font-awesome/scss/icons'; 19 | @import '~font-awesome/scss/screen-reader'; 20 | 21 | 22 | // ---------------------------------------------------------------- FONT AWESOME 23 | 24 | // BOOTSTRAP 4 ----------------------------------------------------------------- 25 | // Core variables and mixins 26 | @import "~bootstrap/scss/variables"; 27 | @import "~bootstrap/scss/mixins"; 28 | @import "~bootstrap/scss/custom"; 29 | 30 | 31 | // Reset and dependencies 32 | @import '~bootstrap/scss/normalize'; 33 | @import '~bootstrap/scss/print'; 34 | 35 | // Core CSS 36 | @import '~bootstrap/scss/reboot'; 37 | @import '~bootstrap/scss/type'; 38 | @import '~bootstrap/scss/images'; 39 | @import '~bootstrap/scss/code'; 40 | @import '~bootstrap/scss/grid'; 41 | @import '~bootstrap/scss/tables'; 42 | @import '~bootstrap/scss/forms'; 43 | @import '~bootstrap/scss/buttons'; 44 | 45 | // Components 46 | @import '~bootstrap/scss/transitions'; 47 | @import '~bootstrap/scss/dropdown'; 48 | @import '~bootstrap/scss/button-group'; 49 | @import '~bootstrap/scss/input-group'; 50 | @import '~bootstrap/scss/custom-forms'; 51 | @import '~bootstrap/scss/nav'; 52 | @import '~bootstrap/scss/navbar'; 53 | @import '~bootstrap/scss/card'; 54 | @import '~bootstrap/scss/breadcrumb'; 55 | @import '~bootstrap/scss/pagination'; 56 | @import '~bootstrap/scss/badge'; 57 | @import '~bootstrap/scss/jumbotron'; 58 | @import '~bootstrap/scss/alert'; 59 | @import '~bootstrap/scss/progress'; 60 | @import '~bootstrap/scss/media'; 61 | @import '~bootstrap/scss/list-group'; 62 | @import '~bootstrap/scss/responsive-embed'; 63 | @import '~bootstrap/scss/close'; 64 | 65 | // Components w/ JavaScript 66 | @import '~bootstrap/scss/modal'; 67 | @import '~bootstrap/scss/tooltip'; 68 | @import '~bootstrap/scss/popover'; 69 | @import '~bootstrap/scss/carousel'; 70 | 71 | // Utility classes 72 | @import '~bootstrap/scss/utilities'; 73 | 74 | // ----------------------------------------------------------------- BOOTSTRAP 4 75 | -------------------------------------------------------------------------------- /examples/angular4/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare const __karma__: any; 17 | declare const require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | const context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /examples/angular4/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "es2015", 6 | "baseUrl": "", 7 | "types": [] 8 | }, 9 | "exclude": [ 10 | "test.ts", 11 | "**/*.spec.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /examples/angular4/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "baseUrl": "", 8 | "types": [ 9 | "jasmine", 10 | "node" 11 | ] 12 | }, 13 | "files": [ 14 | "test.ts" 15 | ], 16 | "include": [ 17 | "**/*.spec.ts", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /examples/angular4/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /examples/angular4/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "baseUrl": "src", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2016", 17 | "dom" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/angular4/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "eofline": true, 15 | "forin": true, 16 | "import-blacklist": [ 17 | true, 18 | "rxjs" 19 | ], 20 | "import-spacing": true, 21 | "indent": [ 22 | true, 23 | "spaces" 24 | ], 25 | "interface-over-type-literal": true, 26 | "label-position": true, 27 | "max-line-length": [ 28 | true, 29 | 140 30 | ], 31 | "member-access": false, 32 | "member-ordering": [ 33 | true, 34 | "static-before-instance", 35 | "variables-before-functions" 36 | ], 37 | "no-arg": true, 38 | "no-bitwise": true, 39 | "no-console": [ 40 | true, 41 | "debug", 42 | "info", 43 | "time", 44 | "timeEnd", 45 | "trace" 46 | ], 47 | "no-construct": true, 48 | "no-debugger": true, 49 | "no-duplicate-super": true, 50 | "no-empty": false, 51 | "no-empty-interface": true, 52 | "no-eval": true, 53 | "no-inferrable-types": [ 54 | true, 55 | "ignore-params" 56 | ], 57 | "no-misused-new": true, 58 | "no-non-null-assertion": false, 59 | "no-shadowed-variable": true, 60 | "no-string-literal": false, 61 | "no-string-throw": true, 62 | "no-switch-case-fall-through": true, 63 | "no-trailing-whitespace": true, 64 | "no-unnecessary-initializer": true, 65 | "no-unused-expression": true, 66 | "no-use-before-declare": true, 67 | "no-var-keyword": true, 68 | "object-literal-sort-keys": false, 69 | "one-line": [ 70 | true, 71 | "check-open-brace", 72 | "check-catch", 73 | "check-else", 74 | "check-whitespace" 75 | ], 76 | "prefer-const": true, 77 | "quotemark": [ 78 | true, 79 | "single" 80 | ], 81 | "radix": true, 82 | "semicolon": [ 83 | "always" 84 | ], 85 | "triple-equals": [ 86 | true, 87 | "allow-null-check" 88 | ], 89 | "typedef-whitespace": [ 90 | true, 91 | { 92 | "call-signature": "nospace", 93 | "index-signature": "nospace", 94 | "parameter": "nospace", 95 | "property-declaration": "nospace", 96 | "variable-declaration": "nospace" 97 | } 98 | ], 99 | "typeof-compare": true, 100 | "unified-signatures": true, 101 | "variable-name": false, 102 | "whitespace": [ 103 | true, 104 | "check-branch", 105 | "check-decl", 106 | "check-operator", 107 | "check-separator", 108 | "check-type" 109 | ], 110 | "directive-selector": [ 111 | false, 112 | "attribute" 113 | ], 114 | "component-selector": [ 115 | true, 116 | "element", 117 | "kebab-case" 118 | ], 119 | "use-input-property-decorator": true, 120 | "use-output-property-decorator": true, 121 | "use-host-property-decorator": true, 122 | "no-input-rename": false, 123 | "no-output-rename": false, 124 | "use-life-cycle-interface": true, 125 | "use-pipe-transform-interface": true, 126 | "component-class-suffix": true, 127 | "directive-class-suffix": true, 128 | "no-access-missing-member": true, 129 | "templates-use-public": true, 130 | "invoke-injectable": true 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var gulp = require('gulp'), 3 | path = require('path'), 4 | ngc = require('@angular/compiler-cli/src/main').main, 5 | rollup = require('gulp-rollup'), 6 | rename = require('gulp-rename'), 7 | del = require('del'), 8 | runSequence = require('run-sequence'), 9 | inlineResources = require('./tools/gulp/inline-resources'); 10 | 11 | const rootFolder = path.join(__dirname); 12 | const srcFolder = path.join(rootFolder, 'src'); 13 | const tmpFolder = path.join(rootFolder, '.tmp'); 14 | const buildFolder = path.join(rootFolder, 'build'); 15 | const distFolder = path.join(rootFolder, 'dist'); 16 | 17 | /** 18 | * 1. Delete /dist folder 19 | */ 20 | gulp.task('clean:dist', function () { 21 | 22 | // Delete contents but not dist folder to avoid broken npm links 23 | // when dist directory is removed while npm link references it. 24 | return deleteFolders([distFolder + '/**', '!' + distFolder]); 25 | }); 26 | 27 | /** 28 | * 2. Clone the /src folder into /.tmp. If an npm link inside /src has been made, 29 | * then it's likely that a node_modules folder exists. Ignore this folder 30 | * when copying to /.tmp. 31 | */ 32 | gulp.task('copy:source', function () { 33 | return gulp.src([`${srcFolder}/**/*`, `!${srcFolder}/node_modules`]) 34 | .pipe(gulp.dest(tmpFolder)); 35 | }); 36 | 37 | /** 38 | * 3. Inline template (.html) and style (.css) files into the the component .ts files. 39 | * We do this on the /.tmp folder to avoid editing the original /src files 40 | */ 41 | gulp.task('inline-resources', function () { 42 | return Promise.resolve() 43 | .then(() => inlineResources(tmpFolder)); 44 | }); 45 | 46 | 47 | /** 48 | * 4. Run the Angular compiler, ngc, on the /.tmp folder. This will output all 49 | * compiled modules to the /build folder. 50 | */ 51 | gulp.task('ngc', function () { 52 | return ngc({ 53 | project: `${tmpFolder}/tsconfig.es5.json` 54 | }) 55 | .then((exitCode) => { 56 | if (exitCode === 1) { 57 | // This error is caught in the 'compile' task by the runSequence method callback 58 | // so that when ngc fails to compile, the whole compile process stops running 59 | throw new Error('ngc compilation failed'); 60 | } 61 | }); 62 | }); 63 | 64 | /** 65 | * 5. Run rollup inside the /build folder to generate our Flat ES module and place the 66 | * generated file into the /dist folder 67 | */ 68 | gulp.task('rollup:fesm', function () { 69 | return gulp.src(`${buildFolder}/**/*.js`) 70 | // transform the files here. 71 | .pipe(rollup({ 72 | 73 | // Bundle's entry point 74 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#entry 75 | entry: `${buildFolder}/index.js`, 76 | 77 | // Allow mixing of hypothetical and actual files. "Actual" files can be files 78 | // accessed by Rollup or produced by plugins further down the chain. 79 | // This prevents errors like: 'path/file' does not exist in the hypothetical file system 80 | // when subdirectories are used in the `src` directory. 81 | allowRealFiles: true, 82 | 83 | // A list of IDs of modules that should remain external to the bundle 84 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#external 85 | external: [ 86 | '@angular/core', 87 | '@angular/common' 88 | ], 89 | 90 | // Format of generated bundle 91 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#format 92 | format: 'es' 93 | })) 94 | .pipe(gulp.dest(distFolder)); 95 | }); 96 | 97 | /** 98 | * 6. Run rollup inside the /build folder to generate our UMD module and place the 99 | * generated file into the /dist folder 100 | */ 101 | gulp.task('rollup:umd', function () { 102 | return gulp.src(`${buildFolder}/**/*.js`) 103 | // transform the files here. 104 | .pipe(rollup({ 105 | 106 | // Bundle's entry point 107 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#entry 108 | entry: `${buildFolder}/index.js`, 109 | 110 | // Allow mixing of hypothetical and actual files. "Actual" files can be files 111 | // accessed by Rollup or produced by plugins further down the chain. 112 | // This prevents errors like: 'path/file' does not exist in the hypothetical file system 113 | // when subdirectories are used in the `src` directory. 114 | allowRealFiles: true, 115 | 116 | // A list of IDs of modules that should remain external to the bundle 117 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#external 118 | external: [ 119 | '@angular/core', 120 | '@angular/common' 121 | ], 122 | 123 | // Format of generated bundle 124 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#format 125 | format: 'umd', 126 | 127 | // Export mode to use 128 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#exports 129 | exports: 'named', 130 | 131 | // The name to use for the module for UMD/IIFE bundles 132 | // (required for bundles with exports) 133 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#modulename 134 | moduleName: 'angular-alternative-validation', 135 | 136 | // See https://github.com/rollup/rollup/wiki/JavaScript-API#globals 137 | globals: { 138 | typescript: 'ts' 139 | } 140 | 141 | })) 142 | .pipe(rename('angular-alternative-validation.umd.js')) 143 | .pipe(gulp.dest(distFolder)); 144 | }); 145 | 146 | /** 147 | * 7. Copy all the files from /build to /dist, except .js files. We ignore all .js from /build 148 | * because with don't need individual modules anymore, just the Flat ES module generated 149 | * on step 5. 150 | */ 151 | gulp.task('copy:build', function () { 152 | return gulp.src([`${buildFolder}/**/*`, `!${buildFolder}/**/*.js`]) 153 | .pipe(gulp.dest(distFolder)); 154 | }); 155 | 156 | /** 157 | * 8. Copy package.json from /src to /dist 158 | */ 159 | gulp.task('copy:manifest', function () { 160 | return gulp.src([`${srcFolder}/package.json`]) 161 | .pipe(gulp.dest(distFolder)); 162 | }); 163 | 164 | /** 165 | * 9. Copy README.md from / to /dist 166 | */ 167 | gulp.task('copy:readme', function () { 168 | return gulp.src([path.join(rootFolder, 'README.MD')]) 169 | .pipe(gulp.dest(distFolder)); 170 | }); 171 | 172 | /** 173 | * 10. Delete /.tmp folder 174 | */ 175 | gulp.task('clean:tmp', function () { 176 | return deleteFolders([tmpFolder]); 177 | }); 178 | 179 | /** 180 | * 11. Delete /build folder 181 | */ 182 | gulp.task('clean:build', function () { 183 | return deleteFolders([buildFolder]); 184 | }); 185 | 186 | gulp.task('compile', function () { 187 | runSequence( 188 | 'clean:dist', 189 | 'copy:source', 190 | 'inline-resources', 191 | 'ngc', 192 | 'rollup:fesm', 193 | 'rollup:umd', 194 | 'copy:build', 195 | 'copy:manifest', 196 | 'copy:readme', 197 | 'clean:build', 198 | 'clean:tmp', 199 | function (err) { 200 | if (err) { 201 | console.log('ERROR:', err.message); 202 | deleteFolders([distFolder, tmpFolder, buildFolder]); 203 | } else { 204 | console.log('Compilation finished succesfully'); 205 | } 206 | }); 207 | }); 208 | 209 | /** 210 | * Watch for any change in the /src folder and compile files 211 | */ 212 | gulp.task('watch', function () { 213 | gulp.watch(`${srcFolder}/**/*`, ['compile']); 214 | }); 215 | 216 | gulp.task('clean', ['clean:dist', 'clean:tmp', 'clean:build']); 217 | 218 | gulp.task('build', ['clean', 'compile']); 219 | gulp.task('build:watch', ['build', 'watch']); 220 | gulp.task('default', ['build:watch']); 221 | 222 | /** 223 | * Deletes the specified folder 224 | */ 225 | function deleteFolders(folders) { 226 | return del(folders); 227 | } 228 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | var cfg = { 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular/cli'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-phantomjs-launcher'), 12 | require('karma-jasmine-html-reporter'), 13 | require('karma-coverage-istanbul-reporter'), 14 | require('@angular/cli/plugins/karma') 15 | ], 16 | client:{ 17 | clearContext: false // leave Jasmine Spec Runner output visible in browser 18 | }, 19 | coverageIstanbulReporter: { 20 | reports: [ 'html', 'lcovonly' ], 21 | fixWebpackSourcePaths: true 22 | }, 23 | angularCli: { 24 | environment: 'dev' 25 | }, 26 | reporters: ['progress', 'kjhtml'], 27 | port: 9876, 28 | colors: true, 29 | logLevel: config.LOG_INFO, 30 | autoWatch: true, 31 | browsers: ['Chrome'], 32 | customLaunchers: { 33 | TRAVIS_CI: { 34 | base: 'PhantomJS' 35 | } 36 | }, 37 | singleRun: true 38 | }; 39 | 40 | 41 | if (process.env.TRAVIS) { 42 | cfg.browsers = ['TRAVIS_CI']; 43 | cfg.singleRun = true; 44 | } 45 | 46 | config.set(cfg); 47 | }; 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-alternative-validation", 3 | "version": "0.3.6", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "build": "gulp build", 9 | "test": "ng test", 10 | "coverage": "ng test --progress=false --watch=false --code-coverage", 11 | "lint": "ng lint", 12 | "e2e": "ng e2e", 13 | "build:watch": "gulp", 14 | "docs": "npm run docs:build", 15 | "docs:build": "compodoc -p tsconfig.json -n angular-alternative-validation -d docs --hideGenerator", 16 | "docs:serve": "npm run docs:build -- -s", 17 | "docs:watch": "npm run docs:build -- -s -w", 18 | "update-example": "npm run build && copyfiles -u 1 dist/**/* examples/angular4/node_modules/angular-alternative-validation", 19 | "changelog": "conventional-changelog --pkg ./src/package.json -p angular -i CHANGELOG.md -s -r 0", 20 | "recommended-bump": "conventional-recommended-bump -p angular", 21 | "github-release": "conventional-github-releaser -p angular -r 0", 22 | "release": "npm publish dist" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/BioPhoton/angular-alternative-validation" 27 | }, 28 | "author": { 29 | "name": "Michael Hladky", 30 | "email": "michael@hladky.at" 31 | }, 32 | "keywords": [ 33 | "angular" 34 | ], 35 | "bugs": { 36 | "url": "https://github.com/BioPhoton/angular-alternative-validation/issues" 37 | }, 38 | "private": true, 39 | "dependencies": { 40 | "@angular/animations": "^4.0.0", 41 | "@angular/common": "^4.0.0", 42 | "@angular/compiler": "^4.0.0", 43 | "@angular/core": "^4.0.0", 44 | "@angular/forms": "^4.0.0", 45 | "@angular/http": "^4.0.0", 46 | "@angular/platform-browser": "^4.0.0", 47 | "@angular/platform-browser-dynamic": "^4.0.0", 48 | "@angular/router": "^4.0.0", 49 | "core-js": "^2.4.1", 50 | "rxjs": "^5.4.1", 51 | "zone.js": "^0.8.14" 52 | }, 53 | "devDependencies": { 54 | "@angular/cli": "1.2.2", 55 | "@angular/compiler-cli": "^4.0.0", 56 | "@angular/language-service": "^4.0.0", 57 | "@types/jasmine": "~2.5.53", 58 | "@types/jasminewd2": "~2.0.2", 59 | "@types/node": "~6.0.60", 60 | "codecov.io": "^0.1.6", 61 | "codelyzer": "~3.0.1", 62 | "coveralls": "^2.13.1", 63 | "intl": "^1.2.5", 64 | "gulp": "^3.9.1", 65 | "gulp-rename": "^1.2.2", 66 | "gulp-rollup": "^2.13.0", 67 | "node-sass-tilde-importer": "^1.0.0", 68 | "run-sequence": "^1.2.2", 69 | "jasmine-core": "~2.6.2", 70 | "jasmine-spec-reporter": "~4.1.0", 71 | "karma": "~1.7.0", 72 | "karma-chrome-launcher": "~2.1.1", 73 | "karma-cli": "~1.0.1", 74 | "karma-coverage-istanbul-reporter": "^1.2.1", 75 | "karma-jasmine": "~1.1.0", 76 | "karma-jasmine-html-reporter": "^0.2.2", 77 | "karma-phantomjs-launcher": "^1.0.4", 78 | "protractor": "~5.1.2", 79 | "ts-node": "~3.0.4", 80 | "tslint": "~5.3.2", 81 | "typescript": "~2.3.3" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /resources/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BioPhoton/angular-alternative-validation/a72f551819bc716c870ce086c7f6c404be244384/resources/demo.gif -------------------------------------------------------------------------------- /scripts/check-remote.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | 3 | # rebuilds and test the latest verstion of the repository (a succeeded travis build is precondition) 4 | 5 | # checks the status of the last build of the current repository 6 | # --no-interactive disables the interactive mode 7 | # source: https://github.com/travis-ci/travis.rb/blob/master/README.md 8 | $state = travis status --no-interactive 9 | echo $state 10 | if ( $state -ne "passed") 11 | { 12 | Write-Host "Invalid travis state $state. State should be passed" -foregroundcolor "red" 13 | Exit 14 | } 15 | Write-Host "checked travis state" -foregroundcolor "green" 16 | # deletes the node_modules folder (move them into trash, more reversable) 17 | # trash node_modules 18 | Write-Host "trashed node_modules" -foregroundcolor "green" 19 | # pulls the latest version 20 | git pull --rebase 21 | Write-Host "git clean and up to date" -foregroundcolor "green" 22 | -------------------------------------------------------------------------------- /scripts/create-changelog.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | 3 | # create changelog 4 | 5 | # copy the src/package.json 6 | # we copy it to have the initial state saved. 7 | # we bump the version update the changelog 8 | # after doing this we use the real package.json and do another version bump 9 | # there to have change log and version bump in separate commits 10 | Copy-Item .\src\package.json "src\_package.json" 11 | # Detect what commit message convention your repository is using 12 | # source: https://github.com/conventional-changelog/conventional-commits-detector/blob/master/README.md 13 | # $preset stores the output of conventional-commits-detector which is angular 14 | $preset = (conventional-commits-detector) 15 | # echo prints a value to screen 16 | # ensures that a convention was detected 17 | echo $preset 18 | # Detect the recommended bump type by the conventional-commit standard 19 | # source: https://github.com/conventional-changelog-archived-repos/conventional-recommended-bump/blob/master/README.md 20 | # $bump stores the recommended bump type 21 | $bump = (conventional-recommended-bump -p angular) 22 | # echo prints a value to screen 23 | # ensures that a bump type was detected 24 | echo $bump 25 | # npm version $bump bumps the version specified in $bump and write the new data back to package.json 26 | # If you run npm version in a git repo, it will also create a version commit and tag. 27 | # This behavior is disabled by --no-git-tag-version 28 | # the var $bump specifies the segment of the version code to bump 29 | cd .\src 30 | npm --no-git-tag-version version $bump 31 | cd .. 32 | # conventional-changelog creates a chagnelog markdown from commits 33 | # -i Read the CHANGELOG from this file 34 | # CHANGELOG.md it the name of the file to read from 35 | # -s Outputting to the infile so you don't need to specify the same file as outfile 36 | # -p Name of the preset you want to use. In this case it is angular that is stored in $preset 37 | conventional-changelog -i CHANGELOG.md -s -p $preset 38 | # add CHANGELOG.md to the commit 39 | git add CHANGELOG.md 40 | # get the content of package.json and json-parse the value 41 | $package = (Get-Content ".\src\package.json" -Raw) | ConvertFrom-Json 42 | $version = $package.version 43 | # commit with comment 44 | git commit -m"docs(CHANGELOG): $version" 45 | # run build again because we want to have the new version in the dist folder 46 | npm run build 47 | # Replace the already bumped package.json with the _package.json initial copy 48 | trash .\src\package.json 49 | Rename-Item -Path ".\src\_package.json" -NewName "package.json" 50 | Write-Host "created changelog $preset" -foregroundcolor "green" 51 | -------------------------------------------------------------------------------- /scripts/detect-project-settings.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | $preset = (conventional-commits-detector) 3 | # echo prints a value to screen 4 | # ensures that a convention was detected 5 | echo $preset 6 | # Detect the recommended bump type by the conventional-commit standard 7 | # source: https://github.com/conventional-changelog-archived-repos/conventional-recommended-bump/blob/master/README.md 8 | # $bump stores the recommended bump type 9 | $bump = (conventional-recommended-bump -p angular) 10 | # echo prints a value to screen 11 | # ensures that a bump type was detected 12 | echo $bump 13 | # npm version $bump bumps the version specified in $bump and write the new data back to package.json 14 | # If you run npm version in a git repo, it will also create a version commit and tag. 15 | # This behavior is disabled by --no-git-tag-version 16 | # the var $bump specifies the segment of the version code to bump 17 | Write-Host "created changelog $preset" -foregroundcolor "green" 18 | -------------------------------------------------------------------------------- /scripts/github-release.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | 3 | # release on git and npm 4 | 5 | # Make a new GitHub release from git metadata based on your commit-convention. In this case angular convention 6 | # source: https://github.com/conventional-changelog/conventional-github-releaser/blob/master/README.md 7 | conventional-github-releaser -p $preset 8 | Write-Host "created github release" -foregroundcolor "green" 9 | 10 | # publish new version on npm 11 | cd .\dist 12 | npm publish 13 | Write-Host "published on npm :-)" -foregroundcolor "green" 14 | -------------------------------------------------------------------------------- /scripts/install.ps1: -------------------------------------------------------------------------------- 1 | # prerequisites: 2 | npm i -g trash-cli conventional-recommended-bump conventional-changelog conventional-github-releaser conventional-commits-detector json 3 | -------------------------------------------------------------------------------- /scripts/release.ps1: -------------------------------------------------------------------------------- 1 | # npm publish with goodies 2 | # prerequisites: 3 | # `npm install -g trash conventional-recommended-bump conventional-changelog conventional-github-releaser conventional-commits-detector json` 4 | # `np` with optional argument `patch`/`minor`/`major`/`` 5 | # defaults to conventional-recommended-bump 6 | # and optional argument preset `angular`/ `jquery` ... 7 | # defaults to conventional-commits-detector 8 | 9 | $ErrorActionPreference = "Stop" 10 | 11 | # rebuilds and test the latest version of the repository (a succeeded travis build is precondition) 12 | 13 | # checks the status of the last build of the current repository 14 | # --no-interactive disables the interactive mode 15 | # source: https://github.com/travis-ci/travis.rb/blob/master/README.md 16 | $state = travis status --no-interactive 17 | echo $state 18 | if ( $state -ne "passed") 19 | { 20 | Write-Host "Invalid travis state $state. State should be passed" -foregroundcolor "red" 21 | Exit 22 | } 23 | Write-Host "checked travis state" -foregroundcolor "green" 24 | # deletes the node_modules folder (move them into trash, more reversable) 25 | # trash node_modules 26 | Write-Host "trashed node_modules" -foregroundcolor "green" 27 | # pulls the latest version 28 | git pull --rebase 29 | # installs the node dependencies 30 | # npm install 31 | # run unit tests 32 | karma start karma.conf.js 33 | Write-Host "run tests" -foregroundcolor "green" 34 | 35 | # create changelog 36 | 37 | # copy the src/package.json 38 | # we copy it to have the initial state saved. 39 | # we bump the version update the changelog 40 | # after doing this we use the real package.json and do another version bump 41 | # there to have change log and version bump in separate commits 42 | Copy-Item .\src\package.json "src\_package.json" 43 | # Detect what commit message convention your repository is using 44 | # source: https://github.com/conventional-changelog/conventional-commits-detector/blob/master/README.md 45 | # $preset stores the output of conventional-commits-detector which is angular 46 | $preset = (conventional-commits-detector) 47 | # echo prints a value to screen 48 | # ensures that a convention was detected 49 | echo $preset 50 | # Detect the recommended bump type by the conventional-commit standard 51 | # source: https://github.com/conventional-changelog-archived-repos/conventional-recommended-bump/blob/master/README.md 52 | # $bump stores the recommended bump type 53 | $bump = (conventional-recommended-bump -p angular) 54 | # echo prints a value to screen 55 | # ensures that a bump type was detected 56 | echo $bump 57 | # npm version $bump bumps the version specified in $bump and write the new data back to package.json 58 | # If you run npm version in a git repo, it will also create a version commit and tag. 59 | # This behavior is disabled by --no-git-tag-version 60 | # the var $bump specifies the segment of the version code to bump 61 | cd .\src 62 | npm --no-git-tag-version version $bump 63 | cd .. 64 | # conventional-changelog creates a chagnelog markdown from commits 65 | # -i Read the CHANGELOG from this file 66 | # CHANGELOG.md it the name of the file to read from 67 | # -s Outputting to the infile so you don't need to specify the same file as outfile 68 | # -p Name of the preset you want to use. In this case it is angular that is stored in $preset 69 | conventional-changelog -i CHANGELOG.md -s -p $preset 70 | # add CHANGELOG.md to the commit 71 | git add CHANGELOG.md 72 | # get the content of package.json and json-parse the value 73 | $package = (Get-Content ".\src\package.json" -Raw) | ConvertFrom-Json 74 | $version = $package.version 75 | # commit with comment 76 | git commit -m"docs(CHANGELOG): $version" 77 | # run build again because we want to have the new version in the dist folder 78 | npm run build 79 | # Replace the already bumped package.json with the _package.json initial copy 80 | trash .\src\package.json 81 | Rename-Item -Path ".\src\_package.json" -NewName "package.json" 82 | Write-Host "created changelog $preset" -foregroundcolor "green" 83 | 84 | 85 | # create version bump 86 | 87 | # npm version $bump bumps the version specified in $bump and write the new data back to package.json 88 | # -m will set a commit message with the version placed by %s 89 | cd .\src 90 | npm --no-git-tag-version version $bump 91 | git add .\package.json 92 | git commit -m "chore(release): $version ($bump)" 93 | git tag $version 94 | cd .. 95 | # pushed the commit 96 | # --follow-tags also pushed the new tags 97 | # source: https://git-scm.com/docs/git-push 98 | git push --follow-tags 99 | Write-Host "pushed repo" -foregroundcolor "green" 100 | 101 | # release on git and npm 102 | 103 | # Make a new GitHub release from git metadata based on your commit-convention. In this case angular convention 104 | # source: https://github.com/conventional-changelog/conventional-github-releaser/blob/master/README.md 105 | conventional-github-releaser -p $preset 106 | Write-Host "created github release" -foregroundcolor "green" 107 | 108 | # publish new version on npm 109 | cd .\dist 110 | npm publish 111 | cd .. 112 | # Write-Host "published on npm :-)" -foregroundcolor "green" 113 | -------------------------------------------------------------------------------- /scripts/setup-and-test.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | 3 | # installs the node dependencies 4 | # npm install 5 | Write-Host "project setup done" -foregroundcolor "green" 6 | 7 | # run unit tests 8 | karma start karma.conf.js 9 | Write-Host "run tests" -foregroundcolor "green" 10 | -------------------------------------------------------------------------------- /scripts/version-bump.ps1: -------------------------------------------------------------------------------- 1 | # create version bump 2 | 3 | # npm version $bump bumps the version specified in $bump and write the new data back to package.json 4 | # -m will set a commit message with the version placed by %s 5 | cd .\src 6 | npm --no-git-tag-version version $bump 7 | git add .\package.json 8 | git commit -m "chore(release): $version ($bump)" 9 | git tag $version 10 | cd .. 11 | # pushed the commit 12 | # --follow-tags also pushed the new tags 13 | # source: https://git-scm.com/docs/git-push 14 | git push --follow-tags 15 | Write-Host "pushed repo" -foregroundcolor "green" 16 | -------------------------------------------------------------------------------- /src/alternative-validation.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import {Component, DebugElement, ViewChild} from '@angular/core'; 2 | import {ComponentFixture, TestBed} from '@angular/core/testing'; 3 | import {AbstractControl, FormControl, FormGroup} from '@angular/forms'; 4 | import {By} from '@angular/platform-browser'; 5 | import {AlternativeValidationDirective} from './alternative-validation.directive'; 6 | import {AlternativeValidationModule} from './index'; 7 | import {IAlternativeValidationConfig} from './struct/alternative-validation-config'; 8 | import {ValidationCollectorService} from './validation-collector.service'; 9 | 10 | @Component({ 11 | template: ` 12 |
13 | 15 |
16 | ` 17 | }) 18 | class TestComponent { 19 | fg: FormGroup = new FormGroup({ 20 | 'target1': new FormControl(''), 21 | }); 22 | config: IAlternativeValidationConfig = { 23 | validator: [ 24 | {name: 'required'}, 25 | {name: 'minLength', params: [3]} 26 | ] 27 | }; 28 | 29 | @ViewChild(AlternativeValidationDirective) 30 | exposedTarget1; 31 | } 32 | 33 | describe('AlternativeValidationDirective', () => { 34 | 35 | let hostComponents: TestComponent; 36 | let alternativeControl: any; 37 | let fixture: ComponentFixture; 38 | let el: DebugElement; 39 | 40 | let target1Input: DebugElement; 41 | let target1InputControl: AbstractControl; 42 | 43 | function setInputValue(inputElem: DebugElement, value) { 44 | inputElem.nativeElement.value = value; 45 | inputElem.triggerEventHandler('input', {target: {value: value}}); 46 | fixture.detectChanges(); 47 | } 48 | 49 | beforeEach(() => { 50 | TestBed.configureTestingModule({ 51 | imports: [ 52 | AlternativeValidationModule.forRoot() 53 | ], 54 | declarations: [ 55 | TestComponent 56 | ], 57 | providers: [ 58 | ValidationCollectorService 59 | ] 60 | }); 61 | fixture = TestBed.createComponent(TestComponent); 62 | hostComponents = fixture.componentInstance; 63 | el = fixture.debugElement; 64 | 65 | target1Input = el.query(By.css('#target1')); 66 | target1InputControl = hostComponents.fg.get('target1'); 67 | alternativeControl = hostComponents.exposedTarget1.control; 68 | }); 69 | 70 | it('should create an instance', () => { 71 | fixture.detectChanges(); 72 | alternativeControl = hostComponents.exposedTarget1.control; 73 | expect(hostComponents).toBeTruthy(); 74 | expect(target1Input).toBeTruthy(); 75 | expect(target1InputControl).toBeTruthy(); 76 | }); 77 | 78 | it('should be accessable over @ViewChild', () => { 79 | fixture.detectChanges(); 80 | alternativeControl = hostComponents.exposedTarget1.control; 81 | expect(alternativeControl).toBeTruthy(); 82 | expect(alternativeControl.valid).toBe(false); 83 | }); 84 | 85 | 86 | it('should stay valid if input changes', () => { 87 | fixture.detectChanges(); 88 | alternativeControl = hostComponents.exposedTarget1.control; 89 | expect(target1InputControl.value).toBe(''); 90 | expect(target1InputControl.valid).toBe(true); 91 | 92 | setInputValue(target1Input, '12'); 93 | expect(target1InputControl.value).toBe('12'); 94 | expect(target1InputControl.valid).toBe(true); 95 | 96 | setInputValue(target1Input, '123'); 97 | expect(target1InputControl.value).toBe('123'); 98 | expect(target1InputControl.valid).toBe(true); 99 | }); 100 | 101 | it('should change state of alternative validation when input changes', () => { 102 | fixture.detectChanges(); 103 | alternativeControl = hostComponents.exposedTarget1.control; 104 | 105 | expect(target1InputControl.value).toBe(''); 106 | expect(alternativeControl.value).toBe(''); 107 | expect(target1InputControl.valid).toBe(true); 108 | expect(alternativeControl.valid).toBe(false); 109 | expect(alternativeControl.hasError('required')).toBe(true); 110 | expect(alternativeControl.hasError('minlength')).toBe(false); 111 | 112 | setInputValue(target1Input, '12'); 113 | expect(target1InputControl.value).toBe('12'); 114 | expect(alternativeControl.value).toBe('12'); 115 | 116 | expect(target1InputControl.valid).toBe(true); 117 | expect(alternativeControl.valid).toBe(false); 118 | expect(alternativeControl.hasError('required')).toBe(false); 119 | expect(alternativeControl.hasError('minlength')).toBe(true); 120 | 121 | setInputValue(target1Input, '123'); 122 | expect(alternativeControl.value).toBe('123'); 123 | expect(target1InputControl.valid).toBe(true); 124 | expect(alternativeControl.valid).toBe(true); 125 | expect(alternativeControl.hasError('required')).toBe(false); 126 | expect(alternativeControl.hasError('minlength')).toBe(false); 127 | }); 128 | 129 | it('should track focus', () => { 130 | fixture.detectChanges(); 131 | alternativeControl = hostComponents.exposedTarget1.control; 132 | target1Input.triggerEventHandler('focus', {}); 133 | expect(hostComponents.exposedTarget1.focus).toBe(true); 134 | 135 | target1Input.triggerEventHandler('blur', {}); 136 | expect(hostComponents.exposedTarget1.focus).toBe(false); 137 | 138 | }); 139 | 140 | it('should be able to set disabled state', () => { 141 | fixture.detectChanges(); 142 | 143 | hostComponents.exposedTarget1.setDisabledState(true); 144 | expect(target1Input.properties.disabled).toBe(true); 145 | 146 | hostComponents.exposedTarget1.setDisabledState(false); 147 | expect(target1Input.properties.disabled).toBe(false); 148 | }); 149 | 150 | it('should be able to reset', () => { 151 | fixture.detectChanges(); 152 | target1InputControl.setValue('test'); 153 | expect(target1InputControl.value).toBe('test'); 154 | expect(hostComponents.exposedTarget1.value).toBe('test'); 155 | 156 | target1InputControl.reset(); 157 | expect(target1InputControl.value).toBeFalsy(); 158 | expect(hostComponents.exposedTarget1.value).toBeFalsy(); 159 | }); 160 | 161 | it('should be able to get the status', () => { 162 | fixture.detectChanges(); 163 | 164 | expect(hostComponents.exposedTarget1.status).toBe('INVALID'); 165 | 166 | setInputValue(target1Input, '123'); 167 | expect(hostComponents.exposedTarget1.status).toBe('VALID'); 168 | }); 169 | 170 | it('should listen on reset events', () => { 171 | fixture.detectChanges(); 172 | 173 | setInputValue(target1Input, '123'); 174 | expect(hostComponents.exposedTarget1.status).toBe('VALID'); 175 | 176 | hostComponents.exposedTarget1.reset(); 177 | expect(hostComponents.exposedTarget1.status).toBe('INVALID'); 178 | }); 179 | 180 | it('should react to composition events', () => { 181 | target1Input.triggerEventHandler('compositionstart', {}); 182 | target1Input.triggerEventHandler('compositionend', {target: {value: 'composition events'}}); 183 | }); 184 | 185 | }); 186 | -------------------------------------------------------------------------------- /src/alternative-validation.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, 3 | ElementRef, 4 | forwardRef, 5 | Host, 6 | Inject, 7 | Input, 8 | OnChanges, 9 | OnDestroy, 10 | OnInit, 11 | Optional, 12 | Renderer2, 13 | SimpleChanges, 14 | SkipSelf 15 | } from '@angular/core'; 16 | import { 17 | AbstractControl, 18 | AbstractControlDirective, 19 | COMPOSITION_BUFFER_MODE, 20 | ControlContainer, 21 | ControlValueAccessor, 22 | FormControl, 23 | NG_VALUE_ACCESSOR 24 | } from '@angular/forms'; 25 | import {ɵgetDOM as getDOM} from '@angular/platform-browser'; 26 | import 'rxjs/add/observable/combineLatest'; 27 | import 'rxjs/add/operator/filter'; 28 | import 'rxjs/add/operator/map'; 29 | import 'rxjs/add/operator/switchMap'; 30 | import 'rxjs/add/operator/takeUntil'; 31 | import {Observable} from 'rxjs/Observable'; 32 | import {Subject} from 'rxjs/Subject'; 33 | import {IAlternativeValidationConfig} from './struct/alternative-validation-config'; 34 | import {ValidationCollectorService} from './validation-collector.service'; 35 | 36 | /** 37 | * We must check whether the agent is Android because composition events 38 | * behave differently between iOS and Android. 39 | */ 40 | function isAndroid(): boolean { 41 | const userAgent = getDOM() ? getDOM().getUserAgent() : ''; 42 | return /android (\d+)/.test(userAgent.toLowerCase()); 43 | } 44 | 45 | const CONTROL_VALUE_ACCESSOR = { 46 | name: 'alternativeValidationValueAccessor', 47 | provide: NG_VALUE_ACCESSOR, 48 | useExisting: forwardRef(() => AlternativeValidationDirective), 49 | multi: true 50 | }; 51 | 52 | @Directive({ 53 | selector: '[alternativeValidation]', 54 | providers: [CONTROL_VALUE_ACCESSOR], 55 | host: { 56 | /* 57 | * Listening to the native input event of the host element. 58 | * On input we call the take the value property of the target element end call 59 | * the handleInput function with it. This renders the new value to the view. 60 | */ 61 | '(input)': 'handleInput($event.target.value)', 62 | /* 63 | * Listening to the native focus event of the host element. 64 | * On focus we call the internal haldleFocus function 65 | */ 66 | '(focus)': 'handleFocus(true)', 67 | /* 68 | * Listening to the native blur event of the host element. 69 | * On blur we call the onTouched function from the formControl 70 | */ 71 | '(blur)': 'handleFocus(false)', 72 | /* 73 | * The compositionstart event is fired when the composition of a passage of text is prepared 74 | * (similar to keydown for a keyboard input, but fires with special characters that require 75 | * a sequence of keys and other inputs such as speech recognition or word suggestion on mobile). 76 | */ 77 | '(compositionstart)': 'compositionStart()', 78 | /* 79 | * The compositionend event is fired when the composition of a passage of text has been completed 80 | * or cancelled 81 | * (fires with special characters that require a sequence of keys and other inputs such as 82 | * speech recognition or word suggestion on mobile). 83 | */ 84 | '(compositionend)': 'compositionEnd($event.target.value)' 85 | }, 86 | exportAs: 'alternativeValidation' 87 | }) 88 | export class AlternativeValidationDirective extends AbstractControlDirective implements ControlValueAccessor, OnInit, OnChanges, OnDestroy { 89 | 90 | // Reference to the fake formControl 91 | control: AbstractControl; 92 | 93 | // Reference to the formControl 94 | realFormControl: AbstractControl; 95 | 96 | @Input() 97 | // The formControlName in the parent 98 | protected formControlName: string; 99 | 100 | // The internal data model 101 | private _value: any = ''; 102 | 103 | // The internal focus state 104 | private _focus = false; 105 | 106 | // The internal disabled state 107 | private _disabled: boolean; 108 | 109 | destroy$ = new Subject(); 110 | 111 | 112 | // The internal state of composing input 113 | protected composing = false; 114 | 115 | @Input('alternativeValidation') 116 | // The config for the alternative validation in the parent 117 | protected config: IAlternativeValidationConfig; 118 | 119 | private onChange: Function = () => {}; 120 | private onTouched: Function = () => {}; 121 | 122 | constructor( 123 | protected renderer: Renderer2, protected elementRef: ElementRef, 124 | @Optional() @Inject(COMPOSITION_BUFFER_MODE) protected compositionMode: boolean, 125 | @Optional() @Host() @SkipSelf() private parentFormContainer: ControlContainer, 126 | protected vs: ValidationCollectorService 127 | ) { 128 | super(); 129 | if (this.compositionMode == null) { 130 | this.compositionMode = !isAndroid(); 131 | } 132 | } 133 | 134 | get focus(): boolean { 135 | return this._focus; 136 | } 137 | 138 | set focus(value: boolean) { 139 | this._focus = value; 140 | } 141 | 142 | /* 143 | * Handel formControl model changes 144 | */ 145 | writeValue(value: any): void { 146 | this.renderViewValue(value); 147 | } 148 | 149 | /* 150 | * Registers the controls onChange function 151 | */ 152 | registerOnChange(fn: (_: any) => void): void { 153 | this.onChange = fn; 154 | } 155 | 156 | /* 157 | * Registers the controls onTouched function 158 | */ 159 | registerOnTouched(fn: () => void): void { 160 | this.onTouched = fn; 161 | } 162 | 163 | /* 164 | * Sets the internal disabled state and renders it to the view 165 | */ 166 | setDisabledState(isDisabled: boolean): void { 167 | this._disabled = isDisabled; 168 | this.renderViewDisabled(isDisabled); 169 | } 170 | 171 | /* 172 | * Depending on the compositionMode and the composing state it 173 | * calls writeValueFromViewToModel with new value 174 | */ 175 | private handleInput(value: any): void { 176 | if (!this.compositionMode || (this.compositionMode && !this.composing)) { 177 | this.writeValueFromViewToModel(value); 178 | } 179 | } 180 | 181 | /* 182 | * Sets the internal focus state and renders it to the view 183 | * It also calls onTouch if a blur happens 184 | */ 185 | private handleFocus(isFocus: boolean): void { 186 | this.focus = isFocus; 187 | if (!isFocus) { 188 | this.onTouched(); 189 | this.updateFakeTouched(this.realFormControl.touched); 190 | } 191 | this.renderViewFocus(isFocus); 192 | } 193 | 194 | /* 195 | * Is called when the compositionStart event is fired. 196 | * It sets the internal composing state to true 197 | */ 198 | private compositionStart(): void { 199 | this.composing = true; 200 | } 201 | 202 | /* 203 | * Is called when the compositionEnd event is fired 204 | * It sets the internal composing state to false 205 | * and triggers the onChange function with the new value. 206 | */ 207 | private compositionEnd(value: any): void { 208 | this.composing = false; 209 | if (this.compositionMode) { 210 | this._value = value; 211 | this.onChange(value); 212 | } 213 | } 214 | 215 | // Directive lifecycle hooks ================================================================== 216 | 217 | ngOnChanges(changes: SimpleChanges) { 218 | this.updateValidators(); 219 | } 220 | 221 | ngOnInit(): void { 222 | this.updateFormControlRef(); 223 | } 224 | 225 | ngOnDestroy() { 226 | this.destroy$.next(true); 227 | } 228 | 229 | // ControlValueAccessor ================================================================== 230 | 231 | protected writeValueFromViewToModel(value: any) { 232 | if (value !== this._value) { 233 | this._value = value; 234 | this.onChange(value); 235 | this.updateFakeValue(value); 236 | } 237 | } 238 | 239 | protected renderViewValue(value: any) { 240 | const normalizedValue = value == null ? '' : value; 241 | this.renderer.setProperty(this.getInputElementRef(), 'value', normalizedValue); 242 | } 243 | 244 | protected renderViewDisabled(isDisabled: boolean) { 245 | this.renderer.setProperty(this.getInputElementRef(), 'disabled', isDisabled); 246 | } 247 | 248 | protected renderViewFocus(isFocus: boolean): void { 249 | this.renderer.setProperty(this.getInputElementRef(), 'focus', isFocus); 250 | } 251 | 252 | // get a safe ref to the input element 253 | private getInputElementRef(): HTMLInputElement { 254 | let input: HTMLInputElement; 255 | if (this.elementRef.nativeElement.tagName === 'INPUT') { 256 | // directive is used directly on an input element 257 | input = this.elementRef.nativeElement; 258 | } else { 259 | // directive is used on an abstracted input element, `ion-input`, `md-input`, etc 260 | input = this.elementRef.nativeElement.getElementsByTagName('INPUT')[0]; 261 | } 262 | 263 | if (!input) { 264 | throw new Error('You can applied the "alternativeValidation" directive only on inputs or elements containing inputs'); 265 | } 266 | 267 | return input; 268 | } 269 | 270 | // FormControl ================================================================== 271 | 272 | private updateFormControlRef() { 273 | this.realFormControl = this.parentFormContainer['form'].controls[this.formControlName]; 274 | 275 | this.updateFakeControlRef(this.realFormControl.value); 276 | this.setupResetObservable(this.realFormControl); 277 | this.setupDisabledObservable(this.realFormControl); 278 | } 279 | 280 | /* 281 | * custom implementation of status getter 282 | * */ 283 | get status(): string { 284 | return this.control ? this.control.status : null; 285 | } 286 | 287 | // Reset handling ============================================================================== 288 | 289 | private setupResetObservable(control: AbstractControl): void { 290 | Observable.combineLatest(control.statusChanges, control.valueChanges) 291 | .takeUntil(this.destroy$.asObservable()) 292 | .filter(() => { 293 | const resetState = { 294 | dirty: false, 295 | pristine: true, 296 | touched: false, 297 | untouched: true 298 | }; 299 | 300 | return Object 301 | .keys(resetState) 302 | .reduce((state, item) => { 303 | return !state ? false : control[item] === resetState[item]; 304 | }, true); 305 | }) 306 | .subscribe(() => { 307 | this.onResetEvent(); 308 | }); 309 | } 310 | 311 | private setupDisabledObservable(control: AbstractControl): void { 312 | Observable.combineLatest(control.statusChanges, control.valueChanges) 313 | .takeUntil(this.destroy$.asObservable()) 314 | .map(() => { 315 | const disabledState = { 316 | valid: false, 317 | invalid: false, 318 | status: 'DISABLED' 319 | }; 320 | 321 | return Object 322 | .keys(disabledState) 323 | .reduce((state, item) => { 324 | return !state ? false : control[item] === disabledState[item]; 325 | }, true); 326 | }) 327 | .subscribe((isDisabled) => { 328 | this.onDisableEvent(isDisabled); 329 | }); 330 | } 331 | 332 | // Alternative validation ============================================================================== 333 | 334 | private onResetEvent() { 335 | this.control.reset(this.realFormControl.value); 336 | } 337 | 338 | private onDisableEvent(isDisabled: boolean) { 339 | if (!isDisabled) { 340 | this.control.enable(); 341 | } else { 342 | this.control.disable(); 343 | } 344 | } 345 | 346 | private updateFakeControlRef(formState: any): void { 347 | this.control = new FormControl(); 348 | this.updateValidators(); 349 | this.control.reset(formState); 350 | } 351 | 352 | private updateValidators(): void { 353 | if (this.config && this.control && this.control instanceof AbstractControl) { 354 | if ('validator' in this.config && Array.isArray(this.config.validator)) { 355 | this.control.setValidators(this.vs.getValidators(this.config.validator)); 356 | } 357 | if ('asyncValidator' in this.config && Array.isArray(this.config.asyncValidator)) { 358 | this.control.setValidators(this.vs.getAsyncValidators(this.config.asyncValidator)); 359 | } 360 | } 361 | } 362 | 363 | private updateFakeValue(value): void { 364 | if (this.control) { 365 | this.control.setValue(value); 366 | this.control.updateValueAndValidity(value); 367 | this.updateFakeDirty(true); 368 | } 369 | } 370 | 371 | private updateFakeTouched(isTouched: boolean): void { 372 | if (isTouched) { 373 | this.control.markAsTouched(); 374 | } else { 375 | this.control.markAsUntouched(); 376 | } 377 | } 378 | 379 | private updateFakeDirty(isDirty: boolean): void { 380 | if (isDirty) { 381 | this.control.markAsDirty(); 382 | } else { 383 | this.control.markAsPristine(); 384 | } 385 | } 386 | 387 | } 388 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {CommonModule} from '@angular/common'; 2 | import {NgModule} from '@angular/core'; 3 | import {ReactiveFormsModule} from '@angular/forms'; 4 | 5 | import {AlternativeValidationDirective} from './alternative-validation.directive'; 6 | import {ValidationCollectorService} from './validation-collector.service'; 7 | 8 | export * from './alternative-validation.directive'; 9 | export * from './validation-collector.service'; 10 | 11 | 12 | @NgModule({ 13 | imports: [ 14 | CommonModule, 15 | ReactiveFormsModule 16 | ], 17 | declarations: [AlternativeValidationDirective], 18 | exports: [AlternativeValidationDirective, ReactiveFormsModule] 19 | }) 20 | export class AlternativeValidationModule { 21 | 22 | static forRoot() { 23 | return { 24 | ngModule: AlternativeValidationModule, 25 | providers: [ValidationCollectorService] 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import {enableProdMode} from '@angular/core'; 2 | import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; 3 | import {AlternativeValidationModule} from './index'; 4 | 5 | enableProdMode(); 6 | platformBrowserDynamic().bootstrapModule(AlternativeValidationModule); 7 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-alternative-validation", 3 | "description": "Angular Alternative Validation - The smoothest way to implement validation hints/warnings for your forms", 4 | "version": "1.0.6", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/BioPhoton/angular-alternative-validation" 8 | }, 9 | "author": { 10 | "name": "Michael Hladky", 11 | "email": "michael@hladky.at" 12 | }, 13 | "keywords": [ 14 | "angular", 15 | "validation", 16 | "warning", 17 | "warnings", 18 | "hint", 19 | "hints", 20 | "validation-warning", 21 | "validation-hint", 22 | "alternative validation", 23 | "directive", 24 | "NG_VALIDATORS", 25 | "NG_ASYNC_VALIDATORS", 26 | "ControlValueAccessor", 27 | "AbstractControlDirective" 28 | ], 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/BioPhoton/angular-alternative-validation/issues" 32 | }, 33 | "module": "angular-alternative-validation.js", 34 | "typings": "angular-alternative-validation.d.ts", 35 | "peerDependencies": { 36 | "@angular/core": "^4.0.0", 37 | "@angular/forms": "^4.0.0", 38 | "rxjs": "^5.1.0", 39 | "zone.js": "^0.8.4" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | import 'core-js/es6/array'; 2 | import 'core-js/es6/date'; 3 | import 'core-js/es6/function'; 4 | import 'core-js/es6/map'; 5 | import 'core-js/es6/math'; 6 | import 'core-js/es6/number'; 7 | import 'core-js/es6/object'; 8 | import 'core-js/es6/parse-float'; 9 | import 'core-js/es6/parse-int'; 10 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 11 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 12 | /** IE10 and IE11 requires the following to support `@angular/animation`. */ 13 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 14 | /** Evergreen browsers require these. **/ 15 | import 'core-js/es6/reflect'; 16 | import 'core-js/es6/regexp'; 17 | import 'core-js/es6/set'; 18 | import 'core-js/es6/string'; 19 | /** 20 | * This file includes polyfills needed by Angular and is loaded before the app. 21 | * You can add your own extra polyfills to this file. 22 | * 23 | * This file is divided into 2 sections: 24 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 25 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 26 | * file. 27 | * 28 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 29 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 30 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 31 | * 32 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 33 | */ 34 | /*************************************************************************************************** 35 | * BROWSER POLYFILLS 36 | */ 37 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 38 | import 'core-js/es6/symbol'; 39 | import 'core-js/es6/weak-map'; 40 | import 'core-js/es7/reflect'; 41 | /*************************************************************************************************** 42 | * APPLICATION IMPORTS 43 | */ 44 | /** 45 | * Date, currency, decimal and percent pipes. 46 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 47 | */ 48 | import 'intl'; // Run `npm install --save intl`. 49 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/ 50 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 51 | /*************************************************************************************************** 52 | * Zone JS is required by Angular itself. 53 | */ 54 | import 'zone.js/dist/zone'; // Included with Angular CLI. 55 | /** 56 | * Need to import at least one locale-data with intl. 57 | */ 58 | // import 'intl/locale-data/jsonp/en'; 59 | -------------------------------------------------------------------------------- /src/struct/alternative-validation-config.ts: -------------------------------------------------------------------------------- 1 | import { IValidatorConfig } from './validator-config'; 2 | export interface IAlternativeValidationConfig { 3 | validator?: IValidatorConfig[]; 4 | asyncValidator?: IValidatorConfig[]; 5 | } 6 | -------------------------------------------------------------------------------- /src/struct/validator-config.ts: -------------------------------------------------------------------------------- 1 | export interface IValidatorConfig { 2 | name: string; 3 | params?: any[]; 4 | } 5 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare const __karma__: any; 17 | declare const require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | const context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /src/tsconfig.es5.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "module": "es2015", 5 | "target": "es5", 6 | "baseUrl": ".", 7 | "stripInternal": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "moduleResolution": "node", 11 | "outDir": "../build", 12 | "rootDir": ".", 13 | "lib": [ 14 | "es2015", 15 | "dom" 16 | ], 17 | "skipLibCheck": true, 18 | "types": [] 19 | }, 20 | "angularCompilerOptions": { 21 | "annotateForClosureCompiler": true, 22 | "strictMetadataEmit": true, 23 | "skipTemplateCodegen": true, 24 | "flatModuleOutFile": "angular-alternative-validation.js", 25 | "flatModuleId": "angular-alternative-validation" 26 | }, 27 | "files": [ 28 | "./index.ts" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "baseUrl": "", 8 | "types": [ 9 | "jasmine", 10 | "node" 11 | ] 12 | }, 13 | "files": [ 14 | "test.ts" 15 | ], 16 | "include": [ 17 | "**/*.spec.ts", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/validation-collector.service.ts: -------------------------------------------------------------------------------- 1 | import {Inject, Injectable, Optional} from '@angular/core'; 2 | import { 3 | AsyncValidatorFn, 4 | NG_ASYNC_VALIDATORS, 5 | NG_VALIDATORS, 6 | ValidatorFn, 7 | Validators 8 | } from '@angular/forms'; 9 | import {IValidatorConfig} from './struct/validator-config'; 10 | @Injectable() 11 | export class ValidationCollectorService { 12 | 13 | constructor( 14 | @Optional() @Inject(NG_VALIDATORS) private NG_VALIDATORS: ValidatorFn[], 15 | @Optional() @Inject(NG_ASYNC_VALIDATORS) private NG_ASYNC_VALIDATORS: AsyncValidatorFn[] 16 | ) { 17 | } 18 | 19 | getValidators(validatorsConfig: IValidatorConfig[]): ValidatorFn[] { 20 | let validators: ValidatorFn[] = []; 21 | 22 | if (validatorsConfig) { 23 | validators = validatorsConfig.map((validatorObj: IValidatorConfig) => { 24 | return this.getValidatorFn(validatorObj.name, validatorObj.params); 25 | }); 26 | } 27 | return validators; 28 | } 29 | 30 | getValidatorFn(validatorName: string, validatorArgs?: any[]): ValidatorFn { 31 | if (!validatorName) { 32 | throw new Error('No validation name given to search for'); 33 | } 34 | const validatorFn = Validators[validatorName] || this.getCustomValidatorFn(validatorName); 35 | 36 | if (!(typeof validatorFn === 'function')) { 37 | throw new Error(`Validator "${validatorName}" is not provided via NG_VALIDATORS`); 38 | } 39 | 40 | const finalFunction = (validatorArgs) ? validatorFn(...validatorArgs) : validatorFn; 41 | 42 | if (typeof finalFunction !== 'function') { 43 | throw new Error(`Validator "${validatorName}" is not provided a function. 44 | Did you provide params for a validator that don't need them?`); 45 | } 46 | 47 | return finalFunction; 48 | } 49 | 50 | getCustomValidatorFn(validatorName: string): ValidatorFn | undefined { 51 | let validatorFn; 52 | 53 | if (!validatorName) { 54 | throw new Error('No validation name given to search for'); 55 | } 56 | 57 | if (this.NG_VALIDATORS) { 58 | validatorFn = this.NG_VALIDATORS.find((fn) => { 59 | return validatorName === fn.name; 60 | }); 61 | } 62 | 63 | return validatorFn; 64 | } 65 | 66 | getAsyncValidators(config: IValidatorConfig[]): AsyncValidatorFn[] { 67 | let asyncValidators: AsyncValidatorFn[] = []; 68 | 69 | if (config) { 70 | asyncValidators = config.map((validatorObj: IValidatorConfig) => { 71 | return this.getCustomAsyncValidatorFn(validatorObj.name, validatorObj.params); 72 | }); 73 | } 74 | return asyncValidators; 75 | } 76 | 77 | getCustomAsyncValidatorFn(validatorName: string, validatorArgs?: any[]): AsyncValidatorFn { 78 | let asyncValidatorFn; 79 | 80 | if (!validatorName) { 81 | throw new Error('No asyncvalidation name given to search for'); 82 | } 83 | 84 | if (this.NG_ASYNC_VALIDATORS) { 85 | 86 | asyncValidatorFn = this.NG_ASYNC_VALIDATORS.find( 87 | (aFn) => { 88 | return validatorName === aFn.name; 89 | }); 90 | } 91 | 92 | if (!(typeof asyncValidatorFn === 'function')) { 93 | throw new Error(`Asyncvalidator "${validatorName}" is not provided via NG_ASYNC_VALIDATORS`); 94 | } 95 | 96 | const finalFunction = (validatorArgs) ? asyncValidatorFn(...validatorArgs) : asyncValidatorFn; 97 | 98 | if (typeof finalFunction !== 'function') { 99 | throw new Error(`Asyncvalidator "${validatorName}" is not provided a function. 100 | Did you provide params for a validator that don't need them?`); 101 | } 102 | 103 | return finalFunction; 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/validation-collector.spec.ts: -------------------------------------------------------------------------------- 1 | import {inject, TestBed} from '@angular/core/testing'; 2 | import { 3 | AbstractControl, 4 | FormControl, 5 | NG_ASYNC_VALIDATORS, 6 | NG_VALIDATORS, 7 | ValidationErrors, 8 | ValidatorFn 9 | } from '@angular/forms'; 10 | import 'rxjs/add/operator/debounceTime'; 11 | import 'rxjs/add/operator/delay'; 12 | import 'rxjs/add/operator/distinctUntilChanged'; 13 | import 'rxjs/add/operator/first'; 14 | 15 | import {Observable} from 'rxjs/Observable'; 16 | import {IValidatorConfig} from './struct/validator-config'; 17 | import {ValidationCollectorService} from './validation-collector.service'; 18 | 19 | export function isBlacklistedName(c: AbstractControl): Observable { 20 | 21 | const validatorName = 'isBlacklistedName'; 22 | const routValidation$ = new Observable(observer => { 23 | 24 | if (c.value && typeof c.value === 'string' && 25 | 'abcde'.indexOf(c.value.toString().toLowerCase()) !== -1) { 26 | observer.next({ 27 | [validatorName]: { 28 | actual: c.value, 29 | mandatoryChars: 'abcde' 30 | } 31 | }); 32 | } else { 33 | observer.next(null); 34 | } 35 | }); 36 | 37 | return routValidation$.debounceTime(500).distinctUntilChanged().delay(2000).first(); 38 | } 39 | 40 | export function validName(c: AbstractControl): ValidationErrors | null { 41 | const validNames = ['Aretha', 'Ella', 'Etta', 'Nina']; 42 | 43 | const isValid = validNames 44 | .map(n => c.value && c.value.indexOf(n) !== -1) 45 | .filter(v => v) 46 | .reduce((prev, curr) => true, false); 47 | 48 | if (!isValid) { 49 | return { 50 | validName: { 51 | validNames 52 | } 53 | }; 54 | } 55 | 56 | return null; 57 | } 58 | 59 | 60 | describe('ValidationCollectorService', () => { 61 | beforeEach(() => { 62 | TestBed.configureTestingModule({ 63 | providers: [ 64 | ValidationCollectorService, 65 | {provide: NG_VALIDATORS, useValue: validName, multi: true}, 66 | {provide: NG_ASYNC_VALIDATORS, useValue: isBlacklistedName, multi: true} 67 | ] 68 | }); 69 | }); 70 | 71 | 72 | it('should be created', inject([ValidationCollectorService], (service: ValidationCollectorService) => { 73 | expect(service).toBeTruthy(); 74 | })); 75 | 76 | describe('getCustomValidatorFn', () => { 77 | 78 | it('should throw if we pass in no validator name', inject([ValidationCollectorService], (service: ValidationCollectorService) => { 79 | const functionName = undefined; 80 | expect(() => { 81 | service.getCustomValidatorFn(functionName); 82 | }).toThrow(new Error('No validation name given to search for')); 83 | })); 84 | 85 | it('should return the function if we request a custom existing function', 86 | inject([ValidationCollectorService], (service: ValidationCollectorService) => { 87 | const validNameError = { 88 | validName: { 89 | validNames: ['Aretha', 'Ella', 'Etta', 'Nina'] 90 | } 91 | }; 92 | const customValidatorFunction = service.getCustomValidatorFn('validName'); 93 | expect(typeof customValidatorFunction).toBe('function'); 94 | const fc: FormControl = new FormControl(''); 95 | expect(customValidatorFunction(fc).validName).toEqual(validNameError.validName); 96 | 97 | fc.setValue('Ella'); 98 | expect(customValidatorFunction(fc)).toEqual(null); 99 | })); 100 | 101 | }); 102 | 103 | describe('getValidatorFn', () => { 104 | 105 | it('should throw if we pass in no validator name', inject([ValidationCollectorService], (service: ValidationCollectorService) => { 106 | const functionName = undefined; 107 | expect(() => { 108 | service.getValidatorFn(functionName); 109 | }).toThrow(new Error('No validation name given to search for')); 110 | })); 111 | 112 | it('should throw if we request a not existing validator', 113 | inject([ValidationCollectorService], (service: ValidationCollectorService) => { 114 | const functionName = 'notExistingFunction'; 115 | expect(() => { 116 | service.getValidatorFn(functionName); 117 | }).toThrow(new Error(`Validator "${functionName}" is not provided via NG_VALIDATORS`)); 118 | })); 119 | 120 | it('should throw if we pass in params for a validator with no params', 121 | inject([ValidationCollectorService], (service: ValidationCollectorService) => { 122 | const functionName = 'required'; 123 | expect(() => { 124 | service.getValidatorFn(functionName, ['some', 'params']); 125 | }).toThrow(new Error(`Validator "${functionName}" is not provided a function. 126 | Did you provide params for a validator that don't need them?`)); 127 | })); 128 | 129 | it('should be able to get the built in validator', 130 | inject([ValidationCollectorService], (service: ValidationCollectorService) => { 131 | const buildInValidatorNames: string[] = [ 132 | 'required', 133 | 'minLength', 134 | 'maxLength', 135 | 'min', 136 | 'max', 137 | 'pattern', 138 | 'email', 139 | 'nullValidator', 140 | 'requiredTrue' 141 | ]; 142 | for (let i = 0; i < buildInValidatorNames.length; i++) { 143 | const func: ValidatorFn = service.getValidatorFn(buildInValidatorNames[i]); 144 | expect(typeof func).toBe('function'); 145 | } 146 | })); 147 | 148 | it('Built in validator without params should work', 149 | inject([ValidationCollectorService], (service: ValidationCollectorService) => { 150 | const requiredError = {required: true}; 151 | const func: ValidatorFn = service.getValidatorFn('required'); 152 | const fc: FormControl = new FormControl(''); 153 | expect(func(fc)).toEqual(requiredError); 154 | fc.setValue('42'); 155 | expect(func(fc)).toEqual(null); 156 | })); 157 | 158 | it('built in validator with params should work', 159 | inject([ValidationCollectorService], (service: ValidationCollectorService) => { 160 | const minlengthError = { 161 | minLength: { 162 | requiredLength: 3, 163 | actualLength: 2 164 | } 165 | }; 166 | const func: ValidatorFn = service.getValidatorFn('minLength', [3]); 167 | 168 | const fc: FormControl = new FormControl(''); 169 | expect(func(fc)).toEqual(null); 170 | 171 | fc.setValue('42'); 172 | expect(func(fc).minlength).toEqual(minlengthError.minLength); 173 | 174 | fc.setValue('108'); 175 | expect(func(fc)).toEqual(null); 176 | })); 177 | 178 | it('should return the function if we request a custom existing function', 179 | inject([ValidationCollectorService], (service: ValidationCollectorService) => { 180 | const validNameError = { 181 | validName: { 182 | validNames: ['Aretha', 'Ella', 'Etta', 'Nina'] 183 | } 184 | }; 185 | const customValidatorFunction = service.getValidatorFn('validName'); 186 | expect(typeof customValidatorFunction).toBe('function'); 187 | const fc: FormControl = new FormControl(''); 188 | expect(customValidatorFunction(fc).validName).toEqual(validNameError.validName); 189 | 190 | fc.setValue('Ella'); 191 | expect(customValidatorFunction(fc)).toEqual(null); 192 | })); 193 | 194 | }); 195 | 196 | describe('getValidators', () => { 197 | 198 | it('should throw if we pass in no validator name', inject([ValidationCollectorService], (service: ValidationCollectorService) => { 199 | const config = [{name: undefined}]; 200 | expect(() => { 201 | service.getValidators(config); 202 | }).toThrow(new Error('No validation name given to search for')); 203 | })); 204 | 205 | it('should throw if we request a not existing validator ', 206 | inject([ValidationCollectorService], (service: ValidationCollectorService) => { 207 | const functionName = 'notExistingFunction'; 208 | expect(() => { 209 | service.getValidatorFn(functionName); 210 | }).toThrow(new Error(`Validator "${functionName}" is not provided via NG_VALIDATORS`)); 211 | })); 212 | 213 | it('should throw if we pass in params for a validator with no params', 214 | inject([ValidationCollectorService], (service: ValidationCollectorService) => { 215 | const config: IValidatorConfig[] = [ 216 | {name: 'required', params: ['some', 'params']} 217 | ]; 218 | 219 | expect(() => { 220 | service.getValidators(config); 221 | }).toThrow(new Error(`Validator "${config[0].name}" is not provided a function. 222 | Did you provide params for a validator that don't need them?`)); 223 | })); 224 | 225 | it('should be able to get the built in validator', 226 | inject([ValidationCollectorService], (service: ValidationCollectorService) => { 227 | const buildInValidatorConfig: IValidatorConfig[] = [ 228 | {name: 'required'}, 229 | {name: 'minLength', params: [3]}, 230 | {name: 'maxLength', params: [3]}, 231 | {name: 'min', params: [3]}, 232 | {name: 'max', params: [3]}, 233 | {name: 'pattern', params: [/[a-z]/g]}, 234 | {name: 'email'}, 235 | {name: 'nullValidator'}, 236 | {name: 'requiredTrue'} 237 | ]; 238 | 239 | const funcs: ValidatorFn[] = service.getValidators(buildInValidatorConfig); 240 | expect(typeof funcs).toBe('object'); 241 | expect(funcs.length).toBe(9); 242 | 243 | })); 244 | 245 | it('built in validator without params should work', 246 | inject([ValidationCollectorService], (service: ValidationCollectorService) => { 247 | const config: IValidatorConfig[] = [{name: 'required'}]; 248 | const requiredError = {required: true}; 249 | const funcs: ValidatorFn[] = service.getValidators(config); 250 | expect(typeof funcs).toBe('object'); 251 | expect(funcs.length).toBe(1); 252 | 253 | const fc: FormControl = new FormControl(''); 254 | expect(funcs[0](fc)).toEqual(requiredError); 255 | fc.setValue('42'); 256 | expect(funcs[0](fc)).toEqual(null); 257 | })); 258 | 259 | it('built in validator with params should work', 260 | inject([ValidationCollectorService], (service: ValidationCollectorService) => { 261 | const config: IValidatorConfig[] = [{name: 'minLength', params: [3]}]; 262 | const minlengthError = { 263 | minLength: { 264 | requiredLength: 3, 265 | actualLength: 2 266 | } 267 | }; 268 | const funcs: ValidatorFn[] = service.getValidators(config); 269 | expect(typeof funcs).toBe('object'); 270 | expect(funcs.length).toBe(1); 271 | 272 | const fc: FormControl = new FormControl(''); 273 | expect(funcs[0](fc)).toEqual(null); 274 | 275 | fc.setValue('42'); 276 | expect(funcs[0](fc).minlength).toEqual(minlengthError.minLength); 277 | 278 | fc.setValue('108'); 279 | expect(funcs[0](fc)).toEqual(null); 280 | })); 281 | 282 | it('should return the function if we request a custom existing function', 283 | inject([ValidationCollectorService], (service: ValidationCollectorService) => { 284 | const config: IValidatorConfig[] = [{name: 'validName'}]; 285 | const customValidatorFunctions = service.getValidators(config); 286 | expect(typeof customValidatorFunctions).toBe('object'); 287 | expect(customValidatorFunctions.length).toBe(1); 288 | })); 289 | 290 | it('custom validator without params should work', 291 | inject([ValidationCollectorService], (service: ValidationCollectorService) => { 292 | const config: IValidatorConfig[] = [{name: 'validName'}]; 293 | const validNameError = { 294 | validName: { 295 | validNames: ['Aretha', 'Ella', 'Etta', 'Nina'] 296 | } 297 | }; 298 | const customValidatorFunctions = service.getValidators(config); 299 | 300 | const fc: FormControl = new FormControl(''); 301 | expect(customValidatorFunctions[0](fc).validName).toEqual(validNameError.validName); 302 | 303 | fc.setValue('Nina'); 304 | expect(customValidatorFunctions[0](fc)).toEqual(null); 305 | })); 306 | 307 | xit('custom validator with params should work', () => { 308 | 309 | }); 310 | 311 | }); 312 | 313 | describe('getCustomAsyncValidatorFn', () => { 314 | 315 | it('should throw if we pass in no asyncvalidator name', 316 | inject([ValidationCollectorService], (service: ValidationCollectorService) => { 317 | const functionName = undefined; 318 | expect(() => { 319 | service.getCustomAsyncValidatorFn(functionName); 320 | }).toThrow(new Error('No asyncvalidation name given to search for')); 321 | })); 322 | 323 | it('should throw if we request a not existing asyncvalidator', 324 | inject([ValidationCollectorService], (service: ValidationCollectorService) => { 325 | const functionName = 'notExistingFunction'; 326 | expect(() => { 327 | service.getCustomAsyncValidatorFn(functionName); 328 | }).toThrow(new Error(`Asyncvalidator "${functionName}" is not provided via NG_ASYNC_VALIDATORS`)); 329 | })); 330 | 331 | it('should throw if we pass in params for a asyncvalidator with no params', 332 | inject([ValidationCollectorService], (service: ValidationCollectorService) => { 333 | const functionName = 'isBlacklistedName'; 334 | expect(() => { 335 | service.getCustomAsyncValidatorFn(functionName, ['some', 'params']); 336 | }).toThrow(new Error(`Asyncvalidator "${functionName}" is not provided a function. 337 | Did you provide params for a validator that don't need them?`)); 338 | })); 339 | 340 | it('custom async validator without params should work', 341 | inject([ValidationCollectorService], (service: ValidationCollectorService) => { 342 | const isBlacklistedNameError = { 343 | isBlacklistedName: { 344 | actual: 'wxy', 345 | mandatoryChars: 'abcde' 346 | } 347 | }; 348 | const func: ValidatorFn = service.getCustomAsyncValidatorFn('isBlacklistedName'); 349 | 350 | const fc: FormControl = new FormControl(''); 351 | func(fc).subscribe((n) => { 352 | expect(n).toEqual(null); 353 | }); 354 | 355 | fc.setValue('wxy'); 356 | func(fc).subscribe((n) => { 357 | expect(n.isBlacklistedName).toEqual(isBlacklistedNameError.isBlacklistedName); 358 | }); 359 | 360 | fc.setValue('b'); 361 | func(fc).subscribe((n) => { 362 | expect(n).toEqual(null); 363 | }); 364 | })); 365 | 366 | xit('custom async validator with params should work', () => { 367 | 368 | }); 369 | 370 | }); 371 | 372 | describe('getAsyncValidators', () => { 373 | 374 | it('should throw if we pass in no asyncvalidator name', inject([ValidationCollectorService], (service: ValidationCollectorService) => { 375 | const config = [{name: undefined}]; 376 | expect(() => { 377 | service.getAsyncValidators(config); 378 | }).toThrow(new Error('No asyncvalidation name given to search for')); 379 | })); 380 | 381 | it('should throw if we request a not existing validator ', 382 | inject([ValidationCollectorService], (service: ValidationCollectorService) => { 383 | const config = [{name: 'notExistingFunction'}]; 384 | expect(() => { 385 | service.getAsyncValidators(config); 386 | }).toThrow(new Error(`Asyncvalidator "${config[0].name}" is not provided via NG_ASYNC_VALIDATORS`)); 387 | })); 388 | 389 | it('should throw if we pass in params for a validator with no params', 390 | inject([ValidationCollectorService], (service: ValidationCollectorService) => { 391 | const config: IValidatorConfig[] = [ 392 | {name: 'isBlacklistedName', params: ['some', 'params']} 393 | ]; 394 | 395 | expect(() => { 396 | service.getAsyncValidators(config); 397 | }).toThrow(new Error(`Asyncvalidator "${config[0].name}" is not provided a function. 398 | Did you provide params for a validator that don't need them?`)); 399 | })); 400 | 401 | it('should return the function if we request a custom existing asyncvalidator', 402 | inject([ValidationCollectorService], (service: ValidationCollectorService) => { 403 | const config: IValidatorConfig[] = [{name: 'isBlacklistedName'}]; 404 | const customAsyncValidatorFunctions = service.getAsyncValidators(config); 405 | expect(typeof customAsyncValidatorFunctions).toBe('object'); 406 | expect(customAsyncValidatorFunctions.length).toBe(1); 407 | })); 408 | 409 | it('custom asyncvalidator without params should work', 410 | inject([ValidationCollectorService], (service: ValidationCollectorService) => { 411 | const config: IValidatorConfig[] = [{name: 'isBlacklistedName'}]; 412 | const isBlacklistedNameError = { 413 | isBlacklistedName: { 414 | actual: '', 415 | mandatoryChars: 'abcde' 416 | } 417 | }; 418 | const customAsyncValidatorFunctions: any[] = service.getAsyncValidators(config); 419 | 420 | const fc: FormControl = new FormControl(''); 421 | customAsyncValidatorFunctions[0](fc).subscribe((n) => { 422 | expect(n.isBlacklistedName).toEqual(isBlacklistedNameError.isBlacklistedName); 423 | }); 424 | 425 | fc.setValue('cd'); 426 | customAsyncValidatorFunctions[0](fc).subscribe((n) => { 427 | expect(n).toEqual(null); 428 | }); 429 | 430 | }) 431 | ); 432 | 433 | xit('custom asyncvalidator with params should work', () => { 434 | 435 | }); 436 | 437 | }) 438 | ; 439 | 440 | }) 441 | ; 442 | -------------------------------------------------------------------------------- /tools/gulp/inline-resources.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // https://github.com/filipesilva/angular-quickstart-lib/blob/master/inline-resources.js 3 | 'use strict'; 4 | 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const glob = require('glob'); 8 | const sass = require('node-sass'); 9 | const tildeImporter = require('node-sass-tilde-importer'); 10 | 11 | /** 12 | * Simple Promiseify function that takes a Node API and return a version that supports promises. 13 | * We use promises instead of synchronized functions to make the process less I/O bound and 14 | * faster. It also simplifies the code. 15 | */ 16 | function promiseify(fn) { 17 | return function () { 18 | const args = [].slice.call(arguments, 0); 19 | return new Promise((resolve, reject) => { 20 | fn.apply(this, args.concat([function (err, value) { 21 | if (err) { 22 | reject(err); 23 | } else { 24 | resolve(value); 25 | } 26 | }])); 27 | }); 28 | }; 29 | } 30 | 31 | const readFile = promiseify(fs.readFile); 32 | const writeFile = promiseify(fs.writeFile); 33 | 34 | /** 35 | * Inline resources in a tsc/ngc compilation. 36 | * @param projectPath {string} Path to the project. 37 | */ 38 | function inlineResources(projectPath) { 39 | 40 | // Match only TypeScript files in projectPath. 41 | const files = glob.sync('**/*.ts', {cwd: projectPath}); 42 | 43 | // For each file, inline the templates and styles under it and write the new file. 44 | return Promise.all(files.map(filePath => { 45 | const fullFilePath = path.join(projectPath, filePath); 46 | return readFile(fullFilePath, 'utf-8') 47 | .then(content => inlineResourcesFromString(content, url => { 48 | // Resolve the template url. 49 | return path.join(path.dirname(fullFilePath), url); 50 | })) 51 | .then(content => writeFile(fullFilePath, content)) 52 | .catch(err => { 53 | console.error('An error occured: ', err); 54 | }); 55 | })); 56 | } 57 | 58 | /** 59 | * Inline resources from a string content. 60 | * @param content {string} The source file's content. 61 | * @param urlResolver {Function} A resolver that takes a URL and return a path. 62 | * @returns {string} The content with resources inlined. 63 | */ 64 | function inlineResourcesFromString(content, urlResolver) { 65 | // Curry through the inlining functions. 66 | return [ 67 | inlineTemplate, 68 | inlineStyle, 69 | removeModuleId 70 | ].reduce((content, fn) => fn(content, urlResolver), content); 71 | } 72 | 73 | /** 74 | * Inline the templates for a source file. Simply search for instances of `templateUrl: ...` and 75 | * replace with `template: ...` (with the content of the file included). 76 | * @param content {string} The source file's content. 77 | * @param urlResolver {Function} A resolver that takes a URL and return a path. 78 | * @return {string} The content with all templates inlined. 79 | */ 80 | function inlineTemplate(content, urlResolver) { 81 | return content.replace(/templateUrl:\s*'([^']+?\.html)'/g, function (m, templateUrl) { 82 | const templateFile = urlResolver(templateUrl); 83 | const templateContent = fs.readFileSync(templateFile, 'utf-8'); 84 | const shortenedTemplate = templateContent 85 | .replace(/([\n\r]\s*)+/gm, ' ') 86 | .replace(/"/g, '\\"'); 87 | return `template: "${shortenedTemplate}"`; 88 | }); 89 | } 90 | 91 | 92 | /** 93 | * Inline the styles for a source file. Simply search for instances of `styleUrls: [...]` and 94 | * replace with `styles: [...]` (with the content of the file included). 95 | * @param urlResolver {Function} A resolver that takes a URL and return a path. 96 | * @param content {string} The source file's content. 97 | * @return {string} The content with all styles inlined. 98 | */ 99 | function inlineStyle(content, urlResolver) { 100 | return content.replace(/styleUrls:\s*(\[[\s\S]*?\])/gm, function (m, styleUrls) { 101 | const urls = eval(styleUrls); 102 | return 'styles: [' 103 | + urls.map(styleUrl => { 104 | const styleFile = urlResolver(styleUrl); 105 | const originContent = fs.readFileSync(styleFile, 'utf-8'); 106 | const styleContent = styleFile.endsWith('.scss') ? buildSass(originContent, styleFile) : originContent; 107 | const shortenedStyle = styleContent 108 | .replace(/([\n\r]\s*)+/gm, ' ') 109 | .replace(/"/g, '\\"'); 110 | return `"${shortenedStyle}"`; 111 | }) 112 | .join(',\n') 113 | + ']'; 114 | }); 115 | } 116 | 117 | /** 118 | * build sass content to css 119 | * @param content {string} the css content 120 | * @param sourceFile {string} the scss file sourceFile 121 | * @return {string} the generated css, empty string if error occured 122 | */ 123 | function buildSass(content, sourceFile) { 124 | try { 125 | const result = sass.renderSync({ 126 | data: content, 127 | file: sourceFile, 128 | importer: tildeImporter 129 | }); 130 | return result.css.toString() 131 | } catch (e) { 132 | console.error('\x1b[41m'); 133 | console.error('at ' + sourceFile + ':' + e.line + ":" + e.column); 134 | console.error(e.formatted); 135 | console.error('\x1b[0m'); 136 | return ""; 137 | } 138 | } 139 | 140 | /** 141 | * Remove every mention of `moduleId: module.id`. 142 | * @param content {string} The source file's content. 143 | * @returns {string} The content with all moduleId: mentions removed. 144 | */ 145 | function removeModuleId(content) { 146 | return content.replace(/\s*moduleId:\s*module\.id\s*,?\s*/gm, ''); 147 | } 148 | 149 | module.exports = inlineResources; 150 | module.exports.inlineResourcesFromString = inlineResourcesFromString; 151 | 152 | // Run inlineResources if module is being called directly from the CLI with arguments. 153 | if (require.main === module && process.argv.length > 2) { 154 | console.log('Inlining resources from project:', process.argv[2]); 155 | return inlineResources(process.argv[2]); 156 | } 157 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "baseUrl": "src", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2016", 17 | "dom" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "eofline": true, 15 | "forin": true, 16 | "import-blacklist": [ 17 | true, 18 | "rxjs" 19 | ], 20 | "import-spacing": true, 21 | "indent": [ 22 | true, 23 | "spaces" 24 | ], 25 | "interface-over-type-literal": true, 26 | "label-position": true, 27 | "max-line-length": [ 28 | true, 29 | 140 30 | ], 31 | "member-access": false, 32 | "member-ordering": [ 33 | true, 34 | "static-before-instance", 35 | "variables-before-functions" 36 | ], 37 | "no-arg": true, 38 | "no-bitwise": true, 39 | "no-console": [ 40 | true, 41 | "debug", 42 | "info", 43 | "time", 44 | "timeEnd", 45 | "trace" 46 | ], 47 | "no-construct": true, 48 | "no-debugger": true, 49 | "no-duplicate-super": true, 50 | "no-empty": false, 51 | "no-empty-interface": true, 52 | "no-eval": true, 53 | "no-inferrable-types": [ 54 | true, 55 | "ignore-params" 56 | ], 57 | "no-misused-new": true, 58 | "no-non-null-assertion": true, 59 | "no-shadowed-variable": true, 60 | "no-string-literal": false, 61 | "no-string-throw": true, 62 | "no-switch-case-fall-through": true, 63 | "no-trailing-whitespace": true, 64 | "no-unnecessary-initializer": true, 65 | "no-unused-expression": true, 66 | "no-use-before-declare": true, 67 | "no-var-keyword": true, 68 | "object-literal-sort-keys": false, 69 | "one-line": [ 70 | true, 71 | "check-open-brace", 72 | "check-catch", 73 | "check-else", 74 | "check-whitespace" 75 | ], 76 | "prefer-const": true, 77 | "quotemark": [ 78 | true, 79 | "single" 80 | ], 81 | "radix": true, 82 | "semicolon": [ 83 | true, 84 | "always" 85 | ], 86 | "triple-equals": [ 87 | true, 88 | "allow-null-check" 89 | ], 90 | "typedef-whitespace": [ 91 | true, 92 | { 93 | "call-signature": "nospace", 94 | "index-signature": "nospace", 95 | "parameter": "nospace", 96 | "property-declaration": "nospace", 97 | "variable-declaration": "nospace" 98 | } 99 | ], 100 | "typeof-compare": true, 101 | "unified-signatures": true, 102 | "variable-name": false, 103 | "whitespace": [ 104 | true, 105 | "check-branch", 106 | "check-decl", 107 | "check-operator", 108 | "check-separator", 109 | "check-type" 110 | ], 111 | "directive-selector": [ 112 | false, 113 | "attribute", 114 | "app", 115 | "camelCase" 116 | ], 117 | "component-selector": [ 118 | false, 119 | "element", 120 | "app", 121 | "kebab-case" 122 | ], 123 | "use-input-property-decorator": true, 124 | "use-output-property-decorator": true, 125 | "use-host-property-decorator": false, 126 | "no-input-rename": false, 127 | "no-output-rename": true, 128 | "use-life-cycle-interface": true, 129 | "use-pipe-transform-interface": true, 130 | "component-class-suffix": true, 131 | "directive-class-suffix": true, 132 | "no-access-missing-member": true, 133 | "templates-use-public": true, 134 | "invoke-injectable": true 135 | } 136 | } 137 | --------------------------------------------------------------------------------