angular material 2 个带有 ng 值访问器的自定义组件

angular material 2 custom component with ng value accessor

我正在研究 angular 4.4 + material beta12 自定义组件,但无法弄清楚我的实现有什么问题

我正在尝试实现以下自定义输入

任务:

  1. 设置值给formControl,一旦我从服务器获取数据(data.productTeam是数据-可以在代码中看到)
  2. 编辑时,formcontrol 应使用值更新(例如:P12DT2H231M)

问题:

  1. 我无法将默认值绑定到 formcontrol。
  2. 没有 ngDefaultControl(没有名称为表单控件的值访问器:'productTeam' 发生错误)

dashboard.component.js

this.CRForm = this.fb.group({
      productTeam: [data.productTeam || '']
});

在Dashboard.html

<mat-form-field  floatPlaceholder="always" >
        <app-mat-custom-form-field #custref formControlName="productTeam" placeholder="P12D" ></app-mat-custom-form-field>
    <!--<app-mat-custom-form-field #custref formControlName="productTeam" placeholder="P12D" ngDefaultControl></app-mat-custom-form-field> -->
      </mat-form-field>
 {{custref.value}} -- gives value eg:[P12DT1H2M] and only if ngDefaultControl
 {{CRForm['controls']['productTeam']['value']}} --not giving any

mat-custom-form-field.ts

import {
  Component,
  OnInit,
  OnDestroy,
  Input,
  HostBinding,
  Optional,
  Renderer2,
  Self,
  forwardRef,
  ElementRef
} from '@angular/core';
import {
  MatFormFieldControl
} from '@angular/material';
import {
  ControlValueAccessor,
  FormGroup,
  FormBuilder,
  NgControl,
  NG_VALUE_ACCESSOR
} from '@angular/forms';
import {
  coerceBooleanProperty
} from '@angular/cdk/coercion';
import {
  FocusMonitor
} from '@angular/cdk/a11y';
import {
  Subject
} from 'rxjs/Subject';

class Duration {
  constructor(public days: number, public hours: number, public minutes:
    number) {}
  getDuration() {
    return 'P' + (this.days || 0) + 'DT' + (this.hours || 0) + 'H' +
      (this.minutes || 0) + 'M';
  }
  setDuration() {}
}
@Component({
  selector: 'app-mat-custom-form-field',
  templateUrl: './mat-custom-form-field.component.html',
  styleUrls: ['./mat-custom-form-field.component.scss'],
  providers: [{
      provide: MatFormFieldControl,
      useExisting: MatCustomFormFieldComponent
    },
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => MatCustomFormFieldComponent),
      multi: true
    }
  ]
})
export class MatCustomFormFieldComponent implements OnInit,
MatFormFieldControl < Duration > , ControlValueAccessor, OnDestroy {
  parts: FormGroup;
  focused = false;
  stateChanges = new Subject < void > ();
  errorState = false;
  controlType = 'my-tel-input';
  private _disabled = false;
  private _required = false;
  private _placeholder: string;
  static nextId = 0;
  @Input()
  get required() {
    return this._required;
  }
  set required(req) {
    this._required = coerceBooleanProperty(req);
    this.stateChanges.next();
  }
  @Input()
  get disabled() {
    return this._disabled;
  }
  set disabled(dis) {
    this._disabled = coerceBooleanProperty(dis);
    this.stateChanges.next();
  }
  /* code for placeholder property */
  @Input()
  get placeholder() {
    return this._placeholder;
  }
  set placeholder(plh) {
    this._placeholder = plh;
    this.stateChanges.next();
  }
  @Input()
  get value(): Duration | null {
    let n = this.parts.value;
    if (n.days && n.hours && n.minutes) {
      return new Duration(n.days, n.hours, n.minutes);
    }
    return null;
  }

  set value(duration: Duration | null) {
    duration = duration || new Duration(0, 0, 0);
    this.parts.setValue({
      days: duration.days,
      hours: duration.hours,
      minutes: duration.minutes
    });
    this.writeValue('P' + (duration.days || 0) + 'DT' + (duration.hours || 0) +
      'H' + (duration.minutes || 0) + 'M');
    this.stateChanges.next();
  }
  onContainerClick(event: MouseEvent) {
    if ((event.target as Element).tagName.toLowerCase() != 'input') {
      this.elRef.nativeElement.querySelector('input').focus();
    }
  }

  /* code to get id and set id*/

  @HostBinding() id = `mat-custom-form-
    field-${MatCustomFormFieldComponent.nextId++}`;

  @HostBinding('class.floating')
  get shouldPlaceholderFloat() {
    return this.focused || !this.empty;
  }

  @HostBinding('attr.aria-describedby') describedBy = '';

  setDescribedByIds(ids: string[]) {
    this.describedBy = ids.join(' ');
  }

  constructor(fb: FormBuilder, private fm: FocusMonitor, private elRef:
    ElementRef,
    renderer: Renderer2, public ngControl: NgControl, ) {
    fm.monitor(elRef.nativeElement, renderer, true).subscribe(origin => {
      this.focused = !!origin;
      this.stateChanges.next();
    });
    ngControl.valueAccessor = this;
    this.parts = fb.group({
      'days': '',
      'hours': '',
      'minutes': '',
    });
  }

  ngOnInit() {}

  ngOnDestroy() {
    this.stateChanges.complete();
    this.fm.stopMonitoring(this.elRef.nativeElement);
  }
  get empty() {
    let n = this.parts.value;
    return !n.area && !n.exchange && !n.subscriber;
  }
  private propagateChange = (_: any) => {};

  public writeValue(a: any) {
    if (a !== undefined) {
      this.parts.setValue({
        days: a.substring(a.lastIndexOf("P") + 1, a.lastIndexOf("D")),
        hours: a.substring(a.lastIndexOf("T") + 1, a.lastIndexOf("H")),
        minutes: a.substring(a.lastIndexOf("H") + 1, a.lastIndexOf("M"))
      });
    }
  };
  public registerOnChange(fn: any) {
    this.propagateChange = fn;
  }

  // not used, used for touch input
  public registerOnTouched() {}
  // change events from the textarea
}

