Developer Guide: Forms

Overview

Forms allow users to enter data into your application. Forms represent the bidirectional data bindings in Angular.

Forms consist of all of the following:

Form

A form groups a set of widgets together into a single logical data-set. A form is created using the <form> element that calls the $formFactory service. The form is responsible for managing the widgets and for tracking validation information.

A form is:

Widgets

In Angular, a widget is the term used for the UI with which the user input. Examples of bult-in Angular widgets are input and select. Widgets provide the rendering and the user interaction logic. Widgets should be declared inside a form, if no form is provided an implicit form $formFactory.rootForm form is used.

Widgets are implemented as Angular controllers. A widget controller:

CSS

Angular-defined widgets and forms set ng-valid and ng-invalid classes on themselves to allow the web-designer a way to style them. If you write your own widgets, then their $render() methods must set the appropriate CSS classes to allow styling. (See CSS)

Example

The following example demonstrates:

   <style>
     .ng-invalid { border: solid 1px red; }
     .ng-form {display: block;}
   </style>
   <script>
   function UserFormCntl() {
     this.state = /^\w\w$/;
     this.zip = /^\d\d\d\d\d$/;
     this.master = {
       customer: 'John Smith',
       address:{
         line1: '123 Main St.',
         city:'Anytown',
         state:'AA',
         zip:'12345'
       }
     };
     this.cancel();
   }

   UserFormCntl.prototype = {
     cancel: function() {
       this.form = angular.copy(this.master);
     },

     save: function() {
       this.master = this.form;
       this.cancel();
     }
   };
   </script>
   <div ng:controller="UserFormCntl">

     <form name="userForm">

       <label>Name:</label><br/>
       <input type="text" name="customer" ng:model="form.customer" required/>
       <span class="error" ng:show="userForm.customer.$error.REQUIRED">
         Customer name is required!</span>
       <br/><br/>

       <ng:form name="addressForm">
         <label>Address:</label> <br/>
         <input type="text" name="line1" size="33" required
                ng:model="form.address.line1"/> <br/>
         <input type="text" name="city" size="12" required
                ng:model="form.address.city"/>,
         <input type="text" name="state" ng:pattern="state" size="2" required
                ng:model="form.address.state"/>
         <input type="text" name="zip" ng:pattern="zip" size="5" required
                ng:model="form.address.zip"/><br/><br/>

         <span class="error" ng:show="addressForm.$invalid">
           Incomplete address:
           <span class="error" ng:show="addressForm.state.$error.REQUIRED">
             Missing state!</span>
           <span class="error" ng:show="addressForm.state.$error.PATTERN">
             Invalid state!</span>
           <span class="error" ng:show="addressForm.zip.$error.REQUIRED">
             Missing zip!</span>
           <span class="error" ng:show="addressForm.zip.$error.PATTERN">
             Invalid zip!</span>
         </span>
       </ng:form>

       <button ng:click="cancel()"
               ng:disabled="{{master.$equals(form)}}">Cancel</button>
       <button ng:click="save()"
               ng:disabled="{{userForm.$invalid || master.$equals(form)}}">
          Save</button>
     </form>

     <hr/>
     Debug View:
     <pre>form={{form}}</pre>
     <pre>master={{master}}</pre>
     <pre>userForm={{userForm}}</pre>
     <pre>addressForm={{addressForm}}</pre>
   </div>
  it('should enable save button', function() {
    expect(element(':button:contains(Save)').attr('disabled')).toBeTruthy();
    input('form.customer').enter('');
    expect(element(':button:contains(Save)').attr('disabled')).toBeTruthy();
    input('form.customer').enter('change');
    expect(element(':button:contains(Save)').attr('disabled')).toBeFalsy();
    element(':button:contains(Save)').click();
    expect(element(':button:contains(Save)').attr('disabled')).toBeTruthy();
  });
  it('should enable cancel button', function() {
    expect(element(':button:contains(Cancel)').attr('disabled')).toBeTruthy();
    input('form.customer').enter('change');
    expect(element(':button:contains(Cancel)').attr('disabled')).toBeFalsy();
    element(':button:contains(Cancel)').click();
    expect(element(':button:contains(Cancel)').attr('disabled')).toBeTruthy();
    expect(element(':input[ng\\:model="form.customer"]').val()).toEqual('John Smith');
  });

Life-cycle

Writing Your Own Widget

