简体   繁体   中英

angular material 2 custom component with ng value accessor

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

那些不使用表单生成器或响应表单的用户,请在输入字段中使用“ ngDefaultControl”作为属性。

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM