如何为离子输入实现货币输入指令
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')
}
}
虽然 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;
}
}
}
}
更新:最初虽然问题出在 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')
}
}
虽然 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;
}
}
}
}