Part III - The Front End

We now have a database containing data, and a basic Python backend which can query this database. All that remains is to display this data. We'll do this in two ways, just because that's how I did it, and I think it's instructional. In both we'll employ AngularJS, which is a JavaScript based framework for creating dynamic, interactive pages with numerous view elements, distinguishing it pretty immediately from the standard, static pages built with the HTML framework.

Angular JS basics

While written in JavaScript, Angular has it's own distinct structure. One declares an app defined as a module which has dependencies on other modules, some of which we will need to build, and some of which are base modules developed for the Angular framework. The goal here is to build a one-page webapp that offers the ability to specify parameters for a query, and then displays it on the same page. We'll do that with Angular's ui-router, which enables the usage of states, e.g. components displayed in views can have multiple states; ngResource, which interfaces with and makes calls to our RESTFUL API; and ui-grid, in the second part, which displays the queried data in a nice grid with some extra functionality. Note that all of these modules have .css styles and .js files which will need to be loaded on our index.html to work properly. Additionally, we'll load some others to make the display prettier.

It would be nice to be able to implement modules step-by-step and show progress as we making things more complex, but, like in the Python app itself, most of the components are interdependnent. Here's a run-down of what we'll be using, and how it's implemented. First, let's create a static folder for our app.js and a folder for partial html templates.

cd someApp
mkdir -p static/partials
cd static
touch app.js
cd partials
touch list.html

In your app.js, define the app:

//Initialize an Angularjs Application
var app = angular.module('someApp', []);

As a reminder, this is what is returned when we call the API by visiting the /api/v1/courses.json endpoint:

{"links": {"self": "/routes/"}, "data": [{"attributes": {"sub_type": 1, "popularity": 1.0, "end_lon": -97.749181, "start_lon": -97.738152, "route_type": 1, "end_lat": 30.267815, "elevation_gain_in_meters": 5.0, "name": "To MJ's", "start_lat": 30.302946, "length_in_meters": 4122.0}, "type": "routes", "id": 1}, {"attributes": {"sub_type": 1, "popularity": 1.0, "end_lon": -122.413445, "start_lon": -122.410612, "route_type": 1, "end_lat": 37.7471, "elevation_gain_in_meters": 899.0, "name": "B2P", "start_lat": 37.747033, "length_in_meters": 80967.0}, "type": "routes", "id": 3}, {"attributes": {"sub_type": 1, "popularity": 1.0, "end_lon": -122.254057, "start_lon": -122.371044, "route_type": 1, "end_lat": 37.541787, "elevation_gain_in_meters": 31.0, "name": "Foster City & Back", "start_lat": 37.586963, "length_in_meters": 17815.0}, "type": "routes", "id": 4}, {"attributes": {"sub_type": 1, "popularity": 0.0, "end_lon": -122.413799, "start_lon": -122.413799, "route_type": 1, "end_lat": 37.749654, "elevation_gain_in_meters": 288.0, "name": "Space Invader", "start_lat": 37.749289, "length_in_meters": 14480.0}, "type": "routes", "id": 5}, {"attributes": {"sub_type": 1, "popularity": 0.0, "end_lon": -122.503545, "start_lon": -122.502923, "route_type": 1, "end_lat": 37.74173, "elevation_gain_in_meters": 491.0, "name": "It's me Mario!", "start_lat": 37.764303, "length_in_meters": 34914.0}, "type": "routes", "id": 6}]}

It's a .json blurb, with the actual data contained in a list under the 'data' attribute.

ngResource

  • Documentation
  • Summary - ngResource provides interaction with our RESTFUL API by way of the service component $resource with some built in HTTP methods. We only need to tell it to include the ones we want.
  • How to use - we're really creating two things here: a module and a "factory" (or service). These are created in a new Angular module, which will have to be included in the dependencies of the main app. We need names for both, because our main app, defined above, will be dependent on the module we'll name someApp.services, and we'll call the factory courseFactory so that we can include it in another module later. We'll define it as a function which returns a a $resource component registered with a query method, and the location in the data of the id column. Note that we specify that the data coming from the query is an array, even though above it's a json blurb!
//Add a dependency on the created module to the main Angularjs Application
var app = angular.module('myApp', ['ngResource', 'someApp.services']);


// Create a Route Resource Object using the resource service component
angular.module('someApp.services', ['ngResource']).factory('courseFactory', function($resource) {
  return $resource('api/v1/courses/:id.json', 
    { id:'@courses.id' }, 
    { 'query': {method: 'GET', isArray:true }},
    { update: {method: 'PATCH' }}, 
    { stripTrailingSlashes: false }
    );
});

UI-Router

  • Documentation
  • Summary - ui-router is a module that will do a lot of work for us. It creates endpoints and tells the app which html files to render and when. Essentially, it is responsible for displaying anything we display, and predicitably, it will interface with our index page. We're going to create a module dependent on the $state sub-module, to make an abstract default state, and a sub-state activated with a button on the index page. Thus, we'll need an interface ("view") there and a controller which will format our data, which we'll cover in the next step.

To facilitate this interface with the index.html, we'll use a corresponding ui-view element in the html file. States require page locations to be specified, or written directly into the state definition itself. We do it both ways below, given the abstract nature of the parent state. All that is needed is the ui-view element.

