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