带有 ng 值访问器的角度材料 2 自定义组件 [英] angular material 2 custom component with ng value accessor

查看:25
本文介绍了带有 ng 值访问器的角度材料 2 自定义组件的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

任务:

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

问题:

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

dashboard.component.js

this.CRForm = this.fb.group({产品团队:[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}} -- 给出值 eg:[P12DT1H2M] 并且仅当 ngDefaultControl{{CRForm['controls']['productTeam']['value']}} --没有给出任何

mat-custom-form-field.ts

import {成分,初始化,在销毁,输入,主机绑定,可选的,渲染器2,自己,转发参考,元素引用来自'@angular/core';进口 {MatForm字段控件来自'@angular/material';进口 {控制值访问器,表单组,表单生成器,控制,NG_VALUE_ACCESSOR来自'@angular/forms';进口 {强制布尔属性来自'@angular/cdk/coercion';进口 {焦点监视器来自'@angular/cdk/a11y';进口 {主题来自rxjs/主题";课时{构造函数(公共天数:数字,公共时间:数字,公共分钟数:数字) {}获取持续时间(){返回 '​​P' + (this.days || 0) + 'DT' + (this.hours || 0) + 'H' +(this.minutes || 0) + 'M';}setDuration() {}}@成分({选择器:'app-mat-custom-form-field',templateUrl: './mat-custom-form-field.component.html',styleUrls: ['./mat-custom-form-field.component.scss'],提供者:[{提供:MatFormFieldControl,useExisting: MatCustomFormFieldComponent},{提供:NG_VALUE_ACCESSOR,useExisting: forwardRef(() => MatCustomFormFieldComponent),多:真}]})导出类 MatCustomFormFieldComponent 实现 OnInit,MatFormFieldControl <持续时间, ControlValueAccessor, OnDestroy {部分:FormGroup;重点=假;stateChanges = 新主题 <空白 >();错误状态 = 假;controlType = '我的电话输入';私人 _disabled = 假;私人 _required = false;私有_占位符:字符串;静态 nextId = 0;@输入()获得必需(){返回 this._required;}设置必需(请求){this._required = coerceBooleanProperty(req);this.stateChanges.next();}@输入()被禁用(){返回 this._disabled;}设置禁用(dis){this._disabled = coerceBooleanProperty(dis);this.stateChanges.next();}/*占位符属性的代码*/@输入()获取占位符(){返回 this._placeholder;}设置占位符(plh){this._placeholder = plh;this.stateChanges.next();}@输入()获取值():持续时间 |空值 {让 n = this.parts.value;if (n.days && n.hours && n.minutes) {返回新的持续时间(n.days, n.hours, n.minutes);}返回空;}设置值(持续时间:持续时间 | null){持续时间 = 持续时间 ||新的持续时间(0, 0, 0);this.parts.setValue({天:持续时间.天,小时:持续时间.小时,分钟:持续时间.分钟});this.writeValue('P' + (duration.days || 0) + 'DT' + (duration.hours || 0) +'H' + (duration.minutes || 0) + 'M');this.stateChanges.next();}onContainerClick(事件:MouseEvent){if ((event.target as Element).tagName.toLowerCase() != 'input') {this.elRef.nativeElement.querySelector('input').focus();}}/* 获取id和设置id的代码*/@HostBinding() id = `mat-custom-form-字段-${MatCustomFormFieldComponent.nextId++}`;@HostBinding('class.floating')得到 shouldPlaceholderFloat() {返回 this.focused ||!this.empty;}@HostBinding('attr.aria-描述的')描述的 = '';setDescribedByIds(ids: string[]) {this.scribedBy = ids.join(' ');}构造函数(fb:FormBuilder,私有fm:FocusMonitor,私有elRef:元素参考,渲染器:Renderer2,公共 ngControl:NgControl,){fm.monitor(elRef.nativeElement, renderer, true).subscribe(origin => {this.focused = !!origin;this.stateChanges.next();});ngControl.valueAccessor = this;this.parts = fb.group({'天': '','小时': '','分钟': '',});}ngOnInit() {}ngOnDestroy() {this.stateChanges.complete();this.fm.stopMonitoring(this.elRef.nativeElement);}得到空(){让 n = this.parts.value;返回 !n.area &&!n.exchange &&!n.订阅者;}私有传播更改 = (_: any) =>{};公共写值(a:任何){如果 (a !== 未定义) {this.parts.setValue({天数:a.substring(a.lastIndexOf("P") + 1, a.lastIndexOf("D")),小时:a.substring(a.lastIndexOf("T") + 1, a.lastIndexOf("H")),分钟:a.substring(a.lastIndexOf("H") + 1, a.lastIndexOf("M"))});}};公共 registerOnChange(fn: any) {this.propagateChange = fn;}//未使用,用于触摸输入公共 registerOnTouched() {}//更改文本区域中的事件}

mat-custom-form-field.html

<div[formGroup]="零件"><input class="area" formControlName="days" size="3"><跨度>&ndash;</跨度><input class="exchange" formControlName="hours" size="3"><跨度>&ndash;</跨度><input class="subscriber" formControlName="minutes" size="3"></div>

解决方案

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

public writeValue(a: string) {if (a && a !== '') {this.parts.setValue({天数:a.substring(a.lastIndexOf('P') + 1, a.lastIndexOf('D')),小时:a.substring(a.lastIndexOf('T') + 1, a.lastIndexOf('H')),分钟:a.substring(a.lastIndexOf('H') + 1, a.lastIndexOf('M'))});}}

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

测试表格

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

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

 @Component({选择器:'app-root',templateUrl: './app.component.html',styleUrls: ['./app.component.css']})导出类 AppComponent {标题 = '应用';数据:字符串;productForm:FormGroup;构造函数(私人FB:FormBuilder){this.productForm = this.fb.group({productTeam: [null]//也可以是像 P12DT2H231M 这样的值});}点击(){this.productForm.controls['productTeam'].patchValue('P12DT2H231M');}}

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

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

this.parts = fb.group({'天': '','小时': '','分钟': '',});this.subs.push(this.parts.valueChanges.subscribe((value: Duration) => {this.propagateChange(value);}));

为了以防万一,我也会在这里留下 product-team-field.component.ts 和 Duration 类的完整代码:

duration.ts

class Duration {构造函数(公共天数:数字,公共时间:数字,公共分钟数:数字) {}toString() {返回 '​​P' + (this.days || 0) + 'DT' + (this.hours || 0) +'H' + (this.minutes || 0) + 'M';}}

product-team-field.component.ts

@Component({选择器:产品团队输入",templateUrl: './product-team-field.component.html',styleUrls: ['./product-team-field.component.css'],提供者:[{提供:MatFormFieldControl,使用现有:ProductTeamControl},{提供:NG_VALUE_ACCESSOR,useExisting: forwardRef(() => ProductTeamControl),多:真}]})导出类 ProductTeamControl 实现 OnInit、OnDestroy、ControlValueAccessor、MatFormFieldControl{静态 nextId = 0;ngControl = 空;部分:FormGroup;重点=假;stateChanges = new Subject();错误状态 = 假;controlType = '产品团队输入';私人 _disabled = 假;私人 _required = false;私有_占位符:字符串;@输入()获得必需(){返回 this._required;}设置必需(请求){this._required = coerceBooleanProperty(req);this.stateChanges.next();}@输入()被禁用(){返回 this._disabled;}设置禁用(dis){this._disabled = coerceBooleanProperty(dis);this.stateChanges.next();}@输入()获取占位符(){返回 this._placeholder;}设置占位符(plh){this._placeholder = plh;this.stateChanges.next();}@输入()获取值():持续时间 |空值 {const n = this.parts.value;if (n.days && n.hours && n.minutes) {返回新的持续时间(n.days, n.hours, n.minutes);}返回空;}设置值(持续时间:持续时间 | null){持续时间 = 持续时间 ||新的持续时间(0, 0, 0);this.writeValue(duration.toString());this.stateChanges.next();}onContainerClick(事件:MouseEvent){if ((event.target as Element).tagName.toLowerCase() !== 'input') {this.elRef.nativeElement.querySelector('input').focus();}}@HostBinding() id = `${this.controlType}-${ProductTeamControl.nextId++}`;@HostBinding('class.floating')得到 shouldPlaceholderFloat() {返回 this.focused ||!this.empty;}@HostBinding('attr.aria-描述的')描述的 = '';setDescribedByIds(ids: string[]) {this.describesBy = ids.join(' ');}私人订阅:订阅[] = [];构造函数(私人 fb:FormBuilder,私人调频:FocusMonitor,私有 elRef: ElementRef,渲染器:渲染器2){this.subs.push(fm.monitor(elRef.nativeElement, renderer, true).subscribe(origin => {this.focused = !!origin;this.stateChanges.next();}));this.parts = fb.group({'天': '','小时': '','分钟': '',});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);}得到空(){const n = this.parts.value;返回 !n.area &&!n.exchange &&!n.订阅者;}私有传播更改 = (_: any) =>{ };公共写值(a:字符串){if (a && a !== '') {this.parts.setValue({天数:a.substring(a.lastIndexOf('P') + 1, a.lastIndexOf('D')),小时:a.substring(a.lastIndexOf('T') + 1, a.lastIndexOf('H')),分钟:a.substring(a.lastIndexOf('H') + 1, a.lastIndexOf('M'))});}}公共 registerOnChange(fn: any) {this.propagateChange = fn;}公共 registerOnTouched(fn: any): void {返回;}public setDisabledState?(isDisabled: boolean): void {this.disabled = isDisabled;}}

I a working on angular 4.4 + material beta12 custom component and not able to figure out what is wrong in my implementation

I am trying to achieve the below custom input

Task:

  1. set value to formControl, once I got data from server(data.productTeam is data-can see in code)
  2. on edit, formcontrol should be updated with values (eg:P12DT2H231M)

Issues:

  1. I am not able to bind default value to formcontrol.
  2. Without ngDefaultControl (No value accessor for form control with name: 'productTeam' error occuring)

dashboard.component.js

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

In 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>

解决方案

First of all i modified your write value fn a bit cause it didn't work for me in case of 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'))
      });
    }
  }

Custom component template stays the same. I consume this component in a sample form like this:

Form for tests

<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 sets up the default value for our control (solving point 1) and also contains a simple click method which emulates the situation when you load your data from the server.

 @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');
  }
}

With this setup you are already able to work with your component and the default value will be set but you won't receive any changes yet.

In order to receive changes in your parent form you need to propagate them using propagateChange callback which is registered in your component(to solve point 2). So the main change to your component code will be a subscription to changes of the component internal form group from which you will propagate it to the upper level:

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

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

And i will also leave here the full code of the product-team-field.component.ts and Duration class just in case:

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';
    }

}

product-team-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;
  }
}

这篇关于带有 ng 值访问器的角度材料 2 自定义组件的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