├── .compodocrc.json ├── .cz-config.js ├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── 01-bug.md │ ├── 02-feature_request.md │ ├── 03-documentation.md │ ├── 04-support.md │ └── 05-other.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmignore ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc.js ├── .release-it.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── RELEASE.md ├── angular.json ├── base.spec.ts ├── commitlint.config.js ├── demo-app ├── ng15 │ ├── .browserslistrc │ ├── .editorconfig │ ├── .eslintrc.json │ ├── .gitignore │ ├── .npmrc │ ├── README.md │ ├── angular.json │ ├── e2e │ │ ├── protractor.conf.js │ │ ├── src │ │ │ ├── app.e2e-spec.ts │ │ │ └── app.po.ts │ │ └── tsconfig.json │ ├── karma.conf.js │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── app │ │ │ ├── app-routing.module.ts │ │ │ ├── app.component.html │ │ │ ├── app.component.scss │ │ │ ├── app.component.spec.ts │ │ │ ├── app.component.ts │ │ │ ├── app.module.ts │ │ │ ├── components │ │ │ │ ├── card │ │ │ │ │ ├── card.component.html │ │ │ │ │ ├── card.component.scss │ │ │ │ │ ├── card.component.ts │ │ │ │ │ ├── card.theme.scss │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── language-selector │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── language-selector.component.html │ │ │ │ │ ├── language-selector.component.scss │ │ │ │ │ ├── language-selector.component.ts │ │ │ │ │ └── language-selector.theme.scss │ │ │ │ ├── navigation │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── navigation.component.html │ │ │ │ │ ├── navigation.component.scss │ │ │ │ │ ├── navigation.component.ts │ │ │ │ │ └── navigation.theme.scss │ │ │ │ ├── simple-form-error │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── simple-form-error.component.html │ │ │ │ │ └── simple-form-error.component.ts │ │ │ │ └── translated-form-error │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── translated-form-error.component.html │ │ │ │ │ └── translated-form-error.component.ts │ │ │ ├── pages │ │ │ │ ├── index.ts │ │ │ │ ├── ngx-forms-example │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── ngx-forms-example.component.html │ │ │ │ │ ├── ngx-forms-example.component.scss │ │ │ │ │ └── ngx-forms-example.component.ts │ │ │ │ ├── reactive-forms-example │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── reactive-forms-example.component.html │ │ │ │ │ ├── reactive-forms-example.component.scss │ │ │ │ │ └── reactive-forms-example.component.ts │ │ │ │ ├── template-driven-forms-example │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── template-driven-forms-example.component.html │ │ │ │ │ ├── template-driven-forms-example.component.scss │ │ │ │ │ └── template-driven-forms-example.component.ts │ │ │ │ └── typed-reactive-forms-example │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── matching-password.ts │ │ │ │ │ ├── typed-reactive-forms-example.component.html │ │ │ │ │ ├── typed-reactive-forms-example.component.scss │ │ │ │ │ └── typed-reactive-forms-example.component.ts │ │ │ ├── parent-error-state-matcher.ts │ │ │ ├── password-validator.ts │ │ │ └── translation.config.ts │ │ ├── assets │ │ │ ├── img │ │ │ │ └── github-icon.svg │ │ │ └── translations │ │ │ │ ├── en.json │ │ │ │ ├── fr.json │ │ │ │ └── nl.json │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── main.ts │ │ ├── polyfills.ts │ │ ├── styles │ │ │ ├── _app.theme.scss │ │ │ ├── _variables.scss │ │ │ └── styles.scss │ │ └── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ └── tsconfig.spec.json └── ng16 │ ├── .browserslistrc │ ├── .editorconfig │ ├── .eslintrc.json │ ├── .gitignore │ ├── .npmrc │ ├── README.md │ ├── angular.json │ ├── e2e │ ├── protractor.conf.js │ ├── src │ │ ├── app.e2e-spec.ts │ │ └── app.po.ts │ └── tsconfig.json │ ├── karma.conf.js │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── app │ │ ├── app-routing.module.ts │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── components │ │ │ ├── card │ │ │ │ ├── card.component.html │ │ │ │ ├── card.component.scss │ │ │ │ ├── card.component.ts │ │ │ │ ├── card.theme.scss │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── language-selector │ │ │ │ ├── index.ts │ │ │ │ ├── language-selector.component.html │ │ │ │ ├── language-selector.component.scss │ │ │ │ ├── language-selector.component.ts │ │ │ │ └── language-selector.theme.scss │ │ │ ├── navigation │ │ │ │ ├── index.ts │ │ │ │ ├── navigation.component.html │ │ │ │ ├── navigation.component.scss │ │ │ │ ├── navigation.component.ts │ │ │ │ └── navigation.theme.scss │ │ │ ├── simple-form-error │ │ │ │ ├── index.ts │ │ │ │ ├── simple-form-error.component.html │ │ │ │ └── simple-form-error.component.ts │ │ │ └── translated-form-error │ │ │ │ ├── index.ts │ │ │ │ ├── translated-form-error.component.html │ │ │ │ └── translated-form-error.component.ts │ │ ├── pages │ │ │ ├── index.ts │ │ │ ├── ngx-forms-example │ │ │ │ ├── index.ts │ │ │ │ ├── ngx-forms-example.component.html │ │ │ │ ├── ngx-forms-example.component.scss │ │ │ │ └── ngx-forms-example.component.ts │ │ │ ├── reactive-forms-example │ │ │ │ ├── index.ts │ │ │ │ ├── reactive-forms-example.component.html │ │ │ │ ├── reactive-forms-example.component.scss │ │ │ │ └── reactive-forms-example.component.ts │ │ │ ├── template-driven-forms-example │ │ │ │ ├── index.ts │ │ │ │ ├── template-driven-forms-example.component.html │ │ │ │ ├── template-driven-forms-example.component.scss │ │ │ │ └── template-driven-forms-example.component.ts │ │ │ └── typed-reactive-forms-example │ │ │ │ ├── index.ts │ │ │ │ ├── matching-password.ts │ │ │ │ ├── typed-reactive-forms-example.component.html │ │ │ │ ├── typed-reactive-forms-example.component.scss │ │ │ │ └── typed-reactive-forms-example.component.ts │ │ ├── parent-error-state-matcher.ts │ │ ├── password-validator.ts │ │ └── translation.config.ts │ ├── assets │ │ ├── img │ │ │ └── github-icon.svg │ │ └── translations │ │ │ ├── en.json │ │ │ ├── fr.json │ │ │ └── nl.json │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles │ │ ├── _app.theme.scss │ │ ├── _variables.scss │ │ └── styles.scss │ └── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ └── tsconfig.spec.json ├── docs ├── DEV_GUIDE.md └── summary.json ├── karma.conf.js ├── ng-package.json ├── package-lock.json ├── package.json ├── public_api.ts ├── release-publish.sh ├── scripts ├── ci │ ├── _ghactions-group.sh │ └── print-logs.sh └── helpers.js ├── src ├── directives.ts ├── directives │ ├── form-errors-group.directive.spec.ts │ ├── form-errors-group.directive.ts │ ├── form-errors.directive.spec.ts │ └── form-errors.directive.ts ├── form-error-component.intf.ts ├── form-error.intf.ts ├── form-errors-config.intf.ts ├── form-errors.module.ts ├── ngx-form-errors.ts ├── services.ts └── services │ ├── form-errors-message.service.spec.ts │ └── form-errors-message.service.ts ├── stylelint.config.js ├── tsconfig.json ├── tsconfig.lib.json ├── tsconfig.lib.prod.json ├── tsconfig.spec.json └── util-functions.sh /.compodocrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@compodoc/compodoc/src/config/schema.json", 3 | "theme": "material", 4 | "tsconfig": "./tsconfig.json", 5 | "output": "./reports/api-docs/ngx-form-errors", 6 | "includes": "./docs", 7 | "includesName": "Developer Guide" 8 | } 9 | -------------------------------------------------------------------------------- /.cz-config.js: -------------------------------------------------------------------------------- 1 | const commitLintConfig = require("./commitlint.config"); 2 | const generateScopes = () => { 3 | let rules = []; 4 | for (rule of commitLintConfig.rules["scope-enum"][2]) { 5 | rules.push({ name: rule }); 6 | } 7 | return rules; 8 | }; 9 | 10 | module.exports = { 11 | //See here for options details https://github.com/leonardoanalista/cz-customizable#options 12 | types: [ 13 | { value: "feat", name: "feat: A new feature" }, 14 | { value: "fix", name: "fix: A bug fix" }, 15 | { value: "docs", name: "docs: Documentation only changes" }, 16 | { 17 | value: "style", 18 | name: "style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)" 19 | }, 20 | { value: "refactor", name: "refactor: A code change that neither fixes a bug nor adds a feature" }, 21 | { value: "perf", name: "perf: A code change that improves performance" }, 22 | { value: "test", name: "test: Adding missing tests or correcting existing tests" }, 23 | { 24 | value: "build", 25 | name: "build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)" 26 | }, 27 | { 28 | value: "ci", 29 | name: "ci: Changes to our CI configuration files and scripts (example scopes: GitHub Actions, Travis, Circle, BrowserStack, SauceLabs)" 30 | }, 31 | { 32 | value: "chore", 33 | name: "chore: Changes to the build process or auxiliary tools and libraries such as documentation generation" 34 | }, 35 | { value: "revert", name: "revert: Reverts a previous commit" } 36 | ], 37 | scopes: generateScopes() 38 | }; 39 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs. 2 | # More information at http://editorconfig.org 3 | 4 | # top-most EditorConfig file 5 | root = true 6 | 7 | # Unix-style newlines with a newline ending every file. 8 | # Tab for indentation, whitespace trimming and UTF-8 encoding 9 | [*] 10 | end_of_line = lf 11 | insert_final_newline = true 12 | indent_style = tab 13 | trim_trailing_whitespace = false 14 | charset = utf-8 15 | 16 | [*.bat] 17 | end_of_line = crlf 18 | 19 | [**.{css,pcss,scss,json,sh,yml}] 20 | indent_style = space 21 | indent_size = 2 22 | 23 | [*.md] 24 | insert_final_newline = false 25 | trim_trailing_whitespace = false 26 | 27 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["@nationalbankbelgium"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "rules": { 8 | "@angular-eslint/component-selector": [ 9 | "error", 10 | { 11 | "prefix": "ngx-form-errors", 12 | "style": "kebab-case", 13 | "type": "element" 14 | } 15 | ], 16 | "@angular-eslint/directive-selector": [ 17 | "error", 18 | { 19 | "prefix": "ngxFormErrors", 20 | "style": "camelCase", 21 | "type": "attribute" 22 | } 23 | ] 24 | } 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # JS and TS files must always use LF for tools to work 5 | *.js eol=lf 6 | *.ts eol=lf 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/01-bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '🐛 Bug Report' 3 | about: Report a bug or regression 4 | title: '' 5 | labels: 'bug' 6 | --- 7 | 8 | # 🐛 Bug Report 9 | 10 | ## Is this a regression? 11 | 12 | 13 |

14 | [ ] No
15 | [ ] Yes, the bug was not present in version: ... 
16 | 
17 | 18 | ## Current behavior 19 | 20 | 21 | 22 | ## Expected behavior 23 | 24 | 25 | 26 | ## 🔬 Minimal reproduction of the problem with instructions 27 | 28 | 32 | 33 | ## Environment 34 | 35 |

36 | Angular version: X.Y.Z
37 | NgxFormErrors version: X.Y.Z
38 | 
39 | 
40 | Browser:
41 | - [ ] Chrome (desktop) version XX
42 | - [ ] Chrome (Android) version XX
43 | - [ ] Chrome (iOS) version XX
44 | - [ ] Firefox version XX
45 | - [ ] Safari (desktop) version XX
46 | - [ ] Safari (iOS) version XX
47 | - [ ] IE version XX
48 | - [ ] Edge version XX
49 |  
50 | For Tooling issues:
51 | - Node version: XX  
52 | - Platform:  
53 | 
54 | Others:
55 | 
56 | 
-------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/02-feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '🚀 Feature request' 3 | about: Request a new feature 4 | title: '' 5 | labels: 'enhancement' 6 | --- 7 | 8 | # 🚀 Feature request 9 | 10 | ## Current behavior 11 | 12 | 13 | 14 | ## Expected behavior 15 | 16 | 17 | 18 | ## What is the motivation / use case for changing the behavior? 19 | 20 | 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/03-documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '📄 Documentation' 3 | about: Documentation issue or request 4 | title: '' 5 | labels: 'documentation' 6 | --- 7 | 8 | # 📄 Documentation 9 | 10 | ## Is this a regression? 11 | 12 | 13 |

14 | [ ] No
15 | [ ] Yes, the issue was not present in version: ... 
16 | 
17 | 18 | ## Current behavior 19 | 20 | 21 | 22 | ## Expected behavior 23 | 24 | 25 | 26 | ## What is the motivation / use case for changing the behavior? 27 | 28 | 29 | 30 | ## Environment 31 | 32 |

