Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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
enter image description here

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>
like image 765
bharath muppa Avatar asked Oct 30 '17 16:10

bharath muppa


1 Answers

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;
  }
}
like image 70
n.yakovenko Avatar answered Oct 12 '22 23:10

n.yakovenko