Skip to content

Commit

Permalink
Merge pull request #545 from christophercr/feature/onEnterKey-directive
Browse files Browse the repository at this point in the history
feat(stark-ui): implement OnEnterKey directive and module
  • Loading branch information
SuperITMan authored Jul 27, 2018
2 parents e328ff1 + 0696959 commit 0edae9b
Show file tree
Hide file tree
Showing 19 changed files with 427 additions and 5 deletions.
1 change: 1 addition & 0 deletions packages/stark-ui/src/modules.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from "./modules/action-bar";
export * from "./modules/app-logo";
export * from "./modules/keyboard-directives";
export * from "./modules/pretty-print";
export * from "./modules/slider";
export * from "./modules/table";
Expand Down
2 changes: 2 additions & 0 deletions packages/stark-ui/src/modules/keyboard-directives.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./keyboard-directives/keyboard-directives.module";
export * from "./keyboard-directives/directives";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./directives/on-enter-key.directive";
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*tslint:disable:completed-docs*/
import { ComponentFixture, fakeAsync, TestBed } from "@angular/core/testing";
import { Component, DebugElement } from "@angular/core";
import { By } from "@angular/platform-browser";
import { STARK_LOGGING_SERVICE } from "@nationalbankbelgium/stark-core";
import { MockStarkLoggingService } from "@nationalbankbelgium/stark-core/testing";
import { StarkOnEnterKeyDirective } from "./on-enter-key.directive";
import Spy = jasmine.Spy;
import createSpy = jasmine.createSpy;

