7

The task I am trying to solve:

Create re-usable input component wrappers to save time when writing template for forms.

Example of what I mean:

Instead of having to write the following template:

<form [formGroup]="myForm">
  <mat-form-field>
    <input matInput placeholder="Email" [formControl]="email" required>
    <mat-error *ngIf="email.invalid">{{getErrorMessage()}}</mat-error>
  </mat-form-field> 
<form>

I would like to write:

<form [formGroup]="myForm">
    <my-input-component [form]="myForm" [myFormControl]="email" [myFormControlName]="'email'" [label]="'Email'"></my-input-component>
</form>

Where my-input-component looks like:

<mat-form-field [formGroup]="form">
    <input
        matInput
        type="text"
        [attr.inputmode]="inputMode"
        [placeholder]="label"
        [formControlName]="myFormControlName"
    />
    <mat-error class="errors" *ngIf="myFormControl.invalid">
        <div>{{ getError() }}</div>
    </mat-error>
</mat-form-field>

This is working as is but I don't know if this is a good approach to pass the FormGroup and FormControls around as bindings.

After searching online I have continuously come across NG_CONTROL_VALUE_ACCESSOR and was a bit confused if that could or should be used in my scenario.

I don't intend for these components to be "custom" in the sense of using a slider as a form control or something of that nature, but rather just wanted "wrappers" to save some time.

Any suggestions or advice on the topic would be greatly appreciated!

2
  • Idk, doesn't look like it's providing a ton of benefit given the complexity of passing things around and the markup. Plus, getErrorMessage() is a contract not explicitly defined or enforced as far as I can see; that's problematic IMHO. Considering there's a finite set of input types you'll have to deal with, have you considered making an EmailInputComponent for example that can be used in an obvious way by anyone needing an email input type? Commented Jul 11, 2019 at 0:08
  • @ChiefTwoPencils You're correct about getErrorMessage(), my thought was that this function inside would cover any validation type that would be applied (would have a default case for one-offs). Also, my intention is to have a few of these wrappers, one for each common type e.g. text input, number input, select dropdown, etc. Commented Jul 11, 2019 at 0:12

1 Answer 1

6

The recommended way to achieve this is, like you have already found out, to implement the ControlValueAccessor interface. This interface was created specifically to create custom form controls. It will create a bridge between your reusable component and the Forms API.

Here's a small example of a reusable input field with a label. You could add your error messages in this template as well.

Component

import { Component, OnInit, Input, Self, Optional } from '@angular/core';
import { ControlValueAccessor, NgControl } from '@angular/forms';

@Component({
  selector: 'custom-input',
  templateUrl: './custom-input.component.html',
  styleUrls: ['./custom-input.component.css']
})
export class CustomInputComponent implements OnInit, ControlValueAccessor {
  @Input() disabled: boolean;
  @Input() label: string;
  @Input() placeholder: string = '';
  @Input() type: 'text' | 'email' | 'password' = 'text';

  value: any = '';

  constructor(
    // Retrieve the dependency only from the local injector,
    // not from parent or ancestors.
    @Self()
    // We want to be able to use the component without a form,
    // so we mark the dependency as optional.
    @Optional()
    private ngControl: NgControl
  ) {
    if (this.ngControl) {
      this.ngControl.valueAccessor = this;
    }
  }

  ngOnInit() {}

  /**
   * Write form value to the DOM element (model => view)
   */
  writeValue(value: any): void {
    this.value = value;
  }

  /**
   * Write form disabled state to the DOM element (model => view)
   */
  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  /**
   * Update form when DOM element value changes (view => model)
   */
  registerOnChange(fn: any): void {
    // Store the provided function as an internal method.
    this.onChange = fn;
  }

  /**
   * Update form when DOM element is blurred (view => model)
   */
  registerOnTouched(fn: any): void {
    // Store the provided function as an internal method.
    this.onTouched = fn;
  }

  private onChange() {}
  private onTouched() {}
}

Template

<label>{{ value }}</label>
<input [type]="type"
       [placeholder]="placeholder"
       [value]="value"
       [disabled]="disabled"
       (input)="onChange($event.target.value)"
       (blur)="onTouched()" />

You can check out this article Creating a custom form component in Angular for more detailed information.

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

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.