diff --git a/showcase/package-lock.json b/showcase/package-lock.json
index 026d14ef3d..da17419d85 100644
--- a/showcase/package-lock.json
+++ b/showcase/package-lock.json
@@ -1706,6 +1706,14 @@
"resolved": "https://registry.npmjs.org/@nationalbankbelgium/code-style/-/code-style-1.1.1.tgz",
"integrity": "sha512-xLdMACQvrdJrqZn3TfuXIwedg68GLRV9+UrLfRDS35oNGMI1Bid/cjuN/wfrLoA4QOQGD5azl3ZToV4FvBzCcw=="
},
+ "@nationalbankbelgium/ngx-form-errors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@nationalbankbelgium/ngx-form-errors/-/ngx-form-errors-1.0.0.tgz",
+ "integrity": "sha512-14eiHMfRlr6fQk/D6Azb59EDMWR/4qHHPApfKih/U9MmTScRCWNzpN54A8jRbMYQX0MvxCHOhMkk31jM3y095g==",
+ "requires": {
+ "tslib": "^1.9.0"
+ }
+ },
"@nationalbankbelgium/stark-build": {
"version": "file:../dist/packages-dist/stark-build/nationalbankbelgium-stark-build-10.0.0-a843447a.tgz",
"integrity": "sha512-YtRtvhJOWDvbYkzW847CSPAL36fLJPvhcfrG2CtznNRLSzzEBG7ywZ51MSlf1EwEXOc/FePoBYY3/eg0/ESZNw==",
diff --git a/showcase/package.json b/showcase/package.json
index 5078135f41..21eff2803b 100644
--- a/showcase/package.json
+++ b/showcase/package.json
@@ -119,6 +119,7 @@
"@angular/platform-server": "~7.2.2",
"@angular/router": "~7.2.2",
"@nationalbankbelgium/code-style": "^1.1.1",
+ "@nationalbankbelgium/ngx-form-errors": "^1.0.0",
"@nationalbankbelgium/stark-core": "file:../dist/packages-dist/stark-core/nationalbankbelgium-stark-core-10.0.0-a843447a.tgz",
"@nationalbankbelgium/stark-rbac": "file:../dist/packages-dist/stark-rbac/nationalbankbelgium-stark-rbac-10.0.0-a843447a.tgz",
"@nationalbankbelgium/stark-ui": "file:../dist/packages-dist/stark-ui/nationalbankbelgium-stark-ui-10.0.0-a843447a.tgz",
diff --git a/showcase/src/app/app-menu.config.ts b/showcase/src/app/app-menu.config.ts
index d827e2ec4c..0506d70ee6 100644
--- a/showcase/src/app/app-menu.config.ts
+++ b/showcase/src/app/app-menu.config.ts
@@ -26,6 +26,13 @@ export const APP_MENU_CONFIG: StarkMenuConfig = {
isVisible: true,
isEnabled: true,
targetState: "news"
+ },
+ {
+ id: "reactive-form-errors",
+ label: "SHOWCASE.NGX_FORM_ERRORS.TITLE",
+ isVisible: true,
+ isEnabled: true,
+ targetState: "reactive-form-errors"
}
]
},
diff --git a/showcase/src/app/welcome/pages/index.ts b/showcase/src/app/welcome/pages/index.ts
index 92141ec039..5d600592cf 100644
--- a/showcase/src/app/welcome/pages/index.ts
+++ b/showcase/src/app/welcome/pages/index.ts
@@ -2,3 +2,4 @@ export * from "./getting-started";
export * from "./home";
export * from "./news";
export * from "./no-content";
+export * from "./reactive-form-errors";
diff --git a/showcase/src/app/welcome/pages/reactive-form-errors/components/card/card.component.html b/showcase/src/app/welcome/pages/reactive-form-errors/components/card/card.component.html
new file mode 100644
index 0000000000..42620e9a7a
--- /dev/null
+++ b/showcase/src/app/welcome/pages/reactive-form-errors/components/card/card.component.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/showcase/src/app/welcome/pages/reactive-form-errors/components/card/card.component.scss b/showcase/src/app/welcome/pages/reactive-form-errors/components/card/card.component.scss
new file mode 100644
index 0000000000..e61ec8f140
--- /dev/null
+++ b/showcase/src/app/welcome/pages/reactive-form-errors/components/card/card.component.scss
@@ -0,0 +1,5 @@
+:host mat-card {
+ box-sizing: border-box;
+ width: 100%;
+ min-height: 100%;
+}
diff --git a/showcase/src/app/welcome/pages/reactive-form-errors/components/card/card.component.ts b/showcase/src/app/welcome/pages/reactive-form-errors/components/card/card.component.ts
new file mode 100644
index 0000000000..526670067e
--- /dev/null
+++ b/showcase/src/app/welcome/pages/reactive-form-errors/components/card/card.component.ts
@@ -0,0 +1,44 @@
+import { Component, HostBinding, Input } from "@angular/core";
+
+type Colors = "primary" | "accent" | "warning" | "success";
+
+@Component({
+ selector: "app-card",
+ templateUrl: "./card.component.html",
+ styleUrls: ["./card.component.scss"]
+})
+export class CardComponent {
+ @HostBinding("class.app-color-primary")
+ public primaryColor!: boolean;
+ @HostBinding("class.app-color-accent")
+ public accentColor!: boolean;
+ @HostBinding("class.app-color-warning")
+ public warningColor!: boolean;
+ @HostBinding("class.app-color-success")
+ public successColor!: boolean;
+
+ @Input()
+ public set color(color: Colors) {
+ this.primaryColor = false;
+ this.accentColor = false;
+ this.warningColor = false;
+ this.successColor = false;
+
+ switch (color) {
+ case "primary":
+ this.primaryColor = true;
+ break;
+ case "accent":
+ this.accentColor = true;
+ break;
+ case "warning":
+ this.warningColor = true;
+ break;
+ case "success":
+ this.successColor = true;
+ break;
+ default:
+ break;
+ }
+ }
+}
diff --git a/showcase/src/app/welcome/pages/reactive-form-errors/components/card/card.theme.scss b/showcase/src/app/welcome/pages/reactive-form-errors/components/card/card.theme.scss
new file mode 100644
index 0000000000..d914a9cd91
--- /dev/null
+++ b/showcase/src/app/welcome/pages/reactive-form-errors/components/card/card.theme.scss
@@ -0,0 +1,20 @@
+app-card.app-color-primary mat-card {
+ background-color: mat-color($primary-palette, 500);
+ color: mat-contrast($primary-palette, 500);
+}
+
+app-card.app-color-accent mat-card {
+ background-color: mat-color($primary-palette, 500);
+ color: mat-contrast($primary-palette, 500);
+}
+
+app-card.app-color-warning mat-card {
+ background-color: mat-color($warning-palette, 500);
+ color: mat-contrast($warning-palette, 500);
+}
+
+app-card.app-color-success mat-card {
+ /*Themes do not have a success map by default*/
+ background-color: mat-color($success-palette, 500);
+ color: mat-contrast($success-palette, 500);
+}
diff --git a/showcase/src/app/welcome/pages/reactive-form-errors/components/card/index.ts b/showcase/src/app/welcome/pages/reactive-form-errors/components/card/index.ts
new file mode 100644
index 0000000000..8151bac4c8
--- /dev/null
+++ b/showcase/src/app/welcome/pages/reactive-form-errors/components/card/index.ts
@@ -0,0 +1 @@
+export * from "./card.component";
diff --git a/showcase/src/app/welcome/pages/reactive-form-errors/components/index.ts b/showcase/src/app/welcome/pages/reactive-form-errors/components/index.ts
new file mode 100644
index 0000000000..a81d5c6e8a
--- /dev/null
+++ b/showcase/src/app/welcome/pages/reactive-form-errors/components/index.ts
@@ -0,0 +1,2 @@
+export * from "./card";
+export * from "./translated-form-error";
diff --git a/showcase/src/app/welcome/pages/reactive-form-errors/components/translated-form-error/index.ts b/showcase/src/app/welcome/pages/reactive-form-errors/components/translated-form-error/index.ts
new file mode 100644
index 0000000000..4fad8db434
--- /dev/null
+++ b/showcase/src/app/welcome/pages/reactive-form-errors/components/translated-form-error/index.ts
@@ -0,0 +1 @@
+export * from "./translated-form-error.component";
diff --git a/showcase/src/app/welcome/pages/reactive-form-errors/components/translated-form-error/translated-form-error.component.html b/showcase/src/app/welcome/pages/reactive-form-errors/components/translated-form-error/translated-form-error.component.html
new file mode 100644
index 0000000000..22e7aaf61f
--- /dev/null
+++ b/showcase/src/app/welcome/pages/reactive-form-errors/components/translated-form-error/translated-form-error.component.html
@@ -0,0 +1 @@
+
{{ error.message | translate: error.params }}
diff --git a/showcase/src/app/welcome/pages/reactive-form-errors/components/translated-form-error/translated-form-error.component.ts b/showcase/src/app/welcome/pages/reactive-form-errors/components/translated-form-error/translated-form-error.component.ts
new file mode 100644
index 0000000000..c441df4199
--- /dev/null
+++ b/showcase/src/app/welcome/pages/reactive-form-errors/components/translated-form-error/translated-form-error.component.ts
@@ -0,0 +1,47 @@
+import { Component, HostBinding, OnInit } from "@angular/core";
+import { LangChangeEvent, TranslateService } from "@ngx-translate/core";
+import { Observable } from "rxjs";
+import { NgxFormErrorComponent, NgxFormFieldError } from "@nationalbankbelgium/ngx-form-errors";
+
+@Component({
+ selector: "app-translated-form-error",
+ templateUrl: "./translated-form-error.component.html"
+})
+export class TranslatedFormErrorComponent implements NgxFormErrorComponent, OnInit {
+ @HostBinding("class")
+ public cssClass = "translated-form-error";
+
+ public errors: NgxFormFieldError[] = [];
+ public errors$!: Observable;
+ public fieldName!: string;
+
+ public constructor(public translateService: TranslateService) {}
+
+ public ngOnInit(): void {
+ this.translateService.onLangChange.subscribe((_ev: LangChangeEvent) => {
+ this.updateTranslateFieldName(this.translateService.instant(this.fieldName));
+ });
+ }
+
+ public subscribeToErrors(): void {
+ this.errors$.subscribe((errors: NgxFormFieldError[]) => {
+ this.errors = errors;
+
+ if (errors.length) {
+ // the formField can be retrieved from the "fieldName" param of any of the errors
+ this.fieldName = errors[0].params.fieldName;
+ this.updateTranslateFieldName(this.translateService.instant(this.fieldName));
+ }
+ });
+ }
+
+ public updateTranslateFieldName(translatedFieldName: string): void {
+ for (const error of this.errors) {
+ error.params = { ...error.params, fieldName: translatedFieldName };
+ }
+ }
+
+ public trackError(index: number): number {
+ return index;
+ }
+}
diff --git a/showcase/src/app/welcome/pages/reactive-form-errors/index.ts b/showcase/src/app/welcome/pages/reactive-form-errors/index.ts
new file mode 100644
index 0000000000..e2c99e3f05
--- /dev/null
+++ b/showcase/src/app/welcome/pages/reactive-form-errors/index.ts
@@ -0,0 +1 @@
+export * from "./reactive-form-errors-page.component";
diff --git a/showcase/src/app/welcome/pages/reactive-form-errors/password-validator.ts b/showcase/src/app/welcome/pages/reactive-form-errors/password-validator.ts
new file mode 100644
index 0000000000..a87ca4f312
--- /dev/null
+++ b/showcase/src/app/welcome/pages/reactive-form-errors/password-validator.ts
@@ -0,0 +1,31 @@
+import { FormControl, FormGroup, ValidationErrors, ValidatorFn } from "@angular/forms";
+
+export class PasswordValidator {
+ // Inspired on: http://plnkr.co/edit/Zcbg2T3tOxYmhxs7vaAm?p=preview
+ public static areEqual(formGroup: FormGroup): ValidationErrors | null {
+ let value: string | undefined;
+ let valid = true;
+
+ for (const key in formGroup.controls) {
+ if (formGroup.controls.hasOwnProperty(key)) {
+ const control: FormControl = formGroup.controls[key];
+
+ if (value === undefined) {
+ value = control.value;
+ } else if (value !== control.value) {
+ valid = false;
+ break;
+ }
+ }
+ }
+
+ /* tslint:disable-next-line:no-null-keyword */
+ return valid ? null : { areEqual: true };
+ }
+}
+
+export function getConfirmPasswordValidator(formGroup: FormGroup): ValidatorFn {
+ return (): ValidationErrors | null => {
+ return PasswordValidator.areEqual(formGroup);
+ };
+}
diff --git a/showcase/src/app/welcome/pages/reactive-form-errors/reactive-form-errors-page.component.html b/showcase/src/app/welcome/pages/reactive-form-errors/reactive-form-errors-page.component.html
new file mode 100644
index 0000000000..dfc04fc54b
--- /dev/null
+++ b/showcase/src/app/welcome/pages/reactive-form-errors/reactive-form-errors-page.component.html
@@ -0,0 +1,230 @@
+
+
+ SHOWCASE.NGX_FORM_ERRORS.TITLE
+
+
SHOWCASE.DEMO.SHARED.EXAMPLE_VIEWER_LIST
+
+
+
diff --git a/showcase/src/app/welcome/pages/reactive-form-errors/reactive-form-errors-page.component.scss b/showcase/src/app/welcome/pages/reactive-form-errors/reactive-form-errors-page.component.scss
new file mode 100644
index 0000000000..f2c59487b0
--- /dev/null
+++ b/showcase/src/app/welcome/pages/reactive-form-errors/reactive-form-errors-page.component.scss
@@ -0,0 +1,68 @@
+@import "~@nationalbankbelgium/stark-ui/assets/styles/media-queries";
+
+button {
+ margin: 8px;
+}
+
+.form-card {
+ mat-form-field {
+ box-sizing: border-box;
+ width: 100%;
+ @media #{$desktop-screen-query} {
+ width: 45%;
+ &:last-child {
+ margin-left: 10%;
+ }
+ }
+ }
+
+ mat-card-actions {
+ display: flex;
+ align-items: flex-start;
+ flex-wrap: wrap;
+ margin: -10px;
+
+ button {
+ margin: 10px;
+ @media #{$mobile-only-query} {
+ width: 100%;
+ }
+ }
+ }
+}
+
+.form-field-info {
+ @media #{$desktop-query} {
+ max-width: 33%;
+ }
+
+ max-width: 100%;
+
+ mat-card-content {
+ margin: 0;
+ padding: 5px 0;
+
+ pre {
+ overflow: auto;
+ box-sizing: border-box;
+ display: block;
+ max-height: 200px;
+
+ margin: inherit;
+ padding: 15px 5px;
+ border-radius: 4px;
+
+ background-color: rgba(0, 0, 0, 0.2);
+
+ word-break: break-all;
+ white-space: pre-wrap;
+
+ /* Non standard for webkit */
+
+ hyphens: auto;
+ &:empty {
+ display: none;
+ }
+ }
+ }
+}
diff --git a/showcase/src/app/welcome/pages/reactive-form-errors/reactive-form-errors-page.component.theme.scss b/showcase/src/app/welcome/pages/reactive-form-errors/reactive-form-errors-page.component.theme.scss
new file mode 100644
index 0000000000..08ca4d0623
--- /dev/null
+++ b/showcase/src/app/welcome/pages/reactive-form-errors/reactive-form-errors-page.component.theme.scss
@@ -0,0 +1,19 @@
+app-card mat-form-field.maximum-height {
+ .mat-form-field-wrapper {
+ padding-bottom: 40px;
+
+ .mat-form-field-underline {
+ bottom: 40px;
+ }
+
+ .mat-form-field-subscript-wrapper {
+ top: calc(100% - 40px);
+ }
+ }
+}
+
+.validation-summary {
+ .translated-form-error div::before {
+ content: "• ";
+ }
+}
diff --git a/showcase/src/app/welcome/pages/reactive-form-errors/reactive-form-errors-page.component.ts b/showcase/src/app/welcome/pages/reactive-form-errors/reactive-form-errors-page.component.ts
new file mode 100644
index 0000000000..89b1b64c6b
--- /dev/null
+++ b/showcase/src/app/welcome/pages/reactive-form-errors/reactive-form-errors-page.component.ts
@@ -0,0 +1,74 @@
+import { Component, Inject } from "@angular/core";
+import { AbstractControl, FormBuilder, FormGroup, Validators } from "@angular/forms";
+import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core";
+import { getConfirmPasswordValidator } from "./password-validator";
+import { ReferenceLink } from "../../../shared/components/reference-block";
+
+@Component({
+ selector: "reactive-forms",
+ templateUrl: "./reactive-form-errors-page.component.html",
+ styleUrls: ["./reactive-form-errors-page.component.scss"]
+})
+export class ReactiveFormErrorsPageComponent {
+ public collapsed: boolean[] = [false, false, true];
+
+ public referenceList: ReferenceLink[] = [
+ {
+ label: "NGX Form errors library",
+ url: "https://github.com/NationalBankBelgium/ngx-form-errors"
+ }
+ ];
+
+ public formGroup: FormGroup;
+ public passwordPattern = "^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9]+$";
+ public showValidationDetails = false;
+ public showValidationSummary = true;
+
+ public constructor(private formBuilder: FormBuilder, @Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService) {
+ this.formGroup = this.formBuilder.group({
+ username: [undefined, Validators.required],
+ matchingPasswords: this.formBuilder.group({
+ password: [
+ "",
+ Validators.compose([
+ Validators.minLength(3),
+ Validators.maxLength(10),
+ Validators.required,
+ // this is for the letters (both uppercase and lowercase) and numbers validation
+ Validators.pattern(this.passwordPattern)
+ ])
+ ],
+ confirmPassword: [""] // validators for this field to be set afterwards (see below)
+ })
+ });
+
+ // setting the validator for confirmPassword field once we have created the form group
+ const confirmPasswordControl = this.formGroup.get("matchingPasswords.confirmPassword");
+ // we need to set the confirmPasswordValidator passing the "matchingPasswords" form group so that the errors of the form group are actually
+ // linked to the "confirmPassword" control because the NgxFormErrors directive is linked to the control and not to the form group!
+ confirmPasswordControl.setValidators([
+ Validators.required,
+ getConfirmPasswordValidator(this.formGroup.get("matchingPasswords"))
+ ]);
+ }
+
+ public getErrorClass(formControlName: string): string {
+ const formCtrl = this.formGroup.get(formControlName) as AbstractControl;
+ return formCtrl.errors && Object.keys(formCtrl.errors).length > 1 ? "maximum-height" : "small-height";
+ }
+
+ public toggleCollapsible(nb: number): void {
+ this.collapsed[nb] = !this.collapsed[nb];
+ }
+ public toggleValidationDetails(): void {
+ this.showValidationDetails = !this.showValidationDetails;
+ }
+
+ public toggleValidationSummary(): void {
+ this.showValidationSummary = !this.showValidationSummary;
+ }
+
+ public onSubmitUserDetails(formGroup: FormGroup): void {
+ this.logger.info("Submitted form:", formGroup.value);
+ }
+}
diff --git a/showcase/src/app/welcome/routes.ts b/showcase/src/app/welcome/routes.ts
index 6ea7383b2a..a27c90de0b 100644
--- a/showcase/src/app/welcome/routes.ts
+++ b/showcase/src/app/welcome/routes.ts
@@ -1,5 +1,11 @@
import { Ng2StateDeclaration } from "@uirouter/angular";
-import { GettingStartedPageComponent, HomePageComponent, NewsPageComponent, NoContentPageComponent } from "./pages";
+import {
+ GettingStartedPageComponent,
+ HomePageComponent,
+ NewsPageComponent,
+ NoContentPageComponent,
+ ReactiveFormErrorsPageComponent
+} from "./pages";
export const NEWS_STATES: Ng2StateDeclaration[] = [
{
@@ -29,6 +35,15 @@ export const NEWS_STATES: Ng2StateDeclaration[] = [
views: { "@": { component: NewsPageComponent } },
parent: "app"
},
+ {
+ name: "reactive-form-errors",
+ url: "^/reactive-form-errors", // use ^ to avoid double slash "//" in the URL after the domain (https://github.com/angular-ui/ui-router/wiki/URL-Routing#absolute-routes-)
+ data: {
+ translationKey: "SHOWCASE.NGX_FORM_ERRORS.TITLE"
+ },
+ views: { "@": { component: ReactiveFormErrorsPageComponent } },
+ parent: "app"
+ },
{
name: "otherwise",
url: "^/otherwise", // use ^ to avoid double slash "//" in the URL after the domain (https://github.com/angular-ui/ui-router/wiki/URL-Routing#absolute-routes-)
diff --git a/showcase/src/app/welcome/welcome.module.ts b/showcase/src/app/welcome/welcome.module.ts
index b7774496ec..de097e1b0b 100644
--- a/showcase/src/app/welcome/welcome.module.ts
+++ b/showcase/src/app/welcome/welcome.module.ts
@@ -1,18 +1,64 @@
import { NgModule } from "@angular/core";
import { UIRouterModule } from "@uirouter/angular";
+import { MatDividerModule } from "@angular/material/divider";
+import { MatFormFieldModule } from "@angular/material/form-field";
+import { MatInputModule } from "@angular/material/input";
+import { NgxFormErrorsModule, NgxFormErrorsMessageService } from "@nationalbankbelgium/ngx-form-errors";
+import { SharedModule } from "../shared";
import { GettingStartedPageComponent, HomePageComponent, NewsPageComponent, NoContentPageComponent } from "./pages";
import { NewsItemComponent } from "./components";
-import { SharedModule } from "../shared";
import { NEWS_STATES } from "./routes";
+import { ReactiveFormErrorsPageComponent } from "./pages/reactive-form-errors";
+import { TranslatedFormErrorComponent } from "./pages/reactive-form-errors/components/translated-form-error";
+import { CardComponent } from "./pages/reactive-form-errors/components/card";
@NgModule({
imports: [
UIRouterModule.forChild({
states: NEWS_STATES
}),
- SharedModule
+ SharedModule,
+ MatDividerModule,
+ MatInputModule,
+ MatFormFieldModule,
+ NgxFormErrorsModule.forRoot({ formErrorComponent: TranslatedFormErrorComponent })
+ ],
+ declarations: [
+ GettingStartedPageComponent,
+ HomePageComponent,
+ NoContentPageComponent,
+ NewsPageComponent,
+ NewsItemComponent,
+ ReactiveFormErrorsPageComponent,
+ TranslatedFormErrorComponent,
+ CardComponent
],
- declarations: [GettingStartedPageComponent, HomePageComponent, NoContentPageComponent, NewsPageComponent, NewsItemComponent],
- exports: [GettingStartedPageComponent, HomePageComponent, NoContentPageComponent, NewsPageComponent, NewsItemComponent]
+ exports: [
+ GettingStartedPageComponent,
+ HomePageComponent,
+ NoContentPageComponent,
+ NewsPageComponent,
+ NewsItemComponent,
+ ReactiveFormErrorsPageComponent
+ ],
+ entryComponents: [TranslatedFormErrorComponent]
})
-export class WelcomeModule {}
+export class WelcomeModule {
+ /* tslint:disable:no-hardcoded-credentials */
+ public constructor(private errorMessageService: NgxFormErrorsMessageService) {
+ this.errorMessageService.addErrorMessages({
+ required: "SHOWCASE.NGX_FORM_ERRORS.FORM.VALIDATION.REQUIRED",
+ "matchingPasswords.password.required": "SHOWCASE.NGX_FORM_ERRORS.FORM.VALIDATION.PASSWORD_REQUIRED",
+ minlength: "SHOWCASE.NGX_FORM_ERRORS.FORM.VALIDATION.PASSWORD.MIN_LENGTH",
+ maxlength: "SHOWCASE.NGX_FORM_ERRORS.FORM.VALIDATION.PASSWORD.MAX_LENGTH",
+ pattern: "SHOWCASE.NGX_FORM_ERRORS.FORM.VALIDATION.PASSWORD.PATTERN",
+ areEqual: "SHOWCASE.NGX_FORM_ERRORS.FORM.VALIDATION.CONFIRM_PASSWORD.ARE_EQUAL"
+ });
+
+ this.errorMessageService.addFieldNames({
+ username: "SHOWCASE.NGX_FORM_ERRORS.FIELDS.ALIAS.USER_NAME",
+ "matchingPasswords.password": "not used, the alias defined via the directive takes precedence over this",
+ "matchingPasswords.confirmPassword": "SHOWCASE.NGX_FORM_ERRORS.FIELDS.ALIAS.CONFIRM_PASSWORD"
+ });
+ }
+}
diff --git a/showcase/src/assets/examples/reactive-form-errors/reactive-form-errors.html b/showcase/src/assets/examples/reactive-form-errors/reactive-form-errors.html
new file mode 100644
index 0000000000..33e0e93a87
--- /dev/null
+++ b/showcase/src/assets/examples/reactive-form-errors/reactive-form-errors.html
@@ -0,0 +1,140 @@
+
diff --git a/showcase/src/assets/examples/reactive-form-errors/reactive-form-errors.scss b/showcase/src/assets/examples/reactive-form-errors/reactive-form-errors.scss
new file mode 100644
index 0000000000..f2c59487b0
--- /dev/null
+++ b/showcase/src/assets/examples/reactive-form-errors/reactive-form-errors.scss
@@ -0,0 +1,68 @@
+@import "~@nationalbankbelgium/stark-ui/assets/styles/media-queries";
+
+button {
+ margin: 8px;
+}
+
+.form-card {
+ mat-form-field {
+ box-sizing: border-box;
+ width: 100%;
+ @media #{$desktop-screen-query} {
+ width: 45%;
+ &:last-child {
+ margin-left: 10%;
+ }
+ }
+ }
+
+ mat-card-actions {
+ display: flex;
+ align-items: flex-start;
+ flex-wrap: wrap;
+ margin: -10px;
+
+ button {
+ margin: 10px;
+ @media #{$mobile-only-query} {
+ width: 100%;
+ }
+ }
+ }
+}
+
+.form-field-info {
+ @media #{$desktop-query} {
+ max-width: 33%;
+ }
+
+ max-width: 100%;
+
+ mat-card-content {
+ margin: 0;
+ padding: 5px 0;
+
+ pre {
+ overflow: auto;
+ box-sizing: border-box;
+ display: block;
+ max-height: 200px;
+
+ margin: inherit;
+ padding: 15px 5px;
+ border-radius: 4px;
+
+ background-color: rgba(0, 0, 0, 0.2);
+
+ word-break: break-all;
+ white-space: pre-wrap;
+
+ /* Non standard for webkit */
+
+ hyphens: auto;
+ &:empty {
+ display: none;
+ }
+ }
+ }
+}
diff --git a/showcase/src/assets/examples/reactive-form-errors/reactive-form-errors.ts b/showcase/src/assets/examples/reactive-form-errors/reactive-form-errors.ts
new file mode 100644
index 0000000000..92f0587c6b
--- /dev/null
+++ b/showcase/src/assets/examples/reactive-form-errors/reactive-form-errors.ts
@@ -0,0 +1,67 @@
+import { Component, Inject } from "@angular/core";
+import { AbstractControl, FormBuilder, FormGroup, ValidationErrors, Validators } from "@angular/forms";
+import { ErrorStateMatcher } from "@angular/material/core";
+import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core";
+import { ParentErrorStateMatcher } from "./parent-error-state-matcher";
+import { PasswordValidator } from "./password-validator";
+
+@Component({
+ selector: "reactive-form-errors",
+ templateUrl: "./reactive-form-errors.html",
+ styleUrls: ["./reactive-form-errors.scss"]
+})
+export class ReactiveFormErrors {
+ public collapsed: boolean[] = [false, false, true];
+
+ public formGroup: FormGroup;
+ public parentErrorStateMatcher: ErrorStateMatcher = new ParentErrorStateMatcher();
+ public passwordPattern = "^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9]+$";
+ public showValidationDetails = false;
+ public showValidationSummary = true;
+
+ public constructor(private formBuilder: FormBuilder, @Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService) {
+ this.formGroup = this.formBuilder.group({
+ username: [undefined, Validators.required],
+ matchingPasswords: this.formBuilder.group(
+ {
+ password: [
+ "",
+ Validators.compose([
+ Validators.minLength(3),
+ Validators.maxLength(10),
+ Validators.required,
+ // this is for the letters (both uppercase and lowercase) and numbers validation
+ Validators.pattern(this.passwordPattern)
+ ])
+ ],
+ confirmPassword: ["", Validators.required]
+ },
+ {
+ validators: (formGroup: AbstractControl): ValidationErrors | null => {
+ return PasswordValidator.areEqual(formGroup);
+ }
+ }
+ )
+ });
+ }
+
+ public getErrorClass(formControlName: string): string {
+ const formCtrl = this.formGroup.get(formControlName) as AbstractControl;
+ return formCtrl.errors && Object.keys(formCtrl.errors).length > 1 ? "maximum-height" : "small-height";
+ }
+
+ public toggleCollapsible(nb: number): void {
+ this.collapsed[nb] = !this.collapsed[nb];
+ }
+ public toggleValidationDetails(): void {
+ this.showValidationDetails = !this.showValidationDetails;
+ }
+
+ public toggleValidationSummary(): void {
+ this.showValidationSummary = !this.showValidationSummary;
+ }
+
+ public onSubmitUserDetails(formGroup: FormGroup): void {
+ this.logger.info("Submitted form:", formGroup.value);
+ }
+}
diff --git a/showcase/src/assets/translations/en.json b/showcase/src/assets/translations/en.json
index 04e5bd366b..726501bdc9 100644
--- a/showcase/src/assets/translations/en.json
+++ b/showcase/src/assets/translations/en.json
@@ -430,6 +430,49 @@
"NEWS": {
"TITLE": "News"
},
+ "NGX_FORM_ERRORS": {
+ "TITLE": "Reactive Forms errors",
+ "EXAMPLE": "Example",
+ "FORM": {
+ "VALIDATION": {
+ "REQUIRED": "{{fieldName}} is required",
+ "PASSWORD_REQUIRED": "{{fieldName}} must be provided",
+ "USER_NAME": {
+ "UNIQUE": "This username has already been taken"
+ },
+ "PASSWORD": {
+ "MAX_LENGTH": "Password cannot be more than {{requiredLength}} characters long",
+ "MIN_LENGTH": "Password must be at least {{requiredLength}} characters long",
+ "PATTERN": "The password must contain at least one uppercase, one lowercase, and one number"
+ },
+ "CONFIRM_PASSWORD": {
+ "ARE_EQUAL": "Password mismatch"
+ }
+ },
+ "HIDE_DETAILS": "Hide validation details",
+ "SHOW_DETAILS": "Show validation details",
+ "HIDE_SUMMARY": "Hide validation summary",
+ "SHOW_SUMMARY": "Show validation summary",
+ "SUBMIT": "Submit"
+ },
+ "FIELDS": {
+ "USER_NAME": "User Name",
+ "PASSWORD": "Password",
+ "CONFIRM_PASSWORD": "Confirm password",
+ "ALIAS": {
+ "USER_NAME": "Your username",
+ "PASSWORD_ALIAS": "A valid password",
+ "CONFIRM_PASSWORD": "Password confirmation"
+ },
+ "INFO": {
+ "HAS_ERRORS": "Has errors: {{hasErrors}}",
+ "HAS_SPECIFIC_ERROR": "Has '{{error}}' error: {{hasError}}",
+ "ERROR": "'{{error}}' error:",
+ "ERRORS": "Errors:",
+ "IS_TOUCHED": "Is touched: {{isTouched}}"
+ }
+ }
+ },
"OTHERWISE": {
"TITLE": "Otherwise"
},
diff --git a/showcase/src/assets/translations/fr.json b/showcase/src/assets/translations/fr.json
index 8918413ed4..1bab57b63f 100644
--- a/showcase/src/assets/translations/fr.json
+++ b/showcase/src/assets/translations/fr.json
@@ -430,6 +430,49 @@
"NEWS": {
"TITLE": "Nouvelles"
},
+ "NGX_FORM_ERRORS": {
+ "TITLE": "Reactive Forms errors",
+ "EXAMPLE": "Exemple",
+ "FORM": {
+ "VALIDATION": {
+ "REQUIRED": "Le champ \"{{fieldName}}\" est requis",
+ "PASSWORD_REQUIRED": "Le champ \"{{fieldName}}\" est requis",
+ "USER_NAME": {
+ "UNIQUE": "Ce nom d'utilisateur est déjà pris"
+ },
+ "PASSWORD": {
+ "MAX_LENGTH": "Le mot de passe ne peut pas contenir plus de {{requiredLength}} caractères",
+ "MIN_LENGTH": "Le mot de passe doit contenir au moins {{requiredLength}} caractères",
+ "PATTERN": "Le mot de passe doit contenir au moins une majuscule, une minuscule et un chiffre."
+ },
+ "CONFIRM_PASSWORD": {
+ "ARE_EQUAL": "Les mots de passe ne correspondent pas"
+ }
+ },
+ "HIDE_DETAILS": "Masquer les détails de validation",
+ "SHOW_DETAILS": "Afficher les détails de validation",
+ "HIDE_SUMMARY": "Masquer le résumé de validation",
+ "SHOW_SUMMARY": "Afficher le résumé de validation",
+ "SUBMIT": "Soumettre"
+ },
+ "FIELDS": {
+ "USER_NAME": "Nom d'utilisateur",
+ "PASSWORD": "Mot de passe",
+ "CONFIRM_PASSWORD": "Confirmez le mot de passe",
+ "ALIAS": {
+ "USER_NAME": "Votre nom d'utilisateur",
+ "PASSWORD_ALIAS": "Un mot de passe valide",
+ "CONFIRM_PASSWORD": "Confirmation du mot de passe"
+ },
+ "INFO": {
+ "HAS_ERRORS": "Contient des erreurs: {{hasErrors}}",
+ "HAS_SPECIFIC_ERROR": "Contient l'erreur '{{error}}': {{hasError}}",
+ "ERROR": "Erreur '{{error}}':",
+ "ERRORS": "Erreurs:",
+ "IS_TOUCHED": "Est touché: {{isTouched}}"
+ }
+ }
+ },
"OTHERWISE": {
"TITLE": "Autre"
},
diff --git a/showcase/src/assets/translations/nl.json b/showcase/src/assets/translations/nl.json
index 75e528c2d9..60350e82d4 100644
--- a/showcase/src/assets/translations/nl.json
+++ b/showcase/src/assets/translations/nl.json
@@ -430,6 +430,49 @@
"NEWS": {
"TITLE": "Nieuws"
},
+ "NGX_FORM_ERRORS": {
+ "TITLE": "Reactive Forms errors",
+ "EXAMPLE": "Voorbeeld",
+ "FORM": {
+ "VALIDATION": {
+ "REQUIRED": "{{fieldName}} is verplicht",
+ "PASSWORD_REQUIRED": "{{fieldName}} moet worden gegeven",
+ "USER_NAME": {
+ "UNIQUE": "Dit gebruikersnaam is al in gebruik"
+ },
+ "PASSWORD": {
+ "MAX_LENGTH": "Wachtwoord mag niet meer dan {{requiredLength}} tekens lang zijn",
+ "MIN_LENGTH": "Wachtwoord moet minimaal {{requiredLength}} tekens lang zijn",
+ "PATTERN": "Het wachtwoord moet minimaal één hoofdletter, één kleine letter en één cijfer bevatten"
+ },
+ "CONFIRM_PASSWORD": {
+ "ARE_EQUAL": "Wachtwoord komt niet overeen"
+ }
+ },
+ "HIDE_DETAILS": "Validatiedetails verbergen",
+ "SHOW_DETAILS": "Validatiedetails weergeven",
+ "HIDE_SUMMARY": "Validatieoverzicht verbergen",
+ "SHOW_SUMMARY": "Validatieoverzicht tonen",
+ "SUBMIT": "Indienen"
+ },
+ "FIELDS": {
+ "USER_NAME": "Gebruikersnaam",
+ "PASSWORD": "Wachtwoord",
+ "CONFIRM_PASSWORD": "Bevestig wachtwoord",
+ "ALIAS": {
+ "USER_NAME": "Uw gebruikersnaam",
+ "PASSWORD_ALIAS": "Een geldig wachtwoord",
+ "CONFIRM_PASSWORD": "Wachtwoordbevestiging"
+ },
+ "INFO": {
+ "HAS_ERRORS": "Veld heeft fouten: {{hasErrors}}",
+ "HAS_SPECIFIC_ERROR": "Veld heeft de '{{error}}' fout: {{hasError}}",
+ "ERROR": "'{{error}}' fout:",
+ "ERRORS": "Fouten:",
+ "IS_TOUCHED": "Veld is aangeraakt: {{isTouched}}"
+ }
+ }
+ },
"OTHERWISE": {
"TITLE": "Andersom"
},
diff --git a/showcase/src/styles/_theme.scss b/showcase/src/styles/_theme.scss
index 34ac649730..a700eff430 100644
--- a/showcase/src/styles/_theme.scss
+++ b/showcase/src/styles/_theme.scss
@@ -16,3 +16,5 @@ Import the local variables file first to set the correct variables, see:
@import "../app/demo-ui/pages/route-search/demo-route-search-page.component-theme";
@import "../app/demo-ui/components/table-regular/table-regular-theme";
@import "../app/styleguide/pages/layout/styleguide-layout-page.theme";
+@import "../app/welcome/pages/reactive-form-errors/components/card/card.theme";
+@import "../app/welcome/pages/reactive-form-errors/reactive-form-errors-page.component.theme";