Lately I’ve been working on an application that forecasts Amazon EC2 spot instance prices. (The forecasting element of this application will be described in a forthcoming blog post once I finalize the forecasting method). The tool needed a user interface, and I decided to write it with AngularJS as a learning exercise. This post describes the AngularJS user interface.
First, a demonstration of the application workflow. The user is offered drop-down menus of Amazon EC2 regions, instance types, and operating systems. The options for instance type and operating system change when the region is changed, and the options for operating system change when the instance type is changed. This page looks like:
When all three selections are made, an image giving the forecast appears:
The images are pre-computed daily with a cron job.
The challenge I faced with AngularJS was getting the menus to automatically update according to changes in parent menu selection. (E.g., changing operating system by region selection). This required some tricky binding to accomplish, which I will demonstrate below.
When the application is loaded, it retrieves from a JSON file information about which options belong in which drop-down menus:
The top level of the JSON file dictionary (I’m using Python terminology here) contains the regions as keys. The second layer contains the instance type as keys. The third layer gives the operating system as keys. The final layer, the array of letters, gives the availability zones. The use of select options as keys caused difficulty when I started with AngularJS, which I will illustrate in the code that follows.
The HTML code looks like:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>EC2 Spot Instance Forecaster</title> <script src="angular.min.js" type="text/javascript"></script> <script src="ec2-forecast.js" type="text/javascript"></script> </head> <body ng-app="ForecastApp"> <div ng-controller="ForecastController"> <h1>EC2 Spot Instance Price Forecaster</h1> <p>Select Amazon EC2 Region</p> <select ng-model="myLocationKey" ng-change="locationChange()" ng-options="location as location for (location, value) in location_dict"></select> <p>Select Amazon EC2 Instance Type</p> <select ng-model="myInstanceKey" ng-change="instanceChange()" ng-options="instance as instance for (instance, value) in location_dict[myLocationKey]"></select> <p>Select Operating System</p> <select ng-model="myOSKey" ng-change="osChange()" ng-options="os as os for (os, value) in location_dict[myLocationKey][myInstanceKey]"></select> <li ng-repeat="plotfile in filenames"> <img ng-src="{{plotfile}}"/> </li> </div> </body> </html>
I assigned the “ng-app” parameter to the body tag, and set the outer div tag to use the “ng-contoller” “ForecastController”. The entire application is contained within this div tag.
For the first select tag, I set the options to the top-level keys of the JSON input. Originally I set “ng-options” to
location for (location, value) in location_dict
where “location_dict” is the entire JSON input object and is defined in the ForecastController scope. However, doing so delivered the entire contents of the dictionary keyed by the selected location to the “ng-model” variable “myLocationKey”. I only wanted the selected key value, so had to use
location as location for (location, value) in location_dict
instead. This delivered the selected key to the myLocationKey variable. I found this aspect of AngularJS very confusing at first.
The next two select tags extracted options using the values of the key set at the previous level. AngularJS automatically took care of binding the correct option list to the menus.
Each select tag has an “ng-change” function. The first two functions ensure that subsequent select options are reset to an unselected state if a change is made. The final “ng-change” function instructs AngularJS to draw the image. AngularJS automatically binds the correct filename of the image to show to the img tag.
JavaScript code implementing these functions follows:
/* * declare the application */ var app = angular.module("ForecastApp", []); /* * declare the main controller for the application */ app.controller("ForecastController", function ($scope, $http, $templateCache) { /* * retrieve the data structure indicating instance types available */ $http({method: 'GET', url: 'forecast_images/dictionary.json', cache: templateCache}). success(function(data, status) { $scope.location_dict = data; }); /* * function to handle changes to location types */ $scope.locationChange = function() { $scope.myInstanceKey = {}; $scope.myOSKey = {}; } /* * function to handle changes to instance types */ $scope.instanceChange = function() { $scope.myOSKey = {}; } /* * function to handle changes to OS type */ $scope.osChange = function() { $scope.alist = $scope.location_dict[$scope.myLocationKey][$scope.myInstanceKey][$scope.myOSKey] $scope.filenames = [] $scope.alist.forEach(function(alistdata) { var filename = 'forecast_images/' + $scope.myInstanceKey + '---' + $scope.myOSKey + '---' + $scope.myLocationKey + alistdata + '.ts.png'; filename = filename.replace(' ', '_').replace(' ', '_').replace(' ', '_').replace(' ', '_'); filename = filename.replace('Linux/UNIX', 'Linux.UNIX'); $scope.filenames.push(filename); }); } });