AngularJS is a JavaScript framework to simplify the development of dynamic web applications by using declarative html annotations that bind to models declared in JavaScript.
Note: We are using Angular version 1. There are later versions of Angular, but they involve a much more complicated configuration. To simplify configuration and increase stability, we decided to go with an earlier version.
For this workshop, we'll first discuss concepts related to Angular, and then work on tasks for updating an incomplete version of the coffeemaker app, in order to better understand Angular.
Angular 1 supports a MVVM (Model-View-ViewModel) architecture, essentially, a UI architecture where changes are synchronized between the view and model using databinding, an automatic way of updating the view whenever the model changes, as well as updating the model whenever the view changes. A ViewModel serves as an intermediary, allowing presentation state and behavior to react to changes in the view or model. This is distinct from other frameworks, such as React and Angular 2, which adopt a component-based model architecture.
To implement a databindings in a view, annotations (via html tag attributes) are added to html.
Coffee: <span id="currentCoffee" ng-bind="ctrl.inventoryMaster.coffee"></span><br />
Milk: <span id="currentMilk" ng-bind="ctrl.inventoryMaster.milk"></span><br />For example, the ng-bind attribute will tell Angular to set the value of the currentCoffee span's text node to be equal to the value of ctrl.inventoryMaster.coffee. If that value is updated in the ViewModel (controller), then the span's text will be updated automatically and vice versa.
Databinding also works for lists. Using the ng-repeat directive, it is possible to bind everything in the orders list to dynamically generated items in html, contained inside a <md-list></md-list>. Within, a <md-list-item></md-list-item>, it is possible to refer to the individuals object's properties, e.g., item.orderNumber.
<md-list>
<md-list-item class="md-2-line" ng-repeat="item in ctrl.orders">
<div class="md-list-item-text">
<h5>Order #{{item.orderNumber}}</h5>
<p>Ordered: {{item.description}}</p>
</div>
</md-list-item>
</md-list>Databinding to inputs in HTML Forms can be more complicated. Here is an example for binding to the input field for coffee. There are multiple features being handled by Angular: detection of input changes (dirty state), error validation, and error feedback.
<div class="form-group col-md-12">
<label class="col-md-2 control-label" for="file">Coffee</label>
<div class="col-md-7">
<input type="text" ng-model="ctrl.inventory.coffee"
name="coffee" class="coffee form-control input-sm"
placeholder="Enter amount of coffee" required="0" />
<div class="has-error" ng-show="addInventoryForm.$dirty">
<span ng-show="addInventoryForm.coffee.$error.required">This
is a required field</span> <span
ng-show="addInventoryForm.coffee.$error.min">Minimum
amount is 0</span> <span ng-show="addInventoryForm.coffee.$invalid">This
field is invalid </span>
</div>
</div>
</div>EXERCISE: Update the index.html to input milk.
Angular provides other annotations to help manage the application's behavior.
The main functionality of the page is contained between the body tags. The opening body tag contains ng-app, which means that the JavaScript is active over the given portion of the code enclosed by the tag. By putting ng-app in body, the entire page support's Angular's JavaScript directives. Setting the class attribute to ng-cloak means that the raw HTML will not be displayed in it's uncompiled form. This reduces the flicker effect while the page loads.
<body ng-app="myApp" class="ng-cloak">Below is a description of the various Angular attributes:
ng-contoller: Adds a controller that's defined in JavaScript.ctrlis the name used in the rest of the page to refer to the controller.ng-submit: Connects the submit action of a form to a function in the controller.ng-model: Connects an input field to a field in the controller.ng-show: Determines if an element is displayed or not. In the example here, the error message will only show if the form is$dirty(or has been interacted with by a user). The error message displayed depends on if there was an$erroror$invalidinput.ng-disabled: Disables an element from editing by a user. In this example, the Submit button is disabled if there are invalid inputs. The Reset Form button is disabled if the form is$pristinewhere the user hasn't yet interacted with it.
Databinding is a two-way process. We must also ensure that the ViewModel has the correct implementation. Sometimes this is referred to as the code-behind a view.
To create a controller, we can do the following:
angular.module('myApp').controller('InventoryController', ['$scope', 'InventoryService', function($scope, InventoryService) {A controller can store the view model's properties:
self.inventoryMaster = {coffee:'', milk:'', sugar:'', chocolate:''};And can provide logic for updating the properties:
function getInventory(){
InventoryService.getInventory()
.then(
function(data) {
self.inventoryMaster = data;
},
function(errResponse){
console.error('Error while getting Inventory');
}
);
}In addition to a controller, you will need a service provider, which provides your controller with values from your model (db). Usually, this is implemented as http requests to a REST API.
Because JavaScript can have asynchronous method calls, we need a way to have code that responses to events. One method is to use promises, which are objects with a callback that can be used to handle asynchronous methods. Angular uses $q, which provides a promise implementation that can be either resolved (completed successfully) or rejected (failed with an error).
function getInventory() {
var deferred = $q.defer();
$http.get(REST_SERVICE_URI)
.then(
function (response) {
deferred.resolve(response.data);
},
function(errResponse){
console.error('Error while getting inventory');
deferred.reject(errResponse);
}
);
return deferred.promise;
} When building new Angular pages, you may not yet have a fully functional REST service working. Indeed, this code is currently using a mock implementation of a REST service, using $httpBackend which enables you to work on a web page until a real REST service is ready. This essentially will listen for requests to certain pages, and return back mock objects in response.
var mockInventory = {coffee: 99, milk: 99};
$httpBackend.whenRoute('GET', '/api/v1/inventory')
.respond(function(method, url, data, headers, params) {
return [200, mockInventory];
});To switch to the real REST service, simply comment out the mock response calls.
-
Update the index.html to allow add display and input bindings for sugar, and chocolate. NOTE: You will also need to update inventory_service.js to update to mockInventory to handle retrieving and setting sugar and chocolate.
-
Extend the implementation of "Order History". Whenever the user adds a new order, update the orders list in inventory_service.js to store a string, so that the order history can be displayed as such:
- Order #1
Ordered: coffee: 99, milk: 99, sugar: 99, chocolate: 99 - Order #2
Ordered: coffee: 1, milk, 1, sugar, 1, chocolate: 1
- Order #1
The Angular templates for CoffeeMaker are in src/main/resources/templates/. You can examine the recipe.html template which supports the add recipe functionality as a source of inspiration for the edit recipe functionality.
After this workshop, you should be able to extend CoffeeMaker to have a new Edit Recipe Form, using Angular, by creating an initial mock service implementation before you complete a REST service for retrieving the real values from the database.
