SportsStore – Orders and Administration with AngularJS: Getting Shipping Details

After showing the user a summary of the products in the cart, I want to capture the shipping details for the order. That takes me to the AngularJS features for working with forms, which you are likely to require in most web applications.

I have created the views/placeOrder.html file to capture the user’s shipping details, which is the view named in one of the routing URLs shown earlier. I am going to introduce a number of form-related features, and to avoid having to repeat largely similar code, I am going to start working with a couple of data properties (for the user’s name and street address) and then add other properties when I have introduced the features I will be using. Listing 8-1 shows the initial content of the placeOrder.html view file.

Listing 8-1. The Contents of the placeOrder.html File

<h2>Check out now</h2>

<p>Please enter your details, and we’ll ship your goods right away!</p>

<div class=”well”>

<h3>Ship to</h3>

<div class=”form-group”>

<label>Name</label>

<input class=”form-control” ng-model=”data.shipping.name” />

</div>

<h3>Address</h3>

<div class=”form-group”>

<label>Street Address</label>

<input class=”form-control” ng-model=”data.shipping.street” />

</div>

<div class=”text-center”>

<button class=”btn btn-primary”>Complete order</button>

</div>

</div>

The first thing to notice about this view is that I have not used the ng-controller directive to specify a controller. That means the view will be supported by the top-level controller, sportsStoreCrtl, which manages the view that contains the ng-view directive (which I introduced in Chapter 7). I make this point because you don’t have to define controllers for partial views, which is convenient when the view doesn’t require any additional behaviors, as is the case here.

The important AngularJS feature in the listing is the use of the ng-model directive on the input elements, like this:

<input class=”form-control” ng-model=”data.shipping.name” />

The ng-model directive sets up a two-way data binding. I explain data bindings in depth in Chapter 10, but the short version is that the kind of data binding I have been using so far in the SportsStore application—the ones that use the {{ and }} characters—are one-way bindings, which means they simply display a value from the scope. The value a one-way binding displays can be filtered, or it can be an expression rather than just a data value, but it a read-only relationship. The value displayed by the binding will be updated if the corresponding value on the scope changes, but that’s the only direction that updates flow in—from the scope to the binding.

Two-way data bindings are used on form elements to allow the user to enter values that change the scope, rather than just displaying them. Updates flow in both directions between the scope and the data binding. An update to the scope data property performed through a JavaScript function, for example, will cause an input element to display the new value, and a new value entered by the user into the input element will update the scope. I explain the use of the ng-model directive in Chapter 10 and the broader AngularJS support for forms in Chapter 12. For this chapter it is enough to know that when the user enters a value into an input element, that value is assigned to the scope property specified by the ng-model directive—either the data.shipping.name property or the data.shipping.street property in this example. You can see how the form looks in the browser in Figure 8-1.

Tip Notice that I don’t have to update the controller so that it defines a data.shipping object on its scope or the individual name or street properties. AngularJS scopes are remarkably flexible and assume that you want to define a property dynamically if it isn’t already defined. I explain this in more detail in Chapter 13.

1. Adding Form Validation

If you have written any kind of web application that uses form elements, then you will already know that users will put just about anything in an input field and that it is unwise to assume that users will have provided meaningful and useful data. To ensure you get the data you expect, AngularJS supports form validation, which allows values to be checked for suitability.

AngularJS form validation is based on honoring standard HTML attributes applied to form elements, such as type and required. Form validation is performed automatically, but some work is required to display validation feedback to the user and to integrate the overall validation results into an application.

Tip HTML5 defined a new set of values for the type attribute on input elements, which can be used to specify that a value should be an e-mail address or a number, for example. As I explain in Chapter 12, AngularJS can validate some of these new values.

1.1. Preparing for Validation

The first step in setting up form validation is to add a form element to the view and add the validation attributes to my input elements. Listing 8-2 shows the changes to the placeOrder.html file.

Listing 8-2. Preparing the placeOrder.html File for Validation

<h2>Check out now</h2>

<p>Please enter your details, and we’ll ship your goods right away!</p>

<form name=”shippingForm” novalidate>

<div class=”well”>

<h3>Ship to</h3>

<div class=”form-group”>

<label>Name</label>

<input class=”form-control” ng-model=”data.shipping.name” required />

</div>

<h3>Address</h3>

<div class=”form-group”>

<label>Street Address</label>

