Anup Sharma
Anup Sharma

Reputation: 2083

Angular Material custom form field control's value is not updated in the calling FormGroup

I made a custom form field control in Angular Material following this guide.

Then I add this control in my FormGroup. But the problem I have here is the FormGroup is not able to get the correct value of the custom control. It always gets undefined. I did check if the correct values are being seeded to the value property in the custom control from the input and it does.

What could be the problem here?

My Custom Control: The Component

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

@Component({
  selector: 'app-test-input',
  templateUrl: './test-input.html',
  styleUrls: ['./test-input.scss'],
  providers: [{ provide: MatFormFieldControl, useExisting: MyTestInput }]
})
export class MyTestInput implements MatFormFieldControl<string>, OnDestroy, ControlValueAccessor {
  static nextId = 0;
  FormGrp: FormGroup;
  stateChanges = new Subject<void>();
  private val: string;
  private ph: string;
  private req = false;
  private dis = false;
  onChange: () => void;
  onTouched: () => void;

  public get value(): string {
    return this.val;
  }

  public set value(val: string) {
    this.val = val;
    this.stateChanges.next();
  }

  controlType = 'my-test-input';

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

  @Input()
  get placeholder() {
    return this.ph;
  }
  set placeholder(plh) {
    this.ph = plh;
    this.stateChanges.next();
  }

  focused = false;

  get empty() {
    return false;
  }

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

  @Input()
  get required() {
    return this.req;
  }
  set required(req) {
    this.req = coerceBooleanProperty(req);
    this.stateChanges.next();
  }

  @Input()
  get disabled(): boolean { return this.dis; }
  set disabled(value: boolean) {
    this.dis = coerceBooleanProperty(value);
    this.stateChanges.next();
  }

  errorState = this.FormGrp == null ? false : this.FormGrp.invalid;

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

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

  onContainerClick(event: MouseEvent) {
    if ((event.target as Element).tagName.toLowerCase() !== 'input') {
      this.elRef.nativeElement.querySelector('input').focus();
    }
    this.onTouched();
  }

  ngOnDestroy() {
    this.stateChanges.complete();
    this.fm.stopMonitoring(this.elRef.nativeElement);
  }


  constructor(
    @Optional() @Self() public ngControl: NgControl,
    fb: FormBuilder, private fm: FocusMonitor, private elRef: ElementRef<HTMLElement>) {
    this.FormGrp = fb.group({
      data: ['', this.required]
    });
    if (ngControl != null) {
      ngControl.valueAccessor = this;
    }
    fm.monitor(elRef.nativeElement, true).subscribe(origin => {
      this.focused = !!origin;
      this.stateChanges.next();
    });
  }

  writeValue(value: any): void {
    this.FormGrp.get('data').setValue(value);
  }

  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  setDisabledState(isDisabled: boolean): void {
    isDisabled ? this.FormGrp.get('data').disable() : this.FormGrp.get('data').enable();
  }

  input() {
    this.value = this.FormGrp.get('data').value;
    this.onChange();
  }
}

The Template:

<div [formGroup]="FormGrp">
    <input formControlName="data" (input)="input()">
</div>

My Calling form:

<form [formGroup]="Form">  
    <mat-form-field>
        <app-test-input formControlName="testControl"></app-test-input>
    </mat-form-field>
    <button>Submit</button>
</form>
<p *ngIf="Form">
    {{Form.value | json}}
</p>

My Calling form definition:

this.Form = fb.group({
  testControl: ['', [Validators.required]]
});

Upvotes: 4

Views: 2708

Answers (1)

yurzui
yurzui

Reputation: 214165

You forgot to pass updated value to onChange method which is part of ControlValueAccessor implementation:

test-input.component.ts

export class TestInputComponent ... {
 onChange = (_: any) => {};

 ...
 input() {
    this.value = this.FormGrp.get('data').value;

    this.onChange(this.value);
                      \/
                  pass newValue
  }

Stackblitz Example

Upvotes: 4

Related Questions