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

+
+ +
+
+ + + + + + + + + +
+ + + + + + + + + + + + + +
+
+ + + + + + +
+ +
+ + SHOWCASE.NGX_FORM_ERRORS.FIELDS.USER_NAME + +
+ {{ + "SHOWCASE.NGX_FORM_ERRORS.FIELDS.INFO.HAS_ERRORS" + | translate: { hasErrors: usernameField.hasErrors } + }} +
+
+ {{ + "SHOWCASE.NGX_FORM_ERRORS.FIELDS.INFO.HAS_SPECIFIC_ERROR" + | translate: { error: "required", hasError: usernameField.hasError("required") } + }} +
+
+ {{ + "SHOWCASE.NGX_FORM_ERRORS.FIELDS.INFO.IS_TOUCHED" + | translate: { isTouched: usernameField.hasState("touched") } + }} +
+
+ {{ "SHOWCASE.NGX_FORM_ERRORS.FIELDS.INFO.ERROR" | translate: { error: "required" } }} +
{{ usernameField.getError("required") | json }}
+
+
+ {{ "SHOWCASE.NGX_FORM_ERRORS.FIELDS.INFO.ERRORS" | translate }} +
{{ usernameField.errors | json }}
+
+
+
+ + + SHOWCASE.NGX_FORM_ERRORS.FIELDS.PASSWORD + +
+ {{ + "SHOWCASE.NGX_FORM_ERRORS.FIELDS.INFO.HAS_ERRORS" + | translate: { hasErrors: passwordField.hasErrors } + }} +
+
+ {{ + "SHOWCASE.NGX_FORM_ERRORS.FIELDS.INFO.HAS_SPECIFIC_ERROR" + | translate: { error: "pattern", hasError: passwordField.hasError("pattern") } + }} +
+
+ {{ + "SHOWCASE.NGX_FORM_ERRORS.FIELDS.INFO.IS_TOUCHED" + | translate: { isTouched: passwordField.hasState("touched") } + }} +
+
+ {{ "SHOWCASE.NGX_FORM_ERRORS.FIELDS.INFO.ERROR" | translate: { error: "pattern" } }} +
{{ passwordField.getError("pattern") | json }}
+
+
+ {{ "SHOWCASE.NGX_FORM_ERRORS.FIELDS.INFO.ERRORS" | translate }} +
{{ passwordField.errors | json }}
+
+
+
+ + + SHOWCASE.NGX_FORM_ERRORS.FIELDS.CONFIRM_PASSWORD + +
+ {{ + "SHOWCASE.NGX_FORM_ERRORS.FIELDS.INFO.HAS_ERRORS" + | translate: { hasErrors: confirmPasswordField.hasErrors } + }} +
+
+ {{ + "SHOWCASE.NGX_FORM_ERRORS.FIELDS.INFO.HAS_SPECIFIC_ERROR" + | translate: { error: "required", hasError: confirmPasswordField.hasError("required") } + }} +
+
+ {{ + "SHOWCASE.NGX_FORM_ERRORS.FIELDS.INFO.IS_TOUCHED" + | translate: { isTouched: confirmPasswordField.hasState("touched") } + }} +
+
+ {{ "SHOWCASE.NGX_FORM_ERRORS.FIELDS.INFO.ERROR" | translate: { error: "required" } }} +
{{ confirmPasswordField.getError("required") | json }}
+
+
+ {{ "SHOWCASE.NGX_FORM_ERRORS.FIELDS.INFO.ERRORS" | translate }} +
{{ confirmPasswordField.errors | json }}
+
+
+
+
+ + + + No validation errors + + +
+ + + + + + + + + + + +
+
+
+
+
+
+ +
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 @@ +
+
+ + + + + + + + + +
+ + + + + + + + + + + + + +
+
+ + + + + + +
+ +
+ + Username + +
Has errors: {{ usernameField.hasErrors }}
+
Has 'required' error: {{ usernameField.hasError("required") }}
+
Is touched? {{ usernameField.hasState("touched") }}
+
+ 'required' error: +
{{ usernameField.getError("required") | json }}
+
+
+ Errors: +
{{ usernameField.errors | json }}
+
+
+
+ + + Password + +
Has errors: {{ passwordField.hasErrors }}
+
Has 'pattern' error: {{ passwordField.hasError("pattern") }}
+
Is touched? {{ passwordField.hasState("touched") }}
+
+ 'pattern' error: +
{{ passwordField.getError("pattern") | json }}
+
+
+ Errors: +
{{ passwordField.errors | json }}
+
+
+
+ + + Confirm password + +
Has errors: {{ confirmPasswordField.hasErrors }}
+
Has 'required' error: {{ confirmPasswordField.hasError("required") }}
+
Is touched? {{ confirmPasswordField.hasState("touched") }}
+
+ 'required' error: +
{{ confirmPasswordField.getError("required") | json }}
+
+
+ Errors: +
{{ confirmPasswordField.errors | json }}
+
+
+
+
+ + + + No validation errors + + +
+ + + + + + + + + + + +
+
+
+
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";