mat-custom-form-field.html

< div[formGroup]="parts">
  < input class="area" formControlName="days" size="3">
    < span> & ndash; < /span>
    < input class="exchange" formControlName="hours" size="3">
    < span> & ndash; < /span>
    < input class="subscriber" formControlName="minutes" size="3">
  < /div>

首先,我稍微修改了您的写入值 fn,因为它在 null 的情况下对我不起作用:

public writeValue(a: string) {
    if (a && a !== '') {
      this.parts.setValue({
        days: a.substring(a.lastIndexOf('P') + 1, a.lastIndexOf('D')),
        hours: a.substring(a.lastIndexOf('T') + 1, a.lastIndexOf('H')),
        minutes: a.substring(a.lastIndexOf('H') + 1, a.lastIndexOf('M'))
      });
    }
  }

自定义组件模板保持不变。 我以这样的示例形式使用此组件:

考试表格

<div>
  <form #form="ngForm" [formGroup]="productForm">
    <mat-form-field>
      <product-team-input formControlName="productTeam" placeholder="P12D" ></product-team-input>
    </mat-form-field>
  </form>
  {{ form.value | json }}
</div>
     

Simple AppComponent 为我们的控件设置默认值(解决点 1),还包含一个简单的单击方法,它模拟从服务器加载数据时的情况。

 @Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'app';

  data: string;
  productForm: FormGroup;

  constructor(private fb: FormBuilder) {
    this.productForm = this.fb.group({
      productTeam: [null] // can be value like P12DT2H231M as well
    });
  }
  onClick() {
    this.productForm.controls['productTeam'].patchValue('P12DT2H231M');
  }
}

通过此设置,您已经可以使用您的组件,并且将设置默认值,但您还不会收到任何更改。

为了在您的父表单中接收更改,您需要使用在您的组件中注册的 propagateChange 回调来传播它们(以解决第 2 点)。 因此,对组件代码的主要更改将是订阅组件内部表单组的更改,您将从中将其传播到上层:

this.parts = fb.group({
  'days': '',
  'hours': '',
  'minutes': '',
});

this.subs.push(this.parts.valueChanges.subscribe((value: Duration) => {
  this.propagateChange(value);
}));

