Angular2 - Create a reusable, validated text input component

时间:2016-08-31 17:44:53

标签: validation angular

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:

  • Setting up two-way binding on the ngModel (changes that I make in the component do not reflect back to the parent).
  • Generating the template reference variable (#name and #address1 attributes) based on an @Input variable to the component.
    • It just occurred to me that perhaps I don't need a separate template reference variable name for each instance of the component. Perhaps I can just use #input since it's only referenced from within that component. Thoughts?
  • I could pass errors or a constraints object to each instance of the component for validation, but I'd like to reduce repetition.

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.

1 个答案:

答案 0 :(得分:2)

我认为您正在寻找的是自定义表单控件。它可以完成你提到的所有事情并减少冗长。这是一个很大的主题,我不是专家,但这里是开始的好地方:Angular 2: Connect your custom control to ngModel with Control Value Accessor.

示例解决方案:

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;
  }
}