Working with Forms

Popular template engines like mustache.js, Handlebars or _.template generate a string. In original Backbone we just replace the bounding element text node with that string. If you have a form in the template its state will be lost with every template update. On the contrary, ngBackbone relies on DOM-based template engine. With every synchronization it doesn't destroy the DOM-subtree, but updates target nodes gracefully. Besides, it takes over the most of routine work we do every time we develop a UI with forms. ngBackbone creates a state model per every input of the specified group and binds them respectively. It subscribes validators for control change/update events and updates the related state models.

Let's see it in action. First we create a simple component that extends FormView:

import { Component, FormView } from "ng-backbone";

@Component({
  el: "ng-hello",
  template: `<form data-ng-group="hello">
    <input name="name" placeholder="Name..." required />
    <p data-ng-text="hello.name.validationMessage"></p>
  </form>
  `
})

class HelloView extends FormView {
}

let app = new HelloView();
app.render();

Here in the template we have an input control with required attribute. This attribute makes the component the control to validate against an empty value. Thus until we type anything in the input the underlying element shows up the validation message.

For an end-user it may seem confusing. They haven't typed yet anything, but error message still show up. We can restrict this message to appear only if any of the group controls were touched:

@Component({
  el: "ng-hello",
  template: `<form data-ng-group="hello">
    <input name="name" placeholder="Name..." required />
    <p data-ng-if="hello.group.dirty" data-ng-text="hello.name.validationMessage"></p>
  </form>
  `
})

As you can see, during initialization FormView extracts all the groups marked with data-ng-group in the template. It extracts all the controls by [name] for each group and creates state models named as groupName.controlName for controls and groupName.group for the group itself.

State model has following properties:

  • value - control actual value

  • valid - true if control's value is valid

  • touched - true if control has been visited

  • dirty - true if control's value has changed

  • badInput - a boolean indicating the user has provided input that the browser is unable to convert.

  • customError - a boolean indicating the element's custom validity message. e.g. custom validator's

    stepMismatch - a boolean indicating the value does not fit the rules determined by the step attribute

    tooLong- a boolean indicating the value exceeds the specified maxlength

  • valueMissing - a boolean indicating the element has a required attribute, but no value.

  • rangeOverflow - a boolean indicating the value is greater than the maximum specified by the max attribute.

  • rangeUnderflow - a boolean indicating the value is less than the minimum specified by the min attribute.

  • typeMismatch - a boolean indicating the value is not in the required syntax

  • patternMismatch - a boolean indicating the value does not match the specified pattern

  • validationMessage - a string containing validation message

FormView listens to control input/change events and updates the ControlState models by the actual content of element ValidityState (see HTML5 Form API). So if you have an input <input required> and until it has a non-empty value the state model property valueMissing is true and valid property is false, validationMessage contains localized (by user agent) error message.

Form Validation

HTML provides a number of input types (e.g. email, tel, url, number) that validate. FormView binds the control ValidityState to the view and therefore we can make advantage of it:

@Component({
  el: "ng-hello",
  template: `<form data-ng-group="account">
    <input name="email" type="email" placeholder="Email..." />
    <p data-ng-text="account.email.validationMessage"></p>
  </form>
  `
})

If you need to customize validation message, you can create a container that shows up according the state of account.email.typeMismatch

@Component({
  el: "ng-hello",
  template: `<form data-ng-group="account">
    <input name="email" type="email" placeholder="Email..." />
    <p data-ng-if="account.email.typeMismatch">
      Bitte geben Sie eine gültige E-Mail-Adresse ein
    </p>
  </form>
  `
})

Moment! But it shows the validation error even before we actually had a chance to provide any input. That can be a bit confusing. Let's improve user experience by showing error message after user started to type in the control (account.email.dirty is true)

@Component({
  el: "ng-hello",
  template: `<form data-ng-group="account">
    <input name="email" type="email" placeholder="Email..." />
    <p data-ng-if="account.email.dirty && account.email.typeMismatch">
      Bitte geben Sie eine gültige E-Mail-Adresse ein
    </p>
  </form>
  `
})

Custom Validation

HTML5 Form API gives us some options for input validation. But it can be not enough or rather inconvenient to use. With ngBackbone we can pass to the view a map of custom asynchronous validators and refer them though data-ng-validate attribute:

@Component({
  el: "ng-hello",
  formValidators: {
    hexcolor( value: string ): Promise<void> {
        let pattern = /^#(?:[0-9a-f]{3}){1,2}$/i;
        if ( pattern.test( value  ) ) {
          return Promise.resolve();
        }
        return Promise.reject( "Please enter a valid hexcolor e.g. #EEEAAA" );
      }
  },
  template: `
    <form data-ng-group="hello">
      <input name="color" data-ng-validate="hexcolor" placeholder="Enter a color hex-code..." />
    </form>
`
})

Every validator must be a callback that returns a Promise. If validation passes the Promise resolves. Otherwise it rejects with error message passed as an argument

We can also target multiple validators per control:

<input data-ng-validate="foo, bar, baz" />

Instead of passing custom validators in object literal we can pass a class extending FormValidators. That unlocks features available for class members. For example, we can use @Debounce decorator to debounce validator method call:

import { Component, FormView, FormValidators, Debounce } from "ng-backbone";

class CustomValidators extends FormValidators {
   @Debounce( 350 )
   name( value: string ): Promise<void> {
      return NamerModel.fetch();
   }
}

@Component({
  el: "ng-hello",
  formValidators: CustomValidators,
  template: `
    <form data-ng-group="hello" novalidate>
      <input name="name" data-ng-validate="name" placeholder="Enter a name..." />
    </form>
`
})

In the example above we debounce name validator that requests a remote server. Thus regardless the speed of typing it's called no more than every 350ms - to avoid spawning of slow XHR requests.

Last updated