【问题标题】:Angular 2 ngModel mutator directiveAngular 2 ngModel mutator 指令
【发布时间】:2017-06-27 15:48:45
【问题描述】:

我想构建一个指令,它可以改变传入和传出输入的值,并与 ngModel 绑定。

假设我想做一个日期突变,每次模型更改时,突变器首先将值更改为正确的格式(例如“2017-05-03 00:00:00”显示为“2017/ 05/03"),在 ngModel 更新视图之前。当视图更改时,mutator 会在 ngModel 更新模型之前更改值(例如,输入“2017/08/03”会将模型设置为“2017-08-03 00:00:00”[时间戳])。

指令会这样使用:

<input [(ngModel)]="someModel" mutate="date:YYYY/MM/DD" />

我的第一反应是获取 Host 组件上的 ControlValueAccessor 和 NgModel 的引用。

import { Directive, ElementRef, Input, 
         Host, OnChanges, Optional, Self, Inject } from '@angular/core';
import { NgModel, ControlValueAccessor, 
         NG_VALUE_ACCESSOR } from '@angular/forms';


@Directive({ 
    selector: '[mutate]',
})
export class MutateDirective {

    constructor(
        @Host() private _ngModel: NgModel, 
        @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) 
            private _controlValueAccessor: ControlValueAccessor[]
    ){
        console.log('mutute construct', _controlValueAccessor);
    }


}

然后我意识到 Angular 2 Forms 类很复杂,我不知道自己在做什么。有什么想法吗?

更新

根据下面的答案,我想出了解决方案:see gist

用法(需要 Moment JS):

<input  mutate="YYYY/MM/DD" inputFormat="YYYY-MM-DD HH:mm:ss" [(ngModel)]="someDate">

【问题讨论】:

    标签: javascript angular angular2-forms


    【解决方案1】:

    您不必在此处对表单进行任何操作。例如,我制作了一个信用卡屏蔽指令,将用户输入格式化为信用卡字符串(基本上每 4 个字符一个空格)。

    import { Directive, ElementRef, HostListener, Input } from '@angular/core';
    
    @Directive({
      selector: '[credit-card]' // Attribute selector
    })
    export class CreditCard {
    
      @HostListener('input', ['$event'])
      confirmFirst(event: any) {
        let val = event.target.value;
        event.target.value = this.setElement(val);
      }
    
      constructor(public element: ElementRef) { }
    
      setElement(val) {
        let num = '';
        var v = val.replace(/\s+/g, '').replace(/[^0-9]/gi, '');
        var matches = v.match(/\d{4,16}/g);
        var match = matches && matches[0] || '';
        var parts = [];
        for (var i = 0, len = match.length; i < len; i += 4) {
          parts.push(match.substring(i, i + 4));
        }
        if (parts.length) {
          num = parts.join(' ').trim();
        } else {
          num = val.trim();
        }
        return num;
      }
    
    }
    

    然后我在这样的模板中使用它:

    <input credit-card type="text" formControlName="cardNo" />
    

    我在这个例子中使用了表单控件,但无论哪种方式都无关紧要。 ngModel 绑定应该可以正常工作。

    【讨论】:

      【解决方案2】:

      简短的回答:您需要在某个类中实现 ControlValueAccessor 并将其作为带有某些指令的 ngModel 的 NG_VALUE_ACCESSOR 提供。这个 ControlValueAccessor 和指令实际上可以是同一个类。

      TL;DR 这不是很明显,但仍然不是很复杂。下面是我的一个日期控件的骨架。这个东西充当 angular 1 ng-model 的解析器/格式化程序对。

      这一切都始于 ngModel 将所有 NG_VALUE_ACCESSOR 注入自身。还有一堆默认提供者,它们都被注入到 ngModel 构造函数中,但是 ngModel 可以区分默认值访问器和用户提供的访问器。所以它选择了一个与之合作。大致看起来像这样:如果有用户的值访问器,那么它将被选中,否则它会退回到从默认值中选择。完成初始设置后。

      控制值访问器应订阅输入元素上的“输入”或其他类似事件,以处理来自它的输入事件。

      当值在外部被改变时,ngModel 在初始化期间选择的值访问器上调用 writeValue() 方法。此方法负责渲染显示值,该值将作为字符串显示给用户。

      在某些时候(通常在模糊事件中)控件可以被标记为已触摸。这也显示出来了。

      请注意:下面的代码不是真正的生产代码,它没有经过测试,它可能包含一些差异或不准确之处,但总的来说它展示了这种方法的整体思想。

      import {
          Directive,
          Input,
          Output,
          SimpleChanges,
          ElementRef,
          Renderer,
          EventEmitter,
          OnInit,
          OnDestroy,
          OnChanges,
          forwardRef
      } from '@angular/core';
      import {Subscription, Observable} from 'rxjs';
      import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
      
      const DATE_INPUT_VALUE_ACCESSOR_PROVIDER = [
          {provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DateInputDirective), multi: true}
      ];
      
      @Directive({
          // [date-input] is just to distinguish where exactly to place this control value accessor
          selector: 'input[date-input]',
          providers: [DATE_INPUT_VALUE_ACCESSOR_PROVIDER],
          host: { 'blur': 'onBlur()', 'input': 'onChange($event)' }
      })
      export class DateInputDirective implements ControlValueAccessor, OnChanges {
      
          @Input('date-input')
          format: string;
      
          model: TimeSpan;
      
          private _onChange: (value: Date) => void = () => {
          };
      
          private _onTouched: () => void = () => {
          };
      
          constructor(private _renderer: Renderer,
                      private _elementRef: ElementRef,
                      // something that knows how to parse value
                      private _parser: DateParseTranslator,
                      // something that knows how to format it back into string
                      private _formatter: DateFormatPipe) {
          }
      
          ngOnInit() {
      
          }
      
          ngOnChanges(changes: SimpleChanges) {
              if (changes['format']) {
                  this.updateText(this.model, true);
              }
          }
      
          onBlur = () => {
              this.updateText(this.model, false);
              this.onTouched();
          };
      
          onChange = ($event: KeyboardEvent) => {
              // the value of an input - don't remember exactly where it is in the event
              // so this part may be incorrect, please check
              let value = $event.target.value;
              let date = this._parser.translate(value);
              this._onChange(date);
          };
      
          onTouched = () => {
              this._onTouched();
          };
      
          registerOnChange = (fn: (value: Date) => void): void => {
              this._onChange = fn;
          };
      
          registerOnTouched = (fn: () => void): void => {
              this._onTouched = fn;
          };
      
          writeValue = (value: Date): void => {
              this.model = value;
              this.updateText(value, true);
          };
      
          updateText = (date: Date, forceUpdate = false) => {
              let textValue = date ? this._formatter.transform(date, this.format) : '';
              if ((!date || !textValue) && !forceUpdate) {
                  return;
              }
              this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', textValue);
          }
      
      }
      

      然后在html模板中:

      <input date-input="DD/MM/YYYY" [(ngModel)]="myModel"/>
      

      【讨论】:

      • 谢谢。这是一种享受。我希望不必处理特定的输入元素 (event.target.value),因此它可以与 ngModel 支持的任何类型的输入兼容。也许通过抓取现有的默认 ControlValueAccessor 并在将其提供给 NgModel 之前对其进行包装。根据您的代码,我想出了这个用于变异日期,GIST
      • 你不能那样做。如果您查看 ng2 源代码,您会发现它们做同样的事情 - 它们只是为不同类型的输入提供了一堆值访问器。
      猜你喜欢
      • 2016-06-23
      • 1970-01-01
      • 2017-06-24
      • 2016-10-02
      • 2015-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多