如何在 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) 时才有效。否则你应该简单地在你设置值的地方转换你的值。

另请查看 FormControlupdateOn 选项。例如设置为 blur 以避免在用户输入过程中出现恼人的变化。