4

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]]
});
2
  • Could you put your code in the stackblitz? Commented Aug 18, 2019 at 12:17
  • @Stefan, as soon as I posted the codes here [stackblitz.com/edit/angular-nfpdqg], yurzui posted the answer. Thanks for the help though. Commented Aug 18, 2019 at 12:57

1 Answer 1

4

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

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.