33 | Angular version: X.Y.Z
34 | NgxFormErrors version: X.Y.Z
35 | 
36 | 
37 | Browser:
38 | - [ ] Chrome (desktop) version XX
39 | - [ ] Chrome (Android) version XX
40 | - [ ] Chrome (iOS) version XX
41 | - [ ] Firefox version XX
42 | - [ ] Safari (desktop) version XX
43 | - [ ] Safari (iOS) version XX
44 | - [ ] IE version XX
45 | - [ ] Edge version XX
46 |  
47 | For Tooling issues:
48 | - Node version: XX  
49 | - Platform:  
50 | 
51 | Others:
52 | 
53 | 
54 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/04-support.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '🚑 Support' 3 | about: Questions and requests for support 4 | title: '' 5 | labels: 'question' 6 | --- 7 | 8 | 🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑 9 | 10 | Please do not file questions or support requests on the GitHub issues tracker. 11 | 12 | You can get your questions answered using other communication channels. Please see: https://github.com/NationalBankBelgium/ngx-form-errors/blob/master/CONTRIBUTING.md#got-a-question-or-problem 13 | 14 | Thank you! 15 | 16 | 🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑🛑 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/05-other.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '💭 Other' 3 | about: Issues that don't fit under anything else 4 | title: '' 5 | labels: '' 6 | --- 7 | 8 | ## What? 9 | 10 | ## When? 11 | 12 | ## How? -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## PR Checklist 2 | 3 | Please check if your PR fulfills the following requirements: 4 | 5 | - [ ] The commit message follows our guidelines: https://github.com/NationalBankBelgium/ngx-form-errors/blob/master/CONTRIBUTING.md#-commit-message-guidelines 6 | - [ ] Tests for the changes have been added (for bug fixes / features) 7 | - [ ] Docs have been added / updated (for bug fixes / features) 8 | 9 | ## PR Type 10 | 11 | What kind of change does this PR introduce? 12 | 13 | 14 | 15 | ``` 16 | [ ] Bugfix 17 | [ ] Feature 18 | [ ] Code style update (formatting, local variables) 19 | [ ] Refactoring (no functional changes, no api changes) 20 | [ ] Build related changes 21 | [ ] CI related changes 22 | [ ] Documentation content changes 23 | [ ] Other... Please describe: 24 | ``` 25 | 26 | ## What is the current behavior? 27 | 28 | 29 | 30 | Issue Number: N/A 31 | 32 | ## What is the new behavior? 33 | 34 | ## Does this PR introduce a breaking change? 35 | 36 | ``` 37 | [ ] Yes 38 | [ ] No 39 | ``` 40 | 41 | 42 | 43 | ## Other information 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #cache angular 2 | /.angular/cache 3 | # Build 4 | dist/ 5 | /dist/ 6 | .awcache/ 7 | .tmp/ 8 | /.tmp/ 9 | tmp/ 10 | 11 | # Reports directory 12 | reports/ 13 | 14 | # Logs 15 | logs/ 16 | *.log 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | 26 | # Bak 27 | *.bak 28 | 29 | # Node 30 | node_modules/ 31 | npm-debug.log 32 | /npm-debug.log.* 33 | 34 | # OS generated files # 35 | Desktop.ini 36 | Thumbs.db 37 | .DS_Store 38 | ehthumbs.db 39 | Icon? 40 | 41 | # JetBrains IDEs 42 | *.iml 43 | .idea/ 44 | .webstorm/ 45 | *.swp 46 | 47 | # IDE - VSCode 48 | .vscode/* 49 | !.vscode/settings.json 50 | !.vscode/tasks.json 51 | !.vscode/launch.json 52 | !.vscode/extensions.json 53 | 54 | # Sublime text 55 | .sublime-gulp.cache 56 | 57 | # Runtime data 58 | pids 59 | *.pid 60 | *.seed 61 | 62 | # Patch files 63 | *.patch 64 | 65 | # Bash 66 | bash.exe.stackdump 67 | 68 | # Angular # 69 | *.ngfactory.ts 70 | *.css.shim.ts 71 | *.ngsummary.json 72 | *.shim.ngstyle.ts 73 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | lint-staged && npm run docs:coverage 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Build 2 | dist/ 3 | .awcache/ 4 | /dist/ 5 | .tmp/ 6 | /.tmp/ 7 | tmp/ 8 | demo-app/ 9 | 10 | # Reports directory 11 | reports/ 12 | 13 | # Logs 14 | logs/ 15 | *.log 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # Bak 26 | *.bak 27 | 28 | # Node 29 | node_modules/ 30 | npm-debug.log 31 | /npm-debug.log.* 32 | 33 | # OS generated files # 34 | Desktop.ini 35 | Thumbs.db 36 | .DS_Store 37 | ehthumbs.db 38 | Icon? 39 | 40 | # JetBrains IDEs 41 | *.iml 42 | .idea/ 43 | .webstorm/ 44 | *.swp 45 | 46 | # IDE - VSCode 47 | .vscode/* 48 | !.vscode/settings.json 49 | !.vscode/tasks.json 50 | !.vscode/launch.json 51 | !.vscode/extensions.json 52 | 53 | # Sublime text 54 | .sublime-gulp.cache 55 | 56 | # Runtime data 57 | pids 58 | *.pid 59 | *.seed 60 | 61 | # Patch files 62 | *.patch 63 | 64 | # Bash 65 | bash.exe.stackdump 66 | 67 | # Angular # 68 | *.ngfactory.ts 69 | *.css.shim.ts 70 | *.ngsummary.json 71 | *.shim.ngstyle.ts 72 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12.22.1 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .angular/ 2 | .git/ 3 | .idea/ 4 | .vscode/ 5 | dist/ 6 | coverage/ 7 | reports/ 8 | node_modules/ 9 | CHANGELOG.md 10 | package.json 11 | package-lock.json 12 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@nationalbankbelgium/code-style/prettier/3.1.x"); 2 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "ci": false, 3 | "dry-run": false, 4 | "verbose": false, 5 | "force": false, 6 | "disable-metrics": true, 7 | "hooks": { 8 | "after:bump": "npm run generate:changelog" 9 | }, 10 | "plugins": { 11 | "@release-it/conventional-changelog": { 12 | "preset": "angular" 13 | } 14 | }, 15 | "git": { 16 | "changelog": "npm run generate:changelog-recent", 17 | "requireCleanWorkingDir": true, 18 | "requireUpstream": true, 19 | "requireCommits": false, 20 | "commit": true, 21 | "commitMessage": "chore(release): release ${version}", 22 | "commitArgs": "", 23 | "tag": true, 24 | "tagName": "${version}", 25 | "tagAnnotation": "${version}", 26 | "push": true, 27 | "pushArgs": ["--follow-tags"], 28 | "pushRepo": "origin" 29 | }, 30 | "npm": { 31 | "publish": false 32 | }, 33 | "github": { 34 | "release": true, 35 | "releaseName": "${version}", 36 | "draft": false, 37 | "tokenRef": "GITHUB_TOKEN", 38 | "assets": null, 39 | "host": null 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of NgxFormErrors project, we pledge to respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities. 4 | 5 | Communication through any of NgxFormErrors's channels (GitHub, Twitter ,Slack, etc.) must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 6 | 7 | We promise to extend courtesy and respect to everyone involved in this project regardless of gender, gender identity, sexual orientation, disability, age, race, ethnicity, religion, or level of experience. We expect anyone contributing to the NgxFormErrors project to do the same. 8 | 9 | If any member of the community violates this code of conduct, the maintainers of the NgxFormErrors project may take action, removing issues, comments, and PRs or blocking accounts as deemed appropriate. 10 | 11 | If you are subject to or witness unacceptable behavior, or have any other concerns, please email us at [alexis.georges@nbb.be](mailto:alexis.georges@nbb.be). 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-2019 National Bank of Belgium 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 | 23 | Except as contained in this notice, the name(s) of the above copyright holders shall not be used in 24 | advertising or otherwise to promote the sale, use or other dealings in this Software without prior written 25 | authorization. 26 | 27 | Jurisdiction : 28 | Any litigation arising between the licensor and the licensee, resulting from the interpretation 29 | of this License, will be subject to the exclusive jurisdiction of the Belgian courts. 30 | Applicable law : 31 | This License shall be governed by the Belgian law, being the law of the Member State 32 | of the European Union where the Licensor has settled its seat. 33 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Releasing NgxFormErrors 2 | 3 | ## Pre-reqs 4 | 5 | ### Local 6 | 7 | On your local machine, you must configure the `GITHUB_TOKEN` environment variable. 8 | It will be used by release-it to push to and create the release page on GitHub (cfr release:prepare section below). 9 | 10 | ### GitHub Actions 11 | 12 | On GitHub Actions, the following should be configured: 13 | 14 | - NPM_TOKEN secret variable 15 | - if 2FA is enabled for the account the only auth-only level can be used: https://docs.npmjs.com/getting-started/using-two-factor-authentication#levels-of-authentication 16 | - that variable MUST NEVER be logged/exposed. If exposed then the token MUST be revoked and the account password changed ASAP 17 | 18 | ## Changelog 19 | 20 | First of all: _Never_ edit CHANGELOG.md manually! 21 | 22 | The changelog will be updated automatically as part of the release process and based on the commit log using conventional-changelog (https://github.com/conventional-changelog) 23 | We use the Angular format for our changelog and for it to work properly, please make sure to respect our commit conventions (see CONTRIBUTING guide). 24 | 25 | ## Creating a release 26 | 27 | Make sure that: 28 | 29 | - all changes have merged into master 30 | - everything is up to date locally 31 | - everything is clean locally 32 | - execute `npm run release` 33 | 34 | Enjoy the show. 35 | 36 | ## Publishing the release on npm 37 | 38 | Once you have pushed the tag, GitHub Actions will handle things from there. 39 | 40 | Once done, you must make sure that the tags are adapted so that the "latest" tag still points to what we consider the latest (i.e., next major/minor)! 41 | Refer to the "Adapting tags of published packages" section below. 42 | 43 | ## What happens once a release is triggered 44 | 45 | ### release 46 | 47 | - first we make sure that there are no local changes (if there are we stop right there) 48 | - then we execute release-it: https://github.com/webpro/release-it which 49 | - bumps the version in the root package.json automatically (determines the bump type to use depending on the commit message logs) 50 | - that version number will be used as basis in the build to adapt all other package.json files 51 | - generates/updates the CHANGELOG.md file using: conventional-changelog: https://github.com/conventional-changelog 52 | - commits both package.json and CHANGELOG.md 53 | - creates a new git tag and pushes it 54 | - creates a github release page and makes it final 55 | 56 | After this, the release is tagged and visible on github 57 | 58 | ### npm packages publish 59 | 60 | Finally, GitHub Actions executes `npm run release:publish`. 61 | 62 | That script makes some checks then, if all succeed it publishes the different packages on npm. 63 | Checks that are performed: 64 | 65 | - NPM_TOKEN environment variable should be defined 66 | - GITHUB_REPOSITORY should be "NationalBankBelgium/ngx-form-errors" 67 | 68 | ## Adapting tags of published packages 69 | 70 | If a published version doesn't have all necessary tags, or if we want to adapt those for some reason (e.g., latest pointing to a patch release rather than the latest major/minor), then we can use the `npm dist-tag` command. 71 | Reference: https://docs.npmjs.com/cli/dist-tag 72 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ngx-form-errors": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "library", 10 | "prefix": "ngx-form-errors", 11 | "architect": { 12 | "build": { 13 | "builder": "@angular-devkit/build-angular:ng-packagr", 14 | "options": { 15 | "tsConfig": "./tsconfig.lib.json", 16 | "project": "./ng-package.json" 17 | }, 18 | "configurations": { 19 | "production": { 20 | "tsConfig": "tsconfig.lib.prod.json" 21 | } 22 | } 23 | }, 24 | "test": { 25 | "builder": "@angular-devkit/build-angular:karma", 26 | "options": { 27 | "main": "./base.spec.ts", 28 | "tsConfig": "./tsconfig.spec.json", 29 | "karmaConfig": "./karma.conf.js" 30 | } 31 | }, 32 | "lint": { 33 | "builder": "@angular-eslint/builder:lint", 34 | "options": { 35 | "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"] 36 | } 37 | } 38 | } 39 | } 40 | }, 41 | "cli": { 42 | "schematicCollections": ["@angular-eslint/schematics"], 43 | "analytics": false 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /base.spec.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* tslint:disable:no-import-side-effect */ 4 | import "core-js/es6"; 5 | import "core-js/es7/reflect"; 6 | import "core-js/stage/4"; 7 | 8 | import "zone.js"; 9 | import "zone.js/dist/long-stack-trace-zone"; 10 | import "zone.js/dist/proxy"; // since zone.js 0.6.15 11 | import "zone.js/dist/sync-test"; 12 | import "zone.js/dist/jasmine-patch"; // put here since zone.js 0.6.14 13 | import "zone.js/dist/async-test"; 14 | import "zone.js/dist/fake-async-test"; 15 | /* tslint:enable */ 16 | 17 | import { TestBed } from "@angular/core/testing"; 18 | import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from "@angular/platform-browser-dynamic/testing"; 19 | 20 | TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { teardown: { destroyAfterEach: false } }); 21 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | 4 | //See here for the rules definition : https://github.com/marionebl/commitlint/blob/master/docs/reference-rules.md 5 | rules: { 6 | "header-max-length": [1, "always", 100], 7 | "scope-enum": [ 8 | 2, 9 | "always", 10 | ["accessibility", "build", "developer-guide", "docs", "qa", "release", "all", "service", "directive", "demo"] 11 | ], 12 | "scope-case": [2, "always", "lowerCase"] 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /demo-app/ng15/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. 13 | -------------------------------------------------------------------------------- /demo-app/ng15/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /demo-app/ng15/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["@nationalbankbelgium"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "rules": { 8 | "@angular-eslint/component-selector": [ 9 | "error", 10 | { 11 | "prefix": "app", 12 | "style": "kebab-case", 13 | "type": "element" 14 | } 15 | ], 16 | "@angular-eslint/directive-selector": [ 17 | "error", 18 | { 19 | "prefix": "app", 20 | "style": "camelCase", 21 | "type": "attribute" 22 | } 23 | ] 24 | } 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /demo-app/ng15/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.angular/cache 36 | /.sass-cache 37 | /connect.lock 38 | /coverage 39 | /libpeerconnection.log 40 | npm-debug.log 41 | yarn-error.log 42 | testem.log 43 | /typings 44 | 45 | # System Files 46 | .DS_Store 47 | Thumbs.db 48 | -------------------------------------------------------------------------------- /demo-app/ng15/.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org 2 | -------------------------------------------------------------------------------- /demo-app/ng15/README.md: -------------------------------------------------------------------------------- 1 | # DemoApp 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 13.4.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|guard|interface|enum|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 | 25 | ## Further help 26 | 27 | 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). 28 | -------------------------------------------------------------------------------- /demo-app/ng15/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "demo-app": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "style": "scss" 14 | } 15 | }, 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/demo-app", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "tsconfig.app.json", 25 | "assets": ["src/favicon.ico", "src/assets"], 26 | "styles": ["node_modules/normalize.css/normalize.css", "src/styles/styles.scss"], 27 | "stylePreprocessorOptions": { 28 | "includePaths": ["src/styles"] 29 | }, 30 | "scripts": [], 31 | "vendorChunk": true, 32 | "extractLicenses": false, 33 | "buildOptimizer": false, 34 | "sourceMap": true, 35 | "optimization": false, 36 | "namedChunks": true 37 | }, 38 | "configurations": { 39 | "production": { 40 | "fileReplacements": [ 41 | { 42 | "replace": "src/environments/environment.ts", 43 | "with": "src/environments/environment.prod.ts" 44 | } 45 | ], 46 | "optimization": true, 47 | "outputHashing": "all", 48 | "sourceMap": false, 49 | "namedChunks": false, 50 | "extractLicenses": true, 51 | "vendorChunk": false, 52 | "buildOptimizer": true, 53 | "budgets": [ 54 | { 55 | "type": "initial", 56 | "maximumWarning": "2mb", 57 | "maximumError": "5mb" 58 | }, 59 | { 60 | "type": "anyComponentStyle", 61 | "maximumWarning": "6kb", 62 | "maximumError": "10kb" 63 | } 64 | ] 65 | } 66 | }, 67 | "defaultConfiguration": "" 68 | }, 69 | "serve": { 70 | "builder": "@angular-devkit/build-angular:dev-server", 71 | "options": { 72 | "browserTarget": "demo-app:build" 73 | }, 74 | "configurations": { 75 | "production": { 76 | "browserTarget": "demo-app:build:production" 77 | } 78 | } 79 | }, 80 | "extract-i18n": { 81 | "builder": "@angular-devkit/build-angular:extract-i18n", 82 | "options": { 83 | "browserTarget": "demo-app:build" 84 | } 85 | }, 86 | "test": { 87 | "builder": "@angular-devkit/build-angular:karma", 88 | "options": { 89 | "main": "src/test.ts", 90 | "polyfills": "src/polyfills.ts", 91 | "tsConfig": "tsconfig.spec.json", 92 | "karmaConfig": "karma.conf.js", 93 | "styles": ["node_modules/normalize.css/normalize.css", "src/styles/styles.scss"], 94 | "stylePreprocessorOptions": { 95 | "includePaths": ["src/styles"] 96 | }, 97 | "scripts": [], 98 | "assets": ["src/favicon.ico", "src/assets"] 99 | } 100 | }, 101 | "lint": { 102 | "builder": "@angular-eslint/builder:lint", 103 | "options": { 104 | "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"] 105 | } 106 | }, 107 | "e2e": { 108 | "builder": "@angular-devkit/build-angular:protractor", 109 | "options": { 110 | "protractorConfig": "e2e/protractor.conf.js", 111 | "devServerTarget": "demo-app:serve" 112 | }, 113 | "configurations": { 114 | "production": { 115 | "devServerTarget": "demo-app:serve:production" 116 | } 117 | } 118 | } 119 | } 120 | } 121 | }, 122 | "cli": { 123 | "schematicCollections": ["@angular-eslint/schematics"], 124 | "analytics": false 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /demo-app/ng15/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require("jasmine-spec-reporter"); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: ["./src/**/*.e2e-spec.ts"], 13 | capabilities: { 14 | browserName: "chrome" 15 | }, 16 | directConnect: true, 17 | baseUrl: "http://localhost:4200/", 18 | framework: "jasmine", 19 | jasmineNodeOpts: { 20 | showColors: true, 21 | defaultTimeoutInterval: 30000, 22 | print: function () {} 23 | }, 24 | onPrepare() { 25 | require("ts-node").register({ 26 | project: require("path").join(__dirname, "./tsconfig.json") 27 | }); 28 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /demo-app/ng15/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from "./app.po"; 2 | import { browser, logging } from "protractor"; 3 | 4 | describe("workspace-project App", () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it("should display welcome message", async () => { 12 | await page.navigateTo(); 13 | const titleText = await page.getTitleText(); 14 | expect(titleText).toEqual("demo-app app is running!"); 15 | }); 16 | 17 | afterEach(async () => { 18 | // Assert that there are no errors emitted from the browser 19 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 20 | const expectedLogEntry: Partial = { 21 | level: logging.Level.SEVERE 22 | }; 23 | expect(logs).not.toContain(jasmine.objectContaining(expectedLogEntry)); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /demo-app/ng15/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from "protractor"; 2 | 3 | export class AppPage { 4 | public navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | public getTitleText() { 9 | return element(by.css("app-root .content span")).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /demo-app/ng15/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es2018", 7 | "types": ["jasmine", "jasminewd2", "node"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /demo-app/ng15/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: "", 7 | frameworks: ["jasmine", "@angular-devkit/build-angular"], 8 | plugins: [ 9 | require("karma-jasmine"), 10 | require("karma-chrome-launcher"), 11 | require("karma-jasmine-html-reporter"), 12 | require("karma-coverage-istanbul-reporter"), 13 | require("@angular-devkit/build-angular/plugins/karma") 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require("path").join(__dirname, "./coverage"), 20 | reports: ["html", "lcovonly", "text-summary"], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ["progress", "kjhtml"], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ["Chrome"], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /demo-app/ng15/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-app", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "clean": "npx rimraf ./dist ./reports", 6 | "clean:modules": "npx rimraf ./node_modules package-lock.json", 7 | "ng": "ng", 8 | "start": "ng serve", 9 | "build": "ng build", 10 | "test": "ng test", 11 | "test:ci": "ng test --watch=false --browsers=ChromeHeadless", 12 | "lint": "ng lint", 13 | "e2e": "ng e2e" 14 | }, 15 | "private": true, 16 | "dependencies": { 17 | "@angular/animations": "^15.2.10", 18 | "@angular/cdk": "^15.2.9", 19 | "@angular/common": "^15.2.10", 20 | "@angular/compiler": "^15.2.10", 21 | "@angular/core": "^15.2.10", 22 | "@angular/forms": "^15.2.10", 23 | "@angular/material": "^15.2.9", 24 | "@angular/platform-browser": "^15.2.10", 25 | "@angular/platform-browser-dynamic": "^15.2.10", 26 | "@angular/router": "^15.2.10", 27 | "@nationalbankbelgium/ngx-form-errors": "../../dist", 28 | "@ngx-translate/core": "^11.0.1", 29 | "core-js": "^3.3.5", 30 | "eligrey-classlist-js-polyfill": "1.2.20180112", 31 | "material-design-icons": "^3.0.1", 32 | "normalize.css": "^8.0.1", 33 | "rxjs": "^7.8.1", 34 | "tslib": "^2.0.0", 35 | "zone.js": "~0.11.4" 36 | }, 37 | "devDependencies": { 38 | "@angular-devkit/build-angular": "15.2.11", 39 | "@angular/cli": "^15.2.11", 40 | "@angular/compiler-cli": "^15.2.10", 41 | "@angular/language-service": "^15.2.10", 42 | "@nationalbankbelgium/code-style": "^1.9.0", 43 | "@nationalbankbelgium/eslint-config": "15.0.1", 44 | "@types/jasmine": "^3.3.8", 45 | "@types/jasminewd2": "^2.0.3", 46 | "@types/node": "^12.11.1", 47 | "jasmine-core": "~3.8.0", 48 | "jasmine-spec-reporter": "~5.0.0", 49 | "karma": "~6.4.2", 50 | "karma-chrome-launcher": "~3.1.0", 51 | "karma-coverage-istanbul-reporter": "~3.0.2", 52 | "karma-jasmine": "~4.0.0", 53 | "karma-jasmine-html-reporter": "^1.5.0", 54 | "protractor": "~7.0.0", 55 | "ts-node": "~7.0.0", 56 | "tslint-config-prettier": "^1.17.0", 57 | "typescript": "~4.9.5" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { RouterModule, Routes } from "@angular/router"; 3 | import { 4 | NgxFormsExampleComponent, 5 | ReactiveFormsExampleComponent, 6 | TemplateDrivenFormsExampleComponent, 7 | TypedReactiveFormsExampleComponent 8 | } from "./pages"; 9 | 10 | const routes: Routes = [ 11 | { path: "", redirectTo: "/template-driven-forms", pathMatch: "full" }, 12 | { path: "reactive-forms", component: ReactiveFormsExampleComponent }, 13 | { path: "template-driven-forms", component: TemplateDrivenFormsExampleComponent }, 14 | { path: "ngx-form-errors", component: NgxFormsExampleComponent }, 15 | { path: "typed-reactive-forms", component: TypedReactiveFormsExampleComponent } 16 | ]; 17 | 18 | @NgModule({ 19 | imports: [RouterModule.forRoot(routes)], 20 | exports: [RouterModule] 21 | }) 22 | export class AppRoutingModule {} 23 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 5 |

Ngx-Form-Errors

6 |

 - Validation messages in Reactive Forms made easy

