如何在 Angular 5 反应式表单输入中使用管道
How to use pipes in Angular 5 reactive form input
我想弄清楚如何在反应形式中使用管道,以便将输入强制转换为货币格式。我已经为此创建了自己的管道,我已经在代码的其他区域进行了测试,所以我知道它可以作为一个简单的管道使用。我的管道名称是 'udpCurrency'
关于堆栈溢出,我能找到的最接近的答案是这个:但是这对我来说不起作用,我怀疑这与我的表单是反应性的事实有关
这里是所有相关代码:
模板
<form [formGroup]="myForm" #f="ngForm">
<input class="form-control col-md-6"
formControlName="amount"
[ngModel]="f.value.amount | udpCurrency"
(ngModelChange)="f.value.amount=$event"
placeholder="Amount">
</form>
组件
import { Component, OnInit, HostListener } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
export class MyComponent implements OnInit {
myForm: FormGroup;
constructor(
private builder: FormBuilder
) {
this.myForm = builder.group({
amount: ['', Validators.required]
});
}
}
错误:
ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined: '. Current value: 'undefined: undefined'
在不知道您的管道代码的情况下,很可能会因为您在何处构建该表单而引发错误。
尝试使用 Angular 的更改检测挂钩在输入已解析后设置该值:
export class MyComponent implements OnInit {
myForm: FormGroup;
constructor(private builder: FormBuilder) { }
ngOnInit() {
this.myForm = builder.group({
amount: ['', Validators.required]
});
}
}
这就是混合使用模板驱动表单和响应式表单时可能发生的情况。你有两个相互争斗的绑定。选择模板驱动或反应形式。如果你想走反应路线,你可以使用 [value]
作为你的管道...
请注意,此管道仅用于在视图中显示所需的输出。
<form [formGroup]="myForm">
<input
[value]="myForm.get('amount').value | udpCurrency"
formControlName="amount"
placeholder="Amount">
</form>
我以为我已经成功了,但事实证明,我错了(并接受了一个错误的答案)。我只是以一种更适合我的新方式重写了我的逻辑,并在上面的评论中回答了 Jacob Roberts 的担忧。这是我的新解决方案:
模板:
<form [formGroup]="myForm">
<input formControlName="amount" placeholder="Amount">
</form>
组件:
import { Component, OnInit, HostListener } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { UdpCurrencyMaskPipe } from '../../../_helpers/udp-currency-mask.pipe';
export class MyComponent implements OnInit {
myForm: FormGroup;
constructor(
private builder: FormBuilder,
private currencyMask: UdpCurrencyMaskPipe,
) {
this.myForm = builder.group({
amount: ['', Validators.required]
});
this.myForm.valueChanges.subscribe(val => {
if (typeof val.amount === 'string') {
const maskedVal = this.currencyMask.transform(val.amount);
if (val.amount !== maskedVal) {
this.myForm.patchValue({amount: maskedVal});
}
}
});
}
}
烟斗:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'udpCurrencyMask'
})
export class UdpCurrencyMaskPipe implements PipeTransform {
amount: any;
transform(value: any, args?: any): any {
let amount = String(value);
const beforePoint = amount.split('.')[0];
let integers = '';
if (typeof beforePoint !== 'undefined') {
integers = beforePoint.replace(/\D+/g, '');
}
const afterPoint = amount.split('.')[1];
let decimals = '';
if (typeof afterPoint !== 'undefined') {
decimals = afterPoint.replace(/\D+/g, '');
}
if (decimals.length > 2) {
decimals = decimals.slice(0, 2);
}
amount = integers;
if (typeof afterPoint === 'string') {
amount += '.';
}
if (decimals.length > 0) {
amount += decimals;
}
return amount;
}
}
现在我在这里学到了一些东西。一种是 Jacob 所说的是真实的,另一种方式只在最初有效,但当值发生变化时不会更新。另一个需要注意的非常重要的事情是,与视图管道相比,我需要一种完全不同类型的遮罩管道。例如,视图中的管道可能采用此值“100”并将其转换为“$100.00”,但是您不希望在键入值时发生这种转换,您只希望在完成键入后发生这种情况。出于这个原因,我创建了我的货币掩码管道,它只是删除非数字并将小数限制为两位。
此处的其他答案对我来说效果不佳,但我找到了一种非常有效的方法。您需要在反应形式 valueChanges 订阅中应用管道转换,但不要发出事件,这样它就不会创建递归循环:
this.formGroup.valueChanges.subscribe(form => {
if (form.amount) {
this.formGroup.patchValue({
amount: this.currencyMask.transform(form.amount)
}, {
emitEvent: false
});
}
});
这还需要您的管道 "unformats" 那里的任何东西,通常就像在管道的转换函数中这样简单:
value = value.replace(/$+/g, '');
我打算编写一个自定义控件,但发现通过 ngModelChange
从 FormControl class 覆盖 "onChange" 更容易。 emitViewToModelChange: false
在您的更新逻辑中至关重要,可以避免更改事件的重复循环。所有到货币的管道都发生在组件中,您不必担心出现控制台错误。
<input matInput placeholder="Amount"
(ngModelChange)="onChange($event)" formControlName="amount" />
@Component({
providers: [CurrencyPipe]
})
export class MyComponent {
form = new FormGroup({
amount: new FormControl('', { validators: Validators.required, updateOn: 'blur' })
});
constructor(private currPipe:CurrencyPipe) {}
onChange(value:string) {
const ctrl = this.form.get('amount') as FormControl;
if(isNaN(<any>value.charAt(0))) {
const val = coerceNumberProperty(value.slice(1, value.length));
ctrl.setValue(this.currPipe.transform(val), { emitEvent: false, emitViewToModelChange: false });
} else {
ctrl.setValue(this.currPipe.transform(value), { emitEvent: false, emitViewToModelChange: false });
}
}
onSubmit() {
const rawValue = this.form.get('amount').value;
// if you need to strip the '$' from your input's value when you save data
const value = rawValue.slice(1, rawValue.length);
// do whatever you need to with your value
}
}
<input type="text" name="name" class="form-control mw-120 text-right"
[value]="finalRow.controls[i].get('rental').value |currency">
这是最有效的,因为您没有用 formControlName 覆盖该值。
添加 formControlName 不会总是正常工作,所以需要小心。
<input type="text" name="name" class="form-control mw-120 text-right"
[value]="finalRow.controls[i].get('rental').value |currency" formControlName="rental">
这可能不会给出预期的结果。
仅使用此代码
<input [value]="value | udpCurrency" name="inputField" type="number" formControlName="amount" />
您可以将管道包装到一个指令中。例如:
import { Directive, Input, OnInit } from '@angular/core';
import { NgControl } from '@angular/forms';
// The selector can restrict usage to a specific input.
// For example: 'input[type="number"][yourPipe]' for number type input.
@Directive({ selector: 'input[yourPipeSelector]' })
export class FormControlYourPipeDirective implements OnInit {
/** Your optional pipe args. Same name as directive to directly pass the value. */
@Input() public yourPipeSelector: unknown;
/** Control event options. For example avoid unintentional change events. */
private eventOptions = {
onlySelf: true,
emitEvent: false,
emitViewToModelChange: false,
};
constructor(private ngControl: NgControl, private yourPipe: YourPipe) {}
public ngOnInit(): void {
this.ngControl.control.valueChanges.subscribe((value) => {
this.ngControl.control.setValue(
this.yourPipe.transform(value, this.yourPipeSelector),
this.eventOptions
);
});
}
}
不要忘记将 YourPipe 添加到管道模块中的 providers。
并将管道模块添加到您要使用它的模块的导入中。 (Angular 基础) ... 然后使用它:
<input formControlName="myValue" [yourPipeSelector]="'some args'" />
瞧
但请注意:您操作的输入元素可以由用户再次编辑。该值应该与输入(及其类型)兼容。
例如:如果您有一个 input[type="number"]
并使用 DecimalPipe,您应该将值 typeof number
设置为输入控件而不是 typeof string
其中数字管道 returns.
另请注意,这仅在您不阻止事件发射 (emitEvent) 时才有效。否则你应该简单地在你设置值的地方转换你的值。
另请查看 FormControl
的 updateOn 选项。例如设置为 blur
以避免在用户输入过程中出现恼人的变化。
我想弄清楚如何在反应形式中使用管道,以便将输入强制转换为货币格式。我已经为此创建了自己的管道,我已经在代码的其他区域进行了测试,所以我知道它可以作为一个简单的管道使用。我的管道名称是 'udpCurrency'
关于堆栈溢出,我能找到的最接近的答案是这个:
这里是所有相关代码:
模板
<form [formGroup]="myForm" #f="ngForm">
<input class="form-control col-md-6"
formControlName="amount"
[ngModel]="f.value.amount | udpCurrency"
(ngModelChange)="f.value.amount=$event"
placeholder="Amount">
</form>
组件
import { Component, OnInit, HostListener } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
export class MyComponent implements OnInit {
myForm: FormGroup;
constructor(
private builder: FormBuilder
) {
this.myForm = builder.group({
amount: ['', Validators.required]
});
}
}
错误:
ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined: '. Current value: 'undefined: undefined'
在不知道您的管道代码的情况下,很可能会因为您在何处构建该表单而引发错误。
尝试使用 Angular 的更改检测挂钩在输入已解析后设置该值:
export class MyComponent implements OnInit {
myForm: FormGroup;
constructor(private builder: FormBuilder) { }
ngOnInit() {
this.myForm = builder.group({
amount: ['', Validators.required]
});
}
}
这就是混合使用模板驱动表单和响应式表单时可能发生的情况。你有两个相互争斗的绑定。选择模板驱动或反应形式。如果你想走反应路线,你可以使用 [value]
作为你的管道...
请注意,此管道仅用于在视图中显示所需的输出。
<form [formGroup]="myForm">
<input
[value]="myForm.get('amount').value | udpCurrency"
formControlName="amount"
placeholder="Amount">
</form>
我以为我已经成功了,但事实证明,我错了(并接受了一个错误的答案)。我只是以一种更适合我的新方式重写了我的逻辑,并在上面的评论中回答了 Jacob Roberts 的担忧。这是我的新解决方案:
模板:
<form [formGroup]="myForm">
<input formControlName="amount" placeholder="Amount">
</form>
组件:
import { Component, OnInit, HostListener } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { UdpCurrencyMaskPipe } from '../../../_helpers/udp-currency-mask.pipe';
export class MyComponent implements OnInit {
myForm: FormGroup;
constructor(
private builder: FormBuilder,
private currencyMask: UdpCurrencyMaskPipe,
) {
this.myForm = builder.group({
amount: ['', Validators.required]
});
this.myForm.valueChanges.subscribe(val => {
if (typeof val.amount === 'string') {
const maskedVal = this.currencyMask.transform(val.amount);
if (val.amount !== maskedVal) {
this.myForm.patchValue({amount: maskedVal});
}
}
});
}
}
烟斗:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'udpCurrencyMask'
})
export class UdpCurrencyMaskPipe implements PipeTransform {
amount: any;
transform(value: any, args?: any): any {
let amount = String(value);
const beforePoint = amount.split('.')[0];
let integers = '';
if (typeof beforePoint !== 'undefined') {
integers = beforePoint.replace(/\D+/g, '');
}
const afterPoint = amount.split('.')[1];
let decimals = '';
if (typeof afterPoint !== 'undefined') {
decimals = afterPoint.replace(/\D+/g, '');
}
if (decimals.length > 2) {
decimals = decimals.slice(0, 2);
}
amount = integers;
if (typeof afterPoint === 'string') {
amount += '.';
}
if (decimals.length > 0) {
amount += decimals;
}
return amount;
}
}
现在我在这里学到了一些东西。一种是 Jacob 所说的是真实的,另一种方式只在最初有效,但当值发生变化时不会更新。另一个需要注意的非常重要的事情是,与视图管道相比,我需要一种完全不同类型的遮罩管道。例如,视图中的管道可能采用此值“100”并将其转换为“$100.00”,但是您不希望在键入值时发生这种转换,您只希望在完成键入后发生这种情况。出于这个原因,我创建了我的货币掩码管道,它只是删除非数字并将小数限制为两位。
此处的其他答案对我来说效果不佳,但我找到了一种非常有效的方法。您需要在反应形式 valueChanges 订阅中应用管道转换,但不要发出事件,这样它就不会创建递归循环:
this.formGroup.valueChanges.subscribe(form => {
if (form.amount) {
this.formGroup.patchValue({
amount: this.currencyMask.transform(form.amount)
}, {
emitEvent: false
});
}
});
这还需要您的管道 "unformats" 那里的任何东西,通常就像在管道的转换函数中这样简单:
value = value.replace(/$+/g, '');
我打算编写一个自定义控件,但发现通过 ngModelChange
从 FormControl class 覆盖 "onChange" 更容易。 emitViewToModelChange: false
在您的更新逻辑中至关重要,可以避免更改事件的重复循环。所有到货币的管道都发生在组件中,您不必担心出现控制台错误。
<input matInput placeholder="Amount"
(ngModelChange)="onChange($event)" formControlName="amount" />
@Component({
providers: [CurrencyPipe]
})
export class MyComponent {
form = new FormGroup({
amount: new FormControl('', { validators: Validators.required, updateOn: 'blur' })
});
constructor(private currPipe:CurrencyPipe) {}
onChange(value:string) {
const ctrl = this.form.get('amount') as FormControl;
if(isNaN(<any>value.charAt(0))) {
const val = coerceNumberProperty(value.slice(1, value.length));
ctrl.setValue(this.currPipe.transform(val), { emitEvent: false, emitViewToModelChange: false });
} else {
ctrl.setValue(this.currPipe.transform(value), { emitEvent: false, emitViewToModelChange: false });
}
}
onSubmit() {
const rawValue = this.form.get('amount').value;
// if you need to strip the '$' from your input's value when you save data
const value = rawValue.slice(1, rawValue.length);
// do whatever you need to with your value
}
}
<input type="text" name="name" class="form-control mw-120 text-right"
[value]="finalRow.controls[i].get('rental').value |currency">
这是最有效的,因为您没有用 formControlName 覆盖该值。
添加 formControlName 不会总是正常工作,所以需要小心。
<input type="text" name="name" class="form-control mw-120 text-right"
[value]="finalRow.controls[i].get('rental').value |currency" formControlName="rental">
这可能不会给出预期的结果。
仅使用此代码
<input [value]="value | udpCurrency" name="inputField" type="number" formControlName="amount" />
您可以将管道包装到一个指令中。例如:
import { Directive, Input, OnInit } from '@angular/core';
import { NgControl } from '@angular/forms';
// The selector can restrict usage to a specific input.
// For example: 'input[type="number"][yourPipe]' for number type input.
@Directive({ selector: 'input[yourPipeSelector]' })
export class FormControlYourPipeDirective implements OnInit {
/** Your optional pipe args. Same name as directive to directly pass the value. */
@Input() public yourPipeSelector: unknown;
/** Control event options. For example avoid unintentional change events. */
private eventOptions = {
onlySelf: true,
emitEvent: false,
emitViewToModelChange: false,
};
constructor(private ngControl: NgControl, private yourPipe: YourPipe) {}
public ngOnInit(): void {
this.ngControl.control.valueChanges.subscribe((value) => {
this.ngControl.control.setValue(
this.yourPipe.transform(value, this.yourPipeSelector),
this.eventOptions
);
});
}
}
不要忘记将 YourPipe 添加到管道模块中的 providers。 并将管道模块添加到您要使用它的模块的导入中。 (Angular 基础) ... 然后使用它:
<input formControlName="myValue" [yourPipeSelector]="'some args'" />
瞧
但请注意:您操作的输入元素可以由用户再次编辑。该值应该与输入(及其类型)兼容。
例如:如果您有一个 input[type="number"]
并使用 DecimalPipe,您应该将值 typeof number
设置为输入控件而不是 typeof string
其中数字管道 returns.
另请注意,这仅在您不阻止事件发射 (emitEvent) 时才有效。否则你应该简单地在你设置值的地方转换你的值。
另请查看 FormControl
的 updateOn 选项。例如设置为 blur
以避免在用户输入过程中出现恼人的变化。