Travesty3
Travesty3

Reputation: 14479

Angular2 - Create a reusable, validated text input component

I'm creating an Angular2 application with a Node backend. I will have forms that submit data to said backend. I want validation on both the client and server side, and I'd like to avoid duplicating these validation rules.

The above is somewhat irrelevant to the actual question, except to say that this is the reason why I'm not using the conventional Angular2 validation methods.

This leaves me with the following HTML structure:

<div class="form-group" [class.has-error]="hasError(name)">
    <label class="control-label" for="name"> Property Name
    <input id="name" class="form-control" type="text" name="name" [(ngModel)]="property.name" #name="ngModel" />
    <div class="alert alert-danger" *ngIf="hasError(name)">{{errors.name}}</div>
</div>

<div class="form-group" [class.has-error]="hasError(address1)">
    <label class="control-label" for="address1"> Address
    <input id="address1" class="form-control" type="text" name="address1" [(ngModel)]="property.address.address1" #address1="ngModel" />
    <div class="alert alert-danger" *ngIf="hasError(address1)">{{errors['address.address1']}}</div>
</div>

I will have some large forms and would like to reduce the verbosity of the above. I am hoping to achieve something similar to the following:

<my-text-input label="Property Name" [(ngModel)]="property.name" name="name"></my-text-input>
<my-text-input label="Address" [(ngModel)]="property.address.address1" name="address1" key="address.address1"></my-text-input>

I'm stumbling trying to achieve this. Particular parts that give me trouble are:

I realize that this is a somewhat broad question, but I believe that a good answer will be widely useful and very valuable to many users, since this is a common scenario. I also realize that I have not actually shown what I've tried (only explained that I have, indeed, put forth effort to solve this on my own), but I'm purposely leaving out code samples of what I've tried because I believe there must be a clean solution to accomplish this, and I don't want the answer to be a small tweak to my ugly, unorthodox code.

Upvotes: 4

Views: 2787

Answers (1)

Mateusz Witkowski
Mateusz Witkowski

Reputation: 1746

I think what you are looking for is custom form control. It can do everything you mentioned and reduce verbosity a lot. It is a large subject and I am not a specialist but here is good place to start: Angular 2: Connect your custom control to ngModel with Control Value Accessor.

Example solution:

propertyEdit.component.ts:

import {Component, DoCheck} from '@angular/core';
import {TextInputComponent} from 'textInput.component';
let validate = require('validate.js');

@Component({
  selector: 'my-property-edit',
  template: `
    <my-text-input [(ngModel)]="property.name" label="Property Name" name="name" [errors]="errors['name']"></my-text-input>
    <my-text-input [(ngModel)]="property.address.address1" label="Address" name="address1" [errors]="errors['address.address1']></my-text-input>
  `,
  directives: [TextInputComponent],
})
export class PropertyEditComponent implements DoCheck {

  public property: any = {name: null, address: {address1: null}};
  public errors: any;
  public constraints: any = {
    name: {
      presence: true,
      length: {minimum: 3},
    },
    'address.address1': {
      presence: {message: "^Address can't be blank"},
      length: {minimum: 3, message: '^Address is too short (minimum is 3 characters)'},
    }
  };

  public ngDoCheck(): void {
    this.validate();
  }

  public validate(): void {
    this.errors = validate(this.property, this.constraints) || {};
  }
}

textInput.component.ts:

import {Component, Input, forwardRef} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR, NgModel} from '@angular/forms';

const noop = (_?: any) => {};

@Component({
  selector: 'my-text-input',
  template: `
    <div class="form-group" [class.has-error]="hasErrors(input)">
      <label class="control-label" [attr.for]="name">{{label}}</label>
      <input class="form-control" type="text" [name]="name" [(ngModel)]="value" #input="ngModel" [id]="name" />
      <div class="alert alert-danger" *ngIf="hasErrors(input)">{{errors}}</div>
    </div>
  `,
  providers: [
    { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TextInputComponent), multi: true },
  ],
})
export class TextInputComponent implements ControlValueAccessor {

  protected _value: any;
  protected onChange: (_: any) => void = noop;
  protected onTouched: () => void = noop;

  @Input() public label: string;
  @Input() public name: string;
  @Input() public errors: any;

  get value(): any {
    return this._value;
  }

  set value(value: any) {
    if (value !== this._value) {
      this._value = value;
      this.onChange(value);
    }
  }

  public writeValue(value: any) {
    if (value !== this._value) {
      this._value = value;
    }
  }

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

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

  public hasErrors(input: NgModel): boolean {
    return input.touched && this.errors != null;
  }
}

Upvotes: 2

Related Questions