Browse Source

feat(admin-ui): Create CurrencyInputComponent

Michael Bromley 7 years ago
parent
commit
bc78b97661

+ 1 - 0
admin-ui/src/app/app.config.ts

@@ -4,3 +4,4 @@ import { LanguageCode } from './data/types/gql-generated-types';
 
 
 export const API_URL = `http://localhost:${API_PORT}`;
 export const API_URL = `http://localhost:${API_PORT}`;
 export const DEFAULT_LANGUAGE: LanguageCode = LanguageCode.en;
 export const DEFAULT_LANGUAGE: LanguageCode = LanguageCode.en;
+export const DEFAULT_CURRENCY = '£';

+ 5 - 0
admin-ui/src/app/common/utilities/get-default-currency.ts

@@ -0,0 +1,5 @@
+import { DEFAULT_CURRENCY } from '../../app.config';
+
+export function getDefaultCurrency(): string {
+    return DEFAULT_CURRENCY;
+}

+ 10 - 0
admin-ui/src/app/shared/components/currency-input/currency-input.component.html

@@ -0,0 +1,10 @@
+<div class="currency-symbol">
+    {{ currencySymbol }}
+</div>
+<input type="number"
+       step="0.01"
+       [value]="_decimalValue"
+       [disabled]="disabled"
+       [readonly]="readonly"
+       (input)="onInput($event.target.value)"
+       (focus)="onTouch()">

+ 20 - 0
admin-ui/src/app/shared/components/currency-input/currency-input.component.scss

@@ -0,0 +1,20 @@
+@import "variables";
+
+:host {
+    position: relative;
+    display: inline-block;
+}
+.currency-symbol {
+    display: flex;
+    align-items: center;
+    background-color: $color-grey-2;
+    position: absolute;
+    left: 0;
+    top: 1px;
+    padding: 3px;
+    border-radius: 3px;
+}
+input {
+    max-width: 115px;
+    padding-left: 40px;
+}

+ 109 - 0
admin-ui/src/app/shared/components/currency-input/currency-input.component.spec.ts

@@ -0,0 +1,109 @@
+import { Component } from '@angular/core';
+import { Type } from '@angular/core/src/type';
+import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { FormsModule } from '@angular/forms';
+import { By } from '@angular/platform-browser';
+
+import { CurrencyInputComponent } from './currency-input.component';
+
+describe('CurrencyInputComponent', () => {
+    beforeEach(async(() => {
+        TestBed.configureTestingModule({
+            imports: [FormsModule],
+            declarations: [TestControlValueAccessorComponent, TestSimpleComponent, CurrencyInputComponent],
+        }).compileComponents();
+    }));
+
+    it(
+        'should display the price as decimal with a simple binding',
+        fakeAsync(() => {
+            const fixture = createAndRunChangeDetection(TestSimpleComponent);
+            const nativeInput = getNativeInput(fixture);
+            expect(nativeInput.value).toBe('1.23');
+        }),
+    );
+
+    it(
+        'should display the price as decimal',
+        fakeAsync(() => {
+            const fixture = createAndRunChangeDetection(TestControlValueAccessorComponent);
+            const nativeInput = getNativeInput(fixture);
+            expect(nativeInput.value).toBe('1.23');
+        }),
+    );
+
+    it(
+        'should display 2 decimal places for multiples of 10',
+        fakeAsync(() => {
+            const fixture = createAndRunChangeDetection(TestControlValueAccessorComponent, 120);
+            const nativeInput = getNativeInput(fixture);
+            expect(nativeInput.value).toBe('1.20');
+        }),
+    );
+
+    it(
+        'should discard decimal places from input value',
+        fakeAsync(() => {
+            const fixture = createAndRunChangeDetection(TestControlValueAccessorComponent, 123.5);
+
+            const nativeInput = getNativeInput(fixture);
+            expect(nativeInput.value).toBe('1.23');
+        }),
+    );
+
+    it(
+        'should correctly round decimal value ',
+        fakeAsync(() => {
+            const fixture = createAndRunChangeDetection(TestControlValueAccessorComponent);
+            const nativeInput = fixture.debugElement.query(By.css('input[type="number"]'));
+            nativeInput.triggerEventHandler('input', { target: { value: 1.13 } });
+            tick();
+            fixture.detectChanges();
+            expect(fixture.componentInstance.price).toBe(113);
+        }),
+    );
+
+    it(
+        'should update model with integer values for values of more than 2 decimal places',
+        fakeAsync(() => {
+            const fixture = createAndRunChangeDetection(TestControlValueAccessorComponent);
+            const nativeInput = fixture.debugElement.query(By.css('input[type="number"]'));
+            nativeInput.triggerEventHandler('input', { target: { value: 1.567 } });
+            tick();
+            fixture.detectChanges();
+            expect(fixture.componentInstance.price).toBe(157);
+        }),
+    );
+
+    function createAndRunChangeDetection<T extends TestControlValueAccessorComponent | TestSimpleComponent>(
+        component: Type<T>,
+        priceValue = 123,
+    ): ComponentFixture<T> {
+        const fixture = TestBed.createComponent(component);
+        fixture.componentInstance.price = priceValue;
+        fixture.detectChanges();
+        tick();
+        fixture.detectChanges();
+        return fixture;
+    }
+
+    function getNativeInput(_fixture: ComponentFixture<TestControlValueAccessorComponent>): HTMLInputElement {
+        return _fixture.debugElement.query(By.css('input[type="number"]')).nativeElement;
+    }
+});
+
+@Component({
+    selector: 'vdr-test-component',
+    template: `<vdr-currency-input [(ngModel)]="price"></vdr-currency-input>`,
+})
+class TestControlValueAccessorComponent {
+    price = 123;
+}
+
+@Component({
+    selector: 'vdr-test-component',
+    template: `<vdr-currency-input [value]="price"></vdr-currency-input>`,
+})
+class TestSimpleComponent {
+    price = 123;
+}

