【问题标题】:How to use pipes in Angular 5 reactive form input如何在 Angular 5 反应式表单输入中使用管道
【发布时间】:2018-09-06 10:22:52
【问题描述】:

我试图弄清楚如何在反应形式中使用管道,以便将输入强制转换为货币格式。我已经为此创建了自己的管道,我已经在代码的其他区域进行了测试,所以我知道它可以作为一个简单的管道工作。我的管道名称是“udpCurrency”

我能找到的关于堆栈溢出的最接近的答案是这个:Using Pipes within ngModel on INPUT Elements in Angular2-View 但是这在我的情况下不起作用,我怀疑这与我的表单是反应性的事实有关

以下是所有相关代码:

模板

<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 angular-reactive-forms angular-pipe


【解决方案1】:

在不知道您的管道代码的情况下,它可能会因为您在哪里构建该表单而引发错误。

在解析输入后尝试使用 Angular 的更改检测钩子设置该值:

export class MyComponent implements OnInit {
  myForm: FormGroup;

  constructor(private builder: FormBuilder) { }    

  ngOnInit() {
    this.myForm = builder.group({
      amount: ['', Validators.required]
    });
  }

}

【讨论】:

  • 我的表单构建发生在构造函数中,但是它依赖于从 http 请求中检索到的其他数据,因此即使它不在 ngOnInit 挂钩中,我相信它是在 ngOnInit 之后调用的。作为测试,我将建筑物移到 ngOnInit 内,但我仍然遇到错误
【解决方案2】:

当您混合模板驱动表单和响应式表单时,会发生这种情况。你有两个相互争斗的绑定。选择模板驱动或反应形式。如果你想走响应式路线,你可以使用[value] 作为你的管道......

请注意,此管道仅用于在视图中显示所需的输出。

<form [formGroup]="myForm">
  <input 
    [value]="myForm.get('amount').value | udpCurrency"
    formControlName="amount" 
    placeholder="Amount">
</form>

【讨论】:

  • 这非常有帮助,谢谢。我发现一堆关于这个主题的帖子都过于复杂,但这很有效。
  • 这最初可以工作,但是当您编辑值时,管道不会重新应用 onblur。这是必须明确处理的事情吗?
  • @JacobRoberts 抱歉,我已经有几个月没有看到你的评论了。我终于明白你在说什么,你是对的。如果您仍然遇到同样的问题,我在下面有一个新答案可能会有所帮助。
  • 这不能按预期工作,因为表单值更改在管道转换之前触发。显示的值看起来正确,但表单值会有所不同。
  • 这不起作用,因为在表单更新视图上的模型后,货币管道再次更新模型并最终尝试通过管道传输字符串值,这是我得到错误的地方。在下面查看我的解决方案以避免这种情况。
【解决方案3】:

我以为我可以做到这一点,但事实证明,我错了(并接受了错误的答案)。我只是以一种更适合我的新方式重新编写了我的逻辑,并在上面的 cmets 中回答了 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”,但是您不希望在输入值时发生这种转换,您只希望在完成输入后发生这种情况。出于这个原因,我创建了我的货币掩码管道,它只是删除了非数字数字并将小数点限制在两个位置。

【讨论】:

    【解决方案4】:

    这里的其他答案对我来说不起作用,但我找到了一种非常有效的方法。您需要在响应式表单 valueChanges 订阅中应用管道转换,但不要发出事件,因此它不会创建递归循环:

    this.formGroup.valueChanges.subscribe(form => {
      if (form.amount) {
        this.formGroup.patchValue({
          amount: this.currencyMask.transform(form.amount)
        }, {
          emitEvent: false
        });
      }
    });
    

    这还要求您的管道“取消格式化”那里的任何内容,这通常就像在管道的转换函数中一样简单:

    value = value.replace(/\$+/g, '');
    

    【讨论】:

    • 它不起作用。如果您通过单击 Backspace 键更改值。比如原来的值是3,你想改变它,点击退格,变成30。再点击退格变成300,以此类推。
    【解决方案5】:

    我打算编写一个自定义控件,但发现通过 ngModelChange 覆盖 FormControl 类中的“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
      }
    }
    

    【讨论】:

      【解决方案6】:
      <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">
      

      这可能不会得到预期的结果。

      【讨论】:

        【解决方案7】:

        仅使用此代码

        <input [value]="value | udpCurrency"  name="inputField" type="number" formControlName="amount" />
        

        【讨论】:

        • 请详细说明您的答案,而不仅仅是粘贴代码,而是让提问者有机会了解您在做什么。
        猜你喜欢
        • 1970-01-01
        • 2023-03-21
        • 1970-01-01
        • 2018-05-31
        • 2021-09-21
        • 2018-12-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多