Chapter 4. Forms, Inputs, and Services
In the previous chapters, we first covered the most basic AngularJS directives and dealt with creating controllers and getting our data from the controllers into the UI. We then looked at how to write tests for the same, using Karma and Jasmine. In this chapter, we will build on the work from Chapter 2 and work on getting the user’s data out of forms in the UI into our controller so that we can then send it to the server, validate it, or do whatever else we might need to.
We will then get into using AngularJS services, and see how we can leverage some of the common existing services as well as create our own. We will also briefly cover when and why you should create AngularJS services.
Working with ng-model
In the previous chapter, we saw the ng-bind
directive, or its equivalent double-curly {{ }}
notation, which allowed us to take the data from our controllers and display it in the UI. That gives us our one-way data-binding, which is powerful in its own regard. But most applications we develop also have user interaction, and parts where the user has to feed in data. From registration forms to profile information, forms are a staple of web applications, and AngularJS provides the ng-model
directive for us to deal with inputs and two-way data-binding:
<!-- File: chapter4/simple-ng-model.html -->
<html
ng-app=
"notesApp"
>
<head><title>
Notes App</title></head>
<body
ng-controller=
"MainCtrl as ctrl"
>
<input
type=
"text"
ng-model=
"ctrl.username"
/>
You typed {{ctrl.username}}<script
src=
"https://ajax.googleapis.com/ajax/libs/angularjs/1.3.11/angular.js"
>
</script>
<script
type=
"text/javascript"
>
angular
.
module
(
'notesApp'
,
[])
.
controller
(
'MainCtrl'
,
[
function
()
{
this
.
username
=
'nothing'
;
}]);
</script>
</body>
</html>
In this example, we define a controller with a variable exposed on its instance called username
. Now, we get its value out into the HTML using the ng-controller
and the double-curly syntax for one-way data-binding. What we have introduced in addition is an input
element. It is a plain text box, but on it we have attached the ng-model
directive. We pointed the value for the ng-model
at the same username
variable on the MainCtrl
. This accomplishes the following things:
-
When the HTML is instantiated and the controller is attached, it gets the current value (in this case,
nothing
as a string) and displays it in our UI. - When the user types, updates, or changes the value in the input box, it updates the model in our controller.
- When the value of the variable changes in the controller (whether because it came from the server, or due to some internal state change), the input field gets the value updated automatically.
The beauty of this is twofold:
- If we need to update the form element in the UI, all we need to do is update the value in the controller. No need to go looking for input fields by IDs or CSS class selectors; just update the model.
- If we need to get the latest value that the user entered into the form or input to validate or send to the server, we just need to grab it from our controller. It will have the latest value in it.
Now let’s add some complexity, and actually deal with forms. Let’s see if we can bring this concept together with an example:
<!-- File: chapter4/simple-ng-model-2.html -->
<html
ng-app=
"notesApp"
>
<head><title>
Notes App</title></head>
<body
ng-controller=
"MainCtrl as ctrl"
>
<input
type=
"text"
ng-model=
"ctrl.username"
>
<input
type=
"password"
ng-model=
"ctrl.password"
>
<button
ng-click=
"ctrl.change()"
>
Change Values</button>
<button
ng-click=
"ctrl.submit()"
>
Submit</button>
<script
src=
"https://ajax.googleapis.com/ajax/libs/angularjs/1.3.11/angular.js"
>
</script>
<script
type=
"text/javascript"
>
angular
.
module
(
'notesApp'
,
[])
.
controller
(
'MainCtrl'
,
[
function
()
{
var
self
=
this
;
self
.
change
=
function
()
{
self
.
username
=
'changed'
;
self
.
password
=
'password'
;
};
self
.
submit
=
function
()
{
console
.
log
(
'User clicked submit with '
,
self
.
username
,
self
.
password
);
};
}]);
</script>
</body>
</html>
We introduced one more input field, which is bound to a field called password
on the controller’s instance. And we added two buttons:
- The first button, Change Values, is to simulate the server sending some data that needs to be updated in the UI. All it does is reassign the values to the username and password fields in the controller with the latest values.
- The second button, Submit, simulates submitting the form to the server. All it does for now is log the value to the console.
The most important thing in both of these is that the controller never reached out into the UI. There was no jQuery selector, no findElementById
, or anything like that. When we need to update the UI, we just update the model fields in the controller. When we need to get the latest and greatest value, we just grab it from the controller. Again, this is the AngularJS way.
Next let us see how to leverage this and work with forms in AngularJS.
Working with Forms
When we work with forms in AngularJS, we heavily leverage the ng-model
directive to get our data into and out of the form. In addition to the data-binding, it is also recommended to structure your model and bindings in such a way to reduce your own effort, as well as the lines of code you write. Let’s take a look at an example:
<!-- File: chapter4/simple-form.html -->
<html
ng-app=
"notesApp"
>
<head><title>
Notes App</title></head>
<body
ng-controller=
"MainCtrl as ctrl"
>
<form
ng-submit=
"ctrl.submit()"
>
<input
type=
"text"
ng-model=
"ctrl.user.username"
>
<input
type=
"password"
ng-model=
"ctrl.user.password"
>
<input
type=
"submit"
value=
"Submit"
>
</form>
<script
src=
"https://ajax.googleapis.com/ajax/libs/angularjs/1.3.11/angular.js"
>
</script>
<script
type=
"text/javascript"
>
angular
.
module
(
'notesApp'
,
[])
.
controller
(
'MainCtrl'
,
[
function
()
{
var
self
=
this
;
self
.
submit
=
function
()
{
console
.
log
(
'User clicked submit with '
,
self
.
user
);
};
}]);
</script>
</body>
</html>
We are still using the same two input fields as last time, but we made a few changes:
-
We wrapped our text fields and button inside a form. And instead of an
ng-click
on the button, we added anng-submit
directive on the form itself. Theng-submit
directive has a few advantages over having anng-click
on a button when it comes to forms. A form submit event can be triggered in multiple ways: clicking the Submit button, or hitting Enter on a text field. Theng-submit
gets triggered on all those events, whereas theng-click
will only be triggered when the user clicks the button. -
Instead of binding to
ctrl.username
andctrl.password
, we bind toctrl.user.username
andctrl.user.password
. Notice that we did not declare a user object in the controller (that is,self.user = {}
). When you useng-model
, AngularJS automatically creates the objects and keys necessary in the chain to instantiate a data-binding connection. In this case, until the user types something into the username or password field, there is no user object. The first letter typed into either the username or password field causes the user object to be created, and the value to be assigned to the correct field in it.
Leverage Data-Binding and Models
When designing your forms and deciding which fields to bind the ng-model
to, you should always consider what format you need the data in. Let’s take the following example to demonstrate:
<!-- File: chapter4/two-forms-databinding.html -->
<html
ng-app=
"notesApp"
>
<head><title>
Notes App</title></head>
<body
ng-controller=
"MainCtrl as ctrl"
>
<form
ng-submit=
"ctrl.submit1()"
>
<input
type=
"text"
ng-model=
"ctrl.username"
>
<input
type=
"password"
ng-model=
"ctrl.password"
>
<input
type=
"submit"
value=
"Submit"
>
</form>
<form
ng-submit=
"ctrl.submit2()"
>
<input
type=
"text"
ng-model=
"ctrl.user.username"
>
<input
type=
"password"
ng-model=
"ctrl.user.password"
>
<input
type=
"submit"
value=
"Submit"
>
</form>
<script
src=
"https://ajax.googleapis.com/ajax/libs/angularjs/1.3.11/angular.js"
>
</script>
<script
type=
"text/javascript"
>
angular
.
module
(
'notesApp'
,
[])
.
controller
(
'MainCtrl'
,
[
function
()
{
var
self
=
this
;
self
.
submit1
=
function
()
{
// Create user object to send to the server
var
user
=
{
username
:
self
.
username
,
password
:
self
.
password
};
console
.
log
(
'First form submit with '
,
user
);
};
self
.
submit2
=
function
()
{
console
.
log
(
'Second form submit with '
,
self
.
user
);
};
}]);
</script>
</body>
</html>
There are two forms in this example, both with the same fields. The first form is bound to a username
and password
directly on the controller, while the second form is bound to a username
and password
key on a user
object in the controller. Both of them trigger an ng-submit
function on submission of a function. Now in the case of the first form, we have to take those fields from the controller and put them into an object, or something similar, before we can send it to the server. In the second case, we can directly take the user
object from the controller and pass it around.
The second flow makes more sense, because we are directly modeling how we want to represent the form as an object in the controller. This removes any additional work we might have to do when we work with the values of the form.
Form Validation and States
We have seen how to create forms, and enable (and leverage) data-binding to get our data in and out of the UI. Now let’s proceed to see how else AngularJS can benefit us when working with forms, and especially with validation and various states of the forms and inputs:
<!-- File: chapter4/form-validation.html -->
<html
ng-app=
"notesApp"
>
<head><title>
Notes App</title></head>
<body
ng-controller=
"MainCtrl as ctrl"
>
<form
ng-submit=
"ctrl.submit()"
name=
"myForm"
>
<input
type=
"text"
ng-model=
"ctrl.user.username"
required
ng-minlength=
"4"
>
<input
type=
"password"
ng-model=
"ctrl.user.password"
required
>
<input
type=
"submit"
value=
"Submit"
ng-disabled=
"myForm.$invalid"
>
</form>
<script
src=
"https://ajax.googleapis.com/ajax/libs/angularjs/1.3.11/angular.js"
>
</script>
<script
type=
"text/javascript"
>
angular
.
module
(
'notesApp'
,
[])
.
controller
(
'MainCtrl'
,
[
function
()
{
var
self
=
this
;
self
.
submit
=
function
()
{
console
.
log
(
'User clicked submit with '
,
self
.
user
);
};
}]);
</script>
</body>
</html>
In this example, we reworked our old example to add some validation. In particular, we want to disable the Submit button if the user has not filled out all the required fields. How do we accomplish this?
-
We give the form a name, which we can refer to later. In this case, it is
myForm
. -
We leverage HTML5 validation tags and add the
required
attribute on each input field. -
We add a validator,
ng-minlength
, which enforces that the minimum length of the value in the input field for the username is four characters. -
On the Submit button, we add an
ng-disabled
directive. This disables the element if the condition is true. -
For the
disable
condition, we leverage the form, which exposes a controller with the current state of the form. In this case, we tell the button to disable itself if the form with the namemyForm
is$invalid
.
Warning
Do note that in case you have added a validator to any input field, the ng-model
value is not set unless and until the form field is valid. That is, in the example that precedes this, unless the minimum length is satisfied, the value in the input field will not be set in the ng-model
variable (ctrl.user.username
). It will remain empty until the minimum length condition is met.
When you use forms (and give them names), AngularJS creates a FormController
that holds the current state of the form as well as some helper methods. You can access the FormController
for a form using the form’s name, as we did in the preceding example using myForm
. Things that are exposed as the state and kept up to date with data-binding are shown in Table 4-1.
Each of the states mentioned in the table (except $error
) are Booleans and can be used to conditionally hide, show, disable, or enable HTML elements in the UI. As the user types or modifies the form, the values are updated as long as you are leveraging ng-model
and the form name.
Error Handling with Forms
We looked at the types of validation you can do at a form level, but what about individual fields? In our previous example, we ensured that both input fields were required fields, and that the minimum length on the username was four. What else can we do? Table 4-2 contains some built-in validations that AngularJS offers.
Validator | Description |
| As previously discussed, this ensures that the field is required, and the field is marked invalid until it is filled out. |
| Unlike |
| We can set the minimum length of the value in the input field with this directive. |
| We can set the maximum length of the value in the input field with this directive. |
| The validity of an input field can be checked against the regular expression pattern specified as part of this directive. |
| Text input with built-in email validation. |
| Text input with number validation. Can also have additional attributes for min and max values of the number itself. |
| If the browser supports it, shows an HTML datepicker. Otherwise, defaults to a text input. The |
| Text input with URL validation. |
In addition to this, we can write our own validators, which we cover in Chapter 13.
Displaying Error Messages
What can we do with all these validators? We can of course check the validity of the form, and disable the Save or Update button accordingly. But we also want to tell the user what went wrong and how to fix it. AngularJS offers two things to solve this problem:
- A model that reflects what exactly is wrong in the form, which we can use to display nicer error messages
- CSS classes automatically added and removed from each of these fields allow us to highlight problems in the form
Let’s first take a look at how to display specific error messages based on the problem with the following example:
<!-- File: chapter4/form-error-messages.html -->
<html
ng-app=
"notesApp"
>
<head><title>
Notes App</title></head>
<body
ng-controller=
"MainCtrl as ctrl"
>
<form
ng-submit=
"ctrl.submit()"
name=
"myForm"
>
<input
type=
"text"
name=
"uname"
ng-model=
"ctrl.user.username"
required
ng-minlength=
"4"
>
<span
ng-show=
"myForm.uname.$error.required"
>
This is a required field</span>
<span
ng-show=
"myForm.uname.$error.minlength"
>
Minimum length required is 4</span>
<span
ng-show=
"myForm.uname.$invalid"
>
This field is invalid</span>
<input
type=
"password"
name=
"pwd"
ng-model=
"ctrl.user.password"
required
>
<span
ng-show=
"myForm.pwd.$error.required"
>
This is a required field</span>
<input
type=
"submit"
value=
"Submit"
ng-disabled=
"myForm.$invalid"
>
</form>
<script
src=
"https://ajax.googleapis.com/ajax/libs/angularjs/1.3.11/angular.js"
>
</script>
<script
type=
"text/javascript"
>
angular
.
module
(
'notesApp'
,
[])
.
controller
(
'MainCtrl'
,
[
function
()
{
var
self
=
this
;
self
.
submit
=
function
()
{
console
.
log
(
'User clicked submit with '
,
self
.
user
);
};
}]);
</script>
</body>
</html>
Nothing in the controller has changed in this example. Instead, we can just focus on the form HTML. Let’s see what changed with the form:
-
First, we added the
name
attribute to both the input fields where we needed validation:uname
for the username box, andpwd
for the password text field. - Then we leverage AngularJS’s form bindings to be able to pick out the errors for each individual field. When we add a name to any input, it creates a model on the form for that particular input, with the error state.
-
So for the username field, we can access it if the field was not entered by accessing
myForm.uname.$error.required
. Similarly, forng-minlength
, the field would bemyForm.uname.$error.minlength
. For the password, we look atmyForm.pwd.$error.required
to see if the field was filled out or not. -
We also accessed the state of the input, similar to the form, by accessing
myForm.uname.$invalid
. All the other form states ($valid
,$pristine
,$dirty
) we saw earlier are also available similarly onmyForm.uname
.
With this, we now have an error message that shows only when a certain type of error is triggered. Each of the validators we saw in Table 4-2 exposes a key on the $error
object, so that we can pick it up and display the error message for that particular error to the user. Need to show the user that a field is required? Then when the user starts typing, show the minimum length, and then finally show a message when he exceeds the maximum length. All these kinds of conditional messages can be shown with the AngularJS validators.
ngMessages
In the previous section, we saw how to display conditional error messages for forms. But there are cases when we have more complex conditions where multiple error conditions might be met. We might also want to use common error messages across all our pages and form fields. To help us with these conditions, there in an optional module in AngularJS called ngMessages
. This is not part of the core AngularJS file (the angular.js file), but an extra JavaScript file you have to include to get this functionality. What the ngMessages
module provides us with is:
-
The ability to cleanly define the messages for each error, instead of relying on complex
if
andshow
conditions - The ability to sequence the error messages by priority
- The ability to inherit and extend error messages across the application
To be able to use ngMessages
in your project, you need to do the following:
- Include the angular-messages.js (or its minified version) in your main index.html
-
Add the
ngMessages
module as a dependency to the main app module -
Use the
ng-messages
andng-message
directives - Optionally define templates for your common error messages
Let’s take a full fledged example of these in action to see how it would work:
<!-- File: chapter4/ng-messages.html -->
<html
ng-app=
"notesApp"
>
<head><title>
Notes App</title></head>
<body
ng-controller=
"MainCtrl as ctrl"
>
<form
ng-submit=
"ctrl.submit1()"
name=
"simpleForm"
>
<input
type=
"email"
name=
"uname"
ng-model=
"ctrl.user1.username"
required
ng-minlength=
"6"
>
<div
ng-messages=
"simpleForm.uname.$error"
ng-messages-include=
"error-messages"
></div>
<input
type=
"password"
name=
"pwd"
ng-model=
"ctrl.user1.password"
required
>
<div
ng-messages=
"simpleForm.pwd.$error"
ng-messages-include=
"error-messages"
></div>
<input
type=
"submit"
value=
"Submit"
ng-disabled=
"simpleForm.$invalid"
>
</form>
<form
ng-submit=
"ctrl.submit2()"
name=
"overriddenForm"
>
<input
type=
"email"
name=
"uname"
ng-model=
"ctrl.user2.username"
required
ng-minlength=
"6"
>
<div
ng-messages=
"overriddenForm.uname.$error"
ng-messages-include=
"error-messages"
>
<div
ng-message=
"required"
>
Please enter a username</div>
</div>
<input
type=
"password"
name=
"pwd"
ng-model=
"ctrl.user2.password"
required
>
<div
ng-messages=
"overriddenForm.pwd.$error"
>
<div
ng-message=
"required"
>
Please enter a password</div>
</div>
<input
type=
"submit"
value=
"Submit"
ng-disabled=
"overriddenForm.$invalid"
>
</form>
<script
src=
"https://ajax.googleapis.com/ajax/libs/angularjs/1.3.11/angular.js"
>
</script>
<script
src=
"https://ajax.googleapis.com/ajax/libs/angularjs/1.3.11/angular-messages.js"
>
</script>
<script
type=
"text/ng-template"
id=
"error-messages"
>
<
div
ng
-
message
=
"required"
>
This
field
is
mandatory
<
/div>
<
div
ng
-
message
=
"minlength"
>
Minimum
length
condition
not
met
<
/div>
<
div
ng
-
message
=
"email"
>
Please
enter
a
valid
address
<
/div>
</script>
<script
type=
"text/javascript"
>
angular
.
module
(
'notesApp'
,
[
'ngMessages'
])
.
controller
(
'MainCtrl'
,
[
function
()
{
var
self
=
this
;
self
.
submit1
=
function
()
{
console
.
log
(
'User clicked submit with '
,
self
.
user1
);
};
self
.
submit2
=
function
()
{
console
.
log
(
'User clicked submit with '
,
self
.
user2
);
};
}]);
</script>
</body>
</html>
This example might seem large and complicated at first, but let’s break it down into smaller chunks to see what’s happening:
- We have two forms within our body, both with two input fields: a username and password text box. The username field in both forms has three validators: an email validator (by specifying the type as email), one to mark it as required, and another to ensure it has a minimum length of four characters. The password field has just the required validator.
-
Before we look at the
ng-messages
directive, we first look at the end of the body tag. In addition to loading angular.js, we also load angular-messages.js, which gives us the source for thengMessages
module. We also addngMessages
as a dependency to ournotesApp
module. -
We have defined a
script
tag with typetext/ng-template
and an ID oferror-messages
. We can use this technique to define HTML partials inline inside our main index.html file. The content of thescript
tag could have been in a separate HTML file as well. -
This HTML inside the
script
tag has our default error messages and the conditions on which to show them. In this particular case, we have therequired
andminlength
error messages defined. -
Now, we jump back to our forms and the
simpleForm
first. After each field, we create a newdiv
tag with theng-messages
directive. We pass the$error
object to theng-messages
directive based on which we want to show error messages. Theng-messages
directive will look for keys within this object, and if it matches a particular condition, that message will be shown in the UI. -
The
ng-messages
directive allows us to define the error messages inline directly (as in the case of thepassword
field inoverriddenForm
), or include an external template using theng-messages-include
attribute along with theng-messages
directive. -
The
ng-messages-include
attribute looks for an inline template; that is, ascript
tag of typetext/ng-template
with the givenid
, or a remote file, which will be loaded asynchronously by AngularJS. -
The order in which we define the messages in the template (or as a child of the
ng-messages
directive) defines the order in which the error messages are shown. We could simultaneously have two or more errors happening on the same field. In this case, the username field could be less than six characters long, as well as an invalid email address. Since we have defined theminlength
ng-message
before theemail
ng-message
, theminlength
error will be shown first, and if that condition is satisfied, then and only then will theemail
validation error be shown. -
We can also override error messages from our common templates. For the
username
field in theoverriddenForm
, we override therequired
error message to be more specific to the use case, while still using theminlength
andemail
error messages from our common templates.ngMessages
is smart enough to override just the ones we have defined and reuse the remaining from the common messages.
By default, ngMessages
shows only the first error message that is true from the list of ng-message
conditions that it has. If in case we need to show all the validations that failed, we can still use the ng-messages
directive by simply specifying the ng-messages-multiple
attribute. This will ensure that all error conditions that are met will have their messages shown.
The ngMessages
module can also be used with any of our custom requirements, and it’s not necessary that it be used only with forms. It just looks at the key values of the given object and displays messages like a switch case statement.
Thus, using the ngMessages
module can greatly simplify and clean up how we show and handle error messages for forms in AngularJS in a consistent, reusable manner.
Styling Forms and States
We saw the various states of the forms (and the inputs): $dirty
, $valid
, and so on. We saw how to display specific error messages and disable buttons based on these conditions, but what if we want to highlight certain input fields or form states using UI and CSS? One option would be to use the form and input states along with the ng-class
directive to, say, add a class dirty
when myForm.$dirty
is true. But AngularJS provides an easier option.
For each of the states we described previously, AngularJS adds and removes the CSS classes shown in Table 4-3 to and from the forms and input elements.
Form state | CSS class applied |
|
|
|
|
|
|
|
|
Similarly, for each of the validators that we add on the input fields, we also get a CSS class in a similarly named fashion, as demonstrated in Table 4-4.
Input state | CSS class applied |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Other than the basic input states, AngularJS takes the name of the validator (number, maxlength, pattern, etc.) and depending on whether or not that particular validator has been satisfied, adds the ng-valid-validator_name
or ng-invalid-validator_name
class, respectively.
Let’s take an example of how this might be used to highlight the input in different ways:
<!-- File: chapter4/form-styling.html -->
<html
ng-app=
"notesApp"
>
<head>
<title>
Notes App</title>
<style>
.username.ng-valid
{
background-color
:
green
;
}
.username.ng-dirty.ng-invalid-required
{
background-color
:
red
;
}
.username.ng-dirty.ng-invalid-minlength
{
background-color
:
lightpink
;
}
</style>
</head>
<body
ng-controller=
"MainCtrl as ctrl"
>
<form
ng-submit=
"ctrl.submit()"
name=
"myForm"
>
<input
type=
"text"
class=
"username"
name=
"uname"
ng-model=
"ctrl.user.username"
required
ng-minlength=
"4"
>
<input
type=
"submit"
value=
"Submit"
ng-disabled=
"myForm.$invalid"
>
</form>
<script
src=
"https://ajax.googleapis.com/ajax/libs/angularjs/1.3.11/angular.js"
>
</script>
<script
type=
"text/javascript"
>
angular
.
module
(
'notesApp'
,
[])
.
controller
(
'MainCtrl'
,
[
function
()
{
var
self
=
this
;
self
.
submit
=
function
()
{
console
.
log
(
'User clicked submit with '
,
self
.
user
);
};
}]);
</script>
</body>
</html>
In this example, we kept the existing functionality of the validators, though we removed the specific error messages. Instead, what we try to do is mark out the required field using CSS classes. So here is what the example accomplishes:
-
When the field is correctly filled out, it turns the input box green. This is done by setting the background color when the CSS class
ng-valid
is applied to our input field. -
We want to display the background as dark red if the user starts typing in, and then undoes it. That is, we want to set the background as red, marking it as a required field, but only after the user modifies the field. So we set the background color to be red if the CSS classes
ng-dirty
(which marks that the user has modified it) andng-invalid-minlength
(which marks that the user has not typed in the necessary amount of characters) are applied.
Similarly, you could add a CSS class that shows a * mark in red if the field is required but not dirty. Using a combination of these CSS classes (and the form and input states) from before, you can easily style and display all the relevant and actionable things to the user about your form.
ngModelOptions
As you might have noticed with ng-model
, any keystroke causes the AngularJS model to immediately update and refresh the UI. This actually causes an entire UI update (known as the digest cycle, covered more in-depth in Chapter 13). But we might not want this behavior. We might instead want to update the model when the user stops typing for half a second, or moves on to the next input element, thus saving some UI update cycles.
With AngularJS 1.3, we can now set up and define exactly how the ng-model
directive works and updates the UI. With ng-model-options
, we can set the following options for how ng-model
works:
-
updateOn
-
This is a string that dictates which events of the input should the
ng-model
directive listen on and update. You can specifydefault
to tell it to listen on the default events of the control (keypress for text boxes, click for checkboxes, and so on), or specify your own, likeblur
. You can give multiple events space separated to this option as well. -
debounce
-
This can be either an integer value or an object.
debounce
sets the period of time in milliseconds AngularJS should wait for the user to stop performing an action before updating the AngularJS model variable. If this is an object instead of an integer, then we can specifydebounce
for each individual event specified inupdateOn
, like{"default": 500, "blur": 0}
for a text box. This directs AngularJS to update the text box when the user stops typing for half a second, or moves away from the input field. A value of0
tells AngularJS to update it immediately. -
allowInvalid
-
false
by default, AngularJS will not set the value in theng-model
variable if the input field is invalid as per the validators present. If we want to set the value regardless of the state of the validator, we can set theallowInvalid
option totrue
. -
getterSetter
-
This is a boolean value that allows us to treat the
ng-model
expression as a getter/setter instead of a variable.
Let’s look at an example of the ng-model-options
in action:
<!-- File: chapter4/ng-model-options.html -->
<html
ng-app=
"modelApp"
>
<head><title>
Ng Model Options</title></head>
<body
ng-controller=
"MainCtrl as ctrl"
>
<div>
<input
type=
"text"
ng-model=
"ctrl.withoutModelOptions"
ng-minlength=
"5"
/>
You typed {{ctrl.withoutModelOptions}}</div>
<div>
<input
type=
"text"
ng-model=
"ctrl.withModelOptions"
ng-model-options=
"ctrl.modelOptions"
ng-minlength=
"5"
/>
You typed {{ctrl.withModelOptions()}}</div>
<script
src=
"https://ajax.googleapis.com/ajax/libs/angularjs/1.3.11/angular.js"
>
</script>
<script
type=
"text/javascript"
>
angular
.
module
(
'modelApp'
,
[])
.
controller
(
'MainCtrl'
,
[
function
()
{
this
.
withoutModelOptions
=
''
;
var
_localVar
=
''
;
this
.
modelOptions
=
{
updateOn
:
'default blur'
,
debounce
:
{
default
:
1000
,
blur
:
0
},
getterSetter
:
true
,
allowInvalid
:
true
};
this
.
withModelOptions
=
function
(
txt
)
{
if
(
angular
.
isDefined
(
txt
))
{
_localVar
=
txt
;
}
else
{
return
_localVar
;
}
};
}]);
</script>
</body>
</html>
In this example, we have two input fields. The first one is bound using ng-model
to the variable withoutModelOptions
. There is a validation applied on it to ensure it contains at least five characters. We also see the value of it in the UI through a binding expression immediately after that (which only triggers after we have typed in five characters).
The second model is bound to a getter/setter function (withModelOptions
). Now normally, this would not work with ng-model
, which is why we also specify an ng-model-options
along with it. Now the value passed to ng-model-options
is an object that can be defined directly in the HTML, or as in this case, refer to a variable from our controller in case of complex options. As part of our ng-model-options
, we have set it to:
- Bind on the default type events and the blur event
- Set the debounce time to 1 second in case of type, and immediate in case of a blur event
- Bind to a getter and setter that is defined in our controller, instead of a variable
- Allow invalids, which ensures that even if the form field is invalid, we still have access to the invalid value in our controller
Note
You can also ådd an ngModelOptions
to a higher-level element (say, the form, or the topmost element containing the entire AngularJS application) to have it apply as default to all your ngModel
in your application, instead of repeating it for each element with the ngModel
directive.
Nested Forms with ng-form
By this point, we know how to create forms and get data into and out of our controllers (by binding it to a model). We have also seen how to perform simple validation, and style and display conditional error messages in AngularJS.
The next part that we want to cover is how to deal with more complicated form structures, and grouping of elements. We sometimes run into cases where we need subsections of our form to be valid as a group, and to check and ascertain its validity. This is not possible with the HTML form
tag because form
tags are not meant to be nested.
AngularJS provides an ng-form
directive, which acts similar to form
but allows nesting, so that we can accomplish the requirement of grouping related form fields under sections:
<!-- File: chapter4/nested-forms.html -->
<html
ng-app
>
<head>
<title>
Notes App</title>
</head>
<body>
<form
novalidate
name=
"myForm"
>
<div>
<input
type=
"text"
class=
"username"
name=
"uname"
ng-model=
"ctrl.user.username"
required=
""
placeholder=
"Username"
ng-minlength=
"4"
/>
<input
type=
"password"
class=
"password"
name=
"pwd"
ng-model=
"ctrl.user.password"
placeholder=
"Password"
required=
""
/>
</div>
<ng-form
name=
"profile"
>
<input
type=
"text"
name=
"firstName"
ng-model=
"ctrl.user.profile.firstName"
placeholder=
"First Name"
required
>
<input
type=
"text"
name=
"middleName"
placeholder=
"Middle Name"
ng-model=
"ctrl.user.profile.middleName"
>
<input
type=
"text"
name=
"lastName"
placeholder=
"Last Name"
ng-model=
"ctrl.user.profile.lastName"
required
>
<input
type=
"date"
name=
"dob"
placeholder=
"Date Of Birth"
ng-model=
"ctrl.user.profile.dob"
>
</ng-form>
<span
ng-show=
"myForm.profile.$invalid"
>
Please fill out the profile information</span>
<input
type=
"submit"
value=
"Submit"
ng-disabled=
"myForm.$invalid"
/>
</form>
<script
src=
"https://ajax.googleapis.com/ajax/libs/angularjs/1.3.11/angular.js"
>
</script>
</body>
</html>
In this example, we nest a subform inside our main form, but because the HTML form element cannot be nested, we use the ng-form
directive to do it. Now we can have substate within our form, evaluate quickly if each section is valid, and leverage the same binding and form states that we have looked at so far. A quick highlight of the features in the example:
-
A subform using the
ng-form
directive. We can give this a name to identify and grab the state of the subform. -
The state of the subform can be accessed directly (
profile.$invalid
) or through the parent form (myForm.profile.$invalid
). -
Individual elements of the form can be accessed as normal (
profile.firstName.$error.required
). -
Subforms and nested forms still affect the outer form (the
myForm.$invalid
istrue
because of the use of the required tags).
You could have subforms and groupings that have their own way of checking and deciding validity, and ng-form
allows you to model that grouping in your HTML.
Other Form Controls
We have dealt with forms, ng-model
s, and bindings, but mostly we have only looked at regular text boxes. Let’s see how to interact and work with other form elements in AngularJS.
Textareas
Textareas in AngularJS work exactly the same as text inputs. That is, to have two-way data-binding with a textarea, and make it a required field, you would do something like:
<textarea ng-model="ctrl.user.address" required></textarea>
All the data-binding, error states, and CSS classes remain as we saw it with text inputs.
Checkboxes
Checkboxes are in some ways easier to deal with because they can only have one of two values: true or false. So an ng-model
two-way data-binding to the checkbox basically takes a Boolean value and assigns the checked state based on it. After that, any changes to the checkbox toggles the state of the model:
<input type="checkbox" ng-model="ctrl.user.agree">
But what if we didn’t have just Boolean values? What if we wanted to assign the string YES or NO to our model, or have the checkbox checked when the value is YES? AngularJS gives two attribute arguments to the checkbox that allow us to specify our custom values for the true and false values. We could accomplish this as follows:
<input type="checkbox" ng-model="ctrl.user.agree" ng-true-value="'YES'" ng-false-value="'NO'">
This sets the value of the agree
field to YES if the user checks the checkbox, and NO if the user unchecks it.
Warning
Notice the single quotes around YES and NO. ng-true-value
and ng-false-value
, as of AngularJS 1.3, take constant expressions as arguments. Prior to AngularJS 1.3, we could specify the true
and false
values directly like ng-true-value="YES"
. As of AngularJS 1.3, though, to be consistent with the rest of the AngularJS directives, ng-true-value
and ng-false-value
take AngularJS constant expressions only.
Do note that you cannot currently pass in a variable reference to ng-true-value
or ng-false-value
. They only take constant strings. You cannot refer to a controller variable for ng-true-value
or ng-false-value
.
But what if we didn’t want the two-way data-binding, and just want to use the checkbox to display the current value of a Boolean? That is, one-way data-binding where the state of the checkbox changes when the value behind it changes, but the value doesn’t change on checking or unchecking the checkbox.
We can accomplish this using the ng-checked
directive, which binds to an AngularJS expression. Whenever the value is true, AngularJS will set the checked property for the input element, and remove and unset it when the value is false. Let’s use the following example to demonstrate all these together:
<!-- File: chapter4/select-example.html -->
<html
ng-app=
"notesApp"
>
<head><title>
Notes App</title></head>
<body
ng-controller=
"MainCtrl as ctrl"
>
<div>
<h2>
What are your favorite sports?</h2>
<div
ng-repeat=
"sport in ctrl.sports"
>
<label
ng-bind=
"sport.label"
></label>
<div>
With Binding:<input
type=
"checkbox"
ng-model=
"sport.selected"
ng-true-value=
"'YES'"
ng-false-value=
"'NO'"
>
</div>
<div>
Using ng-checked:<input
type=
"checkbox"
ng-checked=
"sport.selected === 'YES'"
>
</div>
<div>
Current state: {{sport.selected}}</div>
</div>
</div>
<script
src=
"https://ajax.googleapis.com/ajax/libs/angularjs/1.3.11/angular.js"
>
</script>
<script
type=
"text/javascript"
>
angular
.
module
(
'notesApp'
,
[])
.
controller
(
'MainCtrl'
,
[
function
()
{
var
self
=
this
;
self
.
sports
=
[
{
label
:
'Basketball'
,
selected
:
'YES'
},
{
label
:
'Cricket'
,
selected
:
'NO'
},
{
label
:
'Soccer'
,
selected
:
'NO'
},
{
label
:
'Swimming'
,
selected
:
'YES'
}
];
}]);
</script>
</body>
</html>
With this example, we have an ng-repeat
, which has a checkbox with ng-model
, a checkbox with ng-checked
, and a div with the current state bound to it. The first checkbox uses the traditional two-way data-binding with the ng-model
directive. The second checkbox uses ng-checked
. This means that:
-
When the user checks the first checkbox, the value of
selected
becomesYES
because the true value, set usingng-true-value
, isYES
. This triggers theng-checked
and sets the second box as checked (or unchecked). -
When the user unchecks the first box, the value of
selected
is set toNO
because of theng-false-value
. -
The second checkbox in each repeater element displays the state of the
ng-model
usingng-checked
. This updates the state of the checkbox whenever the model backingng-model
changes. Checking or unchecking the second checkbox itself has no effect on the value of the model.
So if you need two-way data-binding, use ng-model
. If you need one-way data-binding with checkboxes, use ng-checked
.
Radio Buttons
Radio buttons behave similarly to checkboxes, but are slightly different. You can have multiple radio buttons (and you normally do) that each assigns a different value to a model depending on which one is selected. You can specify the value using the traditional value attribute of the input element. Let’s see how that would look:
<div
ng-init=
"user = {gender: 'female'}"
>
<input
type=
"radio"
name=
"gender"
ng-model=
"user.gender"
value=
"male"
>
<input
type=
"radio"
name=
"gender"
ng-model=
"user.gender"
value=
"female"
>
</div>
In this example, we have two radio buttons. We gave them both the same name so that when one is selected, the other gets deselected. Both of them are bound to the same ng-model
(user.gender
). Next, each of them has a value, which is the value that gets stored in user.gender
(male if it is the first radio button; female, otherwise). Finally, we have an ng-init
block surrounding it, which sets the value of user.gender
to be female by default. This has the effect of ensuring that the second checkbox is selected when this snippet of HTML loads.
But what if our values are dynamic? What if the value we needed to assign was decided in our controller, or some other place? For that, AngularJS gives you the ng-value
attribute, which you can use along with the radio buttons. ng-value
takes an AngularJS expression, and the return value of the expression becomes the value that is assigned to the model:
<div
ng-init=
"otherGender = 'other'"
>
<input
type=
"radio"
name=
"gender"
ng-model=
"user.gender"
value=
"male"
>
Male<input
type=
"radio"
name=
"gender"
ng-model=
"user.gender"
value=
"female"
>
Female<input
type=
"radio"
name=
"gender"
ng-model=
"user.gender"
ng-value=
"otherGender"
>
{{otherGender}}</div>
In this example, the third option box takes a dynamic value. In this case, we assign it as part of the initialization block (ng-init
), but in a real application, the initialization could be done from within a controller instead of in the HTML directly. When we say ng-value="otherGender"
, it doesn’t assign otherGender
as a string to user.gender
, but the value of the otherGender
variable, which is other
.
Combo Boxes/Drop-Downs
The final HTML form element (which can be used outside forms as well) is the select box, or the drop-down/combo box as it is commonly known. Let’s take a look at the simplest way you can use select boxes in AngularJS:
<div
ng-init=
"location = 'India'"
>
<select
ng-model=
"location"
>
<option
value=
"USA"
>
USA</option>
<option
value=
"India"
>
India</option>
<option
value=
"Other"
>
None of the above</option>
</select>
</div>
In this example, we have a simple select box that is data-bound to the variable location
. We also initialize the value of location
to India, so when the HTML loads, India is the selected option. When the user selects any of the other options, the value of the value attribute gets assigned to the ng-model
. The standard validators and states also apply to this field, so those can be applied (required
, etc.).
This has a few restrictions, though:
- You need to know the values in the drop-down up front.
- They need to be hardcoded.
- The values can only be strings.
In a truly dynamic app, one or none of these might be true. In such a case, the select
HTML element also has a way of dynamically generating the list of options, and working with objects instead of pure string ng-models
. This is done through the ng-options
directive. Let’s take a look at how this might work:
<!-- File: chapter4/checkbox-example.html -->
<html
ng-app=
"notesApp"
>
<head><title>
Notes App</title></head>
<body
ng-controller=
"MainCtrl as ctrl"
>
<div>
<select
ng-model=
"ctrl.selectedCountryId"
ng-options=
"c.id as c.label for c in ctrl.countries"
>
</select>
Selected Country ID : {{ctrl.selectedCountryId}}</div>
<div>
<select
ng-model=
"ctrl.selectedCountry"
ng-options=
"c.label for c in ctrl.countries"
>
</select>
Selected Country : {{ctrl.selectedCountry}}</div>
<script
src=
"https://ajax.googleapis.com/ajax/libs/angularjs/1.3.11/angular.js"
>
</script>
<script
type=
"text/javascript"
>
angular
.
module
(
'notesApp'
,
[])
.
controller
(
'MainCtrl'
,
[
function
()
{
this
.
countries
=
[
{
label
:
'USA'
,
id
:
1
},
{
label
:
'India'
,
id
:
2
},
{
label
:
'Other'
,
id
:
3
}
];
this
.
selectedCountryId
=
2
;
this
.
selectedCountry
=
this
.
countries
[
1
];
}]);
</script>
</body>
</html>
In this example, we have two select boxes, both bound to different models in our controller. The first select
element is bound to ctrl.selectedCountryId
and the second one is bound to ctrl.selectedCountry
. Note that one is a number, while the other is an actual object. How do we achieve this?
-
We use the
ng-options
attribute on the select dialog, which allows us to repeat an array (or object, similar tong-repeat
from Working with and Displaying Arrays) and display dynamic options. -
The syntax is similar to
ng-repeat
as well, with some additional ability to select what is displayed as the label, and what is bound to the model. -
In the first select box, we have
ng-options="c.id as c.label for c in ctrl.countries"
. This tells AngularJS to create one option for each country in the array of countries. The syntax is as follows:modelValue as labelValue for item in array
. In this case, we tell AngularJS that ourmodelValue
is the ID of each element, the label value is the label key of each array item, and then our typicalfor each
loop. -
In the second select box, we have
ng-options="c.label for c in ctrl.countries"
. Here, when we omit themodelValue
, AngularJS assumes that each item in the repeat is the actual model value, so when we select an item from the second select box, the country object (c) of that option box gets assigned toctrl.selectedCountry
. - Because the backing model for the two select boxes are different, changing one does not affect the value or the selection in the other drop-down.
-
You can also optionally give a grouping clause, for which the syntax would be
ng-options="modelValue as labelValue group by groupValue for item in array"
. Similar to how we specified the model and label values, we can point thegroupValue
at another key in the object (say, continent). -
When you use objects, the clause changes as follows:
modelValue as labelValue group by groupValue for (key, value) in object
.
Note
AngularJS compares the ng-options
individual values with the ng-model
by reference. Thus, even if the two are objects that have the same keys and values, AngularJS will not show that item as selected in the drop-down unless and until they are the same object. We accomplished this in our example by using an item from the array countries
to assign the initial value of the model.
There is a better way to accomplish this, which is through the use of the track by syntax with ng-options
. We could have written the ng-options
as:
ng-options="c.label for c in ctrl.countries track by c.id"
This would ensure that the object c
is compared using the ID field, instead of by reference, which is the default.
Conclusion
We started with the most common requirements, which is getting data in and out of UI forms. We played around with ng-model
, which gives us two-way data-binding to remove most of the boilerplate code we would write when working with forms. We then saw how we could leverage form validation, and show and style error messages. Finally, we saw how to deal with other types of form elements, and the kinds of options AngularJS gives to work with them.
In the next chapter, we start dealing with AngularJS services and then jump into server communication using the $http
service in AngularJS.
Get AngularJS: Up and Running now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.