This example shows how to implement a custom HTML editor widget in Angular.

        <script>
          function EditorCntl() {
            this.htmlContent = '<b>Hello</b> <i>World</i>!';
          }

          function HTMLEditorWidget(element) {
            var self = this;
            var htmlFilter = angular.filter('html');

            this.$parseModel = function() {
              // need to protect for script injection
              try {
                this.$viewValue = htmlFilter(
                  this.$modelValue || '').get();
                if (this.$error.HTML) {
                  // we were invalid, but now we are OK.
                  this.$emit('$valid', 'HTML');
                }
              } catch (e) {
                // if HTML not parsable invalidate form.
                this.$emit('$invalid', 'HTML');
              }
            }

            this.$render = function() {
              element.html(this.$viewValue);
            }

            element.bind('keyup', function() {
              self.$apply(function() {
                self.$emit('$viewChange', element.html());
              });
            });
          }

          angular.directive('ng:html-editor-model', function() {
            function linkFn($formFactory, element) {
              var exp = element.attr('ng:html-editor-model'),
                  form = $formFactory.forElement(element),
                  widget;
              element.attr('contentEditable', true);
              widget = form.$createWidget({
                scope: this,
                model: exp,
                controller: HTMLEditorWidget,
                controllerArgs: [element]});
              // if the element is destroyed, then we need to
              // notify the form.
              element.bind('$destroy', function() {
                widget.$destroy();
              });
            }
            linkFn.$inject = ['$formFactory'];
            return linkFn;
          });
        </script>
        <form name='editorForm' ng:controller="EditorCntl">
          <div ng:html-editor-model="htmlContent"></div>
          <hr/>
          HTML: <br/>
          <textarea ng:model="htmlContent" cols="80"></textarea>
          <hr/>
          <pre>editorForm = {{editorForm}}</pre>
        </form>
      
        it('should enter invalid HTML', function() {
          expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-valid/);
          input('htmlContent').enter('<');
          expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-invalid/);
        });
      

HTML Inputs

