如何为离子输入实现货币输入指令

How to implement a currency input directive for ion-input

更新最初虽然问题出在 ControlValueAccessor 的实现中,但随后确定问题与将 ControlValueAccessor 应用于子元素有关。编辑问题以反映。

我想提供一个属性指令,该指令将以 'dollar' 格式(例如 10.00)显示货币值,但将作为美分(例如 1000)存储在基础模型中。

<!-- cost = 1000 would result in text input value of 10.00
<input type="text" [(ngModel)]="cost" name="cost" currencyInput>
<!-- or in Ionic 2 -->
<ion-input type="text" [(ngModel)]="cost" name="cost-ionic" currencyInput>

以前在 AngularJS 1.x 中,我会在指令 link 函数中使用解析和渲染,如下所示:

(function() {
    'use strict';

    angular.module('app.directives').directive('ndDollarsToCents', ['$parse', function($parse) {
        return {
            require: 'ngModel',
            link: function(scope, element, attrs, ctrl) {
                var listener = function() {
                    element.val((value/100).toFixed(2));
                };

                ctrl.$parsers.push(function(viewValue) {
                    return Math.round(parseFloat(viewValue) * 100);
                });

                ctrl.$render = function() {
                    element.val((ctrl.$viewValue / 100).toFixed(2));
                };

                element.bind('change', listener);
            }
        };
    }]);
})();

在 Ionic 2/Angular 2 中,我使用 ControlValueAccessor 接口实现了如下:

import { Directive, Renderer, ElementRef, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

const CURRENCY_VALUE_ACCESSOR = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => CurrencyInputDirective),
  multi: true
}

@Directive({
    selector: '[currencyInput]',
    host: {
        '(input)': 'handleInput($event.target.value)'
     },
     providers: [ CURRENCY_VALUE_ACCESSOR ]
})
export class CurrencyInputDirective implements ControlValueAccessor, AfterViewInit
{
    onChange = (_: any) => {};
    onTouched = () => {};
    inputElement: HTMLInputElement = null;

    constructor(private renderer: Renderer, private elementRef: ElementRef) {}

    ngAfterViewInit()
    {
        let element = this.elementRef.nativeElement;

        if(element.tagName === 'INPUT')
        {
            this.inputElement = element;
        }
        else
        {
             this.inputElement = element.getElementsByTagName('input')[0];
        }
    }

    registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
    registerOnTouched(fn: () => void): void { this.onTouched = fn; }

    handleInput(value : string)
    {
        if (value)
        {
            value = String(Math.round(parseFloat(value) * 100));
        }

        this.onChange(value);
    }


    writeValue(value: any): void
    {
        if (value)
        {
            value = (parseInt(value) / 100).toFixed(2);
        }

        this.renderer.setElementProperty(this.inputElement, 'value', value);
    }
}

虽然这在应用于直接输入元素时效果很好,但在应用于离子输入时却不起作用。有没有办法让 ControlValueAccessor 应用于离子输入的子输入元素?

您无需使用 ControlValueAccessor 即可实现此目的。使用下面的代码

import { Directive, HostListener, Renderer2, ElementRef } from '@angular/core';
@Directive({
    selector: '[currency]'
})
export class CurrencyDirective{

    constructor(
        private renderer: Renderer2,
        private el: ElementRef
    ){}

    @HostListener('keyup') onKeyUp() {
      this.el.nativeElement.value=this.el.nativeElement.value/100;
      console.log(this.el.nativeElement.value)
     console.log('some thing key upped')

    }
}

LIVE DEMO

虽然 ControlAccessorValue 方法适用于普通 <input> 元素,但我无法让指令充当 <ion-input> 指令的 <input> 子元素的控制访问器(对此的解决方案感兴趣)。

或者以下指令,在应用于 <ion-input> 或普通 <input> 时实现所需的结果:

import { Directive, Renderer, ElementRef, Input, Output, EventEmitter, AfterViewInit } from '@angular/core';

@Directive({
    selector: '[currencyInput]',
    host: {
        '(input)': 'handleInput($event.target.value)'
    },
})
export class CurrencyInputDirective implements AfterViewInit
{

    @Input('currencyInput') currency: number;
    @Output('currencyInputChange') currencyChange: EventEmitter<number> = new EventEmitter<number>();
    inputElement: HTMLInputElement = null;

    constructor(private renderer: Renderer, private el: ElementRef) { }

    ngAfterViewInit()
    {
        let element = this.el.nativeElement;

        if(element.tagName === 'INPUT')
        {
            this.inputElement = element;
        }
        else
        {
            this.inputElement = element.getElementsByTagName('input')[0];
        }

        setTimeout(() => {
            this.renderer.setElementProperty(this.inputElement, 'value', (this.currency/100).toFixed(2));
        }, 150);
    }

    handleInput(value: string)
    {
        let v : number = Math.round(parseFloat(value) * 100)
        this.currencyChange.next(v);
    }
}

然后按如下方式将其应用到元素上:

<ion-input type="text" name="measured_cost" [(currencyInput)]="item.cost">

需要 setTimeout 以确保输入字段由 CurrencyInputDirective 而不是 ionic 初始化(我欢迎更好的选择)。

这很好用,但它只提供一种方式流,即如果 item.cost 在输入元素之外更改,它不会反映在输入元素值中。可以使用 currencyInput 的 setter 方法解决此问题,如下面这个性能较差的解决方案所示:

import { Directive, Renderer, ElementRef, Input, Output, EventEmitter, AfterViewInit } from '@angular/core';
@Directive({
    selector: '[currencyInput]',
    host: {
        '(input)': 'handleInput($event.target.value)'
    },
})
export class CurrencyInputDirective implements AfterViewInit
{
    _cents: number;
    myUpdate: boolean = false;
    inputElement: HTMLInputElement = null;
    @Input('currencyInput')
    set cents(value: number) {
        if(value !== this._cents)
        {
            this._cents = value;
            this.updateElement();
        }
    }
    @Output('currencyInputChange') currencyChange: EventEmitter<number> = new EventEmitter<number>();

    constructor(private renderer: Renderer, private el: ElementRef) { }

    ngAfterViewInit()
    {
        let element = this.el.nativeElement;

        if(element.tagName === 'INPUT')
        {
            this.inputElement = element;
        }
        else
        {
            this.inputElement = element.getElementsByTagName('input')[0];
        }

        setTimeout(() => {
            this.renderer.setElementProperty(this.inputElement, 'value', (this._cents/100).toFixed(2));
        }, 150);
    }

    handleInput(value: string)
    {
        let v : number = Math.round(parseFloat(value) * 100);
        this.myUpdate = true;
        this.currencyChange.next(v);
    }

    updateElement()
    {
        if(this.inputElement)
        {
            let startPos = this.inputElement.selectionStart;
            let endPos = this.inputElement.selectionEnd;

            this.renderer.setElementProperty(this.inputElement, 'value', (this._cents/100).toFixed(2));
            if(this.myUpdate)
            {
                this.inputElement.setSelectionRange(startPos, endPos);
                this.myUpdate = false;
            }
        }
    }
}