//Add a dependency on the created module to the main Angularjs Application
var app = angular.module('myApp', ['ngResource', 'someApp.services','ui-router']);


// Create routes with UI-Router and display the appropriate HTML file for listing routes
angular.module('myApp').config(function($stateProvider, $urlRouterProvider) {

  $urlRouterProvider.otherwise("/");
  
    $stateProvider
      .state('courses', {       
        abstract: true,
        url: '/',
        title: 'Routes',
        template: '<div ui-view></div>'
    })
      .state('courses.list', {
        url: 'list',
        templateUrl: '../static/partials/list.html',
        controller: 'courseListController',      
 
 
  })
});

The CRUD controller: courseListController

  • Summary - We will build a controller which will utilize the resource service/factory we constructed in the first step. Again, this app is pretty simple, so we only need to write methods for get. The function which is ultimately the controller gets declared with $scope and courseFactory, which are, respectively, the binder between the html view and angular (it is a global variale with definable attributes), and the ngResourse directive we defined earlier.
// add a dependency on the created module to the main Angularjs Application
var app = angular.module('myApp', ['ngResource', 'someApp.services','ui-router', 'someApp.controllers']);

angular.module('someApp.controllers', []).controller('courseListController',
function($scope, courseFactory){
    courseFactory.get(function(data){
    $scope.courses = data.data
    });
});

Index page

In the index.html file, we'll first need to load the javascript files and css styles on which our modules depend. The only real components we need are a button to activate the state, and a view in which to display it.

<!doctype html>
<html ng-app='myApp'>
<meta charset="utf-8">
<head>
    <!-- CSS-->
    <link href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.css" rel="stylesheet">
    <!-- JS -->
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.5/angular-resource.js"> </script>
    <script src="https://code.angularjs.org/1.2.0/angular-animate.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.5/angular-route.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.18/angular-ui-router.min.js"></script>
    <script src="static/app.js"></script>
</head>
<div id="main">
 
  <div class="nav">
   
    <ul>
      <!-- ui-sref binds a link to a state (in this case, routes.list state in the app.js) -->
      <li><a ui-sref="routes.list">List Strava Routes</a></li>
    </ul>
  </div>
  <!-- ui-view tells $state where to place your templates. here the routes.list has template list.html -->
  <div ui-view=""></div>
   
</div>
           
</html>

The List Partial

Finally, as noted in the app.js, we render our courses.list state from list.html, and it is where the data is parsed in this first rendition. This is what we'll refer to as a partial html file. We're just going to write up the code for a table with 4 columns: id, Name, Length, and Popularity. Again, take a look at the json blurb above to see where the data we want to put in these columns is located. The ng-repeat directive is used to iterate through the list data.data which is set as the $scope variable.

<h2>Strava User-Created Routes</h2>
<table class="table table-striped table-bordered">
    <tr>
        <th>id</th>
        <th>Name</th>
        <th>Length</th>
        <th>Popularity</th>
    </tr>
    <tr ng-repeat="course in courses">
        <td>{{ course.id }}</td>
        <td>{{ course.attributes.name }}</td>
        <td>{{ course.attributes.length_in_meters }}</td>
        <td>{{ course.attributes.popularity }}</td>
    </tr>
</table>

And that's it! Test it locally to make sure it works. If it's clearly erroring, but nothing is happening, you should ctrl-shift-i to inspect the page and see if there are any javascript errors, which are typically unreported.

Heroku

The hard stuff is out of the way. Before committing and pushing, go back to the app.py and, in the last line, change python app.run(host='0.0.0.0', port=port, debug=True) so that debug=False.

Make a commit to the repo, and, if you're using github, push your commit to that repo. Finally, push the commit to Heroku with

git push heroku master

You'll be prompted for your login info, unless you configured Heroku on your own. That does it for this installation, in the next, we'll implement the Angular directive ui-grid to display the data.

Reference

Your app.js should look like this

//Initialize an Angularjs Application
var app = angular.module('myApp', ['ui.router','ngResource', 'someApp.controllers', 'someApp.services']);
 



// Create a Route Resource Object using the resource service
angular.module('someApp.services', ['ngResource']).factory('courseFactory', function($resource) {
  return $resource('api/v1/routes/:id.json', 
    { id:'@courses.id' }, 
    { 'query': {method: 'GET', isArray:true }},
    { update: {method: 'PATCH' }}, 
    { stripTrailingSlashes: false }
    );
});
 



// Create routes with UI-Router and display the appropriate HTML file for listing routes
angular.module('myApp').config(function($stateProvider, $urlRouterProvider) {

  $urlRouterProvider.otherwise("/");
  
    $stateProvider
      .state('courses', {       
        abstract: true,
        url: '/',
        title: 'Courses',
        template: '<div ui-view></div>'
    })
      .state('courses.list', {
        url: 'list',
        templateUrl: '../static/partials/list2.html',
        controller: 'courseListController',      
 
 
  })
});
 


angular.module('someApp.controllers', []).controller('courseListController',
function($scope, courseFactory){
    courseFactory.get(function(data){
    $scope.courses = data.data
    });
});