Working with Forms with AngularJS: Using Form Elements with Two-Way Data Bindings

Before I get into the detail of the directives that AngularJS provides for working with forms, I am going to revisit the topic of two-way data bindings, which are intrinsically related to form elements because they are able to gather data from the user and, therefore, update the model.

As I explained in Chapter 10, two-way data bindings are created with the ng-model directive, which can be applied to any of the form elements, including input. AngularJS ensures that changes in the form element automatically update the corresponding part of the data model, as demonstrated in Listing 12-2.

Listing 12-2. Using Two-Way Data Bindings in the forms.html File

<table class=”table”>

<thead>

<tr><th>#</th><th>Action</th><th>Done</th></tr>

</thead>

<tr ng-repeat=”item in todos”>

<td>{{$index + 1}}</td>

<td>{{item.action}}</td>

<td>

<input type=”checkbox” ng-model=”item.complete”

</td>

</tr>

</table>

A to-do list that doesn’t let you check off items isn’t a great deal of use. In the listing, I have replaced the inline data binding that displayed the value of the complete property with a check box input element. I have used the ng-model property to create a two-way binding with the complete property. When the page is first loaded, AngularJS uses the complete property to set the initial state of the check box, and when the user checks and unchecks the box, the value of the property is updated. The ng-model directive is an elegant way to apply one of the fundamental features of AngularJS—data binding—in a way that lets the user modify the data model.

You can see the effect of changing the data model by checking and unchecking the input elements in the example. To see that the model is being changed—as opposed to just the state of the input element—keep an eye on the label that displays the number of incomplete to-do items. AngularJS disseminates the changes to the model made by the input element to all of the relevant bindings, causing the item count to be updated, as shown in Figure 12-2.

1. Implicitly Creating Model Properties

The previous example operates on model properties that I explicitly defined when I set up the controller, but you can also use two-way data bindings to implicitly create properties in the data model—a feature that is useful when you are using form elements to gather data from the user in order to create a new object or property in the data model. The best way to understand this is with an example, such as the one in Listing 12-3.

Listing 12-3. Using Implicitly Created Model Properties in the forms.html File

<!DOCTYPE html>

<html ng-app=”exampleApp”>

<head>

<title>Forms</title>

<script src=”angular.js”></script>

<link href=”bootstrap.css” rel=”stylesheet” />

<link href=”bootstrap-theme.css” rel=”stylesheet” />

<script>

angular.module(“exampleApp”, [])

.controller(“defaultCtrl”, function ($scope) {

$scope.todos = [

{ action: “Get groceries”, complete: false },

{ action: “Call plumber”, complete: false },

{ action: “Buy running shoes”, complete: true },

{ action: “Buy flowers”, complete: false },

{ action: “Call family”, complete: false }];

$scope.addNewItem = function (newltem) {

$scope.todos.push({

action: newltem.action + ” (” + newltem.location + “)”,

complete: false

});

);

});

</script>

</head>

<body>

<div id=”todoPanel” class=”panel” ng-controller=”defaultCtrl”>

<h3 class=”panel-header”>

To Do List

<span class=”label label-info”>

{{ (todos | filter: {complete: ‘false’}).length}}

</span>

</h3>

<div class=”row”>

<div class=”col-xs-6”>

<div class=”well”>

<div class=”form-group row”>

<label for=”actionText”>Action:</label>

<input id=”actionText” class=”form-control” ng-model=”newTodo.action”>

</div>

<div class=”form-group row”>

<label for=”actionLocation”>Location:</label>

<select id=”actionLocation” class=”form-control”

ng-model=”newTodo.location”>

<option>Home</option>

<option>Office</option>

<option>Mall</option>

</select>

</div>

<button class=”btn btn-primary btn-block”

ng-click=”addNewItem(newTodo)”>

Add

</button>

</div>

</div>

<div class=”col-xs-6″>

<table class=”table”>

<thead>

<tr><th>#</th><th>Action</th><th>Done</th></tr>

</thead>

<tr ng-repeat=”item in todos”>

<td>{{$index + 1}}</td>

