Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<input (blur)="onBlur()" #input="ngModel" [min]="min" [max]="max"
[attr.min]="min" [attr.max]="max" (ngModelChange)="onInputChange($event)"
[ngModel]="displayValue" [disabled]="disabled"
[placeholder]="placeholder || ''" placement="top"
triggers="focus:blur"/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import {
ComponentFixture,
TestBed,
fakeAsync,
tick
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Component, Type } from '@angular/core';
import { FormsModule, ReactiveFormsModule, FormControl } from '@angular/forms';

import { typeInElement, dispatchFakeEvent } from '../../../testing';
import { NumberInputComponent } from './number-input.component';

@Component({
template: `<ngr-number-input
[formControl]="control"
[placeholder]="'Enter a number'"
[min]="10"
[max]="1000"></ngr-number-input>
`
})
export class SimpleNumberInputComponent {
control = new FormControl();
}

describe('[Component]: NumberInputComponent', () => {
// Creates a test component fixture.
function createComponent<T>(component: Type<T>) {
TestBed.configureTestingModule({
imports: [FormsModule, ReactiveFormsModule],
declarations: [NumberInputComponent, component]
});
TestBed.compileComponents();

return TestBed.createComponent<T>(component);
}

describe('forms integration', () => {
let fixture: ComponentFixture<SimpleNumberInputComponent>;
let input: HTMLInputElement;

beforeEach(() => {
fixture = createComponent(SimpleNumberInputComponent);
fixture.detectChanges();
input = fixture.debugElement.query(By.css('input')).nativeElement;
});

it('should update control value as user types with input value', () => {
typeInElement('10', input);
fixture.detectChanges();

expect(fixture.componentInstance.control.value).toEqual(
10,
`Expected control value to be updated as user types.`
);

typeInElement('100', input);
fixture.detectChanges();

expect(fixture.componentInstance.control.value).toEqual(
100,
`Expected control value to be updated as user types.`
);
});

it('should format input value on blur', fakeAsync(() => {
typeInElement('300', input);
dispatchFakeEvent(input, 'blur');
fixture.detectChanges();
tick();

expect(input.value).toEqual(
'300.00',
`Expected input value to be formatted on blur.`
);

typeInElement('3000', input);
dispatchFakeEvent(input, 'blur');
fixture.detectChanges();
tick();

expect(input.value).toEqual(
'3,000.00',
`Expected input value to be formatted on blur.`
);
}));

it('should fill input correctly if control value is set programatically', fakeAsync(() => {
fixture.componentInstance.control.setValue(100);
fixture.detectChanges();
tick();

expect(input.value).toEqual(
'100.00',
`Expected input to fill with control current formatted value.`
);

fixture.componentInstance.control.setValue(1000);
fixture.detectChanges();
tick();

expect(input.value).toEqual(
'1,000.00',
`Expected input to fill with control current formatted value.`
);
}));

it('should clear the input value if control value is reset programatically', fakeAsync(() => {
typeInElement('200', input);
fixture.detectChanges();
tick();

fixture.componentInstance.control.reset();
fixture.detectChanges();
tick();

expect(input.value).toEqual(
'',
`Expected input value to be empty after control reset.`
);
}));

it('should mark the control as dirty as user types', () => {
expect(fixture.componentInstance.control.dirty).toBe(
false,
`Expected control to start out pristine.`
);

typeInElement('20', input);
fixture.detectChanges();

expect(fixture.componentInstance.control.dirty).toBe(
true,
`Expected control to become dirty when the user types into the input.`
);
});

it('should not mark the control dirty when the value is set programmatically', () => {
expect(fixture.componentInstance.control.dirty).toBe(
false,
`Expected control to start out pristine.`
);

fixture.componentInstance.control.setValue('200');
fixture.detectChanges();

expect(fixture.componentInstance.control.dirty).toBe(
false,
`Expected control to stay pristine if value is set programmatically.`
);
});

it('should clear input value on blur if invalid input was provided', fakeAsync(() => {
typeInElement('invalid', input);
fixture.detectChanges();
tick();

dispatchFakeEvent(input, 'blur');
fixture.detectChanges();
tick();

expect(input.value).toEqual(
'',
`Expected input value to be cleared on blur.`
);
}));
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import {
Component,
EventEmitter,
forwardRef,
HostBinding,
Input,
OnInit,
Output,
Inject,
LOCALE_ID
} from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator
} from '@angular/forms';
import { formatNumber } from '@angular/common';

export const NUMBER_INPUT_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NumberInputComponent), // tslint:disable-line
multi: true
};

export const NUMBER_INPUT_VALIDATOR: any = {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => NumberInputComponent), // tslint:disable-line
multi: true
};

