Search code examples
angularangular-materialform-controlcontrolvalueaccessor

Angular MatFormControl, ControlValueAccessor


I have a custom component, it's a wrap for mat-select. How can I use it in others component's? I was reading about formControl and controlValueAccessor, but I don't understand alot.

my custom.html

    <mat-select>
<mat-option [value]="opt" *ngFor="let op of opt; index as i">{{opt[i]}}</mat-option>
</mat-select>
<input matInput>

my custom.ts

import { Component, OnInit, Input } from '@angular/core';
import { MatFormFieldControl } from '@angular/material/form-field';

@Component({
  selector: 'cust-input',
  templateUrl: './input.component.html',
  styleUrls: ['./input.component.scss'],
  providers: [{provide: MatFormFieldControl, useExisting: InputComponent}]

})
export class InputComponent extends MatFormFieldControl<string> implements OnInit {

  @Input() opt: string;

  setDescribedByIds(ids: string[]): void {
    throw new Error('Method not implemented.');
  }
  onContainerClick(event: MouseEvent): void {
    throw new Error('Method not implemented.');
  }

  constructor() {
    super();
  }

  ngOnInit(): void {
  }

}

my app.html

<mat-form-field>
  <cust-input [opt]="myopt"></cust-input>
</mat-form-field>

my app.ts

import { Component } from '@angular/core';




@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'form';
  myopt = ['a', 'b', 'c', 'd'];



}

I don't need to something like my custmo.html

  <mat-form-field>
     <mat-select>
    <mat-option [value]="opt" *ngFor="let op of opt; index as i">{{opt[i]}}</mat-option>
    </mat-select>
    <input matInput>
    </mat-form-field>

stackbiz


Solution

  • so the answer is: if we want to use our custom wrappe, we need to create a custom form-field-control for it, in our case we need to create custom mat-form-field-control

    //our custom form field control

    import { ValueAccessor } from './value-accessor';
    import { MatFormFieldControl } from '@angular/material/form-field';
    import {
      Input,
      HostBinding,
      Optional,
      Self,
      ElementRef,
      OnDestroy,
      Directive,
    } from '@angular/core';
    import { FocusMonitor } from '@angular/cdk/a11y';
    import { coerceBooleanProperty } from '@angular/cdk/coercion';
    import { NgControl } from '@angular/forms';
    import { Subject } from 'rxjs';
    
    @Directive()
    export class FormControlDirective<T> extends ValueAccessor<T>
      implements MatFormFieldControl<T>, OnDestroy {
      @Input()
      get value() {
        return this._value;
      }
      set value(val: T) {
        if (val !== this._value) {
          this._value = val;
          this.stateChanges.next();
        }
      }
    
      @Input()
      get placeholder() {
        return this._placeholder;
      }
      set placeholder(plc: string) {
        this._placeholder = plc;
        this.stateChanges.next();
      }
    
      @Input()
      get required() {
        return this._required;
      }
      set required(req: boolean) {
        this._required = coerceBooleanProperty(req);
        this.stateChanges.next();
      }
    
      get empty() {
        return !this._value;
      }
    
      constructor(
        @Optional() @Self() public ngControl: NgControl,
        private fM: FocusMonitor,
        private elRef: ElementRef<HTMLElement>
      ) {
        super();
    
        if (this.ngControl != null) {
          this.ngControl.valueAccessor = this;
        }
    
        fM.monitor(elRef.nativeElement, true).subscribe((origin) => {
          this.focused = !!origin;
          this.stateChanges.next();
        });
      }
    
      @HostBinding('class.floating')
      get shouldLabelFloat() {
        return this.focused || !this.empty;
      }
    
      private _value: T | null;
      private _placeholder: string;
      private _required = false;
      nextId = 0;
      stateChanges: Subject<void> = new Subject<void>();
      focused = false;
    
      @HostBinding() id = `${this.nextId++}`;
    
      errorState = false;
      controlType = 'my-select';
      autofilled?: boolean;
    
      @HostBinding('attr.aria-desribedby') describedBy = '';
    
      setDescribedByIds(ids: string[]): void {
        this.describedBy = ids.join('');
      }
      onContainerClick(event: MouseEvent): void {
        if ((event.target as Element).tagName.toLowerCase() === 'input') {
          this.elRef.nativeElement.focus();
        }
      }
    
      ngOnDestroy(): void {
        this.fM.stopMonitoring(this.elRef.nativeElement);
        this.stateChanges.complete();
      }
    }
    

    But, if we want to use ngModel in our wrapper, we also need to create our custom control value accessor

    custo value accesor

    import { ControlValueAccessor } from '@angular/forms';
    
    export class ValueAccessor<T> implements ControlValueAccessor {
      value: T | null;
      onChange: Function;
      onTouched: Function;
      disabled: boolean;
    
      writeValue(val: T): void {
        this.value = val;
      }
      registerOnChange(fn: Function): void {
        this.onChange = fn;
      }
      registerOnTouched(fn: Function): void {
        this.onTouched = fn;
      }
      setDisabledState?(isDisabled: boolean): void {
        this.disabled = isDisabled;
      }
    }
    

    In our custom.wrapper.component.ts we need to extand our form-control, also, look in to custom-form-field-control, and look in to consructor, there is a ngControl, we add it there, for using form-control and value-accessor in one time.

    select.component.ts

    import { Component, Input, OnInit } from '@angular/core';
    import { MatFormFieldControl } from '@angular/material/form-field';
    import { FormControlDirective } from 'src/app/forms/form-control';
    
    @Component({
      selector: 'app-select',
      templateUrl: './select.component.html',
      styleUrls: ['./select.component.scss'],
      providers: [{ provide: MatFormFieldControl, useExisting: SelectComponent }],
    })
    export class SelectComponent extends FormControlDirective<string>
      implements OnInit {
      @Input() option: string;
    
      ngOnInit() {}
    }
    

    in wrapper.component.ts take a look into providers, there, we are telling to Native MatFormFieldControl, something like (Hi, I have my own form-control, let's use it). Next step is to create our wrapp

    select.component.html

    <mat-select [(ngModel)]="value">
      <mat-option [value]="item" *ngFor="let item of option; index as j">{{
        option[j]
      }}</mat-option>
    </mat-select>
    

    So, for now we can use it in other coponents, inside mat-form-field

    app.component.html

    <mat-form-field>
      <mat-label>label</mat-label>
      <app-select [option]="opt" [(ngModel)]="title"></app-select>
    </mat-form-field>
    

    there are some useful links:

    official documintation

    old but very useful post

    And for now, I get another trouble XD Some how I need to use ngModel, exactly in , but It's in the app-select & mat-select at one time. When I finde an answer, I edit this