<td>{{item.action}}</td>

<td>

<input type=”checkbox” ng-model=”item.complete”>

</td>

</tr>

</table>

</div>

</div>

</div>

</body>

</html>

The new HTML elements in this example look more complicated than they really are because of the Bootstrap classes I have applied to get the layout I want. In fact, all we care about is this input element:

<input id=”actionText” class=”form-control” ng-model=”newTodo.action“>

and this select element:

<select id=”actionLocation” class=”form-control” ng-model=”newTodo.location“>

<option>Home</option>

<option>Office</option>

<option>Mall</option>

</select>

They both use the ng-model directive, configured to update model properties that I have not explicitly defined: the newTodo.action and newTodo.location properties. These properties are not part of my domain model, but I need to access the value that the user enters for use in the addNewItem behavior I defined in the controller and that I invoke when the user clicks the button element:

$scope.addNewItem = function (newItem) {

$scope.todos.push({action: newItem.action + ” (” + newItem.location + “)”,

complete: false

});

};

The controller behavior is a function that takes an object with action and location properties and adds a new object to the array of to-do items. You can see how I pass the newTodo object to the behavior in the ng-click directive I applied to the button element:

<button class=”btn btn-primary btn-block” ng-click=”addNewItem(newTodo)“>

Add

</button>

Tip I could have written this behavior so that it works on the $scope.newTodo object directly, rather than accepting an object as an argument, but this approach allows a behavior to be used in multiple places in a view, which becomes important when considering controller inheritance, as described in Chapter 13.

The newTodo object and its action and location properties don’t exist when the forms.html page is first loaded by the browser; the only data in the model is the set of existing to-do items that I have hard-coded in the controller factory function. AngularJS will create the newTodo object automatically when the input or select element is changed and assign a value to its action or location properties based on which element the user is working with.

Because of this flexibility, AngularJS takes a relaxed view of the state of the data model. There are no errors when you retrieve a nonexistent object or property, and when you assign a value to an object or property that doesn’t exist, AngularJS will simply create it for you—producing what is known as an implicitly defined value or object.

Tip I have used the newTodo object to group related properties, but you can also implicitly define properties directly on the $scope object. This is what I did in Chapter 2 when I created the first AngularJS example in this book.

To see the effect, enter some text in the input element, select a value in the select element, and click the Add button. Your interactions with the input and select elements will have created the newTodo object and its properties, and the ng-click directive applied to the button element will invoke the controller behavior that uses these values to create a new to-do item in the list, as shown in Figure 12-3.

2. Checking That the Data Model Object Has Been Created

Using an implicitly defined object on which properties are defined has some benefits, such as being able to call the behavior that processes the data in a clean and simple way. But it has a drawback as well, which you can see if you reload the forms.html file in the browser and click the Add button without editing the input element or selecting an option for the select element. The interface won’t change when you click the button, but you’ll see an error message like this one in the JavaScript console:

TypeError: Cannot read property ‘action’ of undefined

The problem is that my controller behavior is trying to access properties on an object that AngularJS won’t create until the one of the form controls has been modified, triggering the ng-model directive.

When relying on implicit definition, it is important to write your code to cater for the possibility that the objects or properties you are going to use do not yet exist. I have made this a separate example because it is a common problem when coming to grips with AngularJS. In Listing 12-4, you can see how I have modified my behavior so that it checks for the object and its properties.

Listing 12-4. Checking That Implicitly Defined Objects and Properties Exist in the forms.html File

$scope.addNewItem = function (newItem) {

if (angular.isDefined(newItem) && angular.isDefined(newItem.action)

&& angular.isDefined(newItem.location)) {

$scope.todos.push({

action: newItem.action + ” (” + newItem.location + “)”,

complete: false

});

}

};

I have used the angular.isDefined method to check that the newItem object and both of its properties have been defined before I add a new item to the set of to-dos.

Source: Freeman Adam (2014), Pro AngularJS (Expert’s Voice in Web Development), Apress; 1st ed. edition.

Leave a Reply

Your email address will not be published. Required fields are marked *