+ 70 - 0
admin-ui/src/app/shared/components/currency-input/currency-input.component.ts

@@ -0,0 +1,70 @@
+import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+
+import { getDefaultCurrency } from '../../../common/utilities/get-default-currency';
+
+/**
+ * A form input control which displays currency in decimal format, whilst working
+ * with the intege cent value in the background.
+ */
+@Component({
+    selector: 'vdr-currency-input',
+    templateUrl: './currency-input.component.html',
+    styleUrls: ['./currency-input.component.scss'],
+    providers: [
+        {
+            provide: NG_VALUE_ACCESSOR,
+            useExisting: CurrencyInputComponent,
+            multi: true,
+        },
+    ],
+})
+export class CurrencyInputComponent implements ControlValueAccessor, OnChanges {
+    @Input() disabled = false;
+    @Input() readonly = false;
+    @Input() value: number;
+    onChange: (val: any) => void;
+    onTouch: () => void;
+    _decimalValue: string;
+    readonly currencySymbol = getDefaultCurrency();
+
+    ngOnChanges(changes: SimpleChanges) {
+        if ('value' in changes) {
+            this.writeValue(changes['value'].currentValue);
+        }
+    }
+
+    registerOnChange(fn: any) {
+        this.onChange = fn;
+    }
+
+    registerOnTouched(fn: any) {
+        this.onTouch = fn;
+    }
+
+    setDisabledState(isDisabled: boolean) {
+        this.disabled = isDisabled;
+    }
+
+    onInput(value: string) {
+        const integerValue = Math.round(+value * 100);
+        this.onChange(integerValue);
+        const delta = Math.abs(Number(this._decimalValue) - Number(value));
+        if (0.009 < delta && delta < 0.011) {
+            this._decimalValue = this.toNumericString(value);
+        } else {
+            this._decimalValue = value;
+        }
+    }
+
+    writeValue(value: any): void {
+        const numericValue = +value;
+        if (!Number.isNaN(numericValue)) {
+            this._decimalValue = this.toNumericString(Math.floor(value) / 100);
+        }
+    }
+
+    private toNumericString(value: number | string): string {
+        return Number(value).toFixed(2);
+    }
+}

+ 2 - 0
admin-ui/src/app/shared/shared.module.ts

@@ -12,6 +12,7 @@ import {
     ActionBarRightComponent,
     ActionBarRightComponent,
 } from './components/action-bar/action-bar.component';
 } from './components/action-bar/action-bar.component';
 import { ChipComponent } from './components/chip/chip.component';
 import { ChipComponent } from './components/chip/chip.component';
+import { CurrencyInputComponent } from './components/currency-input/currency-input.component';
 import { DataTableColumnComponent } from './components/data-table/data-table-column.component';
 import { DataTableColumnComponent } from './components/data-table/data-table-column.component';
 import { DataTableComponent } from './components/data-table/data-table.component';
 import { DataTableComponent } from './components/data-table/data-table.component';
 import { FormFieldControlDirective } from './components/form-field/form-field-control.directive';
 import { FormFieldControlDirective } from './components/form-field/form-field-control.directive';
@@ -41,6 +42,7 @@ const DECLARATIONS = [
     ActionBarLeftComponent,
     ActionBarLeftComponent,
     ActionBarRightComponent,
     ActionBarRightComponent,
     ChipComponent,
     ChipComponent,
+    CurrencyInputComponent,
     DataTableComponent,
     DataTableComponent,
     DataTableColumnComponent,
     DataTableColumnComponent,
     PaginationControlsComponent,
     PaginationControlsComponent,