@Component({
selector: 'ngr-number-input',
templateUrl: './number-input.component.html',
styles: [],
providers: [NUMBER_INPUT_VALUE_ACCESSOR, NUMBER_INPUT_VALIDATOR]
})
export class NumberInputComponent
implements OnInit, ControlValueAccessor, Validator {
@HostBinding('class.ngr-number-input')
numberInputClass = true;

@Input()
placeholder: string;

@Input()
min: number;

@Input()
max: number;

@Output()
blur: EventEmitter<void> = new EventEmitter();

onChange: () => void;

onTouched: () => void;

value: number | string;

displayValue: string;

disabled: boolean;

constructor(@Inject(LOCALE_ID) private locale: string) {}

ngOnInit() {}

writeValue(value: number): void {
if (this.value !== value) {
this.value = value;
this._updateDisplayValue();
}
}

registerOnChange(fn: any): void {
this.onChange = () => {
if (fn) {
fn(this.value);
}
};
}

registerOnTouched(fn: any): void {
this.onTouched = fn;
}

/**
* Validates the filter control
*/
validate(c: AbstractControl): ValidationErrors | any {
return !isNaN(+this.value) &&
(!this.min || this.value >= this.min) &&
(!this.max || this.value <= this.max)
? null
: {
numberInput: 'Invalid value specified.'
};
}

setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
}

/**
* Called when the selection changed
* @param value
*/
onInputChange(value: string) {
this.displayValue = value;
let prepValue = value;

prepValue = prepValue.replace(',', '.');

const newValue = prepValue.match(/^\d+(\.\d+)?$/)
? parseFloat(prepValue)
: value
? value
: null;

if (this.value !== newValue && this.onChange) {
this.value = newValue;
this.onChange();
}
}

/**
* Called when the user removes focus from the field
*/
onBlur() {
this._updateDisplayValue();
}

private _updateDisplayValue() {
const value = parseFloat(`${this.value}`);

this.displayValue = Number.isNaN(value)
? ''
: formatNumber(value, this.locale, '.2-2');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { NumberInputModule } from './number-input.module';

describe('NumberInputModule', () => {
let numberInputModule: NumberInputModule;

beforeEach(() => {
numberInputModule = new NumberInputModule();
});

it('should create an instance', () => {
expect(numberInputModule).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

import { NumberInputComponent } from './number-input.component';

@NgModule({
imports: [CommonModule, FormsModule, ReactiveFormsModule],
declarations: [NumberInputComponent],
exports: [NumberInputComponent]
})
export class NumberInputModule {}
10 changes: 8 additions & 2 deletions projects/ng-rocketparts/src/lib/ng-rocketparts.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

import { NumberInputModule } from './components/number-input/number-input.module';
import { MaybeAsyncPipe } from './pipes/maybe-async.pipe';

const MODULES = [NumberInputModule];

@NgModule({
imports: [],
imports: [CommonModule, FormsModule, ReactiveFormsModule],
declarations: [MaybeAsyncPipe],
exports: [MaybeAsyncPipe]
exports: [...MODULES, MaybeAsyncPipe]
})
export class NgRocketPartsModule {}
1 change: 1 addition & 0 deletions projects/ng-rocketparts/src/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
* Public API Surface of ng-rocketparts
*/

export * from './lib/components/number-input/number-input.module';
export * from './lib/pipes/maybe-async.pipe';
export * from './lib/ng-rocketparts.module';
Loading