Skip to content

Aurelia forms Blog Post

Sean Hunter edited this page Jul 12, 2017 · 46 revisions

Introducing, Aurelia in Action

Aurela in Action is a book targeted at teaching you what you need to build compelling, real-world applications in Aurelia from the ground up. The book centres around a fictional virtual bookshelf application (similar to Goodreads.com) called my-books. We start off by creating a bare-bones application via the Aurelia CLI, then progressively layer on various features that users would want in a virtual bookshelf. By the end of the book we've built out a fully fledged SPA, giving you the chance familiarise yourself with the many tools available in Aurelia's tool-belt along the way. Save 45% of Aurelia in Action with discount code aureliash at Manning.com. Some of areas covered include:

  • Binding
  • Templating
  • Custom attributes
  • Custom elements
  • Value converters
  • Binding behaviours
  • Working with HTTP
  • Routing
  • Working with forms
  • Inter-component communication patterns
  • Deployment

-- just to name a few.

In this blog post we'll take a sneak peek at some of the topics covered in chapter 7 - Working with forms.

Working with forms in Aurelia

Forms are one of the primary ways that users interact with your website. Well-implemented forms make users happy. Conversely, poorly implemented forms can slow down or stop user's activity on your site. You can address two of the main aspects of well-built forms using Aurelia’s data-binding and validation components. Data-binding can be used to provide rapid feedback, guiding the user on the correct form journey by making their next action obvious. Validation can be used to show users the feedback they need to submit the form in a way that’s unobtrusive and avoids breaking user flow.

One of the pages in the my-books application provides users with the ability to add new books to their bookshelf. In this blog post we'll look at how to add form validation with Bootstrap 4 and the aurelia-validation plugin to ensure that users provide valid values for the book title, and times-read input fields. You can see a screenshot of this form below: my-books form

Feel free to have a poke around with the source code and check out the running application on GistRun https://gist.run/?id=c9c19fbe1e147fc39ac864cf0b694fd1.

Validation

The Aurelia validation plugin allows you to apply validation to view-model fields bound to input elements in the view. It’s built with the Aurelia architecture in mind, and because of this, it fits hand in glove with Aurelia’s binding and templating engine. There are many validation libraries out there, and if you have a favorite library, it’s easy enough to wire it in. The advantage of you get with the Aurelia validation plugin is its ease of use within the Aurelia framework. We'll only be able to scratch the surface of what the validation plugin has to offer here. If you are interested in reading more you can check out Jeremy Danyow's great in-depth article on the Aurelia Hub.

To implement validation in our form we'll need five main components:

  1. The validation controller to orchestrate the entire process
  2. A view-model to validate
  3. Some validation rules that will be applied to the title, times-read view-model properties
  4. A validation renderer to render the results to the view
  5. A view to trigger validation and display results

Validation controller

The ValidationController is a component of the Aurelia validation plugin. Each view-model that requires validation has one or more mapped validation controllers. Validation controllers are orchestrators, responsible for receiving validation triggers (such as an input change or blur event) and responding to these triggers by applying the relevant validation feedback to the view via a validation renderer.

The add-book page consists of a main component edit-book which sits as a custom element inside the main app component. The first step to adding validation to this form is to inject a new ValidationController instance using the NewInstance class. The NewInstance resolver allows us to inject an instance specific to this view-model, overriding the default dependency injection behavior of singleton within a container.

Validation renderer

Because we want to render the validation results using Bootstrap styles, the next step is to implement a validation renderer which you can see in this Gist. The validation renderer is a component of the Aurelia validation plugin that is responsible for rendering validation results to the view. The plugin provides an implementation of this component out of the box, the validation-errors attribute. This custom attribute can be applied to an element and makes all validation errors associated with the element available to both the element and its decedents.

You can then use markup in the view to display validation results to users. Alternatively, you can implement your own custom validation renderer (as we do in this case). This is useful if you want to avoid muddying the view markup with validation display code and when you have a specific style that should be applied. For example, you can implement a Bootstrap validation renderer to show validation results in a Bootstrap style, or a Materialize validation renderer to show validation results in a material design style, and so on. This is my preferred approach to validation, because it keeps the validation display code separated from the main view. With the validation renderer created we need to register it with the validation controller this.controller.addRenderer(new BootstrapFormRenderer()).

Validation rules

Validation rules allow you to apply various constraints to view-model properties. They can be applied either to simple values (such as a string), or to an entire object. Several built-in validation rules come along with the Aurelia framework. In this case we'll use a combination of 3 validation rules to validate two input fields:

  • title : Required validation rule.
  • timesRead : Required validation rule, and custom validation rule to ensure that the user specifies a number which is either 0, or a positive integer. The validation rules will be applied as follows:
ValidationRules
        .ensure('title').required() //ensure that title is provided.
        .ensure('timesRead').required()  //ensure that the user specifies a value for times-read.
        .satisfiesRule('positiveInteger') //validate against the positiveInteger custom validation rule.
        .on(this); //apply the validation rules to the current view-model.

Executing the validation

