Wednesday, 27 March 2013

Templates, Routing and Sharing Data Between Views in an Angular JS Application

Till last post, we saw the capabilities of Angular JS within a single page with a form and how to interact with service using AJAX. Another key feature that Angular JS provides is, view composition. We can compose multiple views on the same page, based on some templates which will be loaded as and when they are required. We will discuss about these features in this post.

To make an HTML page able to compose multiple views using AngularJS, we need to add an element with ng-view attribute. Content will be dynamically added inside this element based on certain rules defined by the programmer. Following is the mark-up inside the body tag after removing all content and adding a div with ng-view attribute:

<body ng-app="shopping">
    <div ng-view>
    </div>
</body>

The entire mark-up that we had earlier on the page is moved to AddRemoveItems.htm. No change has been made to the mark-up.
<div>
    Sort by:
    <select ng-model="sortExpression">
        <option value="Name">Name</option>
        <option value="Price">Price</option>
        <option value="Quantity">Quantity</option>
    </select>
</div>
<div>
    <strong>Filter Results</strong></div>
<table>
    <tr>
        <td>
            By Any:
        </td>
        <td>
            <input type="text" ng-model="search.$" />
        </td>
    </tr>
    <tr>
        <td>
            By Name:
        </td>
        <td>
            <input type="text" ng-model="search.Name" />
        </td>
    </tr>
    <tr>
        <td>
            By Price:
        </td>
        <td>
            <input type="text" ng-model="search.Price" />
        </td>
    </tr>
    <tr>
        <td>
            By Quantity:
        </td>
        <td>
            <input type="text" ng-model="search.Quantity" />
        </td>
    </tr>
</table>
<table border="1">
    <thead>
        <tr>
            <td>
                Name
            </td>
            <td>
                Price
            </td>
            <td>
                Quantity
            </td>
            <td>
                Remove
            </td>
        </tr>
    </thead>
    <tbody ng-repeat="item in items | orderBy:mySortFunction | filter: search">
        <tr>
            <td>
                {{item.Name}}
            </td>
            <td>
                {{item.Price | rupee}}
            </td>
            <td>
                {{item.Quantity}}
            </td>
            <td>
                <input type="button" value="Remove" ng-click="removeItem(item.ID)" />
            </td>
        </tr>
    </tbody>
</table>
<div>
    Total Price: {{totalPrice() | rupee}}</div>
<a ng-click="purchase()">Purchase Items</a>
<form name="itemForm" novalidate>
<table>
    <tr>
        <td>
            Name:
        </td>
        <td>
            <input name="name" type="text" ng-model="item.Name" required ng-pattern="name" />
        </td>
    </tr>
    <tr>
        <td>
            Price:
        </td>
        <td>
            <input name="price" type="text" ng-model="item.Price" required valid-price />
        </td>
    </tr>
    <tr>
        <td>
            Quantity:
        </td>
        <td>
            <input name="quantity" type="number" ng-model="item.Quantity" min="1" max="90" ng-pattern="integerval"
                required />
        </td>
    </tr>
    <tr>
        <td colspan="2">
            <input type="Button" value="Add" ng-click="addItem(item)" ng-disabled="itemForm.$invalid" />
        </td>
    </tr>
</table>
</form>
<div class="errorText">
    {{error}}</div>
<div ng-show="itemForm.name.$dirty && itemForm.name.$invalid">
    <span ng-show="itemForm.name.$error.required">Name cannot be left blank</span> <span
        ng-show="itemForm.name.$error.pattern">Name cannot contain numbers or special characters</span>
</div>
<div ng-show="itemForm.price.$dirty && itemForm.price.$invalid">
    <span ng-show="itemForm.price.$error.required">Price cannot be blank</span> <span
        ng-show="itemForm.price.$error.validPrice">Price should be a number between 50 and
        5000 with maximum 2 digits after decimal point</span>
</div>
<div ng-show="itemForm.quantity.$dirty && itemForm.quantity.$invalid">
    <span ng-show="itemForm.quantity.$error.required">Quantity cannot be blank</span>
    <span ng-show="itemForm.quantity.$error.pattern || itemForm.quantity.$error.min || itemForm.quantity.$error.max">
        Quantity should be an integer between 1 and 90</span>
</div>

Once a user adds all desired items to the cart, he should be able to check out the cart. For this, we should navigate the user to another page where the user can see the items on the cart and checkout the items. I created another template file, CheckoutItems.htm. This template will be rendered when the user clicks on checkout link on the AddremoveItems page. You can check the template from the project on Github.

The aim is to render one of these templates on the page based on the current URL. To make this happen, we need to configure routes to the module. Process of adding routes is similar to adding routes in Global.asax ASP.NET MVC. Each route will hold the following information:

  • URL template of the page
  • Path of the HTML template to be rendered
  • Name of the controller
Following is the syntax of configuring routes to the module:
module.config([‘$routeProvider’,<other dependencies>, function($routeProvider,<other parameters>){
 $routeProvider.when(‘<url-template>’,{templateUrl:’<path-of-html-template>’,controller:<name-of-controller>})
   .otherwise({redirectTo:’<default-url-template-to-render>’});
}]);

To learn more on routing, refer to Angular JS official documentation.

Let’s configure routes to shopping module keeping the above syntax in mind. Following are the templates added to the module in our application:

shoppingModule.config(['$routeProvider', function($routeProvider){
        $routeProvider.when('/addRemoveItems',{templateUrl:'Templates/AddRemoveItems.htm',controller:ShoppingCartCtrl})
                      .when('/checkoutItems',{templateUrl:'Templates/CheckoutItems.htm',controller:CartCheckoutCtrl})
                      .otherwise({redirectTo: '/addRemoveItems'});
    }]);

In the AddRemoveItems view, we should have a hyper link to navigate to the CheckoutItems page. When the user clicks on this link, we should send shopping cart data to the checkout view. Since both views have separate controllers, we have to store the data at a central place to make it accessible in the controller of second page. Let’s add a service to the shopping module to hold this data.
shoppingModule.service('shared',function(){
        var cartItems= [];
        return{
            getCartItems: function(){
                return cartItems;
            },
            setCartItems: function(value){
                cartItems=value;
            }
        };
    })

On click of the link on the AddRemoveItems page, the data has to be assigned to cartItems field in the above service and the control has to be navigated to checkout view. This is done using the following function:
$scope.purchase = function () {
        shared.setCartItems($scope.items);
        $window.location.href = "#/checkoutItems";
};

$window used in the above function is a service in Angular JS that references to the browser’s window object. Since we are using it in the controller, it should be injected through parameter. This data can be accessed in checkout view using the shared service. Following code shows how to do it:
function CartCheckoutCtrl($scope, shared){
    function loadItems() {
        $scope.items = shared.getCartItems();
    }
 
    loadItems();
}

Now we have a tiny shopping cart application with two pages. This is definitely not a great application, but it covers most of the useful features of Angular JS.

Complete code of the application is available in the following github repo: AngularShopping

Cart Run the application setting ShoppingCart-MultipleViews.htm as the start-up page. Notice the URL once the page is loaded on browser. Click on the link, it should move to the Checkout view, with an update to the URL. You can also switch between the views using browser’s back and forward buttons.

The point to be observed during navigation is, the page is not fully refreshed. You won’t see the progress bar showing load status when the navigation happens. This is because, we are rendering a small part of the page(portion in the div marked with ng-view) using a template.

Happy coding!

No comments:

Post a Comment