<input class=”form-control” ng-model=”data.shipping.street” required />

</div>

<div class=”text-center”>

<button class=”btn btn-primary”>Complete order</button>

</div>

</div>

</form>

The form element has three purposes, even though I won’t be using the browser’s built-in support for submitting forms in the SportsStore application.

The first purpose is to enable validation. AngularJS redefines some HTML elements with custom directives to enable special features, and one such element is form. Without a form element, AngularJS won’t validate the contents of elements such as input, select, textarea, and so on.

The second purpose of the form element is to disable any validation that the browser might try to perform, which is done through the application of the novalidate attribute. This attribute is a standard HTML5 feature, and it ensures that only AngularJS is checking the data that the user provides. If you omit the novalidate attribute, then the user may get conflicting or duplicated validation feedback, depending on the browser being used.

The final purpose of the form element is to define a variable that will be used to report on the form validity. This is done through the name attribute, which I have set to shippingForm. You’ll see how this value is used later in this chapter when I display validation feedback and when I wire up the button element so that the user can place the order only when the contents of the form are valid.

In addition to the form element, I have applied the required attribute to the input elements. This is one of the simplest validation attributes that AngularJS recognizes, and it means that the user has to provide a value—any value— for the input element to be valid. See Chapter 12 for details of the other ways in which you can validate form elements.

1.2. Displaying Validation Feedback

Once the form element and the validation attributes are in place, AngularJS starts to validate the data that the user provides, but I have to do a little more work to give the user any feedback. I get into the details in Chapter 12, but there are two kinds of feedback I can use: I can define CSS styles to take advantage of classes that AngularJS assigns valid and invalid form elements to, and I can use scope variables to control the visibility of targeted feedback messages for specific elements. Listing 8-3 shows both kinds of changes.

Listing 8-3. Applying Validation Feedback to the placeOrder.html File

<style>

.ng-invalid { background-color: lightpink; }

.ng-valid { background-color: lightgreen; }

span.error { color: red; font-weight: bold; }

</style>

<h2>Check out now</h2>

<p>Please enter your details, and we’ll ship your goods right away!</p>

<form name=”shippingForm” novalidate>

<div class=”well”>

<h3>Ship to</h3>

<div class=”form-group”>

<label>Name</label>

<input name=”name” class=”form-control”

ng-model=”data.shipping.name” required />

<span class=”error” ng-show=”shippingForm.name.$error.required”>

Please enter a name

</span>

</div>

<h3>Address</h3>

<div class=”form-group”>

<label>Street Address</label>

<input name=”street” class=”form-control”

ng-model=”data.shipping.street” required />

<span class=”error” ng-show=”shippingForm.street.$error.required”>

Please enter a street address

</span>

</div>

<div class=”text-center”>

<button class=”btn btn-primary”>Complete order</button>

</div>

</div>

</form>

AngularJS assigns form elements to the ng-valid and ng-invalid classes, so I started by defining a style element that contains CSS styles that target those classes. Form elements are always in one of these classes, such that one of these styles is always applied.

Tip I am setting up a simple validation configuration for the SportsStore application, the effect of which is that the form is invalid from the moment that it is shown to the user. This isn’t always acceptable, and in Chapter 12 I describe some additional features that AngularJS provides to control when validation messages are displayed.

The CSS styles have the effect of indicating when there is a problem with an input element but provide no indication what the problem is. For that I have to add a name attribute to each element and use some validation data that AngularJS adds to the scope to control the visibility of error messages, like this:

<input name=”street” class=”form-control” ng-model=”data.shipping.street” required />

<span class=”error” ng-show=”shippingForm.street.$error.required”>

Please enter a street address

</span>

In this fragment, I have shown the input element that captures the user’s street address, which I have assigned the name value of street. AngularJS creates a shippingForm.street object on the scope (which is the combination of the name of the form element and the name of the input element). This object defines a $error property, which itself is an object that has properties for each of the validation attributes that the contents of the input element fail to satisfy. Or, to put it another way, if the shippingForm.street.$error.required property is true, then I know that the contents of the street input element are invalid, which I use to display an error message to the user through the application of the ng-show directive. (I explain the validation properties fully in Chapter 12 and the ng-show directive in Chapter 11.) You can see the initial state of the form in Figure 8-2.

I satisfy the required attribute as I enter details into the input elements, which has the effect of switching the color applied to the element from red to green and hiding the error message.