With the plumbing of the validation controller, validation renderer, and validation rules in place, the last step is to configure triggers for when we want to execute the validation rules. While where on the subject of the view-model, we'll add a validation trigger to execute the validation rules on form submit, and only raise an added notification if the validation results return successfully using the validation controller's validate method. This method returns a promise to allow to also handle the case where the validation may take some time (for example a server side validation over HTTP):

 addBook(){
    this.controller.validate().then(result => {
      if(result.valid) this.eventAggregator.publish('book-added');  
     //publish the book-added event only when the edit-book form is valid.
    });
  }

Putting this all together, the edit-book view-model looks like this. There are some elements of this code sample that we won't cover here, but stay tuned as these aspects will be covered in chapter 7 of Aurelia in Action.

import {inject,NewInstance} from 'aurelia-framework'; //import the new instance class
import {EventAggregator} from 'aurelia-event-aggregator'; 
import {BooksApi} from 'books-api';
import {BootstrapFormRenderer} from 'bootstrap-form-renderer'; //import the validation renderer
import {ValidationRules, ValidationController} from 'aurelia-validation'; //import the validation rules and controller

@inject(EventAggregator, BooksApi, NewInstance.of(ValidationController)) 
//inject a new instance of the validation controller
export class BookForm{
  
  constructor(eventAggregator, bookApi, controller){
    this.title = "";
    this.eventAggregator = eventAggregator; 
    this.bookApi = bookApi;
    this.controller = controller;
    this.controller.addRenderer(new BootstrapFormRenderer()); //configure the validation renderer
    this.configureValidationRules(); //configure the validation rules
    this.createEventListeners();
  }
  
  configureValidationRules(){
      ValidationRules.customRule( //create the positiveInteger validation rule
      'positiveInteger',
      (value, obj) => value === null || value === undefined 
        || (Number.isInteger(value) || value >= 0),
      `Books can only be read 0 or more times.` 
      );
      
      ValidationRules
        .ensure('title').required() //ensure a title is specified
        .ensure('timesRead').required() //ensure a value is provided for time-read
        .satisfiesRule('positiveInteger'). //ensure times-read is a valid value
        on(this);
  }
  
  addBook(){
    this.controller.validate().then(result => { //trigger validation when a book is added
      if(result.valid) this.eventAggregator.publish('book-added');  
    });
  }
  
  bind(){
    this.bookApi.getGenres().then(genres => {
      this.genres = genres;
    });
  }
  
  createEventListeners(){
    this.genreSelectedListener =  e => {
      if(e && e.detail){
        this.genre = e.detail.value;  
      }
    };
    this.ratingChangedListener =  e => this.rating = e.rating;
  }
  
  attached(){
      this.selectGenreElement.addEventListener("change", this.genreSelectedListener );
      this.selectGenreElement.addEventListener("change", this.ratingChangedListener );
  }
  
  detached(){
      this.ratingElement.removeEventListener('change', this.ratingChangedListener);
      this.selectGenreElement.removeEventListener('change', this.genreSelectedListener);
  }
  
}

The validate binding behavior

You can enlist view elements for validation using the validate binding behavior provided by the Aurelia validation plugin. For example, applying validation to an input element is a simple matter of <input value.bind='title & validate'/>, which applies the validate binding behavior to the input value binding.

From there, the view can take as large a role as you want in the validation process. You can use either the validation-errors renderer, which means more validation markup in your view, or a custom validation renderer, which incurs little to no involvement from the view in deciding how the validation results should be displayed to the user.

With the view-model in place the last step is to wire up validation triggers in the view itself on the relevant input fields as shown in the following code listing.

<template>
  <require from="typeahead"></require>
  <require from="star-rating"></require>
  <form class="form card" submit.trigger="addBook()">
    <div class="card-block">
      <h4 class="card-title">Add book <i class="fa fa-book" aria-hidden="true"></i></h4>
      <h6 class="card-subtitle mb-2 text-muted">add a book to your bookshelf</h6>
      <hr/>
        <div class="form-group">
          <label for="title">Title</label>
          <input name="title" class="form-control" 
                 placeholder='enter a title' 
                 value.bind="title & validate"></input> 
                 <!-- validation trigger added to title input field -->
        </div>
        <div class="form-group">
          <label for="genre">Genre</label>
          <input value.bind="genre" 
                 typeahead="items.bind:genres" 
                 type="text" 
                 name="genre" 
                 class="form-control"
                 ref="selectGenreElement"></input>
        </div>
        <div class="form-group">
          <label for="times-read">Times read</label>
          <input name=times-read class="form-control" 
                  value.bind="timesRead & validate"></input> 
                  <!-- validation trigger added to times-read input field -->
        </div>
        <hr/>
        <star-rating 
              view-model.ref="starRatingViewModel" 
              ref="ratingElement" 
              rating.bind="rating">
       </star-rating>
      </form>
    </div>
   <div class="card-footer">
      <button type="submit" class="btn btn-primary col-sm-3 push-sm-9" 
              disabled.bind="title.length == 0">
        add
      </button>  
   </div>
  </div>
</template>

This completes our implementation of Bootstrap 4 form validation in the edit-book form. You can see this in action and review the entire source code on GistRun. There are many more goodies hidden away in this form. Check out the Aurelia in Action website to read more. At the time of writing we are currently under the Early Access Program. You can download the first chapter free, and feedback via either the books forum or the GitHub repository is much appreciated.