The most common widgets you will use will be in the form of the standard HTML set. These widgets are bound using the name attribute to an expression. In addition, they can have required attribute to further control their validation.

     <script>
       function Ctrl() {
         this.input1 = '';
         this.input2 = '';
         this.input3 = 'A';
         this.input4 = false;
         this.input5 = 'c';
         this.input6 = [];
       }
     </script>
    <table style="font-size:.9em;" ng:controller="Ctrl">
      <tr>
        <th>Name</th>
        <th>Format</th>
        <th>HTML</th>
        <th>UI</th>
        <th ng:non-bindable>{{input#}}</th>
      </tr>
      <tr>
        <th>text</th>
        <td>String</td>
        <td><tt>&lt;input type="text" ng:model="input1"&gt;</tt></td>
        <td><input type="text" ng:model="input1" size="4"></td>
        <td><tt>{{input1|json}}</tt></td>
      </tr>
      <tr>
        <th>textarea</th>
        <td>String</td>
        <td><tt>&lt;textarea ng:model="input2"&gt;&lt;/textarea&gt;</tt></td>
        <td><textarea ng:model="input2" cols='6'></textarea></td>
        <td><tt>{{input2|json}}</tt></td>
      </tr>
      <tr>
        <th>radio</th>
        <td>String</td>
        <td><tt>
          &lt;input type="radio" ng:model="input3" value="A"&gt;<br>
          &lt;input type="radio" ng:model="input3" value="B"&gt;
        </tt></td>
        <td>
          <input type="radio" ng:model="input3" value="A">
          <input type="radio" ng:model="input3" value="B">
        </td>
        <td><tt>{{input3|json}}</tt></td>
      </tr>
      <tr>
        <th>checkbox</th>
        <td>Boolean</td>
        <td><tt>&lt;input type="checkbox" ng:model="input4"&gt;</tt></td>
        <td><input type="checkbox" ng:model="input4"></td>
        <td><tt>{{input4|json}}</tt></td>
      </tr>
      <tr>
        <th>pulldown</th>
        <td>String</td>
        <td><tt>
          &lt;select ng:model="input5"&gt;<br>
          &nbsp;&nbsp;&lt;option value="c"&gt;C&lt;/option&gt;<br>
          &nbsp;&nbsp;&lt;option value="d"&gt;D&lt;/option&gt;<br>
          &lt;/select&gt;<br>
        </tt></td>
        <td>
          <select ng:model="input5">
            <option value="c">C</option>
            <option value="d">D</option>
          </select>
        </td>
        <td><tt>{{input5|json}}</tt></td>
      </tr>
      <tr>
        <th>multiselect</th>
        <td>Array</td>
        <td><tt>
          &lt;select ng:model="input6" multiple size="4"&gt;<br>
          &nbsp;&nbsp;&lt;option value="e"&gt;E&lt;/option&gt;<br>
          &nbsp;&nbsp;&lt;option value="f"&gt;F&lt;/option&gt;<br>
          &lt;/select&gt;<br>
        </tt></td>
        <td>
          <select ng:model="input6" multiple size="4">
            <option value="e">E</option>
            <option value="f">F</option>
          </select>
        </td>
        <td><tt>{{input6|json}}</tt></td>
      </tr>
    </table>
  

    it('should exercise text', function() {
     input('input1').enter('Carlos');
     expect(binding('input1')).toEqual('"Carlos"');
    });
    it('should exercise textarea', function() {
     input('input2').enter('Carlos');
     expect(binding('input2')).toEqual('"Carlos"');
    });
    it('should exercise radio', function() {
     expect(binding('input3')).toEqual('"A"');
     input('input3').select('B');
     expect(binding('input3')).toEqual('"B"');
     input('input3').select('A');
     expect(binding('input3')).toEqual('"A"');
    });
    it('should exercise checkbox', function() {
     expect(binding('input4')).toEqual('false');
     input('input4').check();
     expect(binding('input4')).toEqual('true');
    });
    it('should exercise pulldown', function() {
     expect(binding('input5')).toEqual('"c"');
     select('input5').option('d');
     expect(binding('input5')).toEqual('"d"');
    });
    it('should exercise multiselect', function() {
     expect(binding('input6')).toEqual('[]');
     select('input6').options('e');
     expect(binding('input6')).toEqual('["e"]');
     select('input6').options('e', 'f');
     expect(binding('input6')).toEqual('["e","f"]');
    });
  

Testing

When unit-testing a controller it may be desirable to have a reference to form and to simulate different form validation states.

This example demonstrates a login form, where the login button is enabled only when the form is properly filled out.

  <div ng:controller="LoginController">
    <form name="loginForm">
      <input type="text" ng:model="username" required/>
      <input type="password" ng:model="password" required/>
      <button ng:disabled="{{!disableLogin()}}" ng:click="login()">Login</login>
    </form>
  </div>

In the unit tests we do not have access to the DOM, and therefore the loginForm reference does not get set on the controller. This example shows how it can be unit-tested, by creating a mock form.

function LoginController() {
  this.disableLogin = function() {
    return this.loginForm.$invalid;
  };
}

describe('LoginController', function() {
  it('should disable login button when form is invalid', function() {
    var scope = angular.scope();
    var loginController = scope.$new(LoginController);

    // In production the 'loginForm' form instance gets set from the view,
    // but in unit-test we have to set it manually.
    loginController.loginForm = scope.$service('$formFactory')();

    expect(loginController.disableLogin()).toBe(false);

    // Now simulate an invalid form
    loginController.loginForm.$emit('$invalid', 'MyReason');
    expect(loginController.disableLogin()).toBe(true);

    // Now simulate a valid form
    loginController.loginForm.$emit('$valid', 'MyReason');
    expect(loginController.disableLogin()).toBe(false);
  });
});

Custom widgets

This example demonstrates a login form, where the password has custom validation rules.

  <div ng:controller="LoginController">
    <form name="loginForm">
      <input type="text" ng:model="username" required/>
      <input type="@StrongPassword" ng:model="password" required/>
      <button ng:disabled="{{!disableLogin()}}" ng:click="login()">Login</login>
    </form>
  </div>

In the unit tests we do not have access to the DOM, and therefore the loginForm and custom input type reference does not get set on the controller. This example shows how it can be unit-tested, by creating a mock form and a mock custom input type.

function LoginController(){
  this.disableLogin = function() {
    return this.loginForm.$invalid;
  };

  this.StrongPassword = function(element) {
    var widget = this;
    element.attr('type', 'password'); // act as password.
    this.$on('$validate', function(){
      widget.$emit(widget.$viewValue.length > 5 ? '$valid' : '$invalid', 'PASSWORD');
    });
  };
}

describe('LoginController', function() {
  it('should disable login button when form is invalid', function() {
    var scope = angular.scope();
    var loginController = scope.$new(LoginController);
    var input = angular.element('<input>');

    // In production the 'loginForm' form instance gets set from the view,
    // but in unit-test we have to set it manually.
    loginController.loginForm = scope.$service('$formFactory')();

    // now instantiate a custom input type
    loginController.loginForm.$createWidget({
      scope: loginController,
      model: 'password',
      alias: 'password',
      controller: loginController.StrongPassword,
      controllerArgs: [input]
    });

    // Verify that the custom password input type sets the input type to password
    expect(input.attr('type')).toEqual('password');

    expect(loginController.disableLogin()).toBe(false);

    // Now simulate an invalid form
    loginController.loginForm.password.$emit('$invalid', 'PASSWORD');
    expect(loginController.disableLogin()).toBe(true);

    // Now simulate a valid form
    loginController.loginForm.password.$emit('$valid', 'PASSWORD');
    expect(loginController.disableLogin()).toBe(false);

    // Changing model state, should also influence the form validity
    loginController.password = 'abc'; // too short so it should be invalid
    scope.$digest();
    expect(loginController.loginForm.password.$invalid).toBe(true);

    // Changeing model state, should also influence the form validity
    loginController.password = 'abcdef'; // should be valid
    scope.$digest();
    expect(loginController.loginForm.password.$valid).toBe(true);
  });
});