7 |
8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | mat-toolbar { 2 | .slogan { 3 | font-size: 16px; 4 | font-style: italic; 5 | line-height: normal; 6 | } 7 | 8 | .spacer { 9 | flex: 1 1 auto; 10 | } 11 | } 12 | 13 | mat-sidenav-container { 14 | flex: 100% 1; 15 | 16 | mat-sidenav { 17 | max-height: 100%; 18 | overflow-y: auto; 19 | } 20 | 21 | mat-sidenav-content { 22 | max-height: 100%; 23 | overflow-y: auto; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; 2 | import { AppComponent } from "./app.component"; 3 | import { NO_ERRORS_SCHEMA } from "@angular/core"; 4 | import { RouterTestingModule } from "@angular/router/testing"; 5 | 6 | describe("AppComponent", () => { 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | return TestBed.configureTestingModule({ 11 | imports: [RouterTestingModule], 12 | declarations: [AppComponent], 13 | schemas: [NO_ERRORS_SCHEMA] 14 | }).compileComponents(); 15 | })); 16 | 17 | beforeEach(() => { 18 | fixture = TestBed.createComponent(AppComponent); 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it("should create the app", () => { 23 | const app: AppComponent = fixture.debugElement.componentInstance; 24 | expect(app).toBeTruthy(); 25 | }); 26 | 27 | it("should render title in a h1 tag", () => { 28 | fixture.detectChanges(); 29 | const compiled: HTMLElement = fixture.debugElement.nativeElement; 30 | const h1Element = compiled.querySelector("h1"); 31 | expect(h1Element).toBeTruthy(); 32 | // tslint:disable-next-line:no-non-null-assertion 33 | expect(h1Element!.textContent).toContain("Ngx-Form-Errors"); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, ViewChild } from "@angular/core"; 2 | import { Event, NavigationEnd, Router } from "@angular/router"; 3 | import { MatSidenav } from "@angular/material/sidenav"; 4 | import { BreakpointObserver, BreakpointState } from "@angular/cdk/layout"; 5 | import { of, Subscription } from "rxjs"; 6 | 7 | const MEDIA_MATCH = "(max-width: 600px)"; 8 | 9 | @Component({ 10 | selector: "app-root", 11 | templateUrl: "./app.component.html", 12 | styleUrls: ["./app.component.scss"] 13 | }) 14 | export class AppComponent implements OnDestroy { 15 | @ViewChild("sidenav") // see https://angular.io/guide/static-query-migration 16 | // see https://angular.io/guide/static-query-migration 17 | private _sidenav!: MatSidenav; 18 | 19 | public mobileQueryMatches = false; 20 | 21 | private _routerSubscription: Subscription; 22 | private _mediaQuerySubscription: Subscription; 23 | 24 | public constructor( 25 | private _router: Router, 26 | public breakpointObserver: BreakpointObserver 27 | ) { 28 | this.mobileQueryMatches = this.breakpointObserver.isMatched(MEDIA_MATCH); 29 | 30 | this._mediaQuerySubscription = this.breakpointObserver.observe([MEDIA_MATCH]).subscribe((state: BreakpointState) => { 31 | this.mobileQueryMatches = state.matches; 32 | }); 33 | 34 | this._routerSubscription = this._router.events.subscribe((value: Event) => { 35 | if (value instanceof NavigationEnd && this._sidenav.mode === "over") { 36 | of(this._sidenav.close()).subscribe(); 37 | } 38 | }); 39 | } 40 | 41 | public ngOnDestroy(): void { 42 | this._mediaQuerySubscription.unsubscribe(); 43 | this._routerSubscription.unsubscribe(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule, DomSanitizer } from "@angular/platform-browser"; 2 | import { NgModule } from "@angular/core"; 3 | import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; 4 | import { FormsModule, ReactiveFormsModule } from "@angular/forms"; 5 | import { HttpClientModule } from "@angular/common/http"; 6 | import { TranslateModule, TranslateService } from "@ngx-translate/core"; 7 | import { MatLegacyButtonModule as MatButtonModule } from "@angular/material/legacy-button"; 8 | import { MatButtonToggleModule } from "@angular/material/button-toggle"; 9 | import { MatLegacyCardModule as MatCardModule } from "@angular/material/legacy-card"; 10 | import { MatLegacyFormFieldModule as MatFormFieldModule } from "@angular/material/legacy-form-field"; 11 | import { MatLegacyInputModule as MatInputModule } from "@angular/material/legacy-input"; 12 | import { MatLegacyListModule as MatListModule } from "@angular/material/legacy-list"; 13 | import { MatSidenavModule } from "@angular/material/sidenav"; 14 | import { MatGridListModule } from "@angular/material/grid-list"; 15 | import { MatIconModule, MatIconRegistry } from "@angular/material/icon"; 16 | import { MatToolbarModule } from "@angular/material/toolbar"; 17 | import { NgxFormErrorsMessageService, NgxFormErrorsModule } from "@nationalbankbelgium/ngx-form-errors"; 18 | import { AppComponent } from "./app.component"; 19 | import { initializeTranslation } from "./translation.config"; 20 | import { AppRoutingModule } from "./app-routing.module"; 21 | import { 22 | CardComponent, 23 | LanguageSelectorComponent, 24 | NavigationComponent, 25 | SimpleFormErrorComponent, 26 | TranslatedFormErrorComponent 27 | } from "./components"; 28 | import { 29 | NgxFormsExampleComponent, 30 | ReactiveFormsExampleComponent, 31 | TemplateDrivenFormsExampleComponent, 32 | TypedReactiveFormsExampleComponent 33 | } from "./pages"; 34 | 35 | /* eslint-disable */ 36 | @NgModule({ 37 | declarations: [ 38 | AppComponent, 39 | LanguageSelectorComponent, 40 | SimpleFormErrorComponent, 41 | TranslatedFormErrorComponent, 42 | ReactiveFormsExampleComponent, 43 | NgxFormsExampleComponent, 44 | TemplateDrivenFormsExampleComponent, 45 | TypedReactiveFormsExampleComponent, 46 | NavigationComponent, 47 | CardComponent 48 | ], 49 | imports: [ 50 | BrowserModule, 51 | BrowserAnimationsModule, 52 | AppRoutingModule, 53 | FormsModule, 54 | HttpClientModule, 55 | MatButtonModule, 56 | MatButtonToggleModule, 57 | MatCardModule, 58 | MatFormFieldModule, 59 | MatGridListModule, 60 | MatInputModule, 61 | MatToolbarModule, 62 | MatListModule, 63 | MatSidenavModule, 64 | MatIconModule, 65 | ReactiveFormsModule, 66 | TranslateModule.forRoot(), 67 | NgxFormErrorsModule.forRoot({ 68 | formErrorComponent: TranslatedFormErrorComponent 69 | }) 70 | ], 71 | exports: [LanguageSelectorComponent], 72 | providers: [], 73 | bootstrap: [AppComponent] 74 | }) 75 | export class AppModule { 76 | public constructor( 77 | private translateService: TranslateService, 78 | private errorMessageService: NgxFormErrorsMessageService, 79 | iconRegistry: MatIconRegistry, 80 | sanitizer: DomSanitizer 81 | ) { 82 | initializeTranslation(this.translateService); 83 | 84 | iconRegistry.addSvgIcon("github", sanitizer.bypassSecurityTrustResourceUrl("assets/img/github-icon.svg")); 85 | 86 | this.errorMessageService.addErrorMessages({ 87 | required: "DEMO.FORM_VALIDATION.WITH_NGX_FORM_ERRORS.REQUIRED", 88 | "matchingPasswords.password.required": "DEMO.FORM_VALIDATION.WITH_NGX_FORM_ERRORS.PASSWORD_REQUIRED", 89 | minlength: "DEMO.FORM_VALIDATION.WITH_NGX_FORM_ERRORS.PASSWORD.MIN_LENGTH", 90 | maxlength: "DEMO.FORM_VALIDATION.WITH_NGX_FORM_ERRORS.PASSWORD.MAX_LENGTH", 91 | pattern: "DEMO.FORM_VALIDATION.WITH_NGX_FORM_ERRORS.PASSWORD.PATTERN" 92 | }); 93 | 94 | this.errorMessageService.addFieldNames({ 95 | username: "DEMO.FIELDS.USER_NAME", 96 | "matchingPasswords.password": "not used, the alias defined via the directive takes precedence over this", 97 | "matchingPasswords.confirmPassword": "DEMO.FIELDS.CONFIRM_PASSWORD" 98 | }); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/components/card/card.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/components/card/card.component.scss: -------------------------------------------------------------------------------- 1 | :host mat-card { 2 | box-sizing: border-box; 3 | width: 100%; 4 | min-height: 100%; 5 | } 6 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/components/card/card.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding, Input } from "@angular/core"; 2 | 3 | @Component({ 4 | selector: "app-card", 5 | templateUrl: "./card.component.html", 6 | styleUrls: ["./card.component.scss"] 7 | }) 8 | export class CardComponent { 9 | @HostBinding("class.app-color-primary") 10 | public primaryColor = false; 11 | @HostBinding("class.app-color-accent") 12 | public accentColor = false; 13 | @HostBinding("class.app-color-warning") 14 | public warningColor = false; 15 | @HostBinding("class.app-color-success") 16 | public successColor = false; 17 | 18 | @Input() 19 | public set color(color: "primary" | "accent" | "warning" | "success") { 20 | this.primaryColor = false; 21 | this.accentColor = false; 22 | this.warningColor = false; 23 | this.successColor = false; 24 | 25 | switch (color) { 26 | case "primary": 27 | this.primaryColor = true; 28 | break; 29 | case "accent": 30 | this.accentColor = true; 31 | break; 32 | case "warning": 33 | this.warningColor = true; 34 | break; 35 | case "success": 36 | this.successColor = true; 37 | break; 38 | default: 39 | break; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/components/card/card.theme.scss: -------------------------------------------------------------------------------- 1 | @use "@angular/material" as mat; 2 | @mixin card-theme($theme) { 3 | $primary: map-get($theme, primary); 4 | $accent: map-get($theme, accent); 5 | $warning: map-get($theme, warn); 6 | $success: map-get($theme, success); 7 | 8 | app-card.app-color-primary mat-card { 9 | background-color: mat.get-color-from-palette($primary, 500); 10 | color: mat.get-contrast-color-from-palette($primary, 500); 11 | } 12 | 13 | app-card.app-color-accent mat-card { 14 | background-color: mat.get-color-from-palette($accent, 500); 15 | color: mat.get-contrast-color-from-palette($accent, 500); 16 | } 17 | 18 | app-card.app-color-warning mat-card { 19 | background-color: mat.get-color-from-palette($warning, 500); 20 | color: mat.get-contrast-color-from-palette($warning, 500); 21 | } 22 | 23 | app-card.app-color-success mat-card { 24 | /*Themes do not have a success map by default*/ 25 | background-color: mat.get-color-from-palette($success, 500); 26 | color: mat.get-contrast-color-from-palette($success, 500); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/components/card/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./card.component"; 2 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./card"; 2 | export * from "./language-selector"; 3 | export * from "./navigation"; 4 | export * from "./simple-form-error"; 5 | export * from "./translated-form-error"; 6 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/components/language-selector/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./language-selector.component"; 2 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/components/language-selector/language-selector.component.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | {{ language | uppercase }} 11 | 12 | 13 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/components/language-selector/language-selector.component.scss: -------------------------------------------------------------------------------- 1 | :host mat-button-toggle-group { 2 | box-sizing: border-box; 3 | width: 100%; 4 | border-radius: 0; 5 | 6 | mat-button-toggle.accent { 7 | flex: 1; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/components/language-selector/language-selector.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding, OnDestroy, OnInit } from "@angular/core"; 2 | import { Subscription } from "rxjs"; 3 | import { LangChangeEvent, TranslateService } from "@ngx-translate/core"; 4 | 5 | /** 6 | * Name of the component 7 | */ 8 | const componentName = "language-selector"; 9 | 10 | /** 11 | * Component to select the application's language from a list of available languages passed as parameter. 12 | */ 13 | @Component({ 14 | selector: "app-language-selector", 15 | templateUrl: "./language-selector.component.html", 16 | styleUrls: ["./language-selector.component.scss"] 17 | }) 18 | export class LanguageSelectorComponent implements OnInit, OnDestroy { 19 | @HostBinding("class") 20 | public cssClass: string = componentName; 21 | 22 | /** 23 | * The currently selected language 24 | */ 25 | public selectedLanguage: string; 26 | 27 | /** 28 | * A reference to the translateService subscription, needed to unsubscribe upon destroy. 29 | */ 30 | private languageChangeSubscription: Subscription; 31 | 32 | public supportedLanguages: string[] = ["en", "fr", "nl"]; 33 | 34 | /** 35 | * Class constructor 36 | * @param translateService - the translation service of the application 37 | */ 38 | public constructor(protected translateService: TranslateService) { 39 | this.selectedLanguage = this.translateService.currentLang; 40 | 41 | this.languageChangeSubscription = this.translateService.onLangChange.subscribe( 42 | (event: LangChangeEvent) => (this.selectedLanguage = event.lang), 43 | () => console.error(componentName + ": an error occurred getting the current language.") 44 | ); 45 | } 46 | 47 | /** 48 | * Component lifecycle hook 49 | */ 50 | public ngOnInit(): void { 51 | console.log(componentName + ": controller initialized"); 52 | } 53 | 54 | /** 55 | * Component lifecycle hook 56 | */ 57 | public ngOnDestroy(): void { 58 | if (this.languageChangeSubscription) { 59 | this.languageChangeSubscription.unsubscribe(); 60 | } 61 | } 62 | 63 | /** 64 | * Change the current language based on the selection made by the user 65 | * @param language - the new language 66 | */ 67 | public changeLanguage(language: string): void { 68 | if (this.selectedLanguage !== language) { 69 | this.selectedLanguage = language; 70 | 71 | this.translateService.use(language); 72 | } 73 | } 74 | 75 | /** 76 | * @ignore 77 | */ 78 | public trackLanguage(index: number): number { 79 | return index; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/components/language-selector/language-selector.theme.scss: -------------------------------------------------------------------------------- 1 | @use "@angular/material" as mat; 2 | @mixin language-selector-theme($theme) { 3 | $primary: map-get($theme, primary); 4 | 5 | .language-selector mat-button-toggle-group mat-button-toggle { 6 | &.active { 7 | &, 8 | &:hover, 9 | &:active { 10 | background-color: mat.get-color-from-palette($primary, 500); 11 | color: mat.get-contrast-color-from-palette($primary, 500); 12 | } 13 | } 14 | 15 | &:hover, 16 | &:focus { 17 | background-color: mat.get-color-from-palette($primary, 100); 18 | color: mat.get-contrast-color-from-palette($primary, 100); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/components/navigation/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./navigation.component"; 2 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/components/navigation/navigation.component.html: -------------------------------------------------------------------------------- 1 | 2 | Template Driven Forms 3 | Reactive Forms 4 | Ngx Form Errors 5 | Typed Reactive Form 6 | 7 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/components/navigation/navigation.component.scss: -------------------------------------------------------------------------------- 1 | :host mat-nav-list { 2 | padding: 0; 3 | 4 | a { 5 | outline: none; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/components/navigation/navigation.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding } from "@angular/core"; 2 | 3 | const componentName = "navigation"; 4 | 5 | @Component({ 6 | selector: "app-navigation", 7 | templateUrl: "./navigation.component.html", 8 | styleUrls: ["./navigation.component.scss"] 9 | }) 10 | export class NavigationComponent { 11 | @HostBinding("class") 12 | public cssClass: string = componentName; 13 | } 14 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/components/navigation/navigation.theme.scss: -------------------------------------------------------------------------------- 1 | @use "@angular/material" as mat; 2 | @mixin app-navigation-theme($theme) { 3 | $primary: map-get($theme, primary); 4 | $accent: map-get($theme, accent); 5 | .navigation mat-nav-list a.mat-list-item { 6 | &.active { 7 | &, 8 | &:hover, 9 | &:active { 10 | background-color: mat.get-color-from-palette($primary, 500); 11 | color: mat.get-contrast-color-from-palette($primary, 500); 12 | } 13 | } 14 | 15 | &:hover { 16 | background-color: mat.get-color-from-palette($primary, 100); 17 | color: mat.get-contrast-color-from-palette($primary, 100); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/components/simple-form-error/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./simple-form-error.component"; 2 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/components/simple-form-error/simple-form-error.component.html: -------------------------------------------------------------------------------- 1 |
{{ error.message }}
2 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/components/simple-form-error/simple-form-error.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding } from "@angular/core"; 2 | import { Observable } from "rxjs"; 3 | import { NgxFormErrorComponent, NgxFormFieldError } from "@nationalbankbelgium/ngx-form-errors"; 4 | 5 | @Component({ 6 | selector: "app-simple-form-error", 7 | templateUrl: "./simple-form-error.component.html" 8 | }) 9 | export class SimpleFormErrorComponent implements NgxFormErrorComponent { 10 | @HostBinding("class") 11 | public cssClass = "simple-form-error"; 12 | 13 | public errors: NgxFormFieldError[] = []; 14 | public errors$!: Observable; 15 | 16 | public subscribeToErrors(): void { 17 | this.errors$.subscribe((errors: NgxFormFieldError[]) => { 18 | this.errors = errors; 19 | }); 20 | } 21 | 22 | public trackError(index: number): number { 23 | return index; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/components/translated-form-error/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./translated-form-error.component"; 2 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/components/translated-form-error/translated-form-error.component.html: -------------------------------------------------------------------------------- 1 | {{ error.message | translate: error.params }} 2 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/components/translated-form-error/translated-form-error.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding, OnInit } from "@angular/core"; 2 | import { LangChangeEvent, TranslateService } from "@ngx-translate/core"; 3 | import { Observable } from "rxjs"; 4 | import { NgxFormErrorComponent, NgxFormFieldError } from "@nationalbankbelgium/ngx-form-errors"; 5 | 6 | @Component({ 7 | selector: "app-translated-form-error", 8 | templateUrl: "./translated-form-error.component.html" 9 | }) 10 | export class TranslatedFormErrorComponent implements NgxFormErrorComponent, OnInit { 11 | @HostBinding("class") 12 | public cssClass = "translated-form-error"; 13 | 14 | public errors: NgxFormFieldError[] = []; 15 | public errors$!: Observable; 16 | public fieldName = "undefined"; 17 | 18 | public constructor(public translateService: TranslateService) {} 19 | 20 | public ngOnInit(): void { 21 | this.translateService.onLangChange.subscribe((_ev: LangChangeEvent) => { 22 | this.translateFieldName(); 23 | }); 24 | } 25 | 26 | public subscribeToErrors(): void { 27 | this.errors$.subscribe((errors: NgxFormFieldError[]) => { 28 | this.errors = errors; 29 | 30 | if (errors.length) { 31 | // the formField can be retrieved from the "fieldName" param of any of the errors 32 | this.fieldName = errors[0].params.fieldName; 33 | this.translateFieldName(); 34 | } 35 | }); 36 | } 37 | 38 | public translateFieldName(): void { 39 | for (const error of this.errors) { 40 | error.params = { ...error.params, fieldName: this.translateService.instant(this.fieldName) }; 41 | } 42 | } 43 | 44 | public getErrorClass(): string { 45 | return this.errors.length > 2 ? "maximum-height" : "small-height"; 46 | } 47 | 48 | public trackError(index: number): number { 49 | return index; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/pages/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ngx-forms-example"; 2 | export * from "./reactive-forms-example"; 3 | export * from "./template-driven-forms-example"; 4 | export * from "./typed-reactive-forms-example"; 5 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/pages/ngx-forms-example/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ngx-forms-example.component"; 2 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/pages/ngx-forms-example/ngx-forms-example.component.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .grid { 4 | display: grid; 5 | grid-template-rows: auto; 6 | grid-template-columns: 1fr 1fr 1fr; 7 | grid-gap: 10px; 8 | } 9 | 10 | .form-card { 11 | grid-column-start: 1; 12 | grid-column-end: 4; 13 | 14 | mat-form-field { 15 | box-sizing: border-box; 16 | width: 100%; 17 | 18 | @media #{$monitor-query} { 19 | width: 45%; 20 | &:last-child { 21 | margin-left: 10%; 22 | } 23 | } 24 | } 25 | 26 | mat-card-actions { 27 | display: flex; 28 | align-items: flex-start; 29 | flex-wrap: wrap; 30 | margin: -10px; 31 | 32 | button { 33 | margin: 10px; 34 | @media #{$mobile-query} { 35 | width: 100%; 36 | } 37 | } 38 | } 39 | } 40 | 41 | .form-field-info { 42 | @media #{$table-query} { 43 | grid-column-start: 1; 44 | grid-column-end: 4; 45 | } 46 | 47 | mat-card-content { 48 | > div { 49 | padding: 5px 0; 50 | } 51 | 52 | pre { 53 | overflow: auto; 54 | box-sizing: border-box; 55 | display: block; 56 | width: 100%; 57 | max-height: 75px; 58 | 59 | margin: inherit; 60 | padding: 5px; 61 | border-radius: 4px; 62 | 63 | background-color: rgba(0, 0, 0, 0.2); 64 | 65 | &:empty { 66 | display: none; 67 | } 68 | } 69 | } 70 | } 71 | 72 | .form-validation-messages { 73 | grid-column-start: 1; 74 | grid-column-end: 4; 75 | } 76 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/pages/ngx-forms-example/ngx-forms-example.component.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @angular-eslint/template/use-track-by-function, @angular-eslint/template/cyclomatic-complexity */ 2 | import { Component } from "@angular/core"; 3 | import { AbstractControl, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, ValidationErrors, Validators } from "@angular/forms"; 4 | import { ErrorStateMatcher } from "@angular/material/core"; 5 | import { ParentErrorStateMatcher } from "../../parent-error-state-matcher"; 6 | import { PasswordValidator } from "../../password-validator"; 7 | 8 | @Component({ 9 | selector: "app-ngx-forms-example", 10 | templateUrl: "./ngx-forms-example.component.html", 11 | styleUrls: ["./ngx-forms-example.component.scss"] 12 | }) 13 | export class NgxFormsExampleComponent { 14 | public formGroup: UntypedFormGroup; 15 | public parentErrorStateMatcher: ErrorStateMatcher = new ParentErrorStateMatcher(); 16 | public passwordPattern = "^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9]+$"; 17 | public showValidationDetails = false; 18 | public showValidationSummary = true; 19 | 20 | public constructor(private formBuilder: UntypedFormBuilder) { 21 | this.formGroup = this.formBuilder.group({ 22 | username: [undefined, Validators.required], 23 | matchingPasswords: new UntypedFormGroup( 24 | { 25 | password: new UntypedFormControl( 26 | "", 27 | Validators.compose([ 28 | Validators.minLength(3), 29 | Validators.maxLength(10), 30 | Validators.required, 31 | Validators.pattern(this.passwordPattern) // this is for the letters (both uppercase and lowercase) and numbers validation 32 | ]) 33 | ), 34 | confirmPassword: new UntypedFormControl("", Validators.required) 35 | }, 36 | { 37 | validators: (formGroup: AbstractControl): ValidationErrors | null => 38 | PasswordValidator.areEqual(formGroup) 39 | } 40 | ) 41 | }); 42 | } 43 | 44 | public toggleValidationDetails(): void { 45 | this.showValidationDetails = !this.showValidationDetails; 46 | } 47 | 48 | public toggleValidationSummary(): void { 49 | this.showValidationSummary = !this.showValidationSummary; 50 | } 51 | 52 | public onSubmitUserDetails(formGroup: UntypedFormGroup): void { 53 | console.log("Submitted form:", formGroup.value); 54 | } 55 | 56 | public getFormStatus(): void { 57 | console.log("Form status", this.formGroup); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/pages/reactive-forms-example/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./reactive-forms-example.component"; 2 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/pages/reactive-forms-example/reactive-forms-example.component.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .grid { 4 | display: grid; 5 | grid-template-rows: auto; 6 | grid-template-columns: 1fr 1fr 1fr; 7 | grid-gap: 10px; 8 | } 9 | 10 | .form-card { 11 | grid-column-start: 1; 12 | grid-column-end: 4; 13 | 14 | mat-form-field { 15 | box-sizing: border-box; 16 | width: 100%; 17 | 18 | @media #{$monitor-query} { 19 | width: 45%; 20 | &:last-child { 21 | margin-left: 10%; 22 | } 23 | } 24 | } 25 | 26 | mat-card-actions { 27 | display: flex; 28 | align-items: flex-start; 29 | flex-wrap: wrap; 30 | margin: -10px; 31 | 32 | button { 33 | margin: 10px; 34 | @media #{$mobile-query} { 35 | width: 100%; 36 | } 37 | } 38 | } 39 | } 40 | 41 | .form-field-info { 42 | @media #{$table-query} { 43 | grid-column-start: 1; 44 | grid-column-end: 4; 45 | } 46 | 47 | mat-card-content { 48 | > div { 49 | padding: 5px 0; 50 | } 51 | 52 | pre { 53 | overflow: auto; 54 | box-sizing: border-box; 55 | display: block; 56 | width: 100%; 57 | max-height: 75px; 58 | 59 | margin: inherit; 60 | padding: 5px; 61 | border-radius: 4px; 62 | 63 | background-color: rgba(0, 0, 0, 0.2); 64 | 65 | &:empty { 66 | display: none; 67 | } 68 | } 69 | } 70 | } 71 | 72 | .form-validation-messages { 73 | grid-column-start: 1; 74 | grid-column-end: 4; 75 | } 76 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/pages/reactive-forms-example/reactive-forms-example.component.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @angular-eslint/template/use-track-by-function, @angular-eslint/template/cyclomatic-complexity */ 2 | import { Component } from "@angular/core"; 3 | import { AbstractControl, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, ValidationErrors, Validators } from "@angular/forms"; 4 | import { ErrorStateMatcher } from "@angular/material/core"; 5 | import { PasswordValidator } from "../../password-validator"; 6 | import { ParentErrorStateMatcher } from "../../parent-error-state-matcher"; 7 | 8 | @Component({ 9 | selector: "app-reactive-forms-example", 10 | templateUrl: "./reactive-forms-example.component.html", 11 | styleUrls: ["./reactive-forms-example.component.scss"] 12 | }) 13 | export class ReactiveFormsExampleComponent { 14 | public formGroup: UntypedFormGroup; 15 | public validationMessages: { [key: string]: { type: string; message: string }[] }; 16 | public parentErrorStateMatcher: ErrorStateMatcher = new ParentErrorStateMatcher(); 17 | public passwordPattern = "^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9]+$"; 18 | public showValidationDetails = false; 19 | public showValidationSummary = true; 20 | 21 | public constructor(private formBuilder: UntypedFormBuilder) { 22 | this.formGroup = this.formBuilder.group({ 23 | username: [undefined, Validators.required], 24 | matchingPasswords: new UntypedFormGroup( 25 | { 26 | password: new UntypedFormControl( 27 | "", 28 | Validators.compose([ 29 | Validators.minLength(3), 30 | Validators.maxLength(10), 31 | Validators.required, 32 | Validators.pattern(this.passwordPattern) // this is for the letters (both uppercase and lowercase) and numbers validation 33 | ]) 34 | ), 35 | confirmPassword: new UntypedFormControl("", Validators.required) 36 | }, 37 | { 38 | validators: (formGroup: AbstractControl): ValidationErrors | null => 39 | PasswordValidator.areEqual(formGroup) 40 | } 41 | ) 42 | }); 43 | 44 | this.validationMessages = { 45 | username: [ 46 | { 47 | type: "required", 48 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.USER_NAME.REQUIRED" 49 | }, 50 | { type: "unique", message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.USER_NAME.UNIQUE" } 51 | ], 52 | password: [ 53 | { 54 | type: "required", 55 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.PASSWORD.REQUIRED" 56 | }, 57 | { 58 | type: "minlength", 59 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.PASSWORD.MIN_LENGTH" 60 | }, 61 | { type: "pattern", message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.PASSWORD.PATTERN" } 62 | ], 63 | confirmPassword: [ 64 | { 65 | type: "required", 66 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.CONFIRM_PASSWORD.REQUIRED" 67 | }, 68 | { 69 | type: "areEqual", 70 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.CONFIRM_PASSWORD.ARE_EQUAL" 71 | } 72 | ] 73 | }; 74 | } 75 | 76 | public toggleValidationDetails(): void { 77 | this.showValidationDetails = !this.showValidationDetails; 78 | } 79 | 80 | public toggleValidationSummary(): void { 81 | this.showValidationSummary = !this.showValidationSummary; 82 | } 83 | 84 | public onSubmitUserDetails(formGroup: UntypedFormGroup): void { 85 | console.log("Submitted form:", formGroup.value); 86 | } 87 | 88 | public getFormStatus(): void { 89 | console.log("Form status", this.formGroup); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/pages/template-driven-forms-example/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./template-driven-forms-example.component"; 2 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/pages/template-driven-forms-example/template-driven-forms-example.component.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .grid { 4 | display: grid; 5 | grid-template-rows: auto; 6 | grid-template-columns: 1fr 1fr 1fr; 7 | grid-gap: 10px; 8 | } 9 | 10 | .form-card { 11 | grid-column-start: 1; 12 | grid-column-end: 4; 13 | 14 | mat-form-field { 15 | box-sizing: border-box; 16 | width: 100%; 17 | 18 | @media #{$monitor-query} { 19 | width: 45%; 20 | &:last-child { 21 | margin-left: 10%; 22 | } 23 | } 24 | } 25 | 26 | mat-card-actions { 27 | display: flex; 28 | align-items: flex-start; 29 | flex-wrap: wrap; 30 | margin: -10px; 31 | 32 | button { 33 | margin: 10px; 34 | @media #{$mobile-query} { 35 | width: 100%; 36 | } 37 | } 38 | } 39 | } 40 | 41 | .form-field-info { 42 | @media #{$table-query} { 43 | grid-column-start: 1; 44 | grid-column-end: 4; 45 | } 46 | 47 | mat-card-content { 48 | > div { 49 | padding: 5px 0; 50 | } 51 | 52 | pre { 53 | overflow: auto; 54 | box-sizing: border-box; 55 | display: block; 56 | width: 100%; 57 | max-height: 75px; 58 | 59 | margin: inherit; 60 | padding: 5px; 61 | border-radius: 4px; 62 | 63 | background-color: rgba(0, 0, 0, 0.2); 64 | 65 | &:empty { 66 | display: none; 67 | } 68 | } 69 | } 70 | } 71 | 72 | .form-validation-messages { 73 | grid-column-start: 1; 74 | grid-column-end: 4; 75 | } 76 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/pages/template-driven-forms-example/template-driven-forms-example.component.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @angular-eslint/template/use-track-by-function, @angular-eslint/template/cyclomatic-complexity */ 2 | import { Component } from "@angular/core"; 3 | import { NgForm } from "@angular/forms"; 4 | import { ErrorStateMatcher } from "@angular/material/core"; 5 | import { ParentErrorStateMatcher } from "../../parent-error-state-matcher"; 6 | 7 | @Component({ 8 | selector: "app-template-driven-forms-example", 9 | templateUrl: "./template-driven-forms-example.component.html", 10 | styleUrls: ["./template-driven-forms-example.component.scss"] 11 | }) 12 | export class TemplateDrivenFormsExampleComponent { 13 | public username = ""; 14 | public password = ""; 15 | public confirmPassword = ""; 16 | 17 | public parentErrorStateMatcher: ErrorStateMatcher = new ParentErrorStateMatcher(); 18 | public passwordPattern = "^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9]+$"; 19 | public validationMessages: { [key: string]: { type: string; message: string }[] }; 20 | public showValidationDetails = false; 21 | public showValidationSummary = true; 22 | 23 | public constructor() { 24 | this.validationMessages = { 25 | username: [ 26 | { 27 | type: "required", 28 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.USER_NAME.REQUIRED" 29 | } 30 | ], 31 | password: [ 32 | { 33 | type: "required", 34 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.PASSWORD.REQUIRED" 35 | }, 36 | { 37 | type: "minlength", 38 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.PASSWORD.MIN_LENGTH" 39 | }, 40 | { type: "pattern", message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.PASSWORD.PATTERN" } 41 | ], 42 | confirmPassword: [ 43 | { 44 | type: "required", 45 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.CONFIRM_PASSWORD.REQUIRED" 46 | }, 47 | { 48 | type: "areEqual", 49 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.CONFIRM_PASSWORD.ARE_EQUAL" 50 | } 51 | ] 52 | }; 53 | } 54 | 55 | public toggleValidationDetails(): void { 56 | this.showValidationDetails = !this.showValidationDetails; 57 | } 58 | 59 | public toggleValidationSummary(): void { 60 | this.showValidationSummary = !this.showValidationSummary; 61 | } 62 | 63 | public onSubmitUserDetails(ngForm: NgForm): void { 64 | console.log("Submitted form:", ngForm.value); 65 | } 66 | 67 | public getFormStatus(ngForm: NgForm): void { 68 | console.log("Form status", ngForm); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/pages/typed-reactive-forms-example/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./typed-reactive-forms-example.component"; 2 | export * from "./matching-password"; 3 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/pages/typed-reactive-forms-example/matching-password.ts: -------------------------------------------------------------------------------- 1 | export interface MatchingPassword { 2 | password: string; 3 | confirmPassword: string; 4 | } 5 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/pages/typed-reactive-forms-example/typed-reactive-forms-example.component.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .grid { 4 | display: grid; 5 | grid-template-rows: auto; 6 | grid-template-columns: 1fr 1fr 1fr; 7 | grid-gap: 10px; 8 | } 9 | 10 | .form-card { 11 | grid-column-start: 1; 12 | grid-column-end: 4; 13 | 14 | mat-form-field { 15 | box-sizing: border-box; 16 | width: 100%; 17 | 18 | @media #{$monitor-query} { 19 | width: 45%; 20 | &:last-child { 21 | margin-left: 10%; 22 | } 23 | } 24 | } 25 | 26 | mat-card-actions { 27 | display: flex; 28 | align-items: flex-start; 29 | flex-wrap: wrap; 30 | margin: -10px; 31 | 32 | button { 33 | margin: 10px; 34 | @media #{$mobile-query} { 35 | width: 100%; 36 | } 37 | } 38 | } 39 | } 40 | 41 | .form-field-info { 42 | @media #{$table-query} { 43 | grid-column-start: 1; 44 | grid-column-end: 4; 45 | } 46 | 47 | mat-card-content { 48 | > div { 49 | padding: 5px 0; 50 | } 51 | 52 | pre { 53 | overflow: auto; 54 | box-sizing: border-box; 55 | display: block; 56 | width: 100%; 57 | max-height: 75px; 58 | 59 | margin: inherit; 60 | padding: 5px; 61 | border-radius: 4px; 62 | 63 | background-color: rgba(0, 0, 0, 0.2); 64 | 65 | &:empty { 66 | display: none; 67 | } 68 | } 69 | } 70 | } 71 | 72 | .form-validation-messages { 73 | grid-column-start: 1; 74 | grid-column-end: 4; 75 | } 76 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/pages/typed-reactive-forms-example/typed-reactive-forms-example.component.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @angular-eslint/template/use-track-by-function, @angular-eslint/template/cyclomatic-complexity */ 2 | import { Component } from "@angular/core"; 3 | import { AbstractControl, FormBuilder, FormControl, FormGroup, ValidationErrors, Validators } from "@angular/forms"; 4 | import { ErrorStateMatcher } from "@angular/material/core"; 5 | import { PasswordValidator } from "../../password-validator"; 6 | import { ParentErrorStateMatcher } from "../../parent-error-state-matcher"; 7 | import { MatchingPassword } from "./matching-password"; 8 | 9 | @Component({ 10 | selector: "app-reactive-forms-example", 11 | templateUrl: "./typed-reactive-forms-example.component.html", 12 | styleUrls: ["./typed-reactive-forms-example.component.scss"] 13 | }) 14 | export class TypedReactiveFormsExampleComponent { 15 | public formGroup: FormGroup; 16 | public validationMessages: { [key: string]: { type: string; message: string }[] }; 17 | public parentErrorStateMatcher: ErrorStateMatcher = new ParentErrorStateMatcher(); 18 | public passwordPattern = "^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9]+$"; 19 | public showValidationDetails = false; 20 | public showValidationSummary = true; 21 | 22 | public constructor(private formBuilder: FormBuilder) { 23 | this.formGroup = this.formBuilder.group({ 24 | username: [undefined, Validators.required], 25 | matchingPasswords: new FormGroup( 26 | { 27 | password: new FormControl( 28 | "", 29 | Validators.compose([ 30 | Validators.minLength(3), 31 | Validators.maxLength(10), 32 | Validators.required, 33 | Validators.pattern(this.passwordPattern) // this is for the letters (both uppercase and lowercase) and numbers validation 34 | ]) 35 | ), 36 | confirmPassword: new FormControl("", Validators.required) 37 | }, 38 | { 39 | validators: (formGroup: AbstractControl): ValidationErrors | null => 40 | PasswordValidator.areEqualTyped(formGroup) 41 | } 42 | ) 43 | }); 44 | 45 | this.validationMessages = { 46 | username: [ 47 | { 48 | type: "required", 49 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.USER_NAME.REQUIRED" 50 | }, 51 | { type: "unique", message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.USER_NAME.UNIQUE" } 52 | ], 53 | matchingPasswords: [ 54 | { 55 | type: "areEqual", 56 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.CONFIRM_PASSWORD.ARE_EQUAL" 57 | } 58 | ], 59 | password: [ 60 | { 61 | type: "required", 62 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.PASSWORD.REQUIRED" 63 | }, 64 | { 65 | type: "minlength", 66 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.PASSWORD.MIN_LENGTH" 67 | }, 68 | { type: "pattern", message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.PASSWORD.PATTERN" } 69 | ], 70 | confirmPassword: [ 71 | { 72 | type: "required", 73 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.CONFIRM_PASSWORD.REQUIRED" 74 | } 75 | ] 76 | }; 77 | } 78 | 79 | public toggleValidationDetails(): void { 80 | this.showValidationDetails = !this.showValidationDetails; 81 | } 82 | 83 | public toggleValidationSummary(): void { 84 | this.showValidationSummary = !this.showValidationSummary; 85 | } 86 | 87 | public onSubmitUserDetails(formGroup: FormGroup): void { 88 | console.log("Submitted form:", formGroup.value); 89 | } 90 | 91 | public getFormStatus(): void { 92 | console.log("Form status", this.formGroup); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/parent-error-state-matcher.ts: -------------------------------------------------------------------------------- 1 | import { UntypedFormControl, FormGroupDirective, NgForm } from "@angular/forms"; 2 | import { ErrorStateMatcher } from "@angular/material/core"; 3 | 4 | /** Error when invalid control is dirty, touched, or submitted. */ 5 | export class ParentErrorStateMatcher implements ErrorStateMatcher { 6 | public isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { 7 | const isSubmitted = !!(form && form.submitted); 8 | const formGroupValid = !!(form && form.valid); 9 | 10 | return !!control && (control.invalid || !formGroupValid) && (control.dirty || control.touched || isSubmitted); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/password-validator.ts: -------------------------------------------------------------------------------- 1 | import { AbstractControl, UntypedFormControl, UntypedFormGroup, ValidationErrors } from "@angular/forms"; 2 | import { MatchingPassword } from "./pages"; 3 | 4 | export class PasswordValidator { 5 | // Inspired on: http://plnkr.co/edit/Zcbg2T3tOxYmhxs7vaAm?p=preview 6 | public static areEqual(formGroup: UntypedFormGroup): ValidationErrors | null { 7 | let value: string | undefined; 8 | let valid = true; 9 | for (const key in formGroup.controls) { 10 | /* eslint-disable-next-line no-prototype-builtins */ 11 | if (formGroup.controls.hasOwnProperty(key)) { 12 | const control: UntypedFormControl = formGroup.controls[key]; 13 | 14 | if (value === undefined) { 15 | value = control.value; 16 | } else { 17 | if (value !== control.value) { 18 | valid = false; 19 | break; 20 | } 21 | } 22 | } 23 | } 24 | 25 | if (valid) { 26 | /* eslint-disable-next-line no-null/no-null */ 27 | return null; 28 | } 29 | 30 | return { 31 | areEqual: true 32 | }; 33 | } 34 | 35 | public static areEqualTyped(formGroup: AbstractControl): ValidationErrors | null { 36 | const matchingPassword = formGroup.value; 37 | 38 | if (matchingPassword.password === matchingPassword.confirmPassword) { 39 | /* eslint-disable-next-line no-null/no-null */ 40 | return null; 41 | } 42 | return { 43 | areEqual: true 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /demo-app/ng15/src/app/translation.config.ts: -------------------------------------------------------------------------------- 1 | import { TranslateService } from "@ngx-translate/core"; 2 | 3 | const translationsEn: object = require("../assets/translations/en.json"); 4 | const translationsFr: object = require("../assets/translations/fr.json"); 5 | const translationsNl: object = require("../assets/translations/nl.json"); 6 | 7 | /** 8 | * @param translateService - The translation service 9 | */ 10 | export function initializeTranslation(translateService: TranslateService): void { 11 | translateService.addLangs(["en", "fr", "nl"]); 12 | translateService.setDefaultLang("en"); 13 | translateService.use("en"); 14 | 15 | translateService.setTranslation("en", translationsEn); 16 | translateService.setTranslation("fr", translationsFr); 17 | translateService.setTranslation("nl", translationsNl); 18 | } 19 | -------------------------------------------------------------------------------- /demo-app/ng15/src/assets/img/github-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /demo-app/ng15/src/assets/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "DEMO": { 3 | "PLACEHOLDERS": { 4 | "USER_NAME": "Username", 5 | "PASSWORD": "Password", 6 | "CONFIRM_PASSWORD": "Confirm password" 7 | }, 8 | "FIELDS": { 9 | "USER_NAME": "Your name", 10 | "PASSWORD_ALIAS": "A valid passcode", 11 | "CONFIRM_PASSWORD": "Password confirmation" 12 | }, 13 | "FORM_VALIDATION": { 14 | "WITHOUT_NGX_FORM_ERRORS": { 15 | "REQUIRED": "This field is required", 16 | "USER_NAME": { 17 | "REQUIRED": "Username is required", 18 | "UNIQUE": "Your username has already been taken" 19 | }, 20 | "PASSWORD": { 21 | "REQUIRED": "Password is required", 22 | "MIN_LENGTH": "Password must be at least 3 characters long", 23 | "PATTERN": "Your password must contain at least one uppercase, one lowercase, and one number" 24 | }, 25 | "CONFIRM_PASSWORD": { 26 | "REQUIRED": "Confirm password is required", 27 | "ARE_EQUAL": "Password mismatch" 28 | } 29 | }, 30 | "WITH_NGX_FORM_ERRORS": { 31 | "REQUIRED": "{{fieldName}} is required", 32 | "PASSWORD_REQUIRED": "{{fieldName}} must be provided", 33 | "USER_NAME": { 34 | "UNIQUE": "Your username has already been taken" 35 | }, 36 | "PASSWORD": { 37 | "MAX_LENGTH": "Password cannot be more than {{requiredLength}} characters long", 38 | "MIN_LENGTH": "Password must be at least {{requiredLength}} characters long", 39 | "PATTERN": "Your password must contain at least one uppercase, one lowercase, and one number" 40 | }, 41 | "CONFIRM_PASSWORD": { 42 | "ARE_EQUAL": "Password mismatch" 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /demo-app/ng15/src/assets/translations/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "DEMO": { 3 | "PLACEHOLDERS": { 4 | "USER_NAME": "Nom d'utilisateur", 5 | "PASSWORD": "Mot de passe", 6 | "CONFIRM_PASSWORD": "Confirmez le mot de passe" 7 | }, 8 | "FIELDS": { 9 | "USER_NAME": "Votre nom d'utilisateur", 10 | "PASSWORD_ALIAS": "Un code d'authentification valide", 11 | "CONFIRM_PASSWORD": "Confirmation mot de passe" 12 | }, 13 | "FORM_VALIDATION": { 14 | "WITHOUT_NGX_FORM_ERRORS": { 15 | "REQUIRED": "Ce champ est nécessaire", 16 | "USER_NAME": { 17 | "REQUIRED": "Nom d'utilisateur est nécessaire", 18 | "UNIQUE": "Votre nom d'utilisateur a déjà été pris" 19 | }, 20 | "PASSWORD": { 21 | "REQUIRED": "Mot de passe est nécessaire", 22 | "MIN_LENGTH": "Mot de passe doit comporter au moins 3 caractères", 23 | "PATTERN": "Votre mot de passe doit contenir au moins une majuscule, une minuscule et un chiffre." 24 | }, 25 | "CONFIRM_PASSWORD": { 26 | "REQUIRED": "Confirmez le mot de passe est nécessaire", 27 | "ARE_EQUAL": "Non concordance des mots de passe passwords" 28 | } 29 | }, 30 | "WITH_NGX_FORM_ERRORS": { 31 | "REQUIRED": "{{fieldName}} est nécessaire", 32 | "PASSWORD_REQUIRED": "{{fieldName}} doit être fourni", 33 | "USER_NAME": { 34 | "UNIQUE": "Votre nom d'utilisateur a déjà été pris" 35 | }, 36 | "PASSWORD": { 37 | "MAX_LENGTH": "Mot de passe ne peut pas y avoir plus de {{requiredLength}} caractères", 38 | "MIN_LENGTH": "Mot de passe doit comporter au moins {{requiredLength}} caractères", 39 | "PATTERN": "Votre mot de passe doit contenir au moins une majuscule, une minuscule et un chiffre." 40 | }, 41 | "CONFIRM_PASSWORD": { 42 | "ARE_EQUAL": "Non concordance des mots de passe passwords" 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /demo-app/ng15/src/assets/translations/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "DEMO": { 3 | "PLACEHOLDERS": { 4 | "USER_NAME": "Gebruikersnaam", 5 | "PASSWORD": "Wachtwoord", 6 | "CONFIRM_PASSWORD": "Bevestig wachtwoord" 7 | }, 8 | "FIELDS": { 9 | "USER_NAME": "Uw gebruikersnaam", 10 | "PASSWORD_ALIAS": "Een geldig wachtwoord", 11 | "CONFIRM_PASSWORD": "Wachtwoordbevestiging" 12 | }, 13 | "FORM_VALIDATION": { 14 | "WITHOUT_NGX_FORM_ERRORS": { 15 | "REQUIRED": "Dit veld is verplicht", 16 | "USER_NAME": { 17 | "REQUIRED": "Gebruikersnaam is verplicht", 18 | "UNIQUE": "Uw gebruikersnaam is al in gebruik" 19 | }, 20 | "PASSWORD": { 21 | "REQUIRED": "Wachtwoord is verplicht", 22 | "MIN_LENGTH": "Wachtwoord moet minimaal 3 tekens lang zijn", 23 | "PATTERN": "Uw wachtwoord moet minimaal één hoofdletter, één kleine letter en één cijfer bevatten" 24 | }, 25 | "CONFIRM_PASSWORD": { 26 | "REQUIRED": "Bevestig wachtwoord is verplicht", 27 | "ARE_EQUAL": "Wachtwoord komt niet overeen" 28 | } 29 | }, 30 | "WITH_NGX_FORM_ERRORS": { 31 | "REQUIRED": "{{fieldName}} is verplicht", 32 | "PASSWORD_REQUIRED": "{{fieldName}} moet worden gegeven", 33 | "USER_NAME": { 34 | "UNIQUE": "Uw gebruikersnaam is al in gebruik" 35 | }, 36 | "PASSWORD": { 37 | "MAX_LENGTH": "Wachtwoord mag niet meer dan {{requiredLength}} tekens lang zijn", 38 | "MIN_LENGTH": "Wachtwoord moet minimaal {{requiredLength}} tekens lang zijn", 39 | "PATTERN": "Uw Wachtwoord moet minimaal één hoofdletter, één kleine letter en één cijfer bevatten" 40 | }, 41 | "CONFIRM_PASSWORD": { 42 | "ARE_EQUAL": "Wachtwoord komt niet overeen" 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /demo-app/ng15/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /demo-app/ng15/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /demo-app/ng15/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NationalBankBelgium/ngx-form-errors/07813d90f41ed2ee572afd0039f26bc7a023f0a4/demo-app/ng15/src/favicon.ico -------------------------------------------------------------------------------- /demo-app/ng15/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NgxFormErrors Showcase 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /demo-app/ng15/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() 12 | .bootstrapModule(AppModule) 13 | .catch((err: any) => console.error(err)); 14 | -------------------------------------------------------------------------------- /demo-app/ng15/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /** 18 | * ************************************************************************************************* 19 | * BROWSER POLYFILLS 20 | */ 21 | /* eslint-disable import/no-unassigned-import */ 22 | /** 23 | * IE9, IE10 and IE11 requires all of the following polyfills. 24 | */ 25 | import "core-js/es"; 26 | 27 | /** 28 | * IE10 and IE11 requires the following for NgClass support on SVG elements 29 | */ 30 | import "eligrey-classlist-js-polyfill"; 31 | 32 | /** 33 | * IE10 and IE11 requires the following for the Reflect API. 34 | */ 35 | import "core-js/proposals/reflect-metadata"; 36 | 37 | /** 38 | * Web Animations `@angular/platform-browser/animations` 39 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 40 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 41 | */ 42 | // import "web-animations-js"; // Run `npm install --save web-animations-js`. 43 | 44 | /** 45 | * By default, zone.js will patch all possible macroTask and DomEvents 46 | * user can disable parts of macroTask/DomEvents patch by setting following flags 47 | * because those flags need to be set before `zone.js` being loaded, and webpack 48 | * will put import in the top of bundle, so user need to create a separate file 49 | * in this directory (for example: zone-flags.ts), and put the following flags 50 | * into that file, and then add the following code before importing zone.js. 51 | * import './zone-flags.ts'; 52 | * 53 | * The flags allowed in zone-flags.ts are listed here. 54 | * 55 | * The following flags will work for all browsers. 56 | * 57 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 58 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 59 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ["scroll", "mousemove"]; // disable patch specified eventNames 60 | * 61 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 62 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 63 | */ 64 | /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */ 65 | (window as any).__Zone_enable_cross_context_check = true; 66 | 67 | /** 68 | * ************************************************************************************************* 69 | * Zone JS is required by default for Angular itself. 70 | */ 71 | import "zone.js"; // Included with Angular CLI. 72 | 73 | /** 74 | * ************************************************************************************************* 75 | * APPLICATION IMPORTS 76 | */ 77 | -------------------------------------------------------------------------------- /demo-app/ng15/src/styles/_app.theme.scss: -------------------------------------------------------------------------------- 1 | @use "@angular/material" as mat; 2 | @import "@angular/material/theming"; 3 | @import "../app/components/navigation/navigation.theme"; 4 | @import "../app/components/language-selector/language-selector.theme"; 5 | @import "../app/components/card/card.theme"; 6 | 7 | $guardsman-red: ( 8 | 50: #f7e1e1, 9 | 100: #eab3b3, 10 | 200: #dd8080, 11 | 300: #cf4d4d, 12 | 400: #c42727, 13 | 500: #ba0101, 14 | 600: #b30101, 15 | 700: #ab0101, 16 | 800: #a30101, 17 | 900: #940000, 18 | A100: #ffbfbf, 19 | A200: #ff8c8c, 20 | A400: #ff5959, 21 | A700: #ff4040, 22 | contrast: ( 23 | 50: $dark-primary-text, 24 | 100: $dark-primary-text, 25 | 200: $dark-primary-text, 26 | 300: $light-primary-text, 27 | 400: $light-primary-text, 28 | 500: $light-primary-text, 29 | 600: $light-primary-text, 30 | 700: $light-primary-text, 31 | 800: $light-primary-text, 32 | 900: $light-primary-text, 33 | A100: $dark-primary-text, 34 | A200: $dark-primary-text, 35 | A400: $dark-primary-text, 36 | A700: $light-primary-text 37 | ) 38 | ); 39 | 40 | $eminence: ( 41 | 50: #ede6f0, 42 | 100: #d3c1da, 43 | 200: #b698c1, 44 | 300: #986ea8, 45 | 400: #824f95, 46 | 500: #6c3082, 47 | 600: #642b7a, 48 | 700: #59246f, 49 | 800: #4f1e65, 50 | 900: #3d1352, 51 | A100: #d58cff, 52 | A200: #c259ff, 53 | A400: #af26ff, 54 | A700: #a60dff, 55 | contrast: ( 56 | 50: $light-primary-text, 57 | 100: $light-primary-text, 58 | 200: $light-primary-text, 59 | 300: $light-primary-text, 60 | 400: $light-primary-text, 61 | 500: $light-primary-text, 62 | 600: $light-primary-text, 63 | 700: $light-primary-text, 64 | 800: $light-primary-text, 65 | 900: $light-primary-text, 66 | A100: $light-primary-text, 67 | A200: $light-primary-text, 68 | A400: $light-primary-text, 69 | A700: $light-primary-text 70 | ) 71 | ); 72 | 73 | $lochinvar: ( 74 | 50: #e6f1f0, 75 | 100: #c0ddda, 76 | 200: #96c6c2, 77 | 300: #6bafa9, 78 | 400: #4c9d96, 79 | 500: #2c8c84, 80 | 600: #27847c, 81 | 700: #217971, 82 | 800: #1b6f67, 83 | 900: #105c54, 84 | A100: #93fff2, 85 | A200: #60ffed, 86 | A400: #2dffe7, 87 | A700: #14ffe4, 88 | contrast: ( 89 | 50: $dark-secondary-text, 90 | 100: $dark-secondary-text, 91 | 200: $dark-secondary-text, 92 | 300: $dark-secondary-text, 93 | 400: $dark-secondary-text, 94 | 500: $light-secondary-text, 95 | 600: $light-secondary-text, 96 | 700: $light-secondary-text, 97 | 800: $light-secondary-text, 98 | 900: $light-secondary-text, 99 | A100: $dark-secondary-text, 100 | A200: $dark-secondary-text, 101 | A400: $dark-secondary-text, 102 | A700: $dark-secondary-text 103 | ) 104 | ); 105 | 106 | // TODO(v15): As of v15 mat.legacy-core no longer includes default typography styles. 107 | // The following line adds: 108 | // 1. Default typography styles for all components 109 | // 2. Styles for typography hierarchy classes (e.g. .mat-headline-1) 110 | // If you specify typography styles for the components you use elsewhere, you should delete this line. 111 | // If you don't need the default component typographies but still want the hierarchy styles, 112 | // you can delete this line and instead use: 113 | // `@include mat.legacy-typography-hierarchy(mat.define-legacy-typography-config());` 114 | @include mat.all-legacy-component-typographies(); 115 | @include mat.legacy-core(); 116 | 117 | $demo-app-primary: mat.define-palette($eminence); 118 | $demo-app-accent: mat.define-palette($lochinvar); 119 | 120 | $demo-app-warn: mat.define-palette($guardsman-red); 121 | $demo-app-success: mat.define-palette(mat.$light-green-palette); 122 | 123 | $demo-mat-theme: mat.define-light-theme($demo-app-primary, $demo-app-accent, $demo-app-warn); 124 | $demo-custom-theme: ( 125 | success: $demo-app-success 126 | ); 127 | 128 | $demo-app-theme: map-merge($demo-mat-theme, $demo-custom-theme); 129 | 130 | $theme: $demo-app-theme; 131 | 132 | @include mat.all-legacy-component-themes($theme); 133 | @include app-navigation-theme($theme); 134 | @include language-selector-theme($theme); 135 | @include card-theme($theme); 136 | -------------------------------------------------------------------------------- /demo-app/ng15/src/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | $monitor-query: "screen and (min-width:1200px)"; 2 | $table-query: "screen and (max-width: 1200px)"; 3 | $mobile-query: "screen and (max-width: 600px)"; 4 | -------------------------------------------------------------------------------- /demo-app/ng15/src/styles/styles.scss: -------------------------------------------------------------------------------- 1 | @import "~material-design-icons/iconfont/material-icons.css"; 2 | @import "app.theme"; 3 | 4 | app-root { 5 | height: 100vh; 6 | display: flex; 7 | flex-direction: column; 8 | } 9 | 10 | .container { 11 | margin: 20px auto; 12 | padding: 20px; 13 | box-sizing: border-box; 14 | max-width: 1200px; 15 | } 16 | -------------------------------------------------------------------------------- /demo-app/ng15/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | /* eslint-disable import/no-unassigned-import */ 4 | import "zone.js/testing"; 5 | import { getTestBed } from "@angular/core/testing"; 6 | import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from "@angular/platform-browser-dynamic/testing"; 7 | 8 | // First, initialize the Angular testing environment. 9 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { 10 | teardown: { destroyAfterEach: false } 11 | }); 12 | -------------------------------------------------------------------------------- /demo-app/ng15/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": ["node"], 6 | "paths": { 7 | "@angular/*": ["./node_modules/@angular/*"] 8 | } 9 | }, 10 | "files": ["src/main.ts", "src/polyfills.ts"], 11 | "include": ["src/**/*.d.ts"], 12 | "exclude": ["src/test.ts", "src/**/*.spec.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /demo-app/ng15/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@nationalbankbelgium/code-style/tsconfig/4.9.x/ng15", 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "rootDir": "../", 7 | "outDir": "./dist/out-tsc", 8 | "declaration": false, 9 | "downlevelIteration": true, 10 | "experimentalDecorators": true, 11 | "module": "es2020", 12 | "moduleResolution": "node", 13 | "importHelpers": true, 14 | "target": "es2022", 15 | "typeRoots": ["node_modules/@types"], 16 | "lib": ["dom", "dom.iterable", "es2022"], 17 | "useDefineForClassFields": false 18 | }, 19 | "angularCompilerOptions": { 20 | "fullTemplateTypeCheck": true, 21 | "strictInjectionParameters": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /demo-app/ng15/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": ["jasmine", "node"] 6 | }, 7 | "files": ["src/test.ts", "src/polyfills.ts"], 8 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /demo-app/ng16/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. 13 | -------------------------------------------------------------------------------- /demo-app/ng16/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /demo-app/ng16/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["@nationalbankbelgium"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "rules": { 8 | "@angular-eslint/component-selector": [ 9 | "error", 10 | { 11 | "prefix": "app", 12 | "style": "kebab-case", 13 | "type": "element" 14 | } 15 | ], 16 | "@angular-eslint/directive-selector": [ 17 | "error", 18 | { 19 | "prefix": "app", 20 | "style": "camelCase", 21 | "type": "attribute" 22 | } 23 | ] 24 | } 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /demo-app/ng16/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.angular/cache 36 | /.sass-cache 37 | /connect.lock 38 | /coverage 39 | /libpeerconnection.log 40 | npm-debug.log 41 | yarn-error.log 42 | testem.log 43 | /typings 44 | 45 | # System Files 46 | .DS_Store 47 | Thumbs.db 48 | -------------------------------------------------------------------------------- /demo-app/ng16/.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org 2 | -------------------------------------------------------------------------------- /demo-app/ng16/README.md: -------------------------------------------------------------------------------- 1 | # DemoApp 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 13.4.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|guard|interface|enum|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 | 25 | ## Further help 26 | 27 | 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). 28 | -------------------------------------------------------------------------------- /demo-app/ng16/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "demo-app": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "style": "scss" 14 | } 15 | }, 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/demo-app", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "tsconfig.app.json", 25 | "assets": ["src/favicon.ico", "src/assets"], 26 | "styles": ["node_modules/normalize.css/normalize.css", "src/styles/styles.scss"], 27 | "stylePreprocessorOptions": { 28 | "includePaths": ["src/styles"] 29 | }, 30 | "scripts": [], 31 | "vendorChunk": true, 32 | "extractLicenses": false, 33 | "buildOptimizer": false, 34 | "sourceMap": true, 35 | "optimization": false, 36 | "namedChunks": true 37 | }, 38 | "configurations": { 39 | "production": { 40 | "fileReplacements": [ 41 | { 42 | "replace": "src/environments/environment.ts", 43 | "with": "src/environments/environment.prod.ts" 44 | } 45 | ], 46 | "optimization": true, 47 | "outputHashing": "all", 48 | "sourceMap": false, 49 | "namedChunks": false, 50 | "extractLicenses": true, 51 | "vendorChunk": false, 52 | "buildOptimizer": true, 53 | "budgets": [ 54 | { 55 | "type": "initial", 56 | "maximumWarning": "2mb", 57 | "maximumError": "5mb" 58 | }, 59 | { 60 | "type": "anyComponentStyle", 61 | "maximumWarning": "6kb", 62 | "maximumError": "10kb" 63 | } 64 | ] 65 | } 66 | }, 67 | "defaultConfiguration": "" 68 | }, 69 | "serve": { 70 | "builder": "@angular-devkit/build-angular:dev-server", 71 | "options": { 72 | "browserTarget": "demo-app:build" 73 | }, 74 | "configurations": { 75 | "production": { 76 | "browserTarget": "demo-app:build:production" 77 | } 78 | } 79 | }, 80 | "extract-i18n": { 81 | "builder": "@angular-devkit/build-angular:extract-i18n", 82 | "options": { 83 | "browserTarget": "demo-app:build" 84 | } 85 | }, 86 | "test": { 87 | "builder": "@angular-devkit/build-angular:karma", 88 | "options": { 89 | "main": "src/test.ts", 90 | "polyfills": "src/polyfills.ts", 91 | "tsConfig": "tsconfig.spec.json", 92 | "karmaConfig": "karma.conf.js", 93 | "styles": ["node_modules/normalize.css/normalize.css", "src/styles/styles.scss"], 94 | "stylePreprocessorOptions": { 95 | "includePaths": ["src/styles"] 96 | }, 97 | "scripts": [], 98 | "assets": ["src/favicon.ico", "src/assets"] 99 | } 100 | }, 101 | "lint": { 102 | "builder": "@angular-eslint/builder:lint", 103 | "options": { 104 | "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"] 105 | } 106 | }, 107 | "e2e": { 108 | "builder": "@angular-devkit/build-angular:protractor", 109 | "options": { 110 | "protractorConfig": "e2e/protractor.conf.js", 111 | "devServerTarget": "demo-app:serve" 112 | }, 113 | "configurations": { 114 | "production": { 115 | "devServerTarget": "demo-app:serve:production" 116 | } 117 | } 118 | } 119 | } 120 | } 121 | }, 122 | "cli": { 123 | "schematicCollections": ["@angular-eslint/schematics"], 124 | "analytics": false 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /demo-app/ng16/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require("jasmine-spec-reporter"); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: ["./src/**/*.e2e-spec.ts"], 13 | capabilities: { 14 | browserName: "chrome" 15 | }, 16 | directConnect: true, 17 | baseUrl: "http://localhost:4200/", 18 | framework: "jasmine", 19 | jasmineNodeOpts: { 20 | showColors: true, 21 | defaultTimeoutInterval: 30000, 22 | print: function () {} 23 | }, 24 | onPrepare() { 25 | require("ts-node").register({ 26 | project: require("path").join(__dirname, "./tsconfig.json") 27 | }); 28 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /demo-app/ng16/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from "./app.po"; 2 | import { browser, logging } from "protractor"; 3 | 4 | describe("workspace-project App", () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it("should display welcome message", async () => { 12 | await page.navigateTo(); 13 | const titleText = await page.getTitleText(); 14 | expect(titleText).toEqual("demo-app app is running!"); 15 | }); 16 | 17 | afterEach(async () => { 18 | // Assert that there are no errors emitted from the browser 19 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 20 | const expectedLogEntry: Partial = { 21 | level: logging.Level.SEVERE 22 | }; 23 | expect(logs).not.toContain(jasmine.objectContaining(expectedLogEntry)); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /demo-app/ng16/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from "protractor"; 2 | 3 | export class AppPage { 4 | public navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | public getTitleText() { 9 | return element(by.css("app-root .content span")).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /demo-app/ng16/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es2018", 7 | "types": ["jasmine", "jasminewd2", "node"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /demo-app/ng16/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: "", 7 | frameworks: ["jasmine", "@angular-devkit/build-angular"], 8 | plugins: [ 9 | require("karma-jasmine"), 10 | require("karma-chrome-launcher"), 11 | require("karma-jasmine-html-reporter"), 12 | require("karma-coverage-istanbul-reporter"), 13 | require("@angular-devkit/build-angular/plugins/karma") 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require("path").join(__dirname, "./coverage"), 20 | reports: ["html", "lcovonly", "text-summary"], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ["progress", "kjhtml"], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ["Chrome"], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /demo-app/ng16/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-app", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "clean": "npx rimraf ./dist ./reports", 6 | "clean:modules": "npx rimraf ./node_modules package-lock.json", 7 | "ng": "ng", 8 | "start": "ng serve", 9 | "build": "ng build", 10 | "test": "ng test", 11 | "test:ci": "ng test --watch=false --browsers=ChromeHeadless", 12 | "lint": "ng lint", 13 | "e2e": "ng e2e" 14 | }, 15 | "private": true, 16 | "dependencies": { 17 | "@angular/animations": "^16.2.12", 18 | "@angular/cdk": "^16.2.14", 19 | "@angular/common": "^16.2.12", 20 | "@angular/compiler": "^16.2.12", 21 | "@angular/core": "^16.2.12", 22 | "@angular/forms": "^16.2.12", 23 | "@angular/material": "^16.2.14", 24 | "@angular/platform-browser": "^16.2.12", 25 | "@angular/platform-browser-dynamic": "^16.2.12", 26 | "@angular/router": "^16.2.12", 27 | "@nationalbankbelgium/ngx-form-errors": "../../dist", 28 | "@ngx-translate/core": "^15.0.0", 29 | "core-js": "^3.3.5", 30 | "eligrey-classlist-js-polyfill": "1.2.20180112", 31 | "material-design-icons": "^3.0.1", 32 | "normalize.css": "^8.0.1", 33 | "rxjs": "^7.8.1", 34 | "tslib": "^2.0.0", 35 | "zone.js": "~0.13.3" 36 | }, 37 | "devDependencies": { 38 | "@angular-devkit/build-angular": "16.2.15", 39 | "@angular/cli": "^16.2.15", 40 | "@angular/compiler-cli": "^16.2.12", 41 | "@angular/language-service": "^16.2.12", 42 | "@nationalbankbelgium/code-style": "^1.9.0", 43 | "@nationalbankbelgium/eslint-config": "16.0.0", 44 | "@types/jasmine": "^3.3.8", 45 | "@types/jasminewd2": "^2.0.3", 46 | "@types/node": "^12.11.1", 47 | "jasmine-core": "~3.8.0", 48 | "jasmine-spec-reporter": "~5.0.0", 49 | "karma": "~6.4.2", 50 | "karma-chrome-launcher": "~3.1.0", 51 | "karma-coverage-istanbul-reporter": "~3.0.2", 52 | "karma-jasmine": "~4.0.0", 53 | "karma-jasmine-html-reporter": "^1.5.0", 54 | "protractor": "~7.0.0", 55 | "ts-node": "~7.0.0", 56 | "tslint-config-prettier": "^1.17.0", 57 | "typescript": "~4.9.5" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { RouterModule, Routes } from "@angular/router"; 3 | import { 4 | NgxFormsExampleComponent, 5 | ReactiveFormsExampleComponent, 6 | TemplateDrivenFormsExampleComponent, 7 | TypedReactiveFormsExampleComponent 8 | } from "./pages"; 9 | 10 | const routes: Routes = [ 11 | { path: "", redirectTo: "/template-driven-forms", pathMatch: "full" }, 12 | { path: "reactive-forms", component: ReactiveFormsExampleComponent }, 13 | { path: "template-driven-forms", component: TemplateDrivenFormsExampleComponent }, 14 | { path: "ngx-form-errors", component: NgxFormsExampleComponent }, 15 | { path: "typed-reactive-forms", component: TypedReactiveFormsExampleComponent } 16 | ]; 17 | 18 | @NgModule({ 19 | imports: [RouterModule.forRoot(routes)], 20 | exports: [RouterModule] 21 | }) 22 | export class AppRoutingModule {} 23 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 5 |

Ngx-Form-Errors

6 |

 - Validation messages in Reactive Forms made easy

7 |
8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | mat-toolbar { 2 | .slogan { 3 | font-size: 16px; 4 | font-style: italic; 5 | line-height: normal; 6 | } 7 | 8 | .spacer { 9 | flex: 1 1 auto; 10 | } 11 | } 12 | 13 | mat-sidenav-container { 14 | flex: 100% 1; 15 | 16 | mat-sidenav { 17 | max-height: 100%; 18 | overflow-y: auto; 19 | } 20 | 21 | mat-sidenav-content { 22 | max-height: 100%; 23 | overflow-y: auto; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; 2 | import { AppComponent } from "./app.component"; 3 | import { NO_ERRORS_SCHEMA } from "@angular/core"; 4 | import { RouterTestingModule } from "@angular/router/testing"; 5 | 6 | describe("AppComponent", () => { 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | return TestBed.configureTestingModule({ 11 | imports: [RouterTestingModule], 12 | declarations: [AppComponent], 13 | schemas: [NO_ERRORS_SCHEMA] 14 | }).compileComponents(); 15 | })); 16 | 17 | beforeEach(() => { 18 | fixture = TestBed.createComponent(AppComponent); 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it("should create the app", () => { 23 | const app: AppComponent = fixture.debugElement.componentInstance; 24 | expect(app).toBeTruthy(); 25 | }); 26 | 27 | it("should render title in a h1 tag", () => { 28 | fixture.detectChanges(); 29 | const compiled: HTMLElement = fixture.debugElement.nativeElement; 30 | const h1Element = compiled.querySelector("h1"); 31 | expect(h1Element).toBeTruthy(); 32 | // tslint:disable-next-line:no-non-null-assertion 33 | expect(h1Element!.textContent).toContain("Ngx-Form-Errors"); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, ViewChild } from "@angular/core"; 2 | import { Event, NavigationEnd, Router } from "@angular/router"; 3 | import { MatSidenav } from "@angular/material/sidenav"; 4 | import { BreakpointObserver, BreakpointState } from "@angular/cdk/layout"; 5 | import { of, Subscription } from "rxjs"; 6 | 7 | const MEDIA_MATCH = "(max-width: 600px)"; 8 | 9 | @Component({ 10 | selector: "app-root", 11 | templateUrl: "./app.component.html", 12 | styleUrls: ["./app.component.scss"] 13 | }) 14 | export class AppComponent implements OnDestroy { 15 | @ViewChild("sidenav") // see https://angular.io/guide/static-query-migration 16 | // see https://angular.io/guide/static-query-migration 17 | private _sidenav!: MatSidenav; 18 | 19 | public mobileQueryMatches = false; 20 | 21 | private _routerSubscription: Subscription; 22 | private _mediaQuerySubscription: Subscription; 23 | 24 | public constructor( 25 | private _router: Router, 26 | public breakpointObserver: BreakpointObserver 27 | ) { 28 | this.mobileQueryMatches = this.breakpointObserver.isMatched(MEDIA_MATCH); 29 | 30 | this._mediaQuerySubscription = this.breakpointObserver.observe([MEDIA_MATCH]).subscribe((state: BreakpointState) => { 31 | this.mobileQueryMatches = state.matches; 32 | }); 33 | 34 | this._routerSubscription = this._router.events.subscribe((value: Event) => { 35 | if (value instanceof NavigationEnd && this._sidenav.mode === "over") { 36 | of(this._sidenav.close()).subscribe(); 37 | } 38 | }); 39 | } 40 | 41 | public ngOnDestroy(): void { 42 | this._mediaQuerySubscription.unsubscribe(); 43 | this._routerSubscription.unsubscribe(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule, DomSanitizer } from "@angular/platform-browser"; 2 | import { NgModule } from "@angular/core"; 3 | import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; 4 | import { FormsModule, ReactiveFormsModule } from "@angular/forms"; 5 | import { HttpClientModule } from "@angular/common/http"; 6 | import { TranslateModule, TranslateService } from "@ngx-translate/core"; 7 | import { MatLegacyButtonModule as MatButtonModule } from "@angular/material/legacy-button"; 8 | import { MatButtonToggleModule } from "@angular/material/button-toggle"; 9 | import { MatLegacyCardModule as MatCardModule } from "@angular/material/legacy-card"; 10 | import { MatLegacyFormFieldModule as MatFormFieldModule } from "@angular/material/legacy-form-field"; 11 | import { MatLegacyInputModule as MatInputModule } from "@angular/material/legacy-input"; 12 | import { MatLegacyListModule as MatListModule } from "@angular/material/legacy-list"; 13 | import { MatSidenavModule } from "@angular/material/sidenav"; 14 | import { MatGridListModule } from "@angular/material/grid-list"; 15 | import { MatIconModule, MatIconRegistry } from "@angular/material/icon"; 16 | import { MatToolbarModule } from "@angular/material/toolbar"; 17 | import { NgxFormErrorsMessageService, NgxFormErrorsModule } from "@nationalbankbelgium/ngx-form-errors"; 18 | import { AppComponent } from "./app.component"; 19 | import { initializeTranslation } from "./translation.config"; 20 | import { AppRoutingModule } from "./app-routing.module"; 21 | import { 22 | CardComponent, 23 | LanguageSelectorComponent, 24 | NavigationComponent, 25 | SimpleFormErrorComponent, 26 | TranslatedFormErrorComponent 27 | } from "./components"; 28 | import { 29 | NgxFormsExampleComponent, 30 | ReactiveFormsExampleComponent, 31 | TemplateDrivenFormsExampleComponent, 32 | TypedReactiveFormsExampleComponent 33 | } from "./pages"; 34 | 35 | /* eslint-disable */ 36 | @NgModule({ 37 | declarations: [ 38 | AppComponent, 39 | LanguageSelectorComponent, 40 | SimpleFormErrorComponent, 41 | TranslatedFormErrorComponent, 42 | ReactiveFormsExampleComponent, 43 | NgxFormsExampleComponent, 44 | TemplateDrivenFormsExampleComponent, 45 | TypedReactiveFormsExampleComponent, 46 | NavigationComponent, 47 | CardComponent 48 | ], 49 | imports: [ 50 | BrowserModule, 51 | BrowserAnimationsModule, 52 | AppRoutingModule, 53 | FormsModule, 54 | HttpClientModule, 55 | MatButtonModule, 56 | MatButtonToggleModule, 57 | MatCardModule, 58 | MatFormFieldModule, 59 | MatGridListModule, 60 | MatInputModule, 61 | MatToolbarModule, 62 | MatListModule, 63 | MatSidenavModule, 64 | MatIconModule, 65 | ReactiveFormsModule, 66 | TranslateModule.forRoot(), 67 | NgxFormErrorsModule.forRoot({ 68 | formErrorComponent: TranslatedFormErrorComponent 69 | }) 70 | ], 71 | exports: [LanguageSelectorComponent], 72 | providers: [], 73 | bootstrap: [AppComponent] 74 | }) 75 | export class AppModule { 76 | public constructor( 77 | private translateService: TranslateService, 78 | private errorMessageService: NgxFormErrorsMessageService, 79 | iconRegistry: MatIconRegistry, 80 | sanitizer: DomSanitizer 81 | ) { 82 | initializeTranslation(this.translateService); 83 | 84 | iconRegistry.addSvgIcon("github", sanitizer.bypassSecurityTrustResourceUrl("assets/img/github-icon.svg")); 85 | 86 | this.errorMessageService.addErrorMessages({ 87 | required: "DEMO.FORM_VALIDATION.WITH_NGX_FORM_ERRORS.REQUIRED", 88 | "matchingPasswords.password.required": "DEMO.FORM_VALIDATION.WITH_NGX_FORM_ERRORS.PASSWORD_REQUIRED", 89 | minlength: "DEMO.FORM_VALIDATION.WITH_NGX_FORM_ERRORS.PASSWORD.MIN_LENGTH", 90 | maxlength: "DEMO.FORM_VALIDATION.WITH_NGX_FORM_ERRORS.PASSWORD.MAX_LENGTH", 91 | pattern: "DEMO.FORM_VALIDATION.WITH_NGX_FORM_ERRORS.PASSWORD.PATTERN" 92 | }); 93 | 94 | this.errorMessageService.addFieldNames({ 95 | username: "DEMO.FIELDS.USER_NAME", 96 | "matchingPasswords.password": "not used, the alias defined via the directive takes precedence over this", 97 | "matchingPasswords.confirmPassword": "DEMO.FIELDS.CONFIRM_PASSWORD" 98 | }); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/components/card/card.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/components/card/card.component.scss: -------------------------------------------------------------------------------- 1 | :host mat-card { 2 | box-sizing: border-box; 3 | width: 100%; 4 | min-height: 100%; 5 | } 6 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/components/card/card.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding, Input } from "@angular/core"; 2 | 3 | @Component({ 4 | selector: "app-card", 5 | templateUrl: "./card.component.html", 6 | styleUrls: ["./card.component.scss"] 7 | }) 8 | export class CardComponent { 9 | @HostBinding("class.app-color-primary") 10 | public primaryColor = false; 11 | @HostBinding("class.app-color-accent") 12 | public accentColor = false; 13 | @HostBinding("class.app-color-warning") 14 | public warningColor = false; 15 | @HostBinding("class.app-color-success") 16 | public successColor = false; 17 | 18 | @Input() 19 | public set color(color: "primary" | "accent" | "warning" | "success") { 20 | this.primaryColor = false; 21 | this.accentColor = false; 22 | this.warningColor = false; 23 | this.successColor = false; 24 | 25 | switch (color) { 26 | case "primary": 27 | this.primaryColor = true; 28 | break; 29 | case "accent": 30 | this.accentColor = true; 31 | break; 32 | case "warning": 33 | this.warningColor = true; 34 | break; 35 | case "success": 36 | this.successColor = true; 37 | break; 38 | default: 39 | break; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/components/card/card.theme.scss: -------------------------------------------------------------------------------- 1 | @use "@angular/material" as mat; 2 | @mixin card-theme($theme) { 3 | $primary: map-get($theme, primary); 4 | $accent: map-get($theme, accent); 5 | $warning: map-get($theme, warn); 6 | $success: map-get($theme, success); 7 | 8 | app-card.app-color-primary mat-card { 9 | background-color: mat.get-color-from-palette($primary, 500); 10 | color: mat.get-contrast-color-from-palette($primary, 500); 11 | } 12 | 13 | app-card.app-color-accent mat-card { 14 | background-color: mat.get-color-from-palette($accent, 500); 15 | color: mat.get-contrast-color-from-palette($accent, 500); 16 | } 17 | 18 | app-card.app-color-warning mat-card { 19 | background-color: mat.get-color-from-palette($warning, 500); 20 | color: mat.get-contrast-color-from-palette($warning, 500); 21 | } 22 | 23 | app-card.app-color-success mat-card { 24 | /*Themes do not have a success map by default*/ 25 | background-color: mat.get-color-from-palette($success, 500); 26 | color: mat.get-contrast-color-from-palette($success, 500); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/components/card/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./card.component"; 2 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./card"; 2 | export * from "./language-selector"; 3 | export * from "./navigation"; 4 | export * from "./simple-form-error"; 5 | export * from "./translated-form-error"; 6 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/components/language-selector/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./language-selector.component"; 2 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/components/language-selector/language-selector.component.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | {{ language | uppercase }} 11 | 12 | 13 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/components/language-selector/language-selector.component.scss: -------------------------------------------------------------------------------- 1 | :host mat-button-toggle-group { 2 | box-sizing: border-box; 3 | width: 100%; 4 | border-radius: 0; 5 | 6 | mat-button-toggle.accent { 7 | flex: 1; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/components/language-selector/language-selector.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding, OnDestroy, OnInit } from "@angular/core"; 2 | import { Subscription } from "rxjs"; 3 | import { LangChangeEvent, TranslateService } from "@ngx-translate/core"; 4 | 5 | /** 6 | * Name of the component 7 | */ 8 | const componentName = "language-selector"; 9 | 10 | /** 11 | * Component to select the application's language from a list of available languages passed as parameter. 12 | */ 13 | @Component({ 14 | selector: "app-language-selector", 15 | templateUrl: "./language-selector.component.html", 16 | styleUrls: ["./language-selector.component.scss"] 17 | }) 18 | export class LanguageSelectorComponent implements OnInit, OnDestroy { 19 | @HostBinding("class") 20 | public cssClass: string = componentName; 21 | 22 | /** 23 | * The currently selected language 24 | */ 25 | public selectedLanguage: string; 26 | 27 | /** 28 | * A reference to the translateService subscription, needed to unsubscribe upon destroy. 29 | */ 30 | private languageChangeSubscription: Subscription; 31 | 32 | public supportedLanguages: string[] = ["en", "fr", "nl"]; 33 | 34 | /** 35 | * Class constructor 36 | * @param translateService - the translation service of the application 37 | */ 38 | public constructor(protected translateService: TranslateService) { 39 | this.selectedLanguage = this.translateService.currentLang; 40 | 41 | this.languageChangeSubscription = this.translateService.onLangChange.subscribe( 42 | (event: LangChangeEvent) => (this.selectedLanguage = event.lang), 43 | () => console.error(componentName + ": an error occurred getting the current language.") 44 | ); 45 | } 46 | 47 | /** 48 | * Component lifecycle hook 49 | */ 50 | public ngOnInit(): void { 51 | console.log(componentName + ": controller initialized"); 52 | } 53 | 54 | /** 55 | * Component lifecycle hook 56 | */ 57 | public ngOnDestroy(): void { 58 | if (this.languageChangeSubscription) { 59 | this.languageChangeSubscription.unsubscribe(); 60 | } 61 | } 62 | 63 | /** 64 | * Change the current language based on the selection made by the user 65 | * @param language - the new language 66 | */ 67 | public changeLanguage(language: string): void { 68 | if (this.selectedLanguage !== language) { 69 | this.selectedLanguage = language; 70 | 71 | this.translateService.use(language); 72 | } 73 | } 74 | 75 | /** 76 | * @ignore 77 | */ 78 | public trackLanguage(index: number): number { 79 | return index; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/components/language-selector/language-selector.theme.scss: -------------------------------------------------------------------------------- 1 | @use "@angular/material" as mat; 2 | @mixin language-selector-theme($theme) { 3 | $primary: map-get($theme, primary); 4 | 5 | .language-selector mat-button-toggle-group mat-button-toggle { 6 | &.active { 7 | &, 8 | &:hover, 9 | &:active { 10 | background-color: mat.get-color-from-palette($primary, 500); 11 | color: mat.get-contrast-color-from-palette($primary, 500); 12 | } 13 | } 14 | 15 | &:hover, 16 | &:focus { 17 | background-color: mat.get-color-from-palette($primary, 100); 18 | color: mat.get-contrast-color-from-palette($primary, 100); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/components/navigation/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./navigation.component"; 2 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/components/navigation/navigation.component.html: -------------------------------------------------------------------------------- 1 | 2 | Template Driven Forms 3 | Reactive Forms 4 | Ngx Form Errors 5 | Typed Reactive Form 6 | 7 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/components/navigation/navigation.component.scss: -------------------------------------------------------------------------------- 1 | :host mat-nav-list { 2 | padding: 0; 3 | 4 | a { 5 | outline: none; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/components/navigation/navigation.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding } from "@angular/core"; 2 | 3 | const componentName = "navigation"; 4 | 5 | @Component({ 6 | selector: "app-navigation", 7 | templateUrl: "./navigation.component.html", 8 | styleUrls: ["./navigation.component.scss"] 9 | }) 10 | export class NavigationComponent { 11 | @HostBinding("class") 12 | public cssClass: string = componentName; 13 | } 14 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/components/navigation/navigation.theme.scss: -------------------------------------------------------------------------------- 1 | @use "@angular/material" as mat; 2 | @mixin app-navigation-theme($theme) { 3 | $primary: map-get($theme, primary); 4 | $accent: map-get($theme, accent); 5 | .navigation mat-nav-list a.mat-list-item { 6 | &.active { 7 | &, 8 | &:hover, 9 | &:active { 10 | background-color: mat.get-color-from-palette($primary, 500); 11 | color: mat.get-contrast-color-from-palette($primary, 500); 12 | } 13 | } 14 | 15 | &:hover { 16 | background-color: mat.get-color-from-palette($primary, 100); 17 | color: mat.get-contrast-color-from-palette($primary, 100); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/components/simple-form-error/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./simple-form-error.component"; 2 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/components/simple-form-error/simple-form-error.component.html: -------------------------------------------------------------------------------- 1 |
{{ error.message }}
2 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/components/simple-form-error/simple-form-error.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding } from "@angular/core"; 2 | import { Observable } from "rxjs"; 3 | import { NgxFormErrorComponent, NgxFormFieldError } from "@nationalbankbelgium/ngx-form-errors"; 4 | 5 | @Component({ 6 | selector: "app-simple-form-error", 7 | templateUrl: "./simple-form-error.component.html" 8 | }) 9 | export class SimpleFormErrorComponent implements NgxFormErrorComponent { 10 | @HostBinding("class") 11 | public cssClass = "simple-form-error"; 12 | 13 | public errors: NgxFormFieldError[] = []; 14 | public errors$!: Observable; 15 | 16 | public subscribeToErrors(): void { 17 | this.errors$.subscribe((errors: NgxFormFieldError[]) => { 18 | this.errors = errors; 19 | }); 20 | } 21 | 22 | public trackError(index: number): number { 23 | return index; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/components/translated-form-error/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./translated-form-error.component"; 2 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/components/translated-form-error/translated-form-error.component.html: -------------------------------------------------------------------------------- 1 | {{ error.message | translate: error.params }} 2 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/components/translated-form-error/translated-form-error.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding, OnInit } from "@angular/core"; 2 | import { LangChangeEvent, TranslateService } from "@ngx-translate/core"; 3 | import { Observable } from "rxjs"; 4 | import { NgxFormErrorComponent, NgxFormFieldError } from "@nationalbankbelgium/ngx-form-errors"; 5 | 6 | @Component({ 7 | selector: "app-translated-form-error", 8 | templateUrl: "./translated-form-error.component.html" 9 | }) 10 | export class TranslatedFormErrorComponent implements NgxFormErrorComponent, OnInit { 11 | @HostBinding("class") 12 | public cssClass = "translated-form-error"; 13 | 14 | public errors: NgxFormFieldError[] = []; 15 | public errors$!: Observable; 16 | public fieldName = "undefined"; 17 | 18 | public constructor(public translateService: TranslateService) {} 19 | 20 | public ngOnInit(): void { 21 | this.translateService.onLangChange.subscribe((_ev: LangChangeEvent) => { 22 | this.translateFieldName(); 23 | }); 24 | } 25 | 26 | public subscribeToErrors(): void { 27 | this.errors$.subscribe((errors: NgxFormFieldError[]) => { 28 | this.errors = errors; 29 | 30 | if (errors.length) { 31 | // the formField can be retrieved from the "fieldName" param of any of the errors 32 | this.fieldName = errors[0].params.fieldName; 33 | this.translateFieldName(); 34 | } 35 | }); 36 | } 37 | 38 | public translateFieldName(): void { 39 | for (const error of this.errors) { 40 | error.params = { ...error.params, fieldName: this.translateService.instant(this.fieldName) }; 41 | } 42 | } 43 | 44 | public getErrorClass(): string { 45 | return this.errors.length > 2 ? "maximum-height" : "small-height"; 46 | } 47 | 48 | public trackError(index: number): number { 49 | return index; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/pages/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ngx-forms-example"; 2 | export * from "./reactive-forms-example"; 3 | export * from "./template-driven-forms-example"; 4 | export * from "./typed-reactive-forms-example"; 5 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/pages/ngx-forms-example/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ngx-forms-example.component"; 2 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/pages/ngx-forms-example/ngx-forms-example.component.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .grid { 4 | display: grid; 5 | grid-template-rows: auto; 6 | grid-template-columns: 1fr 1fr 1fr; 7 | grid-gap: 10px; 8 | } 9 | 10 | .form-card { 11 | grid-column-start: 1; 12 | grid-column-end: 4; 13 | 14 | mat-form-field { 15 | box-sizing: border-box; 16 | width: 100%; 17 | 18 | @media #{$monitor-query} { 19 | width: 45%; 20 | &:last-child { 21 | margin-left: 10%; 22 | } 23 | } 24 | } 25 | 26 | mat-card-actions { 27 | display: flex; 28 | align-items: flex-start; 29 | flex-wrap: wrap; 30 | margin: -10px; 31 | 32 | button { 33 | margin: 10px; 34 | @media #{$mobile-query} { 35 | width: 100%; 36 | } 37 | } 38 | } 39 | } 40 | 41 | .form-field-info { 42 | @media #{$table-query} { 43 | grid-column-start: 1; 44 | grid-column-end: 4; 45 | } 46 | 47 | mat-card-content { 48 | > div { 49 | padding: 5px 0; 50 | } 51 | 52 | pre { 53 | overflow: auto; 54 | box-sizing: border-box; 55 | display: block; 56 | width: 100%; 57 | max-height: 75px; 58 | 59 | margin: inherit; 60 | padding: 5px; 61 | border-radius: 4px; 62 | 63 | background-color: rgba(0, 0, 0, 0.2); 64 | 65 | &:empty { 66 | display: none; 67 | } 68 | } 69 | } 70 | } 71 | 72 | .form-validation-messages { 73 | grid-column-start: 1; 74 | grid-column-end: 4; 75 | } 76 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/pages/ngx-forms-example/ngx-forms-example.component.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @angular-eslint/template/use-track-by-function, @angular-eslint/template/cyclomatic-complexity */ 2 | import { Component } from "@angular/core"; 3 | import { AbstractControl, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, ValidationErrors, Validators } from "@angular/forms"; 4 | import { ErrorStateMatcher } from "@angular/material/core"; 5 | import { ParentErrorStateMatcher } from "../../parent-error-state-matcher"; 6 | import { PasswordValidator } from "../../password-validator"; 7 | 8 | @Component({ 9 | selector: "app-ngx-forms-example", 10 | templateUrl: "./ngx-forms-example.component.html", 11 | styleUrls: ["./ngx-forms-example.component.scss"] 12 | }) 13 | export class NgxFormsExampleComponent { 14 | public formGroup: UntypedFormGroup; 15 | public parentErrorStateMatcher: ErrorStateMatcher = new ParentErrorStateMatcher(); 16 | public passwordPattern = "^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9]+$"; 17 | public showValidationDetails = false; 18 | public showValidationSummary = true; 19 | 20 | public constructor(private formBuilder: UntypedFormBuilder) { 21 | this.formGroup = this.formBuilder.group({ 22 | username: [undefined, Validators.required], 23 | matchingPasswords: new UntypedFormGroup( 24 | { 25 | password: new UntypedFormControl( 26 | "", 27 | Validators.compose([ 28 | Validators.minLength(3), 29 | Validators.maxLength(10), 30 | Validators.required, 31 | Validators.pattern(this.passwordPattern) // this is for the letters (both uppercase and lowercase) and numbers validation 32 | ]) 33 | ), 34 | confirmPassword: new UntypedFormControl("", Validators.required) 35 | }, 36 | { 37 | validators: (formGroup: AbstractControl): ValidationErrors | null => 38 | PasswordValidator.areEqual(formGroup) 39 | } 40 | ) 41 | }); 42 | } 43 | 44 | public toggleValidationDetails(): void { 45 | this.showValidationDetails = !this.showValidationDetails; 46 | } 47 | 48 | public toggleValidationSummary(): void { 49 | this.showValidationSummary = !this.showValidationSummary; 50 | } 51 | 52 | public onSubmitUserDetails(formGroup: UntypedFormGroup): void { 53 | console.log("Submitted form:", formGroup.value); 54 | } 55 | 56 | public getFormStatus(): void { 57 | console.log("Form status", this.formGroup); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/pages/reactive-forms-example/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./reactive-forms-example.component"; 2 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/pages/reactive-forms-example/reactive-forms-example.component.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .grid { 4 | display: grid; 5 | grid-template-rows: auto; 6 | grid-template-columns: 1fr 1fr 1fr; 7 | grid-gap: 10px; 8 | } 9 | 10 | .form-card { 11 | grid-column-start: 1; 12 | grid-column-end: 4; 13 | 14 | mat-form-field { 15 | box-sizing: border-box; 16 | width: 100%; 17 | 18 | @media #{$monitor-query} { 19 | width: 45%; 20 | &:last-child { 21 | margin-left: 10%; 22 | } 23 | } 24 | } 25 | 26 | mat-card-actions { 27 | display: flex; 28 | align-items: flex-start; 29 | flex-wrap: wrap; 30 | margin: -10px; 31 | 32 | button { 33 | margin: 10px; 34 | @media #{$mobile-query} { 35 | width: 100%; 36 | } 37 | } 38 | } 39 | } 40 | 41 | .form-field-info { 42 | @media #{$table-query} { 43 | grid-column-start: 1; 44 | grid-column-end: 4; 45 | } 46 | 47 | mat-card-content { 48 | > div { 49 | padding: 5px 0; 50 | } 51 | 52 | pre { 53 | overflow: auto; 54 | box-sizing: border-box; 55 | display: block; 56 | width: 100%; 57 | max-height: 75px; 58 | 59 | margin: inherit; 60 | padding: 5px; 61 | border-radius: 4px; 62 | 63 | background-color: rgba(0, 0, 0, 0.2); 64 | 65 | &:empty { 66 | display: none; 67 | } 68 | } 69 | } 70 | } 71 | 72 | .form-validation-messages { 73 | grid-column-start: 1; 74 | grid-column-end: 4; 75 | } 76 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/pages/reactive-forms-example/reactive-forms-example.component.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @angular-eslint/template/use-track-by-function, @angular-eslint/template/cyclomatic-complexity */ 2 | import { Component } from "@angular/core"; 3 | import { AbstractControl, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, ValidationErrors, Validators } from "@angular/forms"; 4 | import { ErrorStateMatcher } from "@angular/material/core"; 5 | import { PasswordValidator } from "../../password-validator"; 6 | import { ParentErrorStateMatcher } from "../../parent-error-state-matcher"; 7 | 8 | @Component({ 9 | selector: "app-reactive-forms-example", 10 | templateUrl: "./reactive-forms-example.component.html", 11 | styleUrls: ["./reactive-forms-example.component.scss"] 12 | }) 13 | export class ReactiveFormsExampleComponent { 14 | public formGroup: UntypedFormGroup; 15 | public validationMessages: { [key: string]: { type: string; message: string }[] }; 16 | public parentErrorStateMatcher: ErrorStateMatcher = new ParentErrorStateMatcher(); 17 | public passwordPattern = "^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9]+$"; 18 | public showValidationDetails = false; 19 | public showValidationSummary = true; 20 | 21 | public constructor(private formBuilder: UntypedFormBuilder) { 22 | this.formGroup = this.formBuilder.group({ 23 | username: [undefined, Validators.required], 24 | matchingPasswords: new UntypedFormGroup( 25 | { 26 | password: new UntypedFormControl( 27 | "", 28 | Validators.compose([ 29 | Validators.minLength(3), 30 | Validators.maxLength(10), 31 | Validators.required, 32 | Validators.pattern(this.passwordPattern) // this is for the letters (both uppercase and lowercase) and numbers validation 33 | ]) 34 | ), 35 | confirmPassword: new UntypedFormControl("", Validators.required) 36 | }, 37 | { 38 | validators: (formGroup: AbstractControl): ValidationErrors | null => 39 | PasswordValidator.areEqual(formGroup) 40 | } 41 | ) 42 | }); 43 | 44 | this.validationMessages = { 45 | username: [ 46 | { 47 | type: "required", 48 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.USER_NAME.REQUIRED" 49 | }, 50 | { type: "unique", message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.USER_NAME.UNIQUE" } 51 | ], 52 | password: [ 53 | { 54 | type: "required", 55 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.PASSWORD.REQUIRED" 56 | }, 57 | { 58 | type: "minlength", 59 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.PASSWORD.MIN_LENGTH" 60 | }, 61 | { type: "pattern", message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.PASSWORD.PATTERN" } 62 | ], 63 | confirmPassword: [ 64 | { 65 | type: "required", 66 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.CONFIRM_PASSWORD.REQUIRED" 67 | }, 68 | { 69 | type: "areEqual", 70 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.CONFIRM_PASSWORD.ARE_EQUAL" 71 | } 72 | ] 73 | }; 74 | } 75 | 76 | public toggleValidationDetails(): void { 77 | this.showValidationDetails = !this.showValidationDetails; 78 | } 79 | 80 | public toggleValidationSummary(): void { 81 | this.showValidationSummary = !this.showValidationSummary; 82 | } 83 | 84 | public onSubmitUserDetails(formGroup: UntypedFormGroup): void { 85 | console.log("Submitted form:", formGroup.value); 86 | } 87 | 88 | public getFormStatus(): void { 89 | console.log("Form status", this.formGroup); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/pages/template-driven-forms-example/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./template-driven-forms-example.component"; 2 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/pages/template-driven-forms-example/template-driven-forms-example.component.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .grid { 4 | display: grid; 5 | grid-template-rows: auto; 6 | grid-template-columns: 1fr 1fr 1fr; 7 | grid-gap: 10px; 8 | } 9 | 10 | .form-card { 11 | grid-column-start: 1; 12 | grid-column-end: 4; 13 | 14 | mat-form-field { 15 | box-sizing: border-box; 16 | width: 100%; 17 | 18 | @media #{$monitor-query} { 19 | width: 45%; 20 | &:last-child { 21 | margin-left: 10%; 22 | } 23 | } 24 | } 25 | 26 | mat-card-actions { 27 | display: flex; 28 | align-items: flex-start; 29 | flex-wrap: wrap; 30 | margin: -10px; 31 | 32 | button { 33 | margin: 10px; 34 | @media #{$mobile-query} { 35 | width: 100%; 36 | } 37 | } 38 | } 39 | } 40 | 41 | .form-field-info { 42 | @media #{$table-query} { 43 | grid-column-start: 1; 44 | grid-column-end: 4; 45 | } 46 | 47 | mat-card-content { 48 | > div { 49 | padding: 5px 0; 50 | } 51 | 52 | pre { 53 | overflow: auto; 54 | box-sizing: border-box; 55 | display: block; 56 | width: 100%; 57 | max-height: 75px; 58 | 59 | margin: inherit; 60 | padding: 5px; 61 | border-radius: 4px; 62 | 63 | background-color: rgba(0, 0, 0, 0.2); 64 | 65 | &:empty { 66 | display: none; 67 | } 68 | } 69 | } 70 | } 71 | 72 | .form-validation-messages { 73 | grid-column-start: 1; 74 | grid-column-end: 4; 75 | } 76 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/pages/template-driven-forms-example/template-driven-forms-example.component.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @angular-eslint/template/use-track-by-function, @angular-eslint/template/cyclomatic-complexity */ 2 | import { Component } from "@angular/core"; 3 | import { NgForm } from "@angular/forms"; 4 | import { ErrorStateMatcher } from "@angular/material/core"; 5 | import { ParentErrorStateMatcher } from "../../parent-error-state-matcher"; 6 | 7 | @Component({ 8 | selector: "app-template-driven-forms-example", 9 | templateUrl: "./template-driven-forms-example.component.html", 10 | styleUrls: ["./template-driven-forms-example.component.scss"] 11 | }) 12 | export class TemplateDrivenFormsExampleComponent { 13 | public username = ""; 14 | public password = ""; 15 | public confirmPassword = ""; 16 | 17 | public parentErrorStateMatcher: ErrorStateMatcher = new ParentErrorStateMatcher(); 18 | public passwordPattern = "^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9]+$"; 19 | public validationMessages: { [key: string]: { type: string; message: string }[] }; 20 | public showValidationDetails = false; 21 | public showValidationSummary = true; 22 | 23 | public constructor() { 24 | this.validationMessages = { 25 | username: [ 26 | { 27 | type: "required", 28 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.USER_NAME.REQUIRED" 29 | } 30 | ], 31 | password: [ 32 | { 33 | type: "required", 34 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.PASSWORD.REQUIRED" 35 | }, 36 | { 37 | type: "minlength", 38 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.PASSWORD.MIN_LENGTH" 39 | }, 40 | { type: "pattern", message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.PASSWORD.PATTERN" } 41 | ], 42 | confirmPassword: [ 43 | { 44 | type: "required", 45 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.CONFIRM_PASSWORD.REQUIRED" 46 | }, 47 | { 48 | type: "areEqual", 49 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.CONFIRM_PASSWORD.ARE_EQUAL" 50 | } 51 | ] 52 | }; 53 | } 54 | 55 | public toggleValidationDetails(): void { 56 | this.showValidationDetails = !this.showValidationDetails; 57 | } 58 | 59 | public toggleValidationSummary(): void { 60 | this.showValidationSummary = !this.showValidationSummary; 61 | } 62 | 63 | public onSubmitUserDetails(ngForm: NgForm): void { 64 | console.log("Submitted form:", ngForm.value); 65 | } 66 | 67 | public getFormStatus(ngForm: NgForm): void { 68 | console.log("Form status", ngForm); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/pages/typed-reactive-forms-example/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./typed-reactive-forms-example.component"; 2 | export * from "./matching-password"; 3 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/pages/typed-reactive-forms-example/matching-password.ts: -------------------------------------------------------------------------------- 1 | export interface MatchingPassword { 2 | password: string; 3 | confirmPassword: string; 4 | } 5 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/pages/typed-reactive-forms-example/typed-reactive-forms-example.component.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .grid { 4 | display: grid; 5 | grid-template-rows: auto; 6 | grid-template-columns: 1fr 1fr 1fr; 7 | grid-gap: 10px; 8 | } 9 | 10 | .form-card { 11 | grid-column-start: 1; 12 | grid-column-end: 4; 13 | 14 | mat-form-field { 15 | box-sizing: border-box; 16 | width: 100%; 17 | 18 | @media #{$monitor-query} { 19 | width: 45%; 20 | &:last-child { 21 | margin-left: 10%; 22 | } 23 | } 24 | } 25 | 26 | mat-card-actions { 27 | display: flex; 28 | align-items: flex-start; 29 | flex-wrap: wrap; 30 | margin: -10px; 31 | 32 | button { 33 | margin: 10px; 34 | @media #{$mobile-query} { 35 | width: 100%; 36 | } 37 | } 38 | } 39 | } 40 | 41 | .form-field-info { 42 | @media #{$table-query} { 43 | grid-column-start: 1; 44 | grid-column-end: 4; 45 | } 46 | 47 | mat-card-content { 48 | > div { 49 | padding: 5px 0; 50 | } 51 | 52 | pre { 53 | overflow: auto; 54 | box-sizing: border-box; 55 | display: block; 56 | width: 100%; 57 | max-height: 75px; 58 | 59 | margin: inherit; 60 | padding: 5px; 61 | border-radius: 4px; 62 | 63 | background-color: rgba(0, 0, 0, 0.2); 64 | 65 | &:empty { 66 | display: none; 67 | } 68 | } 69 | } 70 | } 71 | 72 | .form-validation-messages { 73 | grid-column-start: 1; 74 | grid-column-end: 4; 75 | } 76 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/pages/typed-reactive-forms-example/typed-reactive-forms-example.component.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @angular-eslint/template/use-track-by-function, @angular-eslint/template/cyclomatic-complexity */ 2 | import { Component } from "@angular/core"; 3 | import { AbstractControl, FormBuilder, FormControl, FormGroup, ValidationErrors, Validators } from "@angular/forms"; 4 | import { ErrorStateMatcher } from "@angular/material/core"; 5 | import { PasswordValidator } from "../../password-validator"; 6 | import { ParentErrorStateMatcher } from "../../parent-error-state-matcher"; 7 | import { MatchingPassword } from "./matching-password"; 8 | 9 | @Component({ 10 | selector: "app-reactive-forms-example", 11 | templateUrl: "./typed-reactive-forms-example.component.html", 12 | styleUrls: ["./typed-reactive-forms-example.component.scss"] 13 | }) 14 | export class TypedReactiveFormsExampleComponent { 15 | public formGroup: FormGroup; 16 | public validationMessages: { [key: string]: { type: string; message: string }[] }; 17 | public parentErrorStateMatcher: ErrorStateMatcher = new ParentErrorStateMatcher(); 18 | public passwordPattern = "^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9]+$"; 19 | public showValidationDetails = false; 20 | public showValidationSummary = true; 21 | 22 | public constructor(private formBuilder: FormBuilder) { 23 | this.formGroup = this.formBuilder.group({ 24 | username: [undefined, Validators.required], 25 | matchingPasswords: new FormGroup( 26 | { 27 | password: new FormControl( 28 | "", 29 | Validators.compose([ 30 | Validators.minLength(3), 31 | Validators.maxLength(10), 32 | Validators.required, 33 | Validators.pattern(this.passwordPattern) // this is for the letters (both uppercase and lowercase) and numbers validation 34 | ]) 35 | ), 36 | confirmPassword: new FormControl("", Validators.required) 37 | }, 38 | { 39 | validators: (formGroup: AbstractControl): ValidationErrors | null => 40 | PasswordValidator.areEqualTyped(formGroup) 41 | } 42 | ) 43 | }); 44 | 45 | this.validationMessages = { 46 | username: [ 47 | { 48 | type: "required", 49 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.USER_NAME.REQUIRED" 50 | }, 51 | { type: "unique", message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.USER_NAME.UNIQUE" } 52 | ], 53 | matchingPasswords: [ 54 | { 55 | type: "areEqual", 56 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.CONFIRM_PASSWORD.ARE_EQUAL" 57 | } 58 | ], 59 | password: [ 60 | { 61 | type: "required", 62 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.PASSWORD.REQUIRED" 63 | }, 64 | { 65 | type: "minlength", 66 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.PASSWORD.MIN_LENGTH" 67 | }, 68 | { type: "pattern", message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.PASSWORD.PATTERN" } 69 | ], 70 | confirmPassword: [ 71 | { 72 | type: "required", 73 | message: "DEMO.FORM_VALIDATION.WITHOUT_NGX_FORM_ERRORS.CONFIRM_PASSWORD.REQUIRED" 74 | } 75 | ] 76 | }; 77 | } 78 | 79 | public toggleValidationDetails(): void { 80 | this.showValidationDetails = !this.showValidationDetails; 81 | } 82 | 83 | public toggleValidationSummary(): void { 84 | this.showValidationSummary = !this.showValidationSummary; 85 | } 86 | 87 | public onSubmitUserDetails(formGroup: FormGroup): void { 88 | console.log("Submitted form:", formGroup.value); 89 | } 90 | 91 | public getFormStatus(): void { 92 | console.log("Form status", this.formGroup); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/parent-error-state-matcher.ts: -------------------------------------------------------------------------------- 1 | import { UntypedFormControl, FormGroupDirective, NgForm } from "@angular/forms"; 2 | import { ErrorStateMatcher } from "@angular/material/core"; 3 | 4 | /** Error when invalid control is dirty, touched, or submitted. */ 5 | export class ParentErrorStateMatcher implements ErrorStateMatcher { 6 | public isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { 7 | const isSubmitted = !!(form && form.submitted); 8 | const formGroupValid = !!(form && form.valid); 9 | 10 | return !!control && (control.invalid || !formGroupValid) && (control.dirty || control.touched || isSubmitted); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/password-validator.ts: -------------------------------------------------------------------------------- 1 | import { AbstractControl, UntypedFormControl, UntypedFormGroup, ValidationErrors } from "@angular/forms"; 2 | import { MatchingPassword } from "./pages"; 3 | 4 | export class PasswordValidator { 5 | // Inspired on: http://plnkr.co/edit/Zcbg2T3tOxYmhxs7vaAm?p=preview 6 | public static areEqual(formGroup: UntypedFormGroup): ValidationErrors | null { 7 | let value: string | undefined; 8 | let valid = true; 9 | for (const key in formGroup.controls) { 10 | /* eslint-disable-next-line no-prototype-builtins */ 11 | if (formGroup.controls.hasOwnProperty(key)) { 12 | const control: UntypedFormControl = formGroup.controls[key]; 13 | 14 | if (value === undefined) { 15 | value = control.value; 16 | } else { 17 | if (value !== control.value) { 18 | valid = false; 19 | break; 20 | } 21 | } 22 | } 23 | } 24 | 25 | if (valid) { 26 | /* eslint-disable-next-line no-null/no-null */ 27 | return null; 28 | } 29 | 30 | return { 31 | areEqual: true 32 | }; 33 | } 34 | 35 | public static areEqualTyped(formGroup: AbstractControl): ValidationErrors | null { 36 | const matchingPassword = formGroup.value; 37 | 38 | if (matchingPassword.password === matchingPassword.confirmPassword) { 39 | /* eslint-disable-next-line no-null/no-null */ 40 | return null; 41 | } 42 | return { 43 | areEqual: true 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /demo-app/ng16/src/app/translation.config.ts: -------------------------------------------------------------------------------- 1 | import { TranslateService } from "@ngx-translate/core"; 2 | 3 | const translationsEn: object = require("../assets/translations/en.json"); 4 | const translationsFr: object = require("../assets/translations/fr.json"); 5 | const translationsNl: object = require("../assets/translations/nl.json"); 6 | 7 | /** 8 | * @param translateService - The translation service 9 | */ 10 | export function initializeTranslation(translateService: TranslateService): void { 11 | translateService.addLangs(["en", "fr", "nl"]); 12 | translateService.setDefaultLang("en"); 13 | translateService.use("en"); 14 | 15 | translateService.setTranslation("en", translationsEn); 16 | translateService.setTranslation("fr", translationsFr); 17 | translateService.setTranslation("nl", translationsNl); 18 | } 19 | -------------------------------------------------------------------------------- /demo-app/ng16/src/assets/img/github-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /demo-app/ng16/src/assets/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "DEMO": { 3 | "PLACEHOLDERS": { 4 | "USER_NAME": "Username", 5 | "PASSWORD": "Password", 6 | "CONFIRM_PASSWORD": "Confirm password" 7 | }, 8 | "FIELDS": { 9 | "USER_NAME": "Your name", 10 | "PASSWORD_ALIAS": "A valid passcode", 11 | "CONFIRM_PASSWORD": "Password confirmation" 12 | }, 13 | "FORM_VALIDATION": { 14 | "WITHOUT_NGX_FORM_ERRORS": { 15 | "REQUIRED": "This field is required", 16 | "USER_NAME": { 17 | "REQUIRED": "Username is required", 18 | "UNIQUE": "Your username has already been taken" 19 | }, 20 | "PASSWORD": { 21 | "REQUIRED": "Password is required", 22 | "MIN_LENGTH": "Password must be at least 3 characters long", 23 | "PATTERN": "Your password must contain at least one uppercase, one lowercase, and one number" 24 | }, 25 | "CONFIRM_PASSWORD": { 26 | "REQUIRED": "Confirm password is required", 27 | "ARE_EQUAL": "Password mismatch" 28 | } 29 | }, 30 | "WITH_NGX_FORM_ERRORS": { 31 | "REQUIRED": "{{fieldName}} is required", 32 | "PASSWORD_REQUIRED": "{{fieldName}} must be provided", 33 | "USER_NAME": { 34 | "UNIQUE": "Your username has already been taken" 35 | }, 36 | "PASSWORD": { 37 | "MAX_LENGTH": "Password cannot be more than {{requiredLength}} characters long", 38 | "MIN_LENGTH": "Password must be at least {{requiredLength}} characters long", 39 | "PATTERN": "Your password must contain at least one uppercase, one lowercase, and one number" 40 | }, 41 | "CONFIRM_PASSWORD": { 42 | "ARE_EQUAL": "Password mismatch" 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /demo-app/ng16/src/assets/translations/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "DEMO": { 3 | "PLACEHOLDERS": { 4 | "USER_NAME": "Nom d'utilisateur", 5 | "PASSWORD": "Mot de passe", 6 | "CONFIRM_PASSWORD": "Confirmez le mot de passe" 7 | }, 8 | "FIELDS": { 9 | "USER_NAME": "Votre nom d'utilisateur", 10 | "PASSWORD_ALIAS": "Un code d'authentification valide", 11 | "CONFIRM_PASSWORD": "Confirmation mot de passe" 12 | }, 13 | "FORM_VALIDATION": { 14 | "WITHOUT_NGX_FORM_ERRORS": { 15 | "REQUIRED": "Ce champ est nécessaire", 16 | "USER_NAME": { 17 | "REQUIRED": "Nom d'utilisateur est nécessaire", 18 | "UNIQUE": "Votre nom d'utilisateur a déjà été pris" 19 | }, 20 | "PASSWORD": { 21 | "REQUIRED": "Mot de passe est nécessaire", 22 | "MIN_LENGTH": "Mot de passe doit comporter au moins 3 caractères", 23 | "PATTERN": "Votre mot de passe doit contenir au moins une majuscule, une minuscule et un chiffre." 24 | }, 25 | "CONFIRM_PASSWORD": { 26 | "REQUIRED": "Confirmez le mot de passe est nécessaire", 27 | "ARE_EQUAL": "Non concordance des mots de passe passwords" 28 | } 29 | }, 30 | "WITH_NGX_FORM_ERRORS": { 31 | "REQUIRED": "{{fieldName}} est nécessaire", 32 | "PASSWORD_REQUIRED": "{{fieldName}} doit être fourni", 33 | "USER_NAME": { 34 | "UNIQUE": "Votre nom d'utilisateur a déjà été pris" 35 | }, 36 | "PASSWORD": { 37 | "MAX_LENGTH": "Mot de passe ne peut pas y avoir plus de {{requiredLength}} caractères", 38 | "MIN_LENGTH": "Mot de passe doit comporter au moins {{requiredLength}} caractères", 39 | "PATTERN": "Votre mot de passe doit contenir au moins une majuscule, une minuscule et un chiffre." 40 | }, 41 | "CONFIRM_PASSWORD": { 42 | "ARE_EQUAL": "Non concordance des mots de passe passwords" 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /demo-app/ng16/src/assets/translations/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "DEMO": { 3 | "PLACEHOLDERS": { 4 | "USER_NAME": "Gebruikersnaam", 5 | "PASSWORD": "Wachtwoord", 6 | "CONFIRM_PASSWORD": "Bevestig wachtwoord" 7 | }, 8 | "FIELDS": { 9 | "USER_NAME": "Uw gebruikersnaam", 10 | "PASSWORD_ALIAS": "Een geldig wachtwoord", 11 | "CONFIRM_PASSWORD": "Wachtwoordbevestiging" 12 | }, 13 | "FORM_VALIDATION": { 14 | "WITHOUT_NGX_FORM_ERRORS": { 15 | "REQUIRED": "Dit veld is verplicht", 16 | "USER_NAME": { 17 | "REQUIRED": "Gebruikersnaam is verplicht", 18 | "UNIQUE": "Uw gebruikersnaam is al in gebruik" 19 | }, 20 | "PASSWORD": { 21 | "REQUIRED": "Wachtwoord is verplicht", 22 | "MIN_LENGTH": "Wachtwoord moet minimaal 3 tekens lang zijn", 23 | "PATTERN": "Uw wachtwoord moet minimaal één hoofdletter, één kleine letter en één cijfer bevatten" 24 | }, 25 | "CONFIRM_PASSWORD": { 26 | "REQUIRED": "Bevestig wachtwoord is verplicht", 27 | "ARE_EQUAL": "Wachtwoord komt niet overeen" 28 | } 29 | }, 30 | "WITH_NGX_FORM_ERRORS": { 31 | "REQUIRED": "{{fieldName}} is verplicht", 32 | "PASSWORD_REQUIRED": "{{fieldName}} moet worden gegeven", 33 | "USER_NAME": { 34 | "UNIQUE": "Uw gebruikersnaam is al in gebruik" 35 | }, 36 | "PASSWORD": { 37 | "MAX_LENGTH": "Wachtwoord mag niet meer dan {{requiredLength}} tekens lang zijn", 38 | "MIN_LENGTH": "Wachtwoord moet minimaal {{requiredLength}} tekens lang zijn", 39 | "PATTERN": "Uw Wachtwoord moet minimaal één hoofdletter, één kleine letter en één cijfer bevatten" 40 | }, 41 | "CONFIRM_PASSWORD": { 42 | "ARE_EQUAL": "Wachtwoord komt niet overeen" 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /demo-app/ng16/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /demo-app/ng16/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /demo-app/ng16/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NationalBankBelgium/ngx-form-errors/07813d90f41ed2ee572afd0039f26bc7a023f0a4/demo-app/ng16/src/favicon.ico -------------------------------------------------------------------------------- /demo-app/ng16/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NgxFormErrors Showcase 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /demo-app/ng16/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() 12 | .bootstrapModule(AppModule) 13 | .catch((err: any) => console.error(err)); 14 | -------------------------------------------------------------------------------- /demo-app/ng16/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /** 18 | * ************************************************************************************************* 19 | * BROWSER POLYFILLS 20 | */ 21 | /* eslint-disable import/no-unassigned-import */ 22 | /** 23 | * IE9, IE10 and IE11 requires all of the following polyfills. 24 | */ 25 | import "core-js/es"; 26 | 27 | /** 28 | * IE10 and IE11 requires the following for NgClass support on SVG elements 29 | */ 30 | import "eligrey-classlist-js-polyfill"; 31 | 32 | /** 33 | * IE10 and IE11 requires the following for the Reflect API. 34 | */ 35 | import "core-js/proposals/reflect-metadata"; 36 | 37 | /** 38 | * Web Animations `@angular/platform-browser/animations` 39 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 40 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 41 | */ 42 | // import "web-animations-js"; // Run `npm install --save web-animations-js`. 43 | 44 | /** 45 | * By default, zone.js will patch all possible macroTask and DomEvents 46 | * user can disable parts of macroTask/DomEvents patch by setting following flags 47 | * because those flags need to be set before `zone.js` being loaded, and webpack 48 | * will put import in the top of bundle, so user need to create a separate file 49 | * in this directory (for example: zone-flags.ts), and put the following flags 50 | * into that file, and then add the following code before importing zone.js. 51 | * import './zone-flags.ts'; 52 | * 53 | * The flags allowed in zone-flags.ts are listed here. 54 | * 55 | * The following flags will work for all browsers. 56 | * 57 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 58 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 59 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ["scroll", "mousemove"]; // disable patch specified eventNames 60 | * 61 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 62 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 63 | */ 64 | /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */ 65 | (window as any).__Zone_enable_cross_context_check = true; 66 | 67 | /** 68 | * ************************************************************************************************* 69 | * Zone JS is required by default for Angular itself. 70 | */ 71 | import "zone.js"; // Included with Angular CLI. 72 | 73 | /** 74 | * ************************************************************************************************* 75 | * APPLICATION IMPORTS 76 | */ 77 | -------------------------------------------------------------------------------- /demo-app/ng16/src/styles/_app.theme.scss: -------------------------------------------------------------------------------- 1 | @use "@angular/material" as mat; 2 | @import "@angular/material/theming"; 3 | @import "../app/components/navigation/navigation.theme"; 4 | @import "../app/components/language-selector/language-selector.theme"; 5 | @import "../app/components/card/card.theme"; 6 | 7 | $guardsman-red: ( 8 | 50: #f7e1e1, 9 | 100: #eab3b3, 10 | 200: #dd8080, 11 | 300: #cf4d4d, 12 | 400: #c42727, 13 | 500: #ba0101, 14 | 600: #b30101, 15 | 700: #ab0101, 16 | 800: #a30101, 17 | 900: #940000, 18 | A100: #ffbfbf, 19 | A200: #ff8c8c, 20 | A400: #ff5959, 21 | A700: #ff4040, 22 | contrast: ( 23 | 50: $dark-primary-text, 24 | 100: $dark-primary-text, 25 | 200: $dark-primary-text, 26 | 300: $light-primary-text, 27 | 400: $light-primary-text, 28 | 500: $light-primary-text, 29 | 600: $light-primary-text, 30 | 700: $light-primary-text, 31 | 800: $light-primary-text, 32 | 900: $light-primary-text, 33 | A100: $dark-primary-text, 34 | A200: $dark-primary-text, 35 | A400: $dark-primary-text, 36 | A700: $light-primary-text 37 | ) 38 | ); 39 | 40 | $eminence: ( 41 | 50: #ede6f0, 42 | 100: #d3c1da, 43 | 200: #b698c1, 44 | 300: #986ea8, 45 | 400: #824f95, 46 | 500: #6c3082, 47 | 600: #642b7a, 48 | 700: #59246f, 49 | 800: #4f1e65, 50 | 900: #3d1352, 51 | A100: #d58cff, 52 | A200: #c259ff, 53 | A400: #af26ff, 54 | A700: #a60dff, 55 | contrast: ( 56 | 50: $light-primary-text, 57 | 100: $light-primary-text, 58 | 200: $light-primary-text, 59 | 300: $light-primary-text, 60 | 400: $light-primary-text, 61 | 500: $light-primary-text, 62 | 600: $light-primary-text, 63 | 700: $light-primary-text, 64 | 800: $light-primary-text, 65 | 900: $light-primary-text, 66 | A100: $light-primary-text, 67 | A200: $light-primary-text, 68 | A400: $light-primary-text, 69 | A700: $light-primary-text 70 | ) 71 | ); 72 | 73 | $lochinvar: ( 74 | 50: #e6f1f0, 75 | 100: #c0ddda, 76 | 200: #96c6c2, 77 | 300: #6bafa9, 78 | 400: #4c9d96, 79 | 500: #2c8c84, 80 | 600: #27847c, 81 | 700: #217971, 82 | 800: #1b6f67, 83 | 900: #105c54, 84 | A100: #93fff2, 85 | A200: #60ffed, 86 | A400: #2dffe7, 87 | A700: #14ffe4, 88 | contrast: ( 89 | 50: $dark-secondary-text, 90 | 100: $dark-secondary-text, 91 | 200: $dark-secondary-text, 92 | 300: $dark-secondary-text, 93 | 400: $dark-secondary-text, 94 | 500: $light-secondary-text, 95 | 600: $light-secondary-text, 96 | 700: $light-secondary-text, 97 | 800: $light-secondary-text, 98 | 900: $light-secondary-text, 99 | A100: $dark-secondary-text, 100 | A200: $dark-secondary-text, 101 | A400: $dark-secondary-text, 102 | A700: $dark-secondary-text 103 | ) 104 | ); 105 | 106 | // TODO(v15): As of v15 mat.legacy-core no longer includes default typography styles. 107 | // The following line adds: 108 | // 1. Default typography styles for all components 109 | // 2. Styles for typography hierarchy classes (e.g. .mat-headline-1) 110 | // If you specify typography styles for the components you use elsewhere, you should delete this line. 111 | // If you don't need the default component typographies but still want the hierarchy styles, 112 | // you can delete this line and instead use: 113 | // `@include mat.legacy-typography-hierarchy(mat.define-legacy-typography-config());` 114 | @include mat.all-legacy-component-typographies(); 115 | @include mat.legacy-core(); 116 | 117 | $demo-app-primary: mat.define-palette($eminence); 118 | $demo-app-accent: mat.define-palette($lochinvar); 119 | 120 | $demo-app-warn: mat.define-palette($guardsman-red); 121 | $demo-app-success: mat.define-palette(mat.$light-green-palette); 122 | 123 | $demo-mat-theme: mat.define-light-theme($demo-app-primary, $demo-app-accent, $demo-app-warn); 124 | $demo-custom-theme: ( 125 | success: $demo-app-success 126 | ); 127 | 128 | $demo-app-theme: map-merge($demo-mat-theme, $demo-custom-theme); 129 | 130 | $theme: $demo-app-theme; 131 | 132 | @include mat.all-legacy-component-themes($theme); 133 | @include app-navigation-theme($theme); 134 | @include language-selector-theme($theme); 135 | @include card-theme($theme); 136 | -------------------------------------------------------------------------------- /demo-app/ng16/src/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | $monitor-query: "screen and (min-width:1200px)"; 2 | $table-query: "screen and (max-width: 1200px)"; 3 | $mobile-query: "screen and (max-width: 600px)"; 4 | -------------------------------------------------------------------------------- /demo-app/ng16/src/styles/styles.scss: -------------------------------------------------------------------------------- 1 | @import "~material-design-icons/iconfont/material-icons.css"; 2 | @import "app.theme"; 3 | 4 | app-root { 5 | height: 100vh; 6 | display: flex; 7 | flex-direction: column; 8 | } 9 | 10 | .container { 11 | margin: 20px auto; 12 | padding: 20px; 13 | box-sizing: border-box; 14 | max-width: 1200px; 15 | } 16 | -------------------------------------------------------------------------------- /demo-app/ng16/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | /* eslint-disable import/no-unassigned-import */ 4 | import "zone.js/testing"; 5 | import { getTestBed } from "@angular/core/testing"; 6 | import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from "@angular/platform-browser-dynamic/testing"; 7 | 8 | // First, initialize the Angular testing environment. 9 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { 10 | teardown: { destroyAfterEach: false } 11 | }); 12 | -------------------------------------------------------------------------------- /demo-app/ng16/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": ["node"], 6 | "paths": { 7 | "@angular/*": ["./node_modules/@angular/*"] 8 | } 9 | }, 10 | "files": ["src/main.ts", "src/polyfills.ts"], 11 | "include": ["src/**/*.d.ts"], 12 | "exclude": ["src/test.ts", "src/**/*.spec.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /demo-app/ng16/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@nationalbankbelgium/code-style/tsconfig/5.1.x/ng16", 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "rootDir": "../", 7 | "outDir": "./dist/out-tsc", 8 | "declaration": false, 9 | "downlevelIteration": true, 10 | "experimentalDecorators": true, 11 | "module": "es2020", 12 | "moduleResolution": "node", 13 | "importHelpers": true, 14 | "target": "ES2022", 15 | "typeRoots": ["node_modules/@types"], 16 | "lib": ["dom", "dom.iterable", "es2022"], 17 | "useDefineForClassFields": false 18 | }, 19 | "angularCompilerOptions": { 20 | "fullTemplateTypeCheck": true, 21 | "strictInjectionParameters": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /demo-app/ng16/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": ["jasmine", "node"] 6 | }, 7 | "files": ["src/test.ts", "src/polyfills.ts"], 8 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /docs/summary.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Developer Guide", 4 | "file": "DEV_GUIDE.md" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Helpers 2 | const helpers = require("./scripts/helpers"); 3 | const ciInfo = require("ci-info"); 4 | const isCI = process.argv.indexOf("--watch=false") > -1 || ciInfo.isCI; 5 | 6 | const ngxFormErrorsSpecificConfiguration = { 7 | // base path that will be used to resolve all patterns (e.g. files, exclude) 8 | basePath: "", 9 | 10 | // frameworks to use 11 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 12 | frameworks: ["jasmine", "@angular-devkit/build-angular"], 13 | 14 | // list of files to exclude 15 | exclude: [ 16 | helpers.root("src/index.html") // not needed for unit testing 17 | ], 18 | 19 | client: { 20 | clearContext: false // leave Jasmine Spec Runner output visible in browser 21 | }, 22 | 23 | plugins: [ 24 | // Default karma plugins configuration: require("karma-*") 25 | "karma-*", 26 | require("@angular-devkit/build-angular/plugins/karma") 27 | ], 28 | 29 | // test results reporter to use 30 | // possible values: "dots", "progress", "spec", "junit", "mocha", "coverage" (others if you import reporters) 31 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 32 | // https://www.npmjs.com/package/karma-junit-reporter 33 | // https://www.npmjs.com/package/karma-spec-reporter 34 | reporters: isCI ? ["mocha", "progress"] : ["mocha", "progress", "kjhtml", "coverage"], 35 | 36 | // web server port 37 | port: 9876, 38 | 39 | // enable / disable colors in the output (reporters and logs) 40 | colors: true, 41 | 42 | // level of logging 43 | // see: http://karma-runner.github.io/2.0/config/configuration-file.html 44 | // possible values: 45 | // "OFF" = config.LOG_DISABLE 46 | // "ERROR" = config.LOG_ERROR 47 | // "WARN" = config.LOG_WARN 48 | // "INFO" = config.LOG_INFO 49 | // "DEBUG" = config.LOG_DEBUG 50 | // raw value defined in node_modules/karma/lib/constants.js 51 | logLevel: "WARN", 52 | 53 | // enable / disable watching file and executing tests whenever any file changes 54 | autoWatch: true, 55 | 56 | // start these browsers 57 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 58 | browsers: isCI ? ["ChromeHeadlessNoSandbox"] : ["Chrome"], 59 | 60 | // Continuous Integration mode 61 | // if true, Karma captures browsers, runs the tests and exits 62 | singleRun: isCI, 63 | 64 | // If true, tests restart automatically if a file is changed 65 | restartOnFileChange: !isCI, 66 | 67 | // Timeout settings 68 | browserNoActivityTimeout: 30000, 69 | browserDisconnectTolerance: 1, 70 | browserDisconnectTimeout: 30000, 71 | 72 | // Configuration for coverage-istanbul reporter 73 | coverageReporter: { 74 | // base output directory. If you include %browser% in the path it will be replaced with the karma browser name 75 | dir: helpers.root("reports/coverage"), 76 | subdir: ".", 77 | // https://github.com/istanbuljs/istanbuljs/tree/73c25ce79f91010d1ff073aa6ff3fd01114f90db/packages/istanbul-reports/lib 78 | reporters: [{ type: "html" }, { type: "lcovonly" }, { type: "text-summary" }, { type: "clover" }, { type: "json" }] 79 | }, 80 | 81 | // Custom launcher configuration for ChromeHeadless (with Puppeteer) 82 | customLaunchers: { 83 | ChromeHeadlessNoSandbox: { 84 | base: "ChromeHeadless", 85 | // necessary for travis: https://github.com/puppeteer/puppeteer/blob/v7.1.0/docs/troubleshooting.md#setting-up-chrome-linux-sandbox 86 | // as it runs in a container-based environment 87 | flags: ["--no-sandbox", "--disable-setuid-sandbox"] 88 | } 89 | } 90 | }; 91 | 92 | module.exports = { 93 | default: function (config) { 94 | return config.set(ngxFormErrorsSpecificConfiguration); 95 | } 96 | }; 97 | -------------------------------------------------------------------------------- /ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "public_api.ts", 5 | "flatModuleFile": "ngx-form-errors" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /public_api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Entry point for all public APIs of this package. 3 | */ 4 | export * from "./src/ngx-form-errors"; 5 | 6 | // This file only reexports content of the `src` folder. Keep it that way. 7 | -------------------------------------------------------------------------------- /release-publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # TODO 4 | #=================== 5 | # provide support for publishing locally in addition to GitHub Actions 6 | 7 | set -u -e -o pipefail 8 | 9 | VERBOSE=false 10 | TRACE=false 11 | DRY_RUN=false 12 | 13 | # We read from a file because the list is also shared with build.sh 14 | # Not using readarray because it does not handle \r\n 15 | #OLD_IFS=$IFS # save old IFS value 16 | #IFS=$'\r\n' GLOBIGNORE='*' command eval 'ALL_PACKAGES=($(cat ./modules.txt))' 17 | #IFS=$OLD_IFS # restore IFS 18 | PACKAGE=ngx-form-errors 19 | 20 | EXPECTED_REPOSITORY="NationalBankBelgium/ngx-form-errors" 21 | GITHUB_REF=${GITHUB_REF:-""} 22 | 23 | #---------------------------------------------- 24 | # Uncomment block below to test locally 25 | #---------------------------------------------- 26 | #LOGS_DIR=./.tmp/ngx-form-errors/logs 27 | #mkdir -p ${LOGS_DIR} 28 | #LOGS_FILE=${LOGS_DIR}/build-perf.log 29 | #touch ${LOGS_FILE} 30 | #GITHUB_ACTIONS=true 31 | #GITHUB_REPOSITORY="NationalBankBelgium/ngx-form-errors" 32 | #GITHUB_REF="refs/tags/fooBar" 33 | #---------------------------------------------- 34 | 35 | readonly currentDir=$(cd $(dirname $0); pwd) 36 | 37 | source ${currentDir}/scripts/ci/_ghactions-group.sh 38 | source ${currentDir}/util-functions.sh 39 | 40 | cd ${currentDir} 41 | 42 | logInfo "=============================================" 43 | logInfo "NgxFormErrors release publish @ npm" 44 | 45 | for ARG in "$@"; do 46 | case "$ARG" in 47 | --dry-run) 48 | logInfo "=============================================" 49 | logInfo "Dry run enabled!" 50 | DRY_RUN=true 51 | ;; 52 | --verbose) 53 | logInfo "=============================================" 54 | logInfo "Verbose mode enabled!" 55 | VERBOSE=true 56 | ;; 57 | --trace) 58 | logInfo "=============================================" 59 | logInfo "Trace mode enabled!" 60 | TRACE=true 61 | ;; 62 | *) 63 | echo "Unknown option $ARG." 64 | exit 1 65 | ;; 66 | esac 67 | done 68 | logInfo "=============================================" 69 | 70 | PROJECT_ROOT_DIR=`pwd` 71 | logTrace "PROJECT_ROOT_DIR: ${PROJECT_ROOT_DIR}" 1 72 | ROOT_PACKAGES_DIR=${PROJECT_ROOT_DIR} 73 | logTrace "ROOT_PACKAGES_DIR: ${ROOT_PACKAGES_DIR}" 1 74 | 75 | ghActionsGroupStart "publish checks" "no-xtrace" 76 | 77 | if [[ ${GITHUB_ACTIONS} == true ]]; then 78 | logInfo "=============================================" 79 | logInfo "Publishing to npm"; 80 | logInfo "=============================================" 81 | 82 | # Don't even try if not running against the official repo 83 | # We don't want release to run outside of our own little world 84 | if [[ ${GITHUB_REPOSITORY} != ${EXPECTED_REPOSITORY} ]]; then 85 | logInfo "Skipping release because this is not the main repository."; 86 | ghActionsGroupEnd "publish checks" 87 | exit 0; 88 | fi 89 | 90 | logInfo "Verifying if this build has been triggered for a tag" 91 | 92 | if [[ ${GITHUB_REF} != refs/tags/* ]]; then 93 | logInfo "Not publishing because this is not a build triggered for a tag" 1 94 | exit 0; 95 | else 96 | logInfo "This build has been triggered for a tag" 97 | fi 98 | fi 99 | 100 | ghActionsGroupEnd "publish checks" 101 | 102 | logInfo "=============================================" 103 | logInfo "Publishing package" 104 | logInfo "=============================================" 105 | # FIXME Uncomment this once GitHub Actions support nested logs 106 | # See: https://github.community/t5/GitHub-Actions/Feature-Request-Enhancements-to-group-commands-nested-named/m-p/45399 107 | #ghActionsGroupStart "publish" "no-xtrace" 108 | #logInfo "Publishing package" 109 | 110 | ghActionsGroupStart "publishing: ${PACKAGE}" "no-xtrace" 111 | PACKAGE_FOLDER=${ROOT_PACKAGES_DIR}/dist 112 | logTrace "Package path: ${PACKAGE_FOLDER}" 2 113 | cd ${PACKAGE_FOLDER} 114 | TGZ_FILES=`find . -maxdepth 1 -type f | egrep -e ".tgz"`; 115 | for file in ${TGZ_FILES}; do 116 | logInfo "Publishing TGZ file: ${TGZ_FILES}" 2 117 | if [[ ${DRY_RUN} == false ]]; then 118 | if [[ ${GITHUB_REF} =~ /(alpha|beta|rc)/ ]]; then 119 | logTrace "Publishing the release (with tag next)" 2 120 | npm publish ${file} --tag next --access public 121 | else 122 | logTrace "Publishing the release (with tag latest)" 2 123 | npm publish ${file} --access public 124 | fi 125 | else 126 | logTrace "DRY RUN, skipping npm publish!" 2 127 | fi 128 | logInfo "Package published!" 2 129 | done 130 | cd - > /dev/null; # go back to the previous folder without any output 131 | ghActionsGroupEnd "publishing: ${PACKAGE}" 132 | 133 | #ghActionsGroupEnd "publish" 134 | 135 | # Print return arrows as a log separator 136 | ghActionsGroupReturnArrows 137 | -------------------------------------------------------------------------------- /scripts/ci/_ghactions-group.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # private variable to track groups within this script 4 | ghActionsGroupStack=() 5 | 6 | GITHUB_ACTIONS=${GITHUB_ACTIONS:-} 7 | LOGS_FILE=${LOGS_FILE:-""} 8 | 9 | function ghActionsGroupStart() { 10 | local groupName="${0#./} ${1}" 11 | # get current time as nanoseconds since the beginning of the epoch 12 | groupStartTime=$(date +%s%N) 13 | # convert all non alphanum chars except for "-" and "." to "--" 14 | local sanitizedGroupName=${groupName//[^[:alnum:]\-\.]/--} 15 | # strip trailing "-" 16 | sanitizedGroupName=${sanitizedGroupName%-} 17 | # push the groupName onto the stack 18 | ghActionsGroupStack+=("${sanitizedGroupName}|${groupStartTime}") 19 | 20 | echo "" 21 | if [[ ${GITHUB_ACTIONS} == true ]]; then 22 | echo "::group::${groupName}" 23 | fi 24 | local enterArrow="===> ${groupName} ==>==>==>==>==>==>==>==>==>==>==>==>==>==>==>==>==>==>==>==>==>==>==>==>==>==>==>==>==>" 25 | # keep all messages consistently wide 80chars regardless of the groupName 26 | echo ${enterArrow:0:100} 27 | if [[ ${2:-} != "no-xtrace" ]]; then 28 | # turn on verbose mode so that we have better visibility into what's going on 29 | # http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_02_03.html#table_02_01 30 | set -x 31 | fi 32 | } 33 | 34 | function ghActionsGroupEnd() { 35 | set +x 36 | local groupName="${0#./} ${1}" 37 | # convert all non alphanum chars except for "-" and "." to "--" 38 | local sanitizedGroupName=${groupName//[^[:alnum:]\-\.]/--} 39 | # strip trailing "-" 40 | sanitizedGroupName=${sanitizedGroupName%-} 41 | 42 | # consult and update ghActionsGroupStack 43 | local lastGroupIndex=$(expr ${#ghActionsGroupStack[@]} - 1) 44 | local lastGroupString=${ghActionsGroupStack[$lastGroupIndex]} 45 | # split the string by | and then turn that into an array 46 | local lastGroupArray=(${lastGroupString//\|/ }) 47 | local lastSanitizedGroupName=${lastGroupArray[0]} 48 | 49 | if [[ ${GITHUB_ACTIONS} == true ]]; then 50 | local lastGroupStartTime=${lastGroupArray[1]} 51 | local groupFinishTime=$(date +%s%N) 52 | local groupDuration=$(expr ${groupFinishTime} - ${lastGroupStartTime}) 53 | local displayedDuration=$(echo "scale=1; ${groupDuration}/1000000000" | bc | awk '{printf "%.1f\n", $0}') 54 | 55 | # write into build-perf.log file 56 | local logIndent=$(expr ${lastGroupIndex} \* 2) 57 | printf "%6ss%${logIndent}s: %s\n" ${displayedDuration} " " "${groupName}" >> ${LOGS_FILE} 58 | fi 59 | 60 | # pop 61 | ghActionsGroupStack=(${ghActionsGroupStack[@]:0:lastGroupIndex}) 62 | 63 | # check for misalignment 64 | if [[ ${lastSanitizedGroupName} != ${sanitizedGroupName} ]]; then 65 | echo "GitHub Actions group mis-alignment detected! ghActionsGroupEnd expected sanitized fold name '${lastSanitizedGroupName}', but received '${sanitizedGroupName}' (after sanitization)" 66 | exit 1 67 | fi 68 | 69 | local returnArrow="<=== ${groupName} <==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==" 70 | # keep all messages consistently wide 80chars regardless of the groupName 71 | echo ${returnArrow:0:100} 72 | echo "" 73 | if [[ ${GITHUB_ACTIONS} == true ]]; then 74 | echo "::endgroup::" 75 | fi 76 | } 77 | 78 | function ghActionsGroupReturnArrows() { 79 | # print out return arrows so that it's easy to see the end of the script in the log 80 | echo "" 81 | returnArrow="<=== ${0#./} <==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==" 82 | # keep all messages consistently wide 80chars regardless of the groupName 83 | echo ${returnArrow:0:100} 84 | echo "<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<==<===" 85 | echo "" 86 | } 87 | -------------------------------------------------------------------------------- /scripts/ci/print-logs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -u -e -o pipefail 4 | 5 | # Setup environment 6 | readonly thisDir=$(cd $(dirname $0); pwd) 7 | source ${thisDir}/_ghactions-group.sh 8 | 9 | 10 | for FILE in ${LOGS_DIR}/*; do 11 | ghActionsGroupStart "print log file: ${FILE}" 12 | cat $FILE 13 | ghActionsGroupEnd "print log file: ${FILE}" 14 | done 15 | 16 | # Print return arrows as a log separator 17 | ghActionsGroupReturnArrows 18 | -------------------------------------------------------------------------------- /scripts/helpers.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | 4 | /** 5 | * Helper functions. 6 | */ 7 | const _root = path.resolve(process.cwd(), "."); // project root folder 8 | 9 | const currentFolder = path.basename(_root); 10 | 11 | const root = path.join.bind(path, _root); 12 | 13 | function getAngularCliAppConfig() { 14 | const applicationAngularCliConfigPath = root("angular.json"); 15 | 16 | let angularCliConfigPath; 17 | 18 | if (fs.existsSync(applicationAngularCliConfigPath)) { 19 | angularCliConfigPath = applicationAngularCliConfigPath; 20 | } else { 21 | throw new Error("angular.json is not present. Please add this at the root your project because stark-build needs this."); 22 | } 23 | 24 | const angularCliConfig = require(angularCliConfigPath); 25 | if (angularCliConfig.defaultProject && angularCliConfig.projects[angularCliConfig.defaultProject]) { 26 | return angularCliConfig.projects[angularCliConfig.defaultProject]; 27 | } else { 28 | throw new Error("Angular-cli config apps is wrong. Please adapt it to follow Angular CLI way."); 29 | } 30 | } 31 | 32 | module.exports = { 33 | currentFolder, 34 | getAngularCliAppConfig, 35 | root 36 | }; 37 | -------------------------------------------------------------------------------- /src/directives.ts: -------------------------------------------------------------------------------- 1 | export * from "./directives/form-errors-group.directive"; 2 | export * from "./directives/form-errors.directive"; 3 | -------------------------------------------------------------------------------- /src/directives/form-errors-group.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild } from "@angular/core"; 2 | import { ComponentFixture, fakeAsync, TestBed } from "@angular/core/testing"; 3 | import { NgxFormErrorsGroupDirective } from "./form-errors-group.directive"; 4 | 5 | describe("NgxFormErrorsGroupDirective", () => { 6 | const groupName = "dummy-group"; 7 | 8 | @Component({ 9 | selector: "test-component", 10 | template: getTemplate("ngxFormErrorsGroup='{{ dummyGroup }}'") 11 | }) 12 | class TestComponent { 13 | public dummyGroup: string = groupName; 14 | 15 | @ViewChild(NgxFormErrorsGroupDirective, { static: false }) 16 | public formErrorGroup!: NgxFormErrorsGroupDirective; 17 | } 18 | 19 | let fixture: ComponentFixture; 20 | let component: TestComponent; 21 | 22 | function getTemplate(formErrorsGroupDirective: string): string { 23 | return "
"; 24 | } 25 | 26 | function initializeComponentFixture(): void { 27 | fixture = TestBed.createComponent(TestComponent); 28 | component = fixture.componentInstance; 29 | // trigger initial data binding 30 | fixture.detectChanges(); 31 | } 32 | 33 | beforeEach(() => { 34 | return TestBed.configureTestingModule({ 35 | declarations: [NgxFormErrorsGroupDirective, TestComponent] 36 | }); 37 | }); 38 | 39 | describe("when group is defined", () => { 40 | beforeEach(fakeAsync(() => { 41 | // compile template and css 42 | return TestBed.compileComponents(); 43 | })); 44 | 45 | beforeEach(() => { 46 | initializeComponentFixture(); 47 | }); 48 | 49 | it("should set the group correctly", () => { 50 | expect(component.dummyGroup).toBe(groupName); 51 | expect(component.formErrorGroup).toBeDefined(); 52 | expect(component.formErrorGroup.group).toBe(groupName); 53 | }); 54 | }); 55 | 56 | describe("when group is not defined", () => { 57 | beforeEach(fakeAsync(() => { 58 | // the directive should not be used with square brackets "[]" because the input is an string literal! 59 | const newTemplate: string = getTemplate("ngxFormErrorsGroup"); 60 | 61 | TestBed.overrideTemplate(TestComponent, newTemplate); 62 | 63 | // compile template and css 64 | return TestBed.compileComponents(); 65 | })); 66 | 67 | it("should throw an error", () => { 68 | expect(() => initializeComponentFixture()).toThrowError(/no group/); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /src/directives/form-errors-group.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input, OnInit } from "@angular/core"; 2 | 3 | /** 4 | * Directive that defines the group of the form model to be validated. 5 | * The directive exposes the group through the controller to allow access to it by wrapped {@link NgxFormErrorsDirective}(s). 6 | */ 7 | @Directive({ 8 | selector: "[ngxFormErrorsGroup]" 9 | }) 10 | export class NgxFormErrorsGroupDirective implements OnInit { 11 | /** 12 | * The group of the form model 13 | */ 14 | @Input("ngxFormErrorsGroup") 15 | public set group(value: string) { 16 | this._formErrorsGroup = value; 17 | } 18 | 19 | public get group(): string { 20 | return this._formErrorsGroup; 21 | } 22 | 23 | /** 24 | * @ignore 25 | */ 26 | private _formErrorsGroup!: string; 27 | 28 | /** 29 | * Class constructor 30 | */ 31 | // eslint-disable-next-line @typescript-eslint/no-useless-constructor 32 | public constructor() { 33 | // TODO: how to prevent multiple ngxFormErrorsGroup on the same
element? 34 | // if ((elementRef.nativeElement).attributes["ngxFormErrorsGroup"]) { 35 | // throw new Error( 36 | // "NgxFormErrorsGroupDirective is already applied to this element: " + 37 | // (elementRef.nativeElement).tagName + 38 | // ". Add this directive to an element only once" 39 | // ); 40 | // } 41 | } 42 | 43 | /** 44 | * Directive's lifecycle hook 45 | */ 46 | public ngOnInit(): void { 47 | if (!this.group) { 48 | throw new Error("NgxFormErrorsGroupDirective: no group provided."); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/form-error-component.intf.ts: -------------------------------------------------------------------------------- 1 | import { NgxFormFieldError } from "./form-error.intf"; 2 | import { Observable } from "rxjs"; 3 | 4 | /** 5 | * Describes an Error component to be dynamically created by the ngxFormErrors directive. 6 | * `IMPORTANT:` The Error component should be provided in the {@link NgxFormErrorsModule}.forRoot() method and must implement this interface. 7 | */ 8 | export interface NgxFormErrorComponent { 9 | /** 10 | * An observable that emits the validation errors of the form control whenever the control changes 11 | */ 12 | errors$: Observable; 13 | 14 | /** 15 | * Method to be called by the ngxFormErrors directive only. 16 | * It must contain the necessary logic to subscribe to the errors$ observable to update the current validation errors 17 | * from the form control 18 | */ 19 | subscribeToErrors(): void; 20 | } 21 | -------------------------------------------------------------------------------- /src/form-error.intf.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Error object describing the validation errors from a form control 3 | */ 4 | export interface NgxFormFieldError { 5 | /** 6 | * The type of validation (Angular validator name) 7 | */ 8 | error: string; 9 | 10 | /** 11 | * The name of the FormControl whose validation has failed 12 | */ 13 | formControlName: string; 14 | 15 | /** 16 | * The validation error message 17 | */ 18 | message: string; 19 | 20 | /** 21 | * Additional parameters 22 | */ 23 | params: { 24 | /** 25 | * Alias of the validated field. Defined via the "ngxFormErrorsFieldName" attribute of the {@link NgxFormErrorsDirective} 26 | * If no alias is defined for the field then this is the FormControl's name 27 | */ 28 | fieldName: string; 29 | /** 30 | * Any parameters passed to the actual Angular validator 31 | */ 32 | [p: string]: any; 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/form-errors-config.intf.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken, Type } from "@angular/core"; 2 | import { NgxFormErrorComponent } from "./form-error-component.intf"; 3 | 4 | /** 5 | * The InjectionToken version of the config name 6 | */ 7 | export const NGX_FORM_ERRORS_CONFIG: InjectionToken = new InjectionToken("NgxFormErrorsConfig"); 8 | 9 | /** 10 | * Definition of the configuration object for the {@link NgxFormErrorsModule} 11 | */ 12 | export interface NgxFormErrorsConfig { 13 | /** 14 | * Error component to be dynamically created by the ngxFormErrors directive which will display the validation errors. 15 | * This component should implement the {@link NgxFormErrorComponent} interface. 16 | */ 17 | formErrorComponent: Type; 18 | } 19 | -------------------------------------------------------------------------------- /src/form-errors.module.ts: -------------------------------------------------------------------------------- 1 | import { Inject, ModuleWithProviders, NgModule, Optional } from "@angular/core"; 2 | import { CommonModule } from "@angular/common"; 3 | import { FormsModule, ReactiveFormsModule } from "@angular/forms"; 4 | import { NgxFormErrorsDirective, NgxFormErrorsGroupDirective } from "./directives"; 5 | import { NgxFormErrorsMessageService } from "./services"; 6 | import { NGX_FORM_ERRORS_CONFIG, NgxFormErrorsConfig } from "./form-errors-config.intf"; 7 | 8 | @NgModule({ 9 | imports: [CommonModule, FormsModule, ReactiveFormsModule], 10 | declarations: [NgxFormErrorsDirective, NgxFormErrorsGroupDirective], 11 | exports: [NgxFormErrorsDirective, NgxFormErrorsGroupDirective] 12 | }) 13 | export class NgxFormErrorsModule { 14 | /** 15 | * Instantiates the services only once since they should be singletons 16 | * so the forRoot() should be called only by the AppModule 17 | * @see https://angular.io/guide/singleton-services#forroot 18 | * @param formErrorsConfig - Object containing the configuration (if any) for the {@link NgxFormErrorsDirective} 19 | * @returns a module with providers 20 | */ 21 | public static forRoot(formErrorsConfig: NgxFormErrorsConfig): ModuleWithProviders { 22 | return { 23 | ngModule: NgxFormErrorsModule, 24 | providers: [NgxFormErrorsMessageService, { provide: NGX_FORM_ERRORS_CONFIG, useValue: formErrorsConfig }] 25 | }; 26 | } 27 | 28 | /** 29 | * Class constructor 30 | * @param formErrorsConfig - Object containing the configuration (if any) for the {@link NgxFormErrorsDirective} 31 | */ 32 | public constructor(@Optional() @Inject(NGX_FORM_ERRORS_CONFIG) formErrorsConfig: NgxFormErrorsConfig) { 33 | if (!formErrorsConfig || !formErrorsConfig.formErrorComponent) { 34 | throw new Error( 35 | "NgxFormErrorsModule: a config object containing the Error component to be used should be provided via the forRoot() method." 36 | ); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/ngx-form-errors.ts: -------------------------------------------------------------------------------- 1 | export * from "./directives"; 2 | export * from "./services"; 3 | export * from "./form-error.intf"; 4 | export * from "./form-error-component.intf"; 5 | export * from "./form-errors-config.intf"; 6 | export * from "./form-errors.module"; 7 | -------------------------------------------------------------------------------- /src/services.ts: -------------------------------------------------------------------------------- 1 | export * from "./services/form-errors-message.service"; 2 | -------------------------------------------------------------------------------- /src/services/form-errors-message.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | 3 | /** 4 | * Object containing a set of validation error messages where the key is the validation errors (Angular validator name) 5 | * and the value is the validation is the error message 6 | */ 7 | export interface NgxValidationErrorMessages { 8 | [validationError: string]: string; 9 | } 10 | 11 | /** 12 | * Object containing a set of field names where the key is the field name (Angular form control name) 13 | * and the value is the name to be displayed for that field 14 | */ 15 | export interface NgxValidationErrorFieldNames { 16 | [fieldName: string]: string; 17 | } 18 | 19 | /** 20 | * Service to add and retrieve error messages for the different validation errors returned by the {@link NgxFormErrorsDirective} 21 | */ 22 | @Injectable() 23 | export class NgxFormErrorsMessageService { 24 | /** 25 | * @ignore 26 | */ 27 | protected errorMessages: NgxValidationErrorMessages = {}; 28 | /** 29 | * @ignore 30 | */ 31 | protected fieldNames: NgxValidationErrorFieldNames = {}; 32 | 33 | /** 34 | * Adds the given error messages to the set of messages known by this service 35 | * @param messages - An object containing the set of messages to be added 36 | */ 37 | public addErrorMessages(messages: NgxValidationErrorMessages): void { 38 | this.errorMessages = { ...this.errorMessages, ...messages }; 39 | } 40 | 41 | /** 42 | * Returns an object containing all the messages known by this service 43 | */ 44 | public getErrorMessages(): NgxValidationErrorMessages { 45 | return this.errorMessages; 46 | } 47 | 48 | /** 49 | * Tries to find the error message matching the given validation error. Additionally, the form control name and/or the model group 50 | * can be specified to return the error message matching those as well as the validation error. 51 | * This method does its best to find the message that fulfills all or some of the given parameters with the following precedence: 52 | * 53 | * 1.- errorMessages[groupName.formControlName.errorKey] 54 | * 2.- errorMessages[formControlName.errorKey] 55 | * 3.- errorMessages[groupName.errorKey] 56 | * 4.- errorMessages[errorKey] 57 | * 58 | * It returns `undefined` if no message matching any of those params is found. 59 | * @param error - The validation error (Angular validator name) 60 | * @param formControlName - The name of the FormControl 61 | * @param group - The model group to find a match for (if any) 62 | */ 63 | public findErrorMessage(error: string, formControlName?: string, group?: string): string | undefined { 64 | return ( 65 | (group && formControlName && this.errorMessages[`${group}.${formControlName}.${error}`]) || 66 | (formControlName && this.errorMessages[`${formControlName}.${error}`]) || 67 | (group && this.errorMessages[`${group}.${error}`]) || 68 | this.errorMessages[error] 69 | ); 70 | } 71 | 72 | /** 73 | * Add the given field names to the set of field names known by this service 74 | * @param fieldNames - An object containing the set of field names to be added 75 | */ 76 | public addFieldNames(fieldNames: NgxValidationErrorFieldNames): void { 77 | this.fieldNames = { ...this.fieldNames, ...fieldNames }; 78 | } 79 | 80 | /** 81 | * Returns an object containing all the field names known by this service 82 | */ 83 | public getFieldNames(): NgxValidationErrorFieldNames { 84 | return this.fieldNames; 85 | } 86 | 87 | /** 88 | * Returns the field name matching the given name. Additionally, the model group can be specified to return the field name 89 | * matching that group and the name. If no field name matches both, than it returns the one matching the given name. 90 | * Otherwise it returns undefined. 91 | * @param fieldName - The field name (Angular form control name) 92 | * @param group - The model group to find a match for (if any) 93 | */ 94 | public getFieldName(fieldName: string, group?: string): string | undefined { 95 | let fieldNameKey: string = fieldName; 96 | 97 | if (group) { 98 | fieldNameKey = `${group}.${fieldName}`; // concatenating group + field name with a "." 99 | } 100 | 101 | /* eslint-disable no-prototype-builtins */ 102 | if (this.fieldNames.hasOwnProperty(fieldNameKey)) { 103 | return this.fieldNames[fieldNameKey]; 104 | } else if (this.fieldNames.hasOwnProperty(fieldName)) { 105 | return this.fieldNames[fieldName]; 106 | } else { 107 | return undefined; 108 | } 109 | /* eslint-enable no-prototype-builtins */ 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@nationalbankbelgium/code-style/stylelint/13.13.x", "stylelint-config-prettier"] 3 | }; 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@nationalbankbelgium/code-style/tsconfig/5.1.x/ng16", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "rootDir": ".", 6 | "typeRoots": ["./node_modules/@types"], 7 | "lib": ["dom", "dom.iterable", "es2017"], 8 | "paths": {}, 9 | "outDir": "./dist" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "angularCompilerOptions": { 4 | "annotateForClosureCompiler": true, 5 | "skipTemplateCodegen": true, 6 | "enableResourceInlining": true 7 | }, 8 | "buildOnSave": false, 9 | "compileOnSave": false, 10 | "compilerOptions": { 11 | "outDir": "AUTOGENERATED", 12 | "declarationMap": true, 13 | "declarationDir": "AUTOGENERATED", 14 | "inlineSourceMap": true 15 | }, 16 | "exclude": ["node_modules", "dist", "demo-app", "**/*.spec.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "declarationMap": false 5 | }, 6 | "angularCompilerOptions": { 7 | "enableIvy": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": {}, 4 | "files": null, 5 | "include": ["*.ts", "src/**/*.ts"] 6 | } 7 | -------------------------------------------------------------------------------- /util-functions.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Three-Fingered Claw technique :) 4 | # Reference: https://stackoverflow.com/questions/1378274/in-a-bash-script-how-can-i-exit-the-entire-script-if-a-certain-condition-occurs 5 | yell() { echo "$0: $*" >&2; } 6 | die() { yell "$*"; exit 111; } 7 | try() { "$@" || die "cannot $*"; } 8 | 9 | ####################################### 10 | # Echo the passed message if verbose mode is enabled 11 | # Arguments: 12 | # param1 - message to log if verbose mode is enabled 13 | # param2 - depth: spaces to add before the string 14 | ####################################### 15 | logDebug() { 16 | if [[ ${VERBOSE} == true ]] || [[ ${TRACE} == true ]]; then 17 | logInfo "$@" 18 | fi 19 | } 20 | 21 | ####################################### 22 | # Echo the passed message if trace mode is enabled 23 | # Arguments: 24 | # param1 - message to log if trace mode is enabled 25 | # param2 - depth: spaces to add before the string 26 | ####################################### 27 | logTrace() { 28 | if [[ ${TRACE} == true ]]; then 29 | logInfo "$@" 30 | fi 31 | } 32 | 33 | ####################################### 34 | # Echo the passed message 35 | # Arguments: 36 | # param1 - message to log if verbose mode is enabled 37 | # param2 - (optional) depth: spaces to add before the string (defaults to 0) 38 | ####################################### 39 | #log() { 40 | # local message=${1:-NO MESSAGE TO LOG GIVEN TO log function (this is probably a mistake)} 41 | # local numSpaces=${2:-0} 42 | # printf "%${numSpaces}s$message\n" 43 | #} 44 | logInfo() { 45 | local message="${1:-NO MESSAGE TO LOG GIVEN TO log function (this is probably a mistake)}" 46 | local numSpaces="${2:-0}" 47 | printf -v spacing '%*s' "$numSpaces" 48 | printf "${spacing}%s\n" "$message" 49 | } 50 | 51 | ####################################### 52 | # Verifies a directory isn't in the ignored list 53 | # Arguments: 54 | # param1 - Source folder 55 | # param2 - Destination folder 56 | # param3 - Options {Array} 57 | ####################################### 58 | syncFiles() { 59 | logTrace "${FUNCNAME[0]}" 1 60 | logDebug "Syncing files from $1 to $2" 1 61 | cd $1; # we go to the folder to execute it with relative paths 62 | mkdir -p $2 63 | local REL_PATH_TO_DESTINATION=$(perl -e 'use File::Spec; print File::Spec->abs2rel(@ARGV) . "\n"' $2 $1) 64 | # local REL_PATH_TO_DESTINATION=$(realpath --relative-to="." "$2"); 65 | shift 2; # those 2 parameters are not needed anymore 66 | 67 | logTrace "Syncing files using: rsync" 2 68 | if [[ ${TRACE} == true ]]; then 69 | rsync "${@}" ./ $REL_PATH_TO_DESTINATION/ -v 70 | else 71 | rsync "${@}" ./ $REL_PATH_TO_DESTINATION/ 72 | fi 73 | cd - > /dev/null; # go back to the previous folder without any output 74 | } 75 | --------------------------------------------------------------------------------