describe("OnEnterKeyDirective", () => {
@Component({
selector: "test-component",
template: getTemplate("starkOnEnterKey")
})
class TestComponent {
public onEnterKeyHandler: Spy = createSpy("onEnterKeyHandlerSpy");

public onEnterKeyParam: object = { id: "123" };
}

let fixture: ComponentFixture<TestComponent>;

function getTemplate(onEnterKeyDirective: string): string {
return "<input " + "type='text' " + onEnterKeyDirective + ">";
}

function initializeComponentFixture(): void {
fixture = TestBed.createComponent(TestComponent);
// trigger initial data binding
fixture.detectChanges();
}

function triggerKeyPressEvent(inputElement: DebugElement, keyCode: number): void {
const keypressEvent: Event = document.createEvent("Event");
keypressEvent.initEvent("keypress", true, true);
keypressEvent["which"] = keyCode;
inputElement.triggerEventHandler("keypress", keypressEvent);
}

beforeEach(() => {
TestBed.configureTestingModule({
declarations: [StarkOnEnterKeyDirective, TestComponent],
providers: [{ provide: STARK_LOGGING_SERVICE, useValue: new MockStarkLoggingService() }]
});
});

describe("when onEnterKeyHandler is not defined", () => {
beforeEach(
fakeAsync(() => {
// compile template and css
return TestBed.compileComponents();
})
);

beforeEach(() => {
initializeComponentFixture();
});

it("should not do anything when the enter key is pressed and the enter key handler was not provided", () => {
expect(fixture).toBeDefined();
const hostComponent: TestComponent = fixture.componentInstance;

const inputElement: DebugElement = fixture.debugElement.query(By.css("input"));
triggerKeyPressEvent(inputElement, 13);

expect(hostComponent.onEnterKeyHandler).not.toHaveBeenCalled();
});
});

describe("when onEnterKeyHandler is given", () => {
// overriding the components's template
beforeEach(
fakeAsync(() => {
const newTemplate: string = getTemplate(
"[starkOnEnterKey]='onEnterKeyHandler' [starkOnEnterKeyParams]='[onEnterKeyParam]'"
);

TestBed.overrideTemplate(TestComponent, newTemplate);

// compile template and css
return TestBed.compileComponents();
})
);

beforeEach(() => {
initializeComponentFixture();
});

it("should call the given function when Enter key is pressed", () => {
const hostComponent: TestComponent = fixture.componentInstance;

const inputElement: DebugElement = fixture.debugElement.query(By.css("input"));
triggerKeyPressEvent(inputElement, 13);

expect(hostComponent.onEnterKeyHandler).toHaveBeenCalledTimes(1);
expect(hostComponent.onEnterKeyHandler).toHaveBeenCalledWith(hostComponent.onEnterKeyParam);
});

it("should not call the given function when a key other than Enter is pressed", () => {
const hostComponent: TestComponent = fixture.componentInstance;

const inputElement: DebugElement = fixture.debugElement.query(By.css("input"));
triggerKeyPressEvent(inputElement, 1);

expect(hostComponent.onEnterKeyHandler).not.toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Directive, HostListener, Inject, Input, OnInit } from "@angular/core";
import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core";

/**
* Name of the directive
*/
const directiveName: string = "[starkOnEnterKey]";

/**
* Directive that allows the execution of a certain callback whenever the user presses the Enter key in a field.
*
* This directive is used essentially for input tags.
*
* `IMPORTANT:` This directive will have its own context so any properties/methods of the host component where this directive is used will not be accessible.
* In case you want to access any variable or method of your component where you include this directive from the callback function,
* you should bind the component's "this" context to the callback function passed to the directive with the "bind()" method.
*
* For example:
* ```html
* <input name="test" [starkOnEnterKey]="yourCallbackFn.bind(this)" [starkOnEnterKeyParams]="['someValue']">
* ```
*
* Then in your component your callback could be defined like this:
* ```typescript
* public yourCallbackFn(paramValue: string): void {
* this.someProperty; // is the value of the component's "someProperty" due to the ".bind(this)" in the callback passed to the directive, otherwise it would be "undefined"!
* paramValue; // is "someValue" since it was a literal param passed via the [starkOnEnterKeyParams] input
* }
* ```
*/
@Directive({
selector: directiveName
})
export class StarkOnEnterKeyDirective implements OnInit {
/**
* Callback function to be triggered on every Enter key press in the field
*/
/* tslint:disable:no-input-rename */
@Input("starkOnEnterKey") public onEnterKeyHandler: Function;

/**
* Parameters to be passed to the specified callback function
*/
@Input("starkOnEnterKeyParams") public onEnterKeyParams: any[];

/**
* Event handler to be invoked on a "keypress" event in the field
*/
@HostListener("keypress", ["$event"])
public eventHandler(event: KeyboardEvent): void {
if (this.onEnterKeyHandler && event.which === 13) {
this.onEnterKeyHandler(...this.onEnterKeyParams);
event.preventDefault();
}
}

/**
* Class constructor
* @param logger - The logger of the application
*/
public constructor(@Inject(STARK_LOGGING_SERVICE) private logger: StarkLoggingService) {}

/**
* Directive lifecycle hook
*/
public ngOnInit(): void {
this.logger.debug(directiveName + ": directive initialized");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { NgModule } from "@angular/core";
import { StarkOnEnterKeyDirective } from "./directives";

@NgModule({
declarations: [StarkOnEnterKeyDirective],
exports: [StarkOnEnterKeyDirective]
})
export class StarkKeyboardDirectivesModule {}
3 changes: 3 additions & 0 deletions showcase/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
<a mat-list-item uiSref="demo-example-viewer" uiSrefActive="active">
<span matLine>Example Viewer</span>
</a>
<a mat-list-item uiSref="demo-keyboard-directives" uiSrefActive="active">
<span matLine>Keyboard Directives</span>
</a>
<a mat-list-item uiSref="demo-table" uiSrefActive="active">
<span matLine>Table</span>
</a>
Expand Down
3 changes: 2 additions & 1 deletion showcase/src/app/app.routes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ActionBarComponent, ButtonComponent, ExampleViewerComponent, TableComponent } from "./demo";
import { ActionBarComponent, ButtonComponent, ExampleViewerComponent, KeyboardDirectivesComponent, TableComponent } from "./demo";
import { HomeComponent } from "./home";
import { NoContentComponent } from "./no-content";
import { Ng2StateDeclaration } from "@uirouter/angular";
Expand All @@ -8,6 +8,7 @@ export const APP_STATES: Ng2StateDeclaration[] = [
{ name: "demo-action-bar", url: "/demo/action-bar", component: ActionBarComponent },
{ name: "demo-button", url: "/demo/button", component: ButtonComponent },
{ name: "demo-example-viewer", url: "/demo/example-viewer", component: ExampleViewerComponent },
{ name: "demo-keyboard-directives", url: "/demo/keyboard-directives", component: KeyboardDirectivesComponent },
{ name: "demo-table", url: "/demo/table", component: TableComponent },
{ name: "otherwise", url: "/otherwise", component: NoContentComponent }
];
29 changes: 25 additions & 4 deletions showcase/src/app/demo/demo.module.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,52 @@
import { MatButtonModule, MatCardModule, MatIconModule, MatTabsModule, MatTooltipModule, MatSnackBarModule } from "@angular/material";
import {
MatButtonModule,
MatCardModule,
MatIconModule,
MatTabsModule,
MatTooltipModule,
MatSnackBarModule,
MatFormFieldModule,
MatInputModule
} from "@angular/material";
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { TranslateModule } from "@ngx-translate/core";
import { ActionBarComponent } from "./action-bar/action-bar.component";
import { ButtonComponent } from "./button/button.component";
import { ExampleViewerComponent } from "./example-viewer/example-viewer.component";
import { KeyboardDirectivesComponent } from "./keyboard-directives/keyboard-directives.component";
import { TableComponent } from "./table/table.component";
import { SharedModule } from "../shared/shared.module";
import { StarkActionBarModule, StarkSliderModule, StarkTableModule, StarkSvgViewBoxModule } from "@nationalbankbelgium/stark-ui";
import {
StarkActionBarModule,
StarkSliderModule,
StarkTableModule,
StarkSvgViewBoxModule,
StarkKeyboardDirectivesModule
} from "@nationalbankbelgium/stark-ui";

@NgModule({
imports: [
MatButtonModule,
MatCardModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatTooltipModule,
MatSnackBarModule,
MatTabsModule,
CommonModule,
FormsModule,
TranslateModule,
SharedModule,
StarkActionBarModule,
StarkSliderModule,
StarkSvgViewBoxModule,
StarkKeyboardDirectivesModule,
StarkTableModule
],
declarations: [ActionBarComponent, ButtonComponent, ExampleViewerComponent, TableComponent],
exports: [ActionBarComponent, ButtonComponent, ExampleViewerComponent, TableComponent]
declarations: [ActionBarComponent, ButtonComponent, ExampleViewerComponent, KeyboardDirectivesComponent, TableComponent],
exports: [ActionBarComponent, ButtonComponent, ExampleViewerComponent, KeyboardDirectivesComponent, TableComponent]
})
export class DemoModule {}
1 change: 1 addition & 0 deletions showcase/src/app/demo/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from "./action-bar";
export * from "./button";
export * from "./example-viewer";
export * from "./keyboard-directives";
export * from "./table";
export * from "./demo.module";
1 change: 1 addition & 0 deletions showcase/src/app/demo/keyboard-directives/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./keyboard-directives.component";
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<example-viewer [extensions]="['HTML', 'TS']" filesPath="keyboard-directives/on-enter-key-directive"
title="SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.ON_ENTER_KEY_DIRECTIVE">
<form class="on-enter-key-directive-form">
<p translate>SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.DESCRIPTION</p>
<mat-form-field>
<mat-label translate>SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.INPUT_WITH_CONTEXT</mat-label>
<input name="test" [(ngModel)]="inputValue1" matInput placeholder="{{ 'SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.TYPE_AND_PRESS_ENTER' | translate }}"
[starkOnEnterKey]="onEnterKeyCallback.bind(this)"
[starkOnEnterKeyParams]="['input1', inputValue1, 123, { prop: 'someValue' }]">
</mat-form-field>
<mat-form-field>
<mat-label translate>SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.INPUT_WITHOUT_CONTEXT</mat-label>
<input name="test" [(ngModel)]="inputValue2" matInput
placeholder="{{ 'SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.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.INPUT_WITHOUT_CONTEXT_ALTERNATIVE</mat-label>
<input name="test" [(ngModel)]="inputValue3" matInput placeholder="{{ 'SHOWCASE.DEMO.KEYBOARD_DIRECTIVES.TYPE_AND_PRESS_ENTER' | translate }}"
[starkOnEnterKey]="onEnterKeyCallback"
[starkOnEnterKeyParams]="['input3', inputValue3, 123, { prop: 'someValue' }, this]">
</mat-form-field>
</form>
<label>Logging:</label>
<pre><code>{{ logging }}</code></pre>
</example-viewer>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.on-enter-key-directive-form {
display: flex;
flex-direction: column;
}

pre code {
display: block;
height: 200px;
background-color: #cfcfcf;
overflow: auto;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Component, Inject, OnInit } from "@angular/core";
import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core";

@Component({
selector: "showcase-demo-on-enter-key",
styleUrls: ["./keyboard-directives.component.scss"],
templateUrl: "./keyboard-directives.component.html"
})
export class KeyboardDirectivesComponent implements OnInit {
public latestInputValue: string;
public inputValue1: string;
public inputValue2: string;
public inputValue3: string;

public logging: string;

public constructor(@Inject(STARK_LOGGING_SERVICE) private logger: StarkLoggingService) {}

public ngOnInit(): void {
this.latestInputValue = "";
this.inputValue1 = "";
this.inputValue2 = "";
this.inputValue3 = "";
this.logging = "";
}

public onEnterKeyCallback(...paramValues: any[]): void {
switch (paramValues[0]) {
case "input1":
this.updateLogging(this.inputValue1, paramValues);
break;
case "input2":
// this.updateLogging() is not accessible since this onEnterKeyCallback is called without context
let callbackLogging: string = `Callback triggered from ${paramValues[0]}!`;
callbackLogging += ` - Passed params: ${paramValues}`;
callbackLogging += ` - Component this.latestInputValue property: '${this.latestInputValue}'\n`;
// however this.logger is accessible because the OnEnterKey directive has also the "logger" service injected
this.logger.debug(callbackLogging);
break;
case "input3":
// this.updateLogging() is not accessible since this onEnterKeyCallback is called without context
if (paramValues.length === 5) {
// the context of the host component was passed in the last parameter
// so we can access all properties/methods from the component
const parentComponentContext: KeyboardDirectivesComponent = paramValues[4];
parentComponentContext.updateLogging(parentComponentContext.inputValue3, paramValues);
}
break;
default:
break;
}
}

public updateLogging(inputValue: string, paramValues: any[]): void {
if (typeof this.latestInputValue !== "undefined") {
this.latestInputValue = inputValue;
}

let callbackLogging: string = `Callback triggered from ${paramValues[0]}!`;
callbackLogging += ` - Passed params: ${paramValues}`;
callbackLogging += ` - Component this.latestInputValue property: '${this.latestInputValue}'\n`;

// this won't appear in the view when no context is passed to this callback function
this.logging += callbackLogging;
this.logger.debug(callbackLogging);
}
}
Loading

0 comments on commit 0edae9b

Please sign in to comment.