Note I am deliberately simplifying the way I apply validation in this chapter, but AngularJS can be used to create much more subtle and pleasing validation configurations, as I describe in Chapter 12.

1.3. Linking the Button to Validity

In most web applications, the user shouldn’t be able to move to the next step in a process until all the form data has been provided and is valid. To that end, I want to disable the Complete order button when the form is invalid and automatically enable it when the user has completed the form properly.

To do this, I can take advantage of the validation information that AngularJS adds to the scope. In addition to the per-field information that I used in the previous section to display per-element messages, I can get information about the overall state of the form as well. The shippingForm.$invalid property will be set to true when one or more of the input elements is invalid, and I can combine this with the ng-disabled directive to manage the state of the button element. I describe the ng-disabled directive in Chapter 11, but it adds and removes the disabled attribute from the element it has been applied to based on the scope property or expression it is configured with. Listing 8-4 shows how I can tie the state of the button to form validation.

Listing 8-4. Setting the State of the Button in the placeOrder.html File

<div class=”text-center”>

<button ng-disabled=”shippingForm.$invalid”

class=”btn btn-primary”>Complete order</button>

</div>

You can see the effect that the ng-disabled directive has on the button element in Figure 8-3.

2. Adding the Remaining Form Fields

Now that you have seen how AngularJS form validation works, I am going to add the remaining input elements to the form. I avoided this earlier because I wanted to show you the individual validation features without listing duplicate markup, but I can’t go any further without the completed form. Listing 8-5 shows the addition of the remaining input elements and their associated validation messages.

Listing 8-5. Adding the Remaining Form Fields to the placeOrder.html File

<style>

.ng-invalid { background-color: lightpink; }

.ng-valid { background-color: lightgreen; }

span.error { color: red; font-weight: bold; }

</style>

<h2>Check out now</h2>

<p>Please enter your details, and we’ll ship your goods right away!</p>

<form name=”shippingForm” novalidate>

<div class=”well”>

<h3>Ship to</h3>

<div class=”form-group”>

<label>Name</label>

<input name=”name” class=”form-control”

ng-model=”data.shipping.name” required />

<span class=”error” ng-show=”shippingForm.name.$error.required”>

Please enter a name

</span>

</div>

<h3>Address</h3>

<div class=”form-group”>

<label>Street Address</label>

<input name=”street” class=”form-control”

ng-model=”data.shipping.street” required />

<span class=”error” ng-show=”shippingForm.street.$error.required”>

Please enter a street address

</span>

</div>

<div class=”form-group”>

<label>City</label>

<input name=”city” class=”form-control”

ng-model=”data.shipping.city” required />

<span class=”error” ng-show=”shippingForm.city.$error.required”>

Please enter a city

</span>

</div>

<div class=”form-group”>

<label>State</label>

<input name=”state” class=”form-control”

ng-model=”data.shipping.state” required />

<span class=”error” ng-show=”shippingForm.state.$error.required”>

Please enter a state

</span>

</div>

<div class=”form-group”>

<label>Zip</label>

<input name=”zip” class=”form-control”

ng-model=”data.shipping.zip” required />

<span class=”error” ng-show=”shippingForm.zip.$error.required”>

Please enter a zip code

</span>

</div>

<div class=”form-group”>

<label>Country</label>

<input name=”country” class=”form-control”

ng-model=”data.shipping.country” required />

<span class=”error” ng-show=”shippingForm.country.$error.required”>

Please enter a country

</span>

</div>

<h3>Options</h3>

<div class=”checkbox”>

<label>

<input name=”giftwrap” type=”checkbox”

ng-model=”data.shipping.giftwrap” />

Gift wrap these items

</label>

</div>

<div class=”text-center”>

<button ng-disabled=”shippingForm.$invalid”

class=”btn btn-primary”>Complete order</button>

</div>

</div>

</form>

Tip The markup shown in Listing 8-5 is highly duplicative and is the sort of thing that attracts typos. You might be tempted to try to use the ng-repeat directive to generate the input elements from an array of objects that describes each field. This doesn’t work well because of the way that the attribute values for directives like ng-modei and ng-show are evaluated within the scope of the ng-repeat directive. My advice is to simply accept the duplication in the markup, but if you do want a more elegant technique, then read Chapters 15-17, which describe the ways in which you can create custom directives.

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 *