-
+
{{viewUnit}}
diff --git a/src/ng-xform/measure-field/measure-field.component.spec.ts b/src/ng-xform/measure-field/measure-field.component.spec.ts
index fe4e34c..4f946f0 100644
--- a/src/ng-xform/measure-field/measure-field.component.spec.ts
+++ b/src/ng-xform/measure-field/measure-field.component.spec.ts
@@ -1,5 +1,5 @@
import { DebugElement } from '@angular/core';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { async, ComponentFixture, TestBed, tick, fakeAsync } from '@angular/core/testing';
import { FormControl } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { Subject } from 'rxjs';
@@ -36,7 +36,7 @@ describe('MeasureFieldComponent', () => {
modelUnit: 'm'
});
component.ngOnInit();
- component.ngAfterViewInit();
+ component.inputNumber.ngAfterViewInit();
fixture.detectChanges();
inputEl = fixture.debugElement.query(By.css('input'));
});
@@ -56,7 +56,7 @@ describe('MeasureFieldComponent', () => {
it('should render form value', () => {
const initialValue = '22';
component.writeValue({ value: 22, unit: 'm' });
- expect(inputEl.nativeElement.value).toBe(initialValue);
+ expect(component.inputNumber.viewModel).toBe(initialValue);
});
it('should format properly form value', () => {
@@ -65,40 +65,27 @@ describe('MeasureFieldComponent', () => {
expect(component.formattedValue).toBe(formattedValue);
});
- it('should disable form value', () => {
- component.writeValue({ value: 22, unit: 'm' });
-
- component.setDisabledState(true);
- expect(inputEl.nativeElement.disabled).toBe(true);
-
- component.setDisabledState(false);
- expect(inputEl.nativeElement.disabled).toBe(false);
- });
-
it('should update form value', () => {
const newValueString = '15';
- let updatedValue;
- component.registerOnChange((val: any) => updatedValue = val);
inputEl.nativeElement.value = newValueString;
inputEl.nativeElement.dispatchEvent(new Event('input'));
- expect(updatedValue).toEqual(new Measure(15, 'm'));
+ expect(component.control.value).toEqual(new Measure(15, 'm'));
component.changeUnit('cm');
expect(component.formattedValue).toEqual('1500 cm');
- expect(updatedValue).toEqual(new Measure(15, 'm'));
+ expect(component.control.value).toEqual(new Measure(15, 'm'));
component.changeUnit('');
expect(component.formattedValue).toEqual('15 m');
- expect(updatedValue).toEqual(new Measure(15, 'm'));
+ expect(component.control.value).toEqual(new Measure(15, 'm'));
component.field.modelUnit = 'inch';
fixture.detectChanges();
component.ngOnInit();
- component.registerOnChange((val: any) => updatedValue = val);
inputEl.nativeElement.value = newValueString;
inputEl.nativeElement.dispatchEvent(new Event('input'));
- expect(updatedValue).toEqual(new Measure(15, 'inch'));
+ expect(component.control.value).toEqual(new Measure(15, 'inch'));
});
it('should show unit and units dropdown', () => {
diff --git a/src/ng-xform/measure-field/measure-field.component.ts b/src/ng-xform/measure-field/measure-field.component.ts
index 1b4a9f2..6ef1bca 100644
--- a/src/ng-xform/measure-field/measure-field.component.ts
+++ b/src/ng-xform/measure-field/measure-field.component.ts
@@ -1,11 +1,12 @@
-import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
-import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
+import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Unit } from 'mathjs';
import * as math from 'mathjs';
-import { isObservable } from 'rxjs';
+import { isObservable, Subscription } from 'rxjs';
import { BaseDynamicFieldComponent } from '../field-components/base-dynamic-field.component';
import { MeasureField } from '../fields';
+import { InputNumberComponent } from '../number-field/input-number.component';
import { Measure } from './../models/measure';
@@ -26,54 +27,48 @@ import { Measure } from './../models/measure';
multi: true
}],
})
-export class MeasureFieldComponent extends BaseDynamicFieldComponent implements ControlValueAccessor, AfterViewInit,
- OnInit {
+export class MeasureFieldComponent extends BaseDynamicFieldComponent implements ControlValueAccessor, OnInit, OnDestroy {
- @ViewChild('unitsDropdown') unitsDropdown: ElementRef;
+ @ViewChild(InputNumberComponent) inputNumber: InputNumberComponent;
- private input: HTMLInputElement;
private quantity: Unit;
-
+ numberControl = new FormControl();
viewUnit: string;
availableUnits: string[];
_onChange = (value: any) => { };
_onTouched = () => { };
- constructor(private elementRef: ElementRef) {
- super();
- }
-
get formattedValue() {
- return this.quantity ? math.format(this.quantity.to(this.viewUnit), this.field.formatOptions) : '-';
+ return !!this.inputNumber && !!this.control.value ? `${this.inputNumber.formattedValue} ${this.viewUnit}` : '-';
}
ngOnInit() {
super.ngOnInit();
this.setViewUnits();
this.setViewUnit();
+ this.subscriptions.push(
+ this.numberControl.valueChanges.subscribe((value: any) => {
+ let newValue: any;
+ if (!value) {
+ this.quantity = null;
+ newValue = null;
+ } else {
+ this.quantity = math.unit(value, this.viewUnit).to(this.field.modelUnit);
+ newValue = new Measure(
+ this.quantity.toNumber(this.field.modelUnit),
+ this.field.modelUnit
+ );
+ }
+
+ this.control.setValue(newValue, { emitEvent: false });
+ this._onChange(newValue);
+ })
+ );
}
- ngAfterViewInit() {
- this.input = this.elementRef.nativeElement.querySelector('input');
- if (this.input) {
- this.input.onblur = this._onTouched
- }
- this.input.oninput = (event: Event) => {
- const field = event.target as HTMLInputElement;
- if (!field.value) {
- this.quantity = null;
- this._onChange(null);
- return;
- }
-
- const value = Number(field.value);
- this.quantity = math.unit(value, this.viewUnit).to(this.field.modelUnit);
- this._onChange(new Measure(
- this.quantity.toNumber(this.field.modelUnit),
- this.field.modelUnit
- ));
- }
+ ngOnDestroy() {
+ this.subscriptions.forEach(sub => sub.unsubscribe());
}
writeValue(obj: Measure): void {
@@ -92,15 +87,15 @@ export class MeasureFieldComponent extends BaseDynamicFieldComponent {
component.fields = [
new TextField({ key: 'text1', label: 'Text 1' }),
new TextField({ key: 'required', label: 'Required 1', validators: [Validators.required] }),
- new MeasureField({ key: 'measure1', label: 'Measure 1', modelUnit: 'degC' }),
new MultilineField({ key: 'comments', label: 'Comments' }),
new TextField({ key: 'other_color', label: 'Other Color', visibilityFn: (formVal: any) => formVal.color === 1 }),
new NestedFormGroup({
@@ -92,10 +91,6 @@ describe('NgXformComponent', () => {
expectFormTextarea('comments', 'Comments', 'comments here...');
});
- it('should render MeasureField', () => {
- expectFormInput('measure1', 'Measure 1');
- });
-
it('should render nested field', () => {
expectFormInput('city', 'City', 'Ny');
});
@@ -106,7 +101,6 @@ describe('NgXformComponent', () => {
expectFormInput('city', 'City', '');
expectFormInput('text1', 'Text 1', '');
expectFormTextarea('comments', 'Comments', '');
- expectFormInput('measure1', 'Measure 1', undefined);
});
it('should render DateField', () => {
diff --git a/src/ng-xform/ng-xform.module.ts b/src/ng-xform/ng-xform.module.ts
index e5c735f..2b10187 100644
--- a/src/ng-xform/ng-xform.module.ts
+++ b/src/ng-xform/ng-xform.module.ts
@@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common';
import { Inject, LOCALE_ID, NgModule } from '@angular/core';
-import { ReactiveFormsModule } from '@angular/forms';
+import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { NgSelectModule } from '@ng-select/ng-select';
import { bsDatepickerModuleWithProviders, bsDropdownModuleWithProviders } from './ng-xform.module-imports';
@@ -23,11 +23,14 @@ import { RadioGroupFieldComponent } from './radiogroup-field/radiogroup-field.co
import { SelectFieldComponent } from './select-field/select-field.component';
import { NgXformEditSaveComponent } from './ng-xform-edit-save/ng-xform-edit-save.component';
import { EditSaveCancelButtonBarComponent } from './edit-save-cancel-button-bar/edit-save-cancel-button-bar.component';
+import { NumberFieldComponent } from './number-field/number-field.component';
+import { InputNumberComponent } from './number-field/input-number.component';
@NgModule({
imports: [
CommonModule,
+ FormsModule,
ReactiveFormsModule,
PipesModule,
NgSelectModule,
@@ -39,6 +42,7 @@ import { EditSaveCancelButtonBarComponent } from './edit-save-cancel-button-bar/
NgXformEditSaveComponent,
FormGroupComponent,
EditableLabelComponent,
+ NumberFieldComponent,
CheckboxFieldComponent,
CustomFieldComponent,
RadioGroupFieldComponent,
@@ -52,11 +56,13 @@ import { EditSaveCancelButtonBarComponent } from './edit-save-cancel-button-bar/
DateRangeFieldComponent,
FormControlLayoutComponent,
EditSaveCancelButtonBarComponent,
+ InputNumberComponent,
],
exports: [
NgXformComponent,
NgXformEditSaveComponent,
FieldErrorMessageComponent,
+ InputNumberComponent,
PipesModule,
]
})
diff --git a/src/ng-xform/number-field/input-number.component.html b/src/ng-xform/number-field/input-number.component.html
new file mode 100644
index 0000000..a1ba8cf
--- /dev/null
+++ b/src/ng-xform/number-field/input-number.component.html
@@ -0,0 +1 @@
+
diff --git a/src/ng-xform/number-field/input-number.component.spec.ts b/src/ng-xform/number-field/input-number.component.spec.ts
new file mode 100644
index 0000000..f1c99b8
--- /dev/null
+++ b/src/ng-xform/number-field/input-number.component.spec.ts
@@ -0,0 +1,148 @@
+import { registerLocaleData } from '@angular/common';
+import localePt from '@angular/common/locales/pt';
+import { DebugElement, LOCALE_ID } from '@angular/core';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+
+import { NgXformModule } from '../..';
+import { InputNumberComponent } from './input-number.component';
+
+describe('InputNumberComponent', () => {
+
+ describe('Locale pt', () => {
+ let component: InputNumberComponent;
+ let fixture: ComponentFixture;
+ let inputEl: DebugElement;
+ beforeEach(
+ async(() => {
+ registerLocaleData(localePt, 'pt');
+ TestBed.configureTestingModule({
+ imports: [
+ NgXformModule
+ ],
+ providers: [
+ { provide: LOCALE_ID, useValue: 'pt'}
+ ],
+ }).compileComponents();
+ })
+ );
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(InputNumberComponent);
+ component = fixture.componentInstance;
+ component.ngAfterViewInit();
+ fixture.detectChanges();
+ inputEl = fixture.debugElement.query(By.css('input'));
+ });
+
+ it('should render NumberField', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should disable form value', () => {
+ component.setDisabledState(true);
+ expect(inputEl.nativeElement.disabled).toBe(true);
+
+ component.setDisabledState(false);
+ expect(inputEl.nativeElement.disabled).toBe(false);
+ });
+
+ it('should render form exponential value', () => {
+ component.writeValue(2.2324789456e+31);
+ expect(component.formattedValue).toBe('2,2324789456e+31');
+ expect(component.viewModel).toBe('2,2324789456e+31');
+ });
+
+ it('should render form decimal value', () => {
+ component.writeValue(2.23247894);
+ expect(component.formattedValue).toBe('2,23247894');
+ expect(component.viewModel).toBe('2,23247894');
+ });
+
+ it('should render form integer value', () => {
+ component.writeValue(234);
+ expect(component.formattedValue).toBe('234');
+ expect(component.viewModel).toBe('234');
+ });
+
+ it('should render form long value as exponential', () => {
+ component.writeValue(232478);
+ expect(component.formattedValue).toBe('2,32478e+5');
+ expect(component.viewModel).toBe('2,32478e+5');
+ });
+
+ it('should update form value', () => {
+ const newValueString = '15,654';
+ let updatedValue;
+
+ component.registerOnChange((val: any) => updatedValue = val);
+ inputEl.nativeElement.value = newValueString;
+ inputEl.nativeElement.dispatchEvent(new Event('input'));
+ expect(updatedValue).toEqual(15.654);
+ });
+ });
+
+ describe('Locale en', () => {
+ let component: InputNumberComponent;
+ let fixture: ComponentFixture;
+ let inputEl: DebugElement;
+ beforeEach(
+ async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ NgXformModule
+ ],
+ providers: [
+ { provide: LOCALE_ID, useValue: 'en'}
+ ],
+ }).compileComponents();
+ })
+ );
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(InputNumberComponent);
+ component = fixture.componentInstance;
+ component.ngAfterViewInit();
+ fixture.detectChanges();
+ inputEl = fixture.debugElement.query(By.css('input'));
+ });
+
+ it('should render NumberField', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should render form exponential value', () => {
+ component.writeValue(2.2324789456e+31);
+ expect(component.formattedValue).toBe('2.2324789456e+31');
+ expect(component.viewModel).toBe('2.2324789456e+31');
+ });
+
+ it('should render form decimal value', () => {
+ component.writeValue(2.23247894);
+ expect(component.formattedValue).toBe('2.23247894');
+ expect(component.viewModel).toBe('2.23247894');
+ });
+
+ it('should render form integer value', () => {
+ component.writeValue(234);
+ expect(component.formattedValue).toBe('234');
+ expect(component.viewModel).toBe('234');
+ });
+
+ it('should render form long value as exponential', () => {
+ component.writeValue(232478);
+ expect(component.formattedValue).toBe('2.32478e+5');
+ expect(component.viewModel).toBe('2.32478e+5');
+ });
+
+ it('should update form value', () => {
+ const newValueString = '15.654';
+ let updatedValue;
+
+ component.registerOnChange((val: any) => updatedValue = val);
+ inputEl.nativeElement.value = newValueString;
+ inputEl.nativeElement.dispatchEvent(new Event('input'));
+ expect(updatedValue).toEqual(15.654);
+ });
+ });
+});
diff --git a/src/ng-xform/number-field/input-number.component.ts b/src/ng-xform/number-field/input-number.component.ts
new file mode 100644
index 0000000..416d9d9
--- /dev/null
+++ b/src/ng-xform/number-field/input-number.component.ts
@@ -0,0 +1,144 @@
+import {
+ AfterViewInit,
+ Component,
+ ElementRef,
+ EventEmitter,
+ Inject,
+ Input,
+ LOCALE_ID,
+ Output,
+ Renderer2,
+} from '@angular/core';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+import { NumberSymbol, getLocaleNumberSymbol } from '@angular/common';
+import * as math from 'mathjs';
+
+import { KeyCode as KC } from './number-utils';
+
+
+/**
+ * Component to generate input field form numbers
+ */
+@Component({
+ selector: 'ng-xform-input-number',
+ templateUrl: './input-number.component.html',
+ providers: [{
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: InputNumberComponent,
+ multi: true
+ }],
+})
+export class InputNumberComponent implements ControlValueAccessor, AfterViewInit {
+
+ @Output() paste = new EventEmitter();
+ @Output() keypress = new EventEmitter();
+ @Input() formatOptions = { notation: 'auto'};
+ @Input() inputClass = '';
+ @Input() inputStyle = '';
+ @Input() inputId = '';
+ viewModel = '';
+
+ private input: HTMLInputElement;
+ private isValidNumber: RegExp;
+ private allowedKeyCodes = [
+ KC.Plus,
+ KC.Minus,
+ KC.E,
+ KC.Enter,
+ KC.Number0,
+ KC.Number1,
+ KC.Number2,
+ KC.Number3,
+ KC.Number4,
+ KC.Number5,
+ KC.Number6,
+ KC.Number7,
+ KC.Number8,
+ KC.Number9,
+ ];
+ private thousandSeparator: string;
+ private decimalSeparator: string;
+
+ _onChange = (value: any) => { };
+ _onTouched = () => { };
+
+ constructor(private elementRef: ElementRef, private renderer: Renderer2, @Inject(LOCALE_ID) private locale: string) {
+ this.thousandSeparator = getLocaleNumberSymbol(locale, NumberSymbol.Group);
+ this.decimalSeparator = getLocaleNumberSymbol(locale, NumberSymbol.Decimal);
+ const decimalSymbolKeyCode = this.decimalSeparator.charCodeAt(0);
+ this.allowedKeyCodes.push(decimalSymbolKeyCode);
+ this.isValidNumber = new RegExp(
+ `^(([+\\-]?(?:(?:\\d{1,3}(?:\\${this.thousandSeparator}\\d{1,3})+)|\\d*))(?:\\${this.decimalSeparator}(\\d*))?)` +
+ `(?:(?:[e]+([+\-]?\\d*)))?$`
+ );
+ }
+
+ ngAfterViewInit() {
+ this.input = this.elementRef.nativeElement.querySelector('input');
+ this.input.oninput = () => {
+ this._onChange(this.getValueAsNumber());
+ };
+ this.input.onkeypress = (event: KeyboardEvent) => {
+ this.keypress.emit(event);
+ if (this.allowedKeyCodes.indexOf(event.keyCode) < 0 || !this.isValidNumber.test(this.getFutureValue(event))) {
+ event.preventDefault();
+ }
+ }
+ this.input.onpaste = (event: Event) => this.paste.emit(event);
+ }
+
+ private getFutureValue(event: KeyboardEvent) {
+ const input: any = event['target'];
+ return input.value.slice(0, input.selectionStart) +
+ event.key +
+ input.value.slice(input.selectionEnd, input.value.length);
+ }
+
+ public get formattedValue() {
+ return this.viewModel || '-';
+ }
+
+ private getValueAsNumber(): number {
+ if (!this.isValidNumber.test(this.viewModel)) {
+ return NaN;
+ }
+ const value = this.viewModel.replace(this.thousandSeparator, '').replace(this.decimalSeparator, '.');
+ return Number(value);
+ }
+
+ private toLocaleString(value: number) {
+ let formatedValue = math.format(value, this.formatOptions);
+ if (this.decimalSeparator !== '.') {
+ return formatedValue.replace('.', this.decimalSeparator);
+ }
+ return formatedValue;
+ }
+
+ writeValue(value: any): void {
+ if (value == null) {
+ this.viewModel = '';
+ return;
+ }
+ this.viewModel = this.toLocaleString(Number(value));
+ }
+
+ registerOnChange(fn: any): void {
+ this._onChange = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ this._onTouched = fn;
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ if (!this.input) {
+ return;
+ }
+ if (isDisabled) {
+ this.renderer.setAttribute(this.input, 'disabled', undefined);
+ } else {
+ this.renderer.removeAttribute(this.input, 'disabled');
+ }
+ }
+
+}
diff --git a/src/ng-xform/number-field/number-field.component.html b/src/ng-xform/number-field/number-field.component.html
new file mode 100644
index 0000000..a18d9ff
--- /dev/null
+++ b/src/ng-xform/number-field/number-field.component.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/ng-xform/number-field/number-field.component.ts b/src/ng-xform/number-field/number-field.component.ts
new file mode 100644
index 0000000..961cbce
--- /dev/null
+++ b/src/ng-xform/number-field/number-field.component.ts
@@ -0,0 +1,20 @@
+import { AfterViewInit, Component, ElementRef, Inject, LOCALE_ID, Renderer2 } from '@angular/core';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+import * as math from 'mathjs';
+
+import { BaseDynamicFieldComponent } from '../field-components/base-dynamic-field.component';
+import { NumberField } from '../fields';
+
+
+/**
+ * Component to generate a bootstrap form field of general type number
+ *
+ * :editing: Flag to control component state
+ * :form: FormGroup containing the field
+ * :field: Intance of field configurations
+ */
+@Component({
+ selector: 'ng-xform-number-field',
+ templateUrl: './number-field.component.html',
+})
+export class NumberFieldComponent extends BaseDynamicFieldComponent { }
diff --git a/src/ng-xform/number-field/number-utils.ts b/src/ng-xform/number-field/number-utils.ts
new file mode 100644
index 0000000..ea62c11
--- /dev/null
+++ b/src/ng-xform/number-field/number-utils.ts
@@ -0,0 +1,18 @@
+export enum KeyCode {
+ Enter = 13,
+ Plus = 43,
+ Comma = 44,
+ Minus = 45,
+ Period = 46,
+ Number0 = 48,
+ Number1 = 49,
+ Number2 = 50,
+ Number3 = 51,
+ Number4 = 52,
+ Number5 = 53,
+ Number6 = 54,
+ Number7 = 55,
+ Number8 = 56,
+ Number9 = 57,
+ E = 101
+}