我也会在这里留下产品团队的完整代码-field.component.ts 和持续时间 class 以防万一:

duration.ts

class Duration {
      constructor(public days: number, public hours: number, public minutes:
        number) {
         }

    toString() {
      return 'P' + (this.days || 0) + 'DT' + (this.hours || 0) +
      'H' + (this.minutes || 0) + 'M';
    }

}

产品团队-field.component.ts

@Component({
  selector: 'product-team-input',
  templateUrl: './product-team-field.component.html',
  styleUrls: ['./product-team-field.component.css'],
  providers: [{
    provide: MatFormFieldControl,
    useExisting: ProductTeamControl
  },
  {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => ProductTeamControl),
    multi: true
  }]
})
export class ProductTeamControl implements OnInit, OnDestroy, ControlValueAccessor, MatFormFieldControl<Duration> {
  static nextId = 0;
  ngControl = null;
  parts: FormGroup;
  focused = false;
  stateChanges = new Subject<void>();
  errorState = false;
  controlType = 'product-team-input';
  private _disabled = false;
  private _required = false;
  private _placeholder: string;

  @Input()
  get required() {
    return this._required;
  }
  set required(req) {
    this._required = coerceBooleanProperty(req);
    this.stateChanges.next();
  }
  @Input()
  get disabled() {
    return this._disabled;
  }
  set disabled(dis) {
    this._disabled = coerceBooleanProperty(dis);
    this.stateChanges.next();
  }
  @Input()
  get placeholder() {
    return this._placeholder;
  }
  set placeholder(plh) {
    this._placeholder = plh;
    this.stateChanges.next();
  }
  @Input()
  get value(): Duration | null {
    const n = this.parts.value;
    if (n.days && n.hours && n.minutes) {
      return new Duration(n.days, n.hours, n.minutes);
    }
    return null;
  }

  set value(duration: Duration | null) {
    duration = duration || new Duration(0, 0, 0);
    this.writeValue(duration.toString());
    this.stateChanges.next();
  }
  onContainerClick(event: MouseEvent) {
    if ((event.target as Element).tagName.toLowerCase() !== 'input') {
      this.elRef.nativeElement.querySelector('input').focus();
    }
  }

  @HostBinding() id = `${this.controlType}-${ProductTeamControl.nextId++}`;

  @HostBinding('class.floating')
  get shouldPlaceholderFloat() {
    return this.focused || !this.empty;
  }

  @HostBinding('attr.aria-describedby') describedBy = '';

  setDescribedByIds(ids: string[]) {
    this.describedBy = ids.join(' ');
  }

  private subs: Subscription[] = [];

  constructor(
    private fb: FormBuilder,
    private fm: FocusMonitor,
    private elRef: ElementRef,
    renderer: Renderer2) {

    this.subs.push(fm.monitor(elRef.nativeElement, renderer, true).subscribe(origin => {
      this.focused = !!origin;
      this.stateChanges.next();
    }));

    this.parts = fb.group({
      'days': '',
      'hours': '',
      'minutes': '',
    });

    this.subs.push(this.parts.valueChanges.subscribe((value: Duration) => {
      this.propagateChange(value);
    }));
  }

  ngOnInit() { }

  ngOnDestroy() {
    this.stateChanges.complete();
    this.subs.forEach(s => s.unsubscribe());
    this.fm.stopMonitoring(this.elRef.nativeElement);
  }
  get empty() {
    const n = this.parts.value;
    return !n.area && !n.exchange && !n.subscriber;
  }

  private propagateChange = (_: any) => { };

  public writeValue(a: string) {
    if (a && a !== '') {
      this.parts.setValue({
        days: a.substring(a.lastIndexOf('P') + 1, a.lastIndexOf('D')),
        hours: a.substring(a.lastIndexOf('T') + 1, a.lastIndexOf('H')),
        minutes: a.substring(a.lastIndexOf('H') + 1, a.lastIndexOf('M'))
      });
    }
  }
  public registerOnChange(fn: any) {
    this.propagateChange = fn;
  }

  public registerOnTouched(fn: any): void {
    return;
  }

  public setDisabledState?(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }
}

那些不使用表单生成器或反应式表单的人,请在您的输入字段中使用 "ngDefaultControl" 作为属性。