4

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 Answer 1

2

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;
  }
}
Sign up to request clarification or add additional context in comments.

2 Comments

I was just looking at that and implementing that solution. Thanks for the pointer. It still feels a little unorthodox, with having to set up the providers and the onChange and onTouched callbacks, so I will leave this question open in hopes of a cleaner solution, but so far this is looking somewhat promising.
I added my implemented solution to your answer since it is the relevant implementation of the article you suggested, fit to the needs of this question. I'm still hoping for another solution to help get rid of the ugly providers and boilerplate methods.

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.