Skip to content

Commit

Permalink
feat(stark-ui): add directive for transforming input value before pas…
Browse files Browse the repository at this point in the history
…sing it to a ngControl

  - added directive `[starkTransformInput]`
  - added partial demo
  - small refactor demo page
  - added tests for directive

ISSUES CLOSED: NationalBankBelgium#1099
  • Loading branch information
carlo-nomes committed Feb 4, 2019
1 parent b74eabb commit 084b687
Show file tree
Hide file tree
Showing 13 changed files with 326 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./directives/on-enter-key.directive";
export * from "./directives/restrict-input.directive";
export * from "./directives/transform-input.directive";
Original file line number Diff line number Diff line change
@@ -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: "<input [(ngModel)]='value' starkTransformInput='upper'/>"
})
class TestComponent {
public value: string = "";
}

let fixture: ComponentFixture<TestComponent>;

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 = (<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: "<input [formControl]='formControl' starkTransformInput='lower'/>"
})
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<TestComponent>;

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 = (<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'");
});

});
Original file line number Diff line number Diff line change
@@ -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 = (<HTMLInputElement>event.target).value;
const transformed: any = this._transformation(value);
if (transformed !== value) {
this._elementRef.nativeElement.value = transformed;
this._onChange(transformed);
} else {
this._onChange(value);
}
}

}
Original file line number Diff line number Diff line change
@@ -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 {}
3 changes: 2 additions & 1 deletion showcase/src/app/demo-ui/demo-ui.module.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -79,6 +79,7 @@ import {
}),
CommonModule,
FormsModule,
ReactiveFormsModule,
MatButtonModule,
MatButtonToggleModule,
MatCardModule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ <h1 translate>SHOWCASE.DEMO.SHARED.EXAMPLE_VIEWER_LIST</h1>
filesPath="keyboard-directives/on-enter-key-directive"
exampleTitle="SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.ON_ENTER_KEY.TITLE"
>
<form class="on-enter-key-directive-form">
<form fxLayout="column">
<p translate>SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.ON_ENTER_KEY.DESCRIPTION</p>
<mat-form-field>
<mat-label translate>SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.ON_ENTER_KEY.INPUT_WITH_CONTEXT</mat-label>
<input
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' }]"
/>
Expand All @@ -25,18 +25,18 @@ <h1 translate>SHOWCASE.DEMO.SHARED.EXAMPLE_VIEWER_LIST</h1>
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' }]"
/>
</mat-form-field>
<mat-form-field>
<mat-label translate>SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.ON_ENTER_KEY.INPUT_WITHOUT_CONTEXT_ALTERNATIVE </mat-label>
<mat-label translate>SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.ON_ENTER_KEY.INPUT_WITHOUT_CONTEXT_ALTERNATIVE</mat-label>
<input
name="test"
[(ngModel)]="inputValue3"
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"
[starkOnEnterKeyParams]="['input3', inputValue3, 123, { prop: 'someValue' }, this]"
/>
Expand All @@ -51,32 +51,32 @@ <h1 translate>SHOWCASE.DEMO.SHARED.EXAMPLE_VIEWER_LIST</h1>
filesPath="keyboard-directives/restrict-input-directive"
exampleTitle="SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.RESTRICT_INPUT.TITLE"
>
<form class="restrict-input-directive-form">
<form fxLayout="column">
<p translate>SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.RESTRICT_INPUT.DESCRIPTION</p>
<mat-form-field>
<mat-label translate>SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.RESTRICT_INPUT.INPUT_ONLY_NUMBERS</mat-label>
<input
name="test"
matInput
placeholder="{{ 'SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.RESTRICT_INPUT.TYPE_A_VALUE' | translate }}"
placeholder="{{ 'SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.TYPE_A_VALUE' | translate }}"
starkRestrictInput="\d"
/>
</mat-form-field>
<mat-form-field>
<mat-label translate>SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.RESTRICT_INPUT.INPUT_ALPHANUMERICAL_CHARACTERS </mat-label>
<mat-label translate>SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.RESTRICT_INPUT.INPUT_ALPHANUMERICAL_CHARACTERS</mat-label>
<input
name="test"
matInput
placeholder="{{ 'SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.RESTRICT_INPUT.TYPE_A_VALUE' | translate }}"
[placeholder]="'SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.TYPE_A_VALUE' | translate"
starkRestrictInput="^[A-Za-z0-9]*$"
/>
</mat-form-field>
<mat-form-field>
<mat-label translate>SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.RESTRICT_INPUT.INPUT_NO_SPECIAL_CHARACTERS </mat-label>
<mat-label translate>SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.RESTRICT_INPUT.INPUT_NO_SPECIAL_CHARACTERS</mat-label>
<input
name="test"
matInput
placeholder="{{ 'SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.RESTRICT_INPUT.TYPE_A_VALUE' | translate }}"
[placeholder]="'SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.TYPE_A_VALUE' | translate"
starkRestrictInput="\w"
/>
</mat-form-field>
Expand All @@ -85,11 +85,38 @@ <h1 translate>SHOWCASE.DEMO.SHARED.EXAMPLE_VIEWER_LIST</h1>
<input
name="test"
matInput
placeholder="{{ 'SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.RESTRICT_INPUT.TYPE_A_VALUE' | translate }}"
[placeholder]="'SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.TYPE_A_VALUE' | translate"
starkRestrictInput="^[A-Z]*$"
/>
</mat-form-field>
</form>
</example-viewer>

<example-viewer
[extensions]="['HTML', 'TS']"
filesPath="keyboard-directives/transform-input-directive"
exampleTitle="SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.TRANSFORM_INPUT.TITLE"
>
<div fxLayout fxLayoutGap="10px">
<mat-form-field fxFlex>
<mat-label translate>SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.TRANSFORM_INPUT.ONLY_UPPER_CASE_LABEL</mat-label>
<input
matInput
[placeholder]="'SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.TYPE_A_VALUE' | translate"
[formControl]="transformFormControl"
starkTransformInput="upper"
/>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label translate>SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.TRANSFORM_INPUT.ONLY_LOWER_CASE_LABEL</mat-label>
<input
matInput
[placeholder]="'SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.TYPE_A_VALUE' | translate"
[(ngModel)]="transformValue"
starkTransformInput="lower"
/>
</mat-form-field>
</div>
</example-viewer>
</section>
<stark-reference-block [links]="referenceList"></stark-reference-block>
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Loading

0 comments on commit 084b687

Please sign in to comment.