Skip to content

Commit

Permalink
fix: (platform) step input component defects (#3234)
Browse files Browse the repository at this point in the history
* fix step-input entered value handling

* fix tests
  • Loading branch information
dimamarksman committed Sep 14, 2020
1 parent 326d085 commit f0b2f4c
Show file tree
Hide file tree
Showing 11 changed files with 157 additions and 78 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
</fd-docs-section-title>
<description>This example shows how Step Input behaves with reactive form</description>
<component-example>
<fdp-platform-number-step-input-form-example></fdp-platform-number-step-input-form-example>
<fdp-platform-number-step-input-reactive-example></fdp-platform-number-step-input-reactive-example>
</component-example>
<code-example [exampleFiles]="numberStepInputForm"></code-example>

Expand All @@ -27,6 +27,6 @@
>This example shows how Step Input behaves with a template-driven form with an initial value set.</description
>
<component-example>
<fdp-platform-number-step-input-template-form-example></fdp-platform-number-step-input-template-form-example>
<fdp-platform-number-step-input-template-example></fdp-platform-number-step-input-template-example>
</component-example>
<code-example [exampleFiles]="numberStepInputTemplateForm"></code-example>
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const MAX_VALUE = 20;
const MIN_VALUE = 10;

@Component({
selector: 'fdp-platform-number-step-input-form-example',
selector: 'fdp-platform-number-step-input-reactive-example',
templateUrl: './platform-number-step-input-reactive-example.component.html'
})
export class PlatformNumberStepInputFormExampleComponent implements AfterViewInit {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Component } from '@angular/core';
import { ValidatorFn, Validators } from '@angular/forms';

@Component({
selector: 'fdp-platform-number-step-input-template-form-example',
selector: 'fdp-platform-number-step-input-template-example',
templateUrl: './platform-number-step-input-template-example.component.html'
})
export class PlatformNumberStepInputTemplateFormExampleComponent {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
</ng-template>

<ng-template #renderer>
<div (click)="control && control.onContainerClick($event)" [horizontal]="labelLayout === 'horizontal'" fd-form-item>
<div [horizontal]="labelLayout === 'horizontal'" fd-form-item>
<label [attr.for]="id" [inlineHelp]="!!hint" [required]="editable && required" fd-form-label>
<span *ngIf="!noLabelLayout">{{ label }}</span>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,24 @@ export abstract class StepInputComponent extends BaseInput implements OnInit {
/** @hidden */
private _align$: BehaviorSubject<StepInputAlign> = new BehaviorSubject<StepInputAlign>(null);

/** @hidden */
private _pendingEnteredValue: number | Error | null = null;

/** @hidden */
private get _currentValue(): number | null {
const pendingValue = this._pendingEnteredValue;

if (pendingValue instanceof Error) {
return null;
}

if (pendingValue === null) {
return this._value;
}

return pendingValue;
}

/** @hidden */
constructor(
cd: ChangeDetectorRef,
Expand Down Expand Up @@ -229,14 +247,15 @@ export abstract class StepInputComponent extends BaseInput implements OnInit {
writeValue(value: number): void {
super.writeValue(value);
this._updateViewValue();
this._resetPendingEnteredValue();
}

/** Increase value */
increase(step = this._getStepValueForIncrease()): void {
if (!this.canIncrement) {
return;
}
const value = addAndCutFloatingNumberDistortion(this._value, step);
const value = addAndCutFloatingNumberDistortion(this._currentValue, step);
this.value = this._validateValueByLimits(value);
this._emitChangedValue();
}
Expand All @@ -246,7 +265,7 @@ export abstract class StepInputComponent extends BaseInput implements OnInit {
if (!this.canDecrement) {
return;
}
const value = addAndCutFloatingNumberDistortion(this._value, -step);
const value = addAndCutFloatingNumberDistortion(this._currentValue, -step);
this.value = this._validateValueByLimits(value);
this._emitChangedValue();
}
Expand All @@ -264,15 +283,29 @@ export abstract class StepInputComponent extends BaseInput implements OnInit {
}

/**@hidden
* Commit new entered value from view.
* catch value during entering from view.
*/
commitEnteredValue(value: string): void {
onEnterValue(value: string): void {
const parsedValue = this.parseValueInFocusMode(value);
let newValue = parsedValue;
let pendingValue = parsedValue;

if (pendingValue !== null) {
pendingValue = this._validateValueByLimits(pendingValue);
}

if (newValue !== null) {
newValue = this._validateValueByLimits(newValue);
// could not parse value, value is invalid
if (pendingValue === null) {
this._setPendingEnteredValue(new Error('value is invalid'));
} else {
this._setPendingEnteredValue(pendingValue);
}
}

/**@hidden
* Commit entered value from view.
*/
commitEnteredValue(): void {
const newValue = this._currentValue;

const needToUpdateView = this._value === newValue;

Expand All @@ -292,7 +325,7 @@ export abstract class StepInputComponent extends BaseInput implements OnInit {
*/
onFocus(): void {
super._onFocusChanged(true);
// When focus happend by "Tab" key an input field value gets selected by default.
// When focus happens by "Tab" key an input field value gets selected by default.
// In cases when new_formatted_value !== previous_value the selection gets lost.
// So it's needed to track selection before new value rendering and restore it.
const { selectionStart, selectionEnd } = this._getValueSelection();
Expand Down Expand Up @@ -390,7 +423,7 @@ export abstract class StepInputComponent extends BaseInput implements OnInit {
private _getStepValue(action: StepInputStepFunctionAction): number {
// steFn has precedence
if (typeof this._stepFn === 'function') {
const calculatedStep = this._stepFn(this._value, action);
const calculatedStep = this._stepFn(this._currentValue, action);
return calculatedStep;
}
return this.step;
Expand Down Expand Up @@ -475,4 +508,14 @@ export abstract class StepInputComponent extends BaseInput implements OnInit {
);
}
}

/** @hidden */
private _setPendingEnteredValue(value: number | Error): void {
this._pendingEnteredValue = value;
}

/** @hidden */
private _resetPendingEnteredValue(): void {
this._pendingEnteredValue = null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,18 @@
[class.is-error]="isErrorState"
>
<button
fd-button
fdType="transparent"
class="fd-step-input__button"
fdpStepInputDecrement
tabindex="-1"
type="button"
fd-button
fdType="transparent"
[compact]="isCompact"
[glyph]="showLabels ? null : 'less'"
fdpStepInputDecrement
[attr.aria-disabled]="disabled || !canDecrement"
[attr.title]="decrementLabel"
[attr.aria-label]="decrementLabel"
[attr.aria-controls]="id"
[glyph]="showLabels ? null : 'less'"
>
{{ showLabels ? decrementLabel : '' }}
</button>
Expand All @@ -41,17 +41,17 @@
/>
<button
class="fd-step-input__button"
tabindex="-1"
type="button"
fd-button
fdType="transparent"
[compact]="isCompact"
[glyph]="showLabels ? null : 'add'"
fdpStepInputIncrement
tabindex="-1"
type="button"
[attr.aria-disabled]="disabled || !canIncrement"
[attr.title]="incrementLabel"
[attr.aria-label]="incrementLabel"
[attr.aria-controls]="id"
[glyph]="showLabels ? null : 'add'"
>
{{ showLabels ? incrementLabel : '' }}
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,29 +139,25 @@ describe('NumberStepInputComponent main functionality', () => {

it('Should not allow value be less than minimum', () => {
const nativeElement: HTMLInputElement = getInputDebugElement().nativeElement;
const commitEnteredValueSpy = spyOn(stepInputComponent, 'commitEnteredValue').and.callThrough();
const enteredValue = (component.min - 1).toString();
nativeElement.value = enteredValue;
nativeElement.dispatchEvent(new InputEvent('input'));
nativeElement.dispatchEvent(new InputEvent('change'));

fixture.detectChanges();

expect(commitEnteredValueSpy).toHaveBeenCalledWith(enteredValue);

expect(stepInputComponent.value).toEqual(component.min);
});

it('Should not allow value be more than maximum', () => {
const nativeElement: HTMLInputElement = getInputDebugElement().nativeElement;
const commitEnteredValueSpy = spyOn(stepInputComponent, 'commitEnteredValue').and.callThrough();
const enteredValue = (component.max + 1).toString();
nativeElement.value = enteredValue;
nativeElement.dispatchEvent(new InputEvent('input'));
nativeElement.dispatchEvent(new InputEvent('change'));

fixture.detectChanges();

expect(commitEnteredValueSpy).toHaveBeenCalledWith(enteredValue);

expect(stepInputComponent.value).toEqual(component.max);
});

Expand Down Expand Up @@ -391,20 +387,20 @@ describe('NumberStepInputComponent main functionality', () => {
expect(component.value).toEqual(value);
});

it('Should commit changes on input "change" event', () => {
it('Should catch changes on "input" event and apply them on "change" event', () => {
const value = 10;
const step = 2;

component.value = value;
component.step = step;
fixture.detectChanges();

const inputChangeEvent = new InputEvent('change');
const inputEl: HTMLInputElement = getInputDebugElement().nativeElement;

inputEl.focus();
inputEl.value = '25';
inputEl.dispatchEvent(inputChangeEvent);
inputEl.dispatchEvent(new InputEvent('input'));
inputEl.dispatchEvent(new InputEvent('change'));
fixture.detectChanges();

expect(component.value).toEqual(25);
Expand Down Expand Up @@ -542,7 +538,8 @@ describe('Basic number Step Input withing platforms form', () => {

expect(stepInputEl.classes['is-error']).not.toBeTrue();

stepInputComponent.commitEnteredValue('not a valid number');
stepInputComponent.onEnterValue('not a valid number');
stepInputComponent.commitEnteredValue();
await wait(fixture);

expect(formControl.value).toBe(null);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Directive, HostListener, OnDestroy } from '@angular/core';
import { fromEvent, timer, interval, Subject } from 'rxjs';
import { switchMap, takeUntil } from 'rxjs/operators';

/**
* This is a base step input directive to be used for increase/decrease buttons.
*/
@Directive()
export abstract class StepInputActionButton implements OnDestroy {
/** @hidden */
protected _destroyed = new Subject<void>();

/**
* @hidden
* Indicates if action can be handled
*/
abstract canHandleAction(): boolean;

/**
* @hidden
* Step input button action handler
*/
abstract runAction(): void;

/** @hidden */
ngOnDestroy(): void {
this._destroyed.next();
this._destroyed.complete();
}

/** @hidden */
@HostListener('mousedown')
click(): void {
if (!this.canHandleAction()) {
return;
}

// Run action while button is pressed
timer(500)
.pipe(
switchMap(() => interval(40)),
takeUntil(fromEvent(window, 'mouseup', { capture: true, once: true }))
)
.pipe(takeUntil(this._destroyed))
.subscribe(() => this.runAction());

this.runAction();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,27 @@ export class StepInputControlDirective {

/**
* @hidden
* Handle "change" event
* Handle "input" event to keep track of what user is entering
*/
@HostListener('input')
onInput(): void {
if (!this.stepInput.canChangeValue) {
return;
}
const value: string = ((event.target as HTMLInputElement).value || '').trim();
this.stepInput.onEnterValue(value);
}

/**
* @hidden
* Handle "change" event to commit entered value
*/
@HostListener('change')
onChange(): void {
if (!this.stepInput.canChangeValue) {
return;
}
const value: string = ((event.target as HTMLInputElement).value || '').trim();
this.stepInput.commitEnteredValue(value);
this.stepInput.commitEnteredValue();
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,25 @@
import { Directive, HostListener, SkipSelf } from '@angular/core';
import { startWith } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { Directive, SkipSelf } from '@angular/core';

import { StepInputComponent } from './base.step-input';
import { streamUntilMouseUp$ } from './step-input-increment.directive';
import { StepInputActionButton } from './step-input-action-button';

/**
* This Directive is used to be assigned to decrement button.
*/
@Directive({
selector: '[fdpStepInputDecrement]'
})
export class StepInputDecrementDirective {
export class StepInputDecrementDirective extends StepInputActionButton {
/** @hidden */
private _streamUntilMouseUp$: Observable<number> = streamUntilMouseUp$;

/** @hidden */
constructor(@SkipSelf() private stepInput: StepInputComponent) {}

/** @hidden */
@HostListener('mousedown', ['$event'])
click($event: Event): void {
if (!this.stepInput.canChangeValue) {
return;
}
constructor(@SkipSelf() private stepInput: StepInputComponent) {
super();
}

$event.preventDefault();
canHandleAction(): boolean {
return !!this.stepInput.canChangeValue;
}

this._streamUntilMouseUp$.pipe(startWith(null)).subscribe(() => {
this.stepInput.decrease();
});
runAction(): void {
this.stepInput.decrease();
}
}

0 comments on commit f0b2f4c

Please sign in to comment.