diff --git a/packages/stark-ui/src/modules/keyboard-directives/directives.ts b/packages/stark-ui/src/modules/keyboard-directives/directives.ts index 3cbc7b907c..05c3f63118 100644 --- a/packages/stark-ui/src/modules/keyboard-directives/directives.ts +++ b/packages/stark-ui/src/modules/keyboard-directives/directives.ts @@ -1,2 +1,3 @@ export * from "./directives/on-enter-key.directive"; export * from "./directives/restrict-input.directive"; +export * from "./directives/transform-input.directive"; diff --git a/packages/stark-ui/src/modules/keyboard-directives/directives/transform-input.directive.spec.ts b/packages/stark-ui/src/modules/keyboard-directives/directives/transform-input.directive.spec.ts new file mode 100644 index 0000000000..29e5d78d7a --- /dev/null +++ b/packages/stark-ui/src/modules/keyboard-directives/directives/transform-input.directive.spec.ts @@ -0,0 +1,108 @@ +import { Component, OnInit } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { FormControl, FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { By } from "@angular/platform-browser"; +import { MockStarkLoggingService } from "@nationalbankbelgium/stark-core/testing"; +import { STARK_LOGGING_SERVICE } from "@nationalbankbelgium/stark-core"; +import { StarkTransformInputDirective } from "./transform-input.directive"; + +/** + * Mocks an InputEvent on the element with the given value. + * @param value - the value to set the element to. + * @param element - the HTMLInputElement to mock the event on + */ +const mockInputEvent: (value: string, element: HTMLInputElement) => void = (value: string, element: HTMLInputElement): void => { + const event: Event = document.createEvent("Event"); + event.initEvent("input", true, true); + element.value = value; + element.dispatchEvent(event); +}; + +describe("TransformsInputDirective with ngModel", () => { + /** + * mocked wrapper for the directive + */ + @Component({ + selector: "test-component", + template: "" + }) + class TestComponent { + public value: string = ""; + } + + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [StarkTransformInputDirective, TestComponent], + imports: [FormsModule], + providers: [ + { provide: STARK_LOGGING_SERVICE, useValue: new MockStarkLoggingService() } + ] + }); + + fixture = TestBed.createComponent(TestComponent); + // trigger initial data binding + fixture.detectChanges(); + }); + + it("should set the value to uppercase", () => { + expect(fixture.componentInstance.value).toBe("", "field 'value' should start as empty string "); + + const inputElement: HTMLInputElement = (fixture.debugElement.query(By.css("input")).nativeElement); + mockInputEvent("a", inputElement); + fixture.detectChanges(); + + expect(fixture.componentInstance.value).toBe("A", "field 'value' should have been updated to upper case 'A'"); + }); + +}); + +describe("TransformsInputDirective with formControl", () => { + /** + * mocked wrapper for the directive + */ + @Component({ + selector: "test-component", + template: "" + }) + class TestComponent implements OnInit { + public formControl: FormControl = new FormControl(""); + + public ngOnInit(): void { + this.formControl.valueChanges.subscribe(this.onValueChanges); + } + + public onValueChanges(): void {/*noop*/} + } + + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [StarkTransformInputDirective, TestComponent], + imports: [ReactiveFormsModule], + providers: [ + { provide: STARK_LOGGING_SERVICE, useValue: new MockStarkLoggingService() } + ] + }); + + fixture = TestBed.createComponent(TestComponent); + spyOn(fixture.componentInstance, "onValueChanges").calls.reset(); + + // trigger initial data binding + fixture.detectChanges(); + }); + + it("should set the value to uppercase", () => { + expect(fixture.componentInstance.formControl.value).toBe("", "FormControl value should start as empty string "); + + const inputElement: HTMLInputElement = (fixture.debugElement.query(By.css("input")).nativeElement); + mockInputEvent("A", inputElement); + fixture.detectChanges(); + + expect(fixture.componentInstance.onValueChanges).toHaveBeenCalledTimes(1); + expect(fixture.componentInstance.formControl.value).toBe("a", "FormControl value should be lower case 'a'"); + }); + +}); diff --git a/packages/stark-ui/src/modules/keyboard-directives/directives/transform-input.directive.ts b/packages/stark-ui/src/modules/keyboard-directives/directives/transform-input.directive.ts new file mode 100644 index 0000000000..99843eadf5 --- /dev/null +++ b/packages/stark-ui/src/modules/keyboard-directives/directives/transform-input.directive.ts @@ -0,0 +1,112 @@ +import { Directive, ElementRef, forwardRef, Input, Renderer2 } from "@angular/core"; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; + +export type TransformationType = "upper" | "lower" | ((value: any) => any); + +export const STARK_TRANSFORM_INPUT_PROVIDER: any = { + provide: NG_VALUE_ACCESSOR, + // tslint:disable-next-line:no-forward-ref + useExisting: forwardRef(() => StarkTransformInputDirective), + multi: true +}; + +/** + * Directive to transform the value of an input / textarea before it is passed on to the ngControl (ngModel, formControl). + * It is possible to pass 'upper', 'lower' or a custom function to transform the value. + * This is mostly based on {@link: @angular/forms/DefaultValueAccessor} + */ +@Directive({ + // tslint:disable-next-line:directive-selector + selector: "[starkTransformInput]", + providers: [STARK_TRANSFORM_INPUT_PROVIDER], + host: { + "(input)": "onInput($event)", + "(blur)": "_onTouched()" + } +}) +export class StarkTransformInputDirective implements ControlValueAccessor { + public _transformation: (value: any) => any; + + // tslint:disable-next-line:no-input-rename + @Input("starkTransformInput") + public set transformation(transformation: TransformationType) { + switch (transformation) { + case "upper": + this._transformation = (v: string) => v.toUpperCase(); + break; + case "lower": + this._transformation = (v: string) => v.toLocaleLowerCase(); + break; + default: + this._transformation = transformation; + break; + } + } + + /** + * @internal + * The registered callback function called when an input event occurs on the input element. + */ + public _onChange: (_: any) => void = (_: any) => {/*noop*/}; + + /** + * @internal + * The registered callback function called when a blur event occurs on the input element. + */ + public _onTouched: () => void = () => {/*noop*/}; + + /** + * Class constructor + * @param _renderer - Angular renderer + * @param _elementRef - Reference to the element + */ + public constructor(private _renderer: Renderer2, + private _elementRef: ElementRef) { + } + + /** + * Sets the "value" property on the input element. + * + * @param value The checked value + */ + public writeValue(value: any): void { + const normalizedValue: any = value === null ? "" : value; + this._renderer.setProperty(this._elementRef.nativeElement, "value", normalizedValue); + } + + /** + * Registers a function called when the control value changes. + * + * @param fn The callback function + */ + public registerOnChange(fn: (_: any) => void): void { this._onChange = fn; } + + /** + * Registers a function called when the control is touched. + * + * @param fn The callback function + */ + public registerOnTouched(fn: () => void): void { this._onTouched = fn; } + + /** + * Sets the "disabled" property on the input element. + * + * @param isDisabled The disabled value + */ + public setDisabledState(isDisabled: boolean): void { + this._renderer.setProperty(this._elementRef.nativeElement, "disabled", isDisabled); + } + + /** @internal */ + public onInput(event: Event): void { + const value: any = (event.target).value; + const transformed: any = this._transformation(value); + if (transformed !== value) { + this._elementRef.nativeElement.value = transformed; + this._onChange(transformed); + } else { + this._onChange(value); + } + } + +} diff --git a/packages/stark-ui/src/modules/keyboard-directives/keyboard-directives.module.ts b/packages/stark-ui/src/modules/keyboard-directives/keyboard-directives.module.ts index 007412c877..b7c7511a92 100644 --- a/packages/stark-ui/src/modules/keyboard-directives/keyboard-directives.module.ts +++ b/packages/stark-ui/src/modules/keyboard-directives/keyboard-directives.module.ts @@ -1,8 +1,8 @@ import { NgModule } from "@angular/core"; -import { StarkOnEnterKeyDirective, StarkRestrictInputDirective } from "./directives"; +import { StarkTransformInputDirective, StarkOnEnterKeyDirective, StarkRestrictInputDirective } from "./directives"; @NgModule({ - declarations: [StarkOnEnterKeyDirective, StarkRestrictInputDirective], - exports: [StarkOnEnterKeyDirective, StarkRestrictInputDirective] + declarations: [StarkOnEnterKeyDirective, StarkRestrictInputDirective, StarkTransformInputDirective], + exports: [StarkOnEnterKeyDirective, StarkRestrictInputDirective, StarkTransformInputDirective] }) export class StarkKeyboardDirectivesModule {} diff --git a/showcase/src/app/demo-ui/demo-ui.module.ts b/showcase/src/app/demo-ui/demo-ui.module.ts index 7e3037f73a..ab55c56330 100644 --- a/showcase/src/app/demo-ui/demo-ui.module.ts +++ b/showcase/src/app/demo-ui/demo-ui.module.ts @@ -1,6 +1,6 @@ import { NgModule } from "@angular/core"; import { CommonModule } from "@angular/common"; -import { FormsModule } from "@angular/forms"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { MAT_DATE_FORMATS } from "@angular/material/core"; import { MatCheckboxModule } from "@angular/material/checkbox"; import { MatButtonModule } from "@angular/material/button"; @@ -79,6 +79,7 @@ import { }), CommonModule, FormsModule, + ReactiveFormsModule, MatButtonModule, MatButtonToggleModule, MatCardModule, diff --git a/showcase/src/app/demo-ui/pages/keyboard-directives/demo-keyboard-directives-page.component.html b/showcase/src/app/demo-ui/pages/keyboard-directives/demo-keyboard-directives-page.component.html index f30a8ac282..2e5fcb9502 100644 --- a/showcase/src/app/demo-ui/pages/keyboard-directives/demo-keyboard-directives-page.component.html +++ b/showcase/src/app/demo-ui/pages/keyboard-directives/demo-keyboard-directives-page.component.html @@ -6,7 +6,7 @@

SHOWCASE.DEMO.SHARED.EXAMPLE_VIEWER_LIST

filesPath="keyboard-directives/on-enter-key-directive" exampleTitle="SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.ON_ENTER_KEY.TITLE" > -
+

SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.ON_ENTER_KEY.DESCRIPTION

SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.ON_ENTER_KEY.INPUT_WITH_CONTEXT @@ -14,7 +14,7 @@

SHOWCASE.DEMO.SHARED.EXAMPLE_VIEWER_LIST

name="test" [(ngModel)]="inputValue1" matInput - placeholder="{{ 'SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.ON_ENTER_KEY.TYPE_AND_PRESS_ENTER' | translate }}" + [placeholder]="'SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.ON_ENTER_KEY.TYPE_AND_PRESS_ENTER' | translate" [starkOnEnterKey]="onEnterKeyCallback.bind(this)" [starkOnEnterKeyParams]="['input1', inputValue1, 123, { prop: 'someValue' }]" /> @@ -25,18 +25,18 @@

SHOWCASE.DEMO.SHARED.EXAMPLE_VIEWER_LIST

name="test" [(ngModel)]="inputValue2" matInput - placeholder="{{ 'SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.ON_ENTER_KEY.TYPE_PRESS_ENTER_AND_CHECK_CONSOLE' | translate }}" + [placeholder]="'SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.ON_ENTER_KEY.TYPE_PRESS_ENTER_AND_CHECK_CONSOLE' | translate" [starkOnEnterKey]="onEnterKeyCallback" [starkOnEnterKeyParams]="['input2', inputValue2, 123, { prop: 'someValue' }]" />
- SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.ON_ENTER_KEY.INPUT_WITHOUT_CONTEXT_ALTERNATIVE + SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.ON_ENTER_KEY.INPUT_WITHOUT_CONTEXT_ALTERNATIVE @@ -51,32 +51,32 @@

SHOWCASE.DEMO.SHARED.EXAMPLE_VIEWER_LIST

filesPath="keyboard-directives/restrict-input-directive" exampleTitle="SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.RESTRICT_INPUT.TITLE" > - +

SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.RESTRICT_INPUT.DESCRIPTION

SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.RESTRICT_INPUT.INPUT_ONLY_NUMBERS - SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.RESTRICT_INPUT.INPUT_ALPHANUMERICAL_CHARACTERS + SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.RESTRICT_INPUT.INPUT_ALPHANUMERICAL_CHARACTERS - SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.RESTRICT_INPUT.INPUT_NO_SPECIAL_CHARACTERS + SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.RESTRICT_INPUT.INPUT_NO_SPECIAL_CHARACTERS @@ -85,11 +85,38 @@

SHOWCASE.DEMO.SHARED.EXAMPLE_VIEWER_LIST

+ + +
+ + SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.TRANSFORM_INPUT.ONLY_UPPER_CASE_LABEL + + + + SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.TRANSFORM_INPUT.ONLY_LOWER_CASE_LABEL + + +
+
diff --git a/showcase/src/app/demo-ui/pages/keyboard-directives/demo-keyboard-directives-page.component.scss b/showcase/src/app/demo-ui/pages/keyboard-directives/demo-keyboard-directives-page.component.scss index 9bed527bce..4035ed81d1 100644 --- a/showcase/src/app/demo-ui/pages/keyboard-directives/demo-keyboard-directives-page.component.scss +++ b/showcase/src/app/demo-ui/pages/keyboard-directives/demo-keyboard-directives-page.component.scss @@ -1,13 +1,3 @@ -.on-enter-key-directive-form { - display: flex; - flex-direction: column; -} - -.restrict-input-directive-form { - display: flex; - flex-direction: column; -} - pre code { display: block; height: 200px; diff --git a/showcase/src/app/demo-ui/pages/keyboard-directives/demo-keyboard-directives-page.component.ts b/showcase/src/app/demo-ui/pages/keyboard-directives/demo-keyboard-directives-page.component.ts index 5752803add..eaa19a91a2 100644 --- a/showcase/src/app/demo-ui/pages/keyboard-directives/demo-keyboard-directives-page.component.ts +++ b/showcase/src/app/demo-ui/pages/keyboard-directives/demo-keyboard-directives-page.component.ts @@ -1,6 +1,7 @@ import { Component, Inject, OnInit } from "@angular/core"; import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core"; import { ReferenceLink } from "../../../shared/components"; +import { FormControl } from "@angular/forms"; @Component({ selector: "demo-keyboard-directives", @@ -12,12 +13,14 @@ export class DemoKeyboardDirectivesPageComponent implements OnInit { public inputValue1: string; public inputValue2: string; public inputValue3: string; - + public transformFormControl: FormControl = new FormControl("", [], []); + public transformValue: string = ""; public logging: string; public referenceList: ReferenceLink[]; - public constructor(@Inject(STARK_LOGGING_SERVICE) private logger: StarkLoggingService) {} + public constructor(@Inject(STARK_LOGGING_SERVICE) private logger: StarkLoggingService) { + } /** * Component lifecycle hook @@ -37,8 +40,14 @@ export class DemoKeyboardDirectivesPageComponent implements OnInit { { label: "Stark Restrict Input directive", url: "https://stark.nbb.be/api-docs/stark-ui/latest/directives/StarkRestrictInputDirective.html" + }, + { + label: "Stark Transform Input directive", + url: "https://stark.nbb.be/api-docs/stark-ui/latest/directives/StarkTransformInputDirective.html" } ]; + + this.transformFormControl.valueChanges.subscribe((v: string) => this.logger.debug("formControl value changed: ", v)); } public onEnterKeyCallback(...paramValues: any[]): void { diff --git a/showcase/src/assets/examples/keyboard-directives/transform-input-directive.html b/showcase/src/assets/examples/keyboard-directives/transform-input-directive.html new file mode 100644 index 0000000000..a4ec47de4b --- /dev/null +++ b/showcase/src/assets/examples/keyboard-directives/transform-input-directive.html @@ -0,0 +1,10 @@ +
+ + Only upper case + + + + Only lower case + + +
diff --git a/showcase/src/assets/examples/keyboard-directives/transform-input-directive.ts b/showcase/src/assets/examples/keyboard-directives/transform-input-directive.ts new file mode 100644 index 0000000000..b82754dae1 --- /dev/null +++ b/showcase/src/assets/examples/keyboard-directives/transform-input-directive.ts @@ -0,0 +1,16 @@ +import { Component, Inject } from "@angular/core"; +import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core"; +import { FormControl } from "@angular/forms"; + +@Component({ + selector: "demo-transform-input", + templateUrl: "./demo-transform-input.component.html" +}) +export class DemoTransformInputComponent { + public formControl: FormControl = new FormControl(""); + public value: string = ""; + + public constructor(@Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService) { + this.formControl.valueChanges.subscribe((v: string) => this.logger.debug("formControl value changed: ", v)); + } +} diff --git a/showcase/src/assets/translations/en.json b/showcase/src/assets/translations/en.json index 5fa2ce8433..228000a330 100644 --- a/showcase/src/assets/translations/en.json +++ b/showcase/src/assets/translations/en.json @@ -98,10 +98,15 @@ "INPUT_ONLY_NUMBERS": "Accept only numbers", "INPUT_ALPHANUMERICAL_CHARACTERS": "Accept only alphanumeric characters", "INPUT_NO_SPECIAL_CHARACTERS": "Accept all except special characters", - "INPUT_UPPERCASE_CHARACTERS": "Accept only uppercase characters", - "TYPE_A_VALUE": "Type a value" + "INPUT_UPPERCASE_CHARACTERS": "Accept only uppercase characters" }, - "TITLE": "Keyboard directive" + "TRANSFORM_INPUT": { + "TITLE": "Transform Input Directive", + "ONLY_UPPER_CASE_LABEL": "Upper case only", + "ONLY_LOWER_CASE_LABEL": "Lower case only" + }, + "TITLE": "Keyboard directive", + "TYPE_A_VALUE": "Type a value" }, "LANGUAGE_SELECTOR": { "DROPDOWN": "Language selector in 'dropdown' mode", diff --git a/showcase/src/assets/translations/fr.json b/showcase/src/assets/translations/fr.json index 72742c2b34..5798b751e3 100644 --- a/showcase/src/assets/translations/fr.json +++ b/showcase/src/assets/translations/fr.json @@ -98,10 +98,15 @@ "INPUT_ONLY_NUMBERS": "Accepter seulement les nombres", "INPUT_ALPHANUMERICAL_CHARACTERS": "Accepter uniquement les caractères alphanumériques", "INPUT_NO_SPECIAL_CHARACTERS": "Accepter tous les caractères sauf les caractères spéciaux", - "INPUT_UPPERCASE_CHARACTERS": "Accepter uniquement les majuscules", - "TYPE_A_VALUE": "Taper une valeur" + "INPUT_UPPERCASE_CHARACTERS": "Accepter uniquement les majuscules" }, - "TITLE": "Keyboard directive" + "TRANSFORM_INPUT": { + "TITLE": "Transform Input Directive", + "ONLY_UPPER_CASE_LABEL": "Uniquement les majuscules", + "ONLY_LOWER_CASE_LABEL": "Uniquement les minuscules" + }, + "TITLE": "Keyboard directive", + "TYPE_A_VALUE": "Taper une valeur" }, "LANGUAGE_SELECTOR": { "DROPDOWN": "Sélecteur de langue en mode 'dropdown'", diff --git a/showcase/src/assets/translations/nl.json b/showcase/src/assets/translations/nl.json index 50de144dd6..c5d8065447 100644 --- a/showcase/src/assets/translations/nl.json +++ b/showcase/src/assets/translations/nl.json @@ -98,10 +98,15 @@ "INPUT_ONLY_NUMBERS": "Aanvaard enkel cijfers", "INPUT_ALPHANUMERICAL_CHARACTERS": "Aanvaard enkel alfanumerische karakters", "INPUT_NO_SPECIAL_CHARACTERS": "Aanvaard alle karakters, behalve speciale karakters", - "INPUT_UPPERCASE_CHARACTERS": "Aanvaard enkel hoofdletters", - "TYPE_A_VALUE": "Geef een waarde in" + "INPUT_UPPERCASE_CHARACTERS": "Aanvaard enkel hoofdletters" }, - "TITLE": "Keyboard directive" + "TRANSFORM_INPUT": { + "TITLE": "Transform Input Directive", + "ONLY_UPPER_CASE_LABEL": "Alleen hoofdletters", + "ONLY_LOWER_CASE_LABEL": "Alleen kleine letters" + }, + "TITLE": "Keyboard directive", + "TYPE_A_VALUE": "Geef een waarde in" }, "LANGUAGE_SELECTOR": { "DROPDOWN": "Taal selector in 'dropdown' mode",