Sunday, November 30, 2014

Try web development in Go with Slothful Soda, a small web app in the GGAP Stack (Part 2)


If you followed along with my last tutorial on the GGAP Stack, when we finished the tutorial, we had written a web server in Go for our web app Slothful Soda. This app had a Postgres database of locations where you can get Slothful Soda delivered, which we accessed on the server-side in Go using a database package called GORP. Our routing was handled with a combination of plain Go HTTP routes for files in our public directory (like images and CSS) and routes from a Gorilla mux Router for serving the data from our database as well as the main Angular.js app the user will be accessing. For this tutorial we will be writing that Angular.js app.

As I mentioned at the end of the last tutorial, we wrote all the Go code for Slothful Soda in that tutorial, so this is going to be nothing but Angular.js, but we are going to see how the Angular.js app in this tutorial integrates with the Go code we wrote in the last tutorial.

You can see the final product at andyhaskell.github.io/Slothful-Soda and you can see the repository for the app at github.com/AndyHaskell/Slothful-Soda.

Preparing our index.html file for the Angular.js app

First we are going to need to get Angular.js, and since this will be a Google Maps app, we are also getting the Google Maps API. Additionally, we will also be getting Twitter Bootstrap, a very popular CSS framework that we will be using to quickly add a design to our web app. So first, replace the contents of the <head> tag of views/index.html with:

<title>Slothful Soda, Rehydrate Slothfully</title>

<meta charset="utf-8">

<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.0/css/bootstrap.min.css">
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=YOUR_GOOGLE_MAPS_API_KEY&sensor=true"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0/angular-route.js"></script>
                                                                                
And where it says YOUR_GOOGLE_MAPS_API_KEY, replace YOUR_GOOGLE_MAPS_API_KEY with your Google Maps API key.


The files we are getting are Twitter Bootstrap, the core Angular.js file (for Angular's main functionality), angular-route.js (for making it so Angular.js app has its own routing logic), and the Google Maps API, which we will be using to create a Google Map.

You can download Twitter Bootstrap and the Angular files yourself and serve them from the public/scripts directory as well if you would rather get the files that way but for this tutorial I chose to just download them on their CDNs so I could keep the number of files in the app's GitHub repository small.


Now that we have our files, let's add the parts of the HTML for our Angular.js app.

First, replace <html> with <html ng-app="app">, which uses the ngApp directive tell Angular.js that our Angular.js app is the whole HTML file.


In the <head>, add the line

<script src="/scripts/app.js"></script>

to get the main file for our Angular.js app, which we are about to make.


Then, replace the contents of the HTML <body> with:

<div id="main" class="container" ng-view></div>

Which uses the ngView directive to tell Angular.js we will be rendering the content of the Angular.js app in that <div>.


Now let's make a basic app.js file and the views it will render.

Adding app.js and our views

First, in public/scripts, make a file called app.js and in it add this code:
app = angular.module('app', ['ngRoute']);             //1

app.config(function($routeProvider){                  //2
  $routeProvider
    .when('/', {templateUrl: '/partials/main.html'})  //3
    .when('/map', {templateUrl: '/partials/map.html'})
    .otherwise({redirectTo: '/'});                    //4
});

                                                                                
This is where we are defining our Angular.js app and its routing logic. Here's what's going on:

1. We define an Angular.js module app that uses the ngRoute module (from angular-route.js) as a dependency for our routing logic.

2. For giving the app its routing logic, we will use the $routeProvider service.

3. If the user goes to the path '/', in the ng-view div, render the main.html partial. If the user goes to '/map', render the map.html partial.

4. If the user goes to any other path, redirect the user to '/' and since we're there, render main.html.


To get this to work, we are going to need some partial HTML files for Angular.js to work with, so in public/partials, make a main.html file and add this HTML:
<div>
  <h1 class="center">Slothful Soda, Rehydrate Slothfully</h1>
 
  <img id="hibiscus-img" src="/images/hibiscus.png" />
  <p class="center">
    With all the people playing Ultimate at parks in Boston, water just won't
    cut it as a way to rehydrate. That's why the sloths living in Cambridge's
    Fresh Pond invented Slothful Soda, a delicious soda made from hibiscus
    flowers for the most slothful way to re-hydrate after your game. Rehydrate slothfully!
  </p>
</div>
                                                                                

And make a map.html file with this placeholder HTML:
<div id="locations">
  <h3>Find fields where you can buy Slothful Soda</h3>
  <h1>Locations coming soon</h1>
</div>
                                                                                

Then in the slothful-soda directory, run slothful-soda and go to localhost:1123 and you should get:


And if you go to localhost:1123/#/map, you should get:

This is the routing logic for our Angular app:


Getting rid of that #

Very cool, now we have our Angular.js routing done. But there's one pet peeve I have with the URLs. Notice how when you request the main page the URL automatically changes from localhost:1123 to localhost:1123/#? And to request the map page you have to go to localhost:1123/#/map?

That # gives our Angular.js app the feel of being all one page even though in effect we are having it function as two pages. Our Angular.js app is everything that the user interacts with directly and that means for the user, this app is the whole website, so that # has got to go. Luckily, Angular.js gives us some HTML5 magic for getting rid of the #.

First, in the <head> of index.html, add this line:

<base href=”/”>

This line tells the browser where to direct relative links in the HTML. For example, if the href of the <base> tag is “/”, clicking a link to <a href=”someRoute”> would link you to “/someRoute”. But if it the base's href was “/somewhere-else/”, then <a href=”someRoute”> would link you to “/somewhere-else/someRoute”. Having your <base> tag set up is necessary for this Angular.js trick.


Now in app.js, change your code to this:
app = angular.module('app', ['ngRoute']);

app.config(function($routeProvider, $locationProvider){//1
  $routeProvider
    .when('/', {templateUrl: '/partials/main.html'})
    .when('/map', {templateUrl: '/partials/map.html'})
    .otherwise({redirectTo: '/'}); 

    $locationProvider.html5Mode(true);                 //2
});
                                                                                
Here's what's going on:

1. We are using the $locationProvider service from Angular.js, which lets us work with the URL at the top of the browser in our web app.

2. $locationProvider.html5Mode basically makes it so your Angular.js app has URLs that look like different pages of a website instead of different sections of the same page. For older browsers, though, it keeps the #-based Angular.js app URLs for older browsers.

Now you can get to the main page by going to localhost:1123 and you can get to the map page by going to localhost:1123/map instead of localhost:1123/#/map.

By the way, shoutout to scotch.io for this post that let me figure out this part of Angular.js routing and to VerdantRefuge for this post that helped me figure out how to do the html5Mode routing on Slothful Soda's GitHub page.

Getting the locations from the database to our Angular.js app

Near the end of the last tutorial, we made one of the routes (/locations) in our Go web app fetch the locations in our database and then serve data in JSON. Now on the Angular.js side of the app, we have a /map route so to populate it with data we are going to need to fetch the data from /locations and then on the client-side, use the data to build our map.

First we are going to need an Angular.js controller for the map page, so in public/scripts, add a controllers directory.

Then, in the controllers directory, add a file called MapCtrl.js with this code:
app.controller('MapCtrl', function($scope, $http){ //1 
  $scope.locations = [];

  $scope.initialize = function(){
    $http.get('/locations').success(function(data){//2
      $scope.locations = data;                     //3
    }).error(function(data){                       //4 
      $scope.locations = [];
    }); 
  };
});
                                                                                
Here's what the code does:

1. In our Angular.js app, we are creating a controller that will use the Angular $http service.

2. When our controller's initialize method is called, the $http service will make a GET request to the /locations route on our Gorilla router to try to get the data.

3. If we get data back, the controller's locations array will now hold the data (which is automatically converted from a JSON array to a JavaScript array)

4. If we get an error, the locations array will just be an empty array.


Now that we have a controller that gets our locations, let's display them.

In views/index.html, to load the MapCtrl.js script, add this <script> tag to the HTML file's <head>:

<script src="/scripts/controllers/MapCtrl.js"></script>


Next, to use the MapCtrl controller, we will need to add it to public/scripts/app.js too, so change the .when('/map', …) route to:

.when('/map', {templateUrl: '/partials/map.html', controller: 'MapCtrl'})

Which tells the Angular.js app to have our view use MapCtrl as its controller when we're on the /map route.


Finally, in public/partials/map.html, change the HTML to:
<div id="locations" ng-init="initialize()"> 
  <h3>Find fields where you can buy Slothful Soda</h3>
  <div id="locations-box"> 
    <ul> 
      <li ng-repeat="location in locations">{{location.Name}}: ({{location.Lat}}, {{location.Lng}})</li> 
    </ul>
  </div>
</div>
                                                                                

Notice in the first line, we are calling the MapCtrl controller's initialize method on initialization, which will get the data. Also, in our <ul>, we take the data from MapCtrl's locations array and display each location with ng-repeat.

Now if we run slothful-soda and go to localhost:1123/map, we should get:


This is what getting the MapCtrl controller and the locations data looks like:


Adding a header and some custom CSS

Now we have a list of locations, but instead of having just a list of locations, we are going to have a Google Map displaying where to get Slothful Soda. Since we are about to make that, now is a good time to add a navbar header to our webpages to make navigation easier.

Since we're adding all this stuff, let's add a CSS stylesheet for the web app. In public/styles, add a stylesheet called main.css and add in this code (no need to read the CSS, just know it's there to build on the default designs in Twitter Bootstrap):
 html{ 
  width:  100%;
  height: 100%;
}

body{ 
  width:       100%;
  height:      100%; 
  padding-top: 70px;
}

p{ 
  font-size: 1.6em;
}

#navbar{ 
  height: 50px;
}
#navbar a:hover{ 
  color: #FF0088;
}
#logo{ 
  color: #FF0088;
  padding-top: 12px;
}
.logo-img{ 
  width:  30px;
  height: 30px;
}
.navbar-brand { 
  color: #FF0088;
}
.navbar-brand > img{ 
  display: inline;
}

#main{ 
  height: 100%;
}
#locations-box{ 
  margin-bottom: 15px;
}
#locations{ 
  width:  100%;
  height: 100%;
}
#map{ 
  width:  100%;
  height: 72%;
}

.center{ 
  text-align: center;
}
#hibiscus-img{ 
  width:  300px;
  height: 300px;
}
#sloth-img{ 
  width:  240px;
  height: 300px;
}
                                                                                

And in views/index.html, to include the CSS add this line in the <head> of the HTML:

<link rel="stylesheet" href="/styles/main.css">


Let's also add a header to make getting around the app easier. In public/partials, make a file called header.html and add this HTML:
<div id="navbar" class="navbar navbar-inverse navbar-fixed-top"> 
  <ul class="nav navbar-nav">
    <li> 
      <a id="logo" class="navbar-brand" href="/">
        <img class="logo-img" alt="Hibiscus logo" src="/images/hibiscus.png" /> 
        Slothful soda 
      </a>
    </li> 
    <li><a href="/">Main page</a></li>
    <li><a href="/map">Map of our locations</a></li> 
  </ul>
</div>
                                                                                

To make it so that this header is on our pages, in views/index.html in the first line of the <body>, add this line:

<div ng-include="'/partials/header.html'"></div>

which uses ng-include to get the header.html partial view. Since this is outside of the ng-view div, this header will be on the top of all pages of our Angular.js app.


Since we got our CSS ready to go, while we're at it let's update public/partials/main.html to
<div> 
  <h1 class="center">Slothful Soda, Rehydrate Slothfully</h1>

  <h1 class="center">
    <img id="sloth-img" src="/images/sloth.jpg" /> + 
    <img id="hibiscus-img" src="/images/hibiscus.png" /> = AWESOME SODA
  </h1> 
  <p class="center">
    With all the people playing Ultimate at parks in Boston, water just won't 
    cut it as a way to rehydrate. That's why the sloths living in Cambridge's
    Fresh Pond invented Slothful Soda, a delicious soda made from hibiscus 
    flowers for the most slothful way to re-hydrate after your game.
Rehydrate slothfully! 
  </p>
</div>
                                                                                

and now if you run slothful-soda and go to localhost:1123 you should get:


Okay, now that we have our CSS and navbar and main page view all set, let's get cracking on that Google Map!

Adding the Google Map

For adding in the Google Map, we are going to have the map be part of our MapCtrl controller.

In public/scripts/controllers/MapCtrl.js, change the code to this:
app.controller('MapCtrl', function($scope, $http){ 
  $scope.locations = []; 
  $scope.markers   = []; 
  $scope.map       = null;

  var ourLat = 42.388282,
  ourLng     = -71.153968;

  var initMap = function(){
    var options = {                                                      //1 
      center   : {lat: ourLat, lng: ourLng},
      mapTypeId: google.maps.MapTypeId.HYBRID, 
      zoom     : 12
    };

    var map = new google.maps.Map(document.getElementById('map'), options);
    $scope.map = map; 
  }; 

  //Initialize the map and the markers
  $scope.initialize = function(){ 
    $http.get('/locations').success(function(data){
      $scope.locations = data; 
      initMap();                                                         //2 
    }).error(function(data){ 
      $scope.locations = []; 
      initMap(); 
    });
  };
});
                                                                                
The main thing to focus on is initMap. Here's what's going on:

1. We are creating a Google Map in the map div, which will be a street-satellite HYBRID map centered at the Cambridge Fresh Pond, and we will be storing the map object in $scope.map.

2. In $scope.initialize, after we get the locations, we call initMap to make the map.


Now in public/partials/map.html after the locations-box div, add this div:

<div id="map"></div>


And a Google Map should render zoomed in on the Boston area. Now to add the markers, in initMap after the line $scope.map = map; add this code:
 for (var i = 0; i < $scope.locations.length; i++) { 
  var currentLoc = $scope.locations[i];                               //1

  var markerOptions = {
    position: new google.maps.LatLng(currentLoc.Lat, currentLoc.Lng), 
    title   : currentLoc.Name,
    visible : true, 
    map     : map
  };

  var marker = new google.maps.Marker(markerOptions);                  //2
  $scope.markers.push(marker);
}
                                                                                
1. For each index in the $scope.locations array, we get the location object at that array index and create a JavaScript object called markerOptions storing the location data.

2. We then make a Google Maps marker from the markerOptions object and add it to the $scope.markers array so the MapCtrl controller can access the Google Maps markers. Creating the marker also puts the marker onto our Google Map. 


Now if we run slothful-soda and go to localhost:1123/map, we should get:


 
Cool! Now we have a list of locations where you can get Slothful Soda on a Google Map!

Filtering the locations by distance

For the last step of this tutorial, we are going to make an input box that makes it so people can search for all locations for Slothful Soda within x miles of the Cambridge Fresh Pond. So if someone types in 5, it will search for all locations within 5 miles of the Fresh Pond and you will see all locations. But if they type in 2, it will only show markers for Danehy Park and Hodgkins Park since they are within 2 miles of the Fresh Pond. So for this feature, let's start by adding the HTML for this feature. Change the HTML in public/partials/map.html to
<div id="locations" ng-init="initialize()">
  <h3>Find fields where you can buy Slothful Soda</h3>
  <div id="locations-box"> 
    Show all locations within:
    <input id="" ng-model="distance" ng-keyup="refreshDistance()" />
    miles of the Cambridge Fresh Pond 
  </div>
  <div id="map"></div>
</div>

                                                                                
ng-model=”distance” means what's in the input box is bound to $scope.distance in MapCtrl, so if you type a number, say, 1.123, into the input box, Angular.js will automatically set $scope.distance in MapCtrl to 1.123. This is an example of Angular's two-way data binding.

Also notice ng-keyup=”refreshDistance()”. refreshDistance is the function we will be calling to update the map each time someone types into the input box.

Now let's add the code to get this feature up and running!

 
Adding code for our distance feature

Since we are working with distance, let's make an Angular.js service called Distance for handling the distance from the Fresh Pond to different parks in Boston. The reason why I want to make an Angular.js service instead of just putting a distance function in MapCtrl is for better modularity. We are making it so MapCtrl works with rendering the map and displaying the markers, so a distance function feels pretty stand-alone.

To make our Distance service, first in public/scripts, make a new directory called services and in that directory add a file Distance.js with this code:
app.factory('Distance', function(){ 
  //Earth's radius in miles
  var RADIUS_OF_EARTH = 3959;

  //Degrees to radians function
  var radians = function(degrees){ 
    return degrees * (Math.PI/180);
  };

  //Great circle distance function
  var distance = function(lat1, lng1, lat2, lng2){ 
    var lat1 = radians(lat1),
        lng1 = radians(lng1), 
        lat2 = radians(lat2),
        lng2 = radians(lng2), 
        angle = Math.atan(
          Math.sqrt( 
            Math.pow((Math.cos(lat2)*Math.sin(lng2-lng1)),2) +
            Math.pow((Math.cos(lat1)*Math.sin(lat2)-Math.sin(lat1)* 
            Math.cos(lat2)*Math.cos(lat2-lat1)),2))/
          (Math.sin(lat1)*Math.sin(lat2)+ 
           Math.cos(lat1)*Math.cos(lat2)*Math.cos(lng2-lng1)));

    return angle * RADIUS_OF_EARTH;
  };

  return {distance: distance};
});
                                                                                
No need to read the code. Just know that we are creating a service in our Angular.js app called Distance and it exports a function for the distance between two latitude-longitude coordinates in miles with the great-circle distance formula.


Now in views/index.html, add this <script> tag to the <head> so we can use our Angular.js service.

<script src="/scripts/services/Distance.js"></script>


And in our MapCtrl.js file, first change the first line of the controller's definition to:

app.controller('MapCtrl', function(Distance, $scope, $http){

to include our Distance service in the controller.


Now MapCtrl can use the Distance service, so now let's use it.

In the first few lines defining MapCtrl, add these variable declarations:

$scope.lastDistance = 5;
$scope.distance     = 5;

$scope.distance is the distance currently in the input box (as a string).

$scope.lastDistance will be the most recent distance stored in the input box (as a number), so if someone types an invalid distance into the input box (like “this is not a distance”), we still have a valid distance we can display.


At the bottom of the controller's code, add this function:
//Show only markers on the map within the specified number of miles
$scope.refreshDistance = function(){ 
  var miles = parseFloat($scope.distance);                                   //1
  miles = isNaN(miles) ? $scope.lastDistance : miles;                        //2 
  $scope.lastDistance = miles;                                               //3

  var markers = $scope.markers;

  for (var i = 0; i < markers.length; i++) {
    var pos = markers[i].getPosition(); 
    var lat = pos.lat(),
        lng = pos.lng();

    markers[i].setVisible(Distance.distance(ourLat,ourLng,lat,lng) <= miles);//4
  }
}
                                                                                
refreshDistance takes the distance in the input box and displays only the markers on the map that are closer to the Fresh Pond than that distance. Here's how it works:

1. We take the string $scope.distance in the input box, convert it to a number, and store it in miles.

2. If the input box doesn't contain a valid number, miles has a value of NaN, so miles will store $scope.lastDistance so we still have a valid distance to use.

3. We update $scope.lastDistance to store the value in miles.

4. For each Google Maps marker on the map, we display it if the distance between the marker and the Fresh Pond is less than miles. We determine the distance using the distance function in our Distance service.


Now, at the end of the initMap function, add the line

$scope.refreshDistance();

so refreshDistance is called when the map is created.


No need for a screenshot here since this is the final product. Run slothful-soda and go to localhost:1123/map and your map page should now look like the page on http://andyhaskell.github.io/Slothful-Soda/map

Congratulations! Now you have made a small web app written in the GGAP Stack and I hope this got you interested in doing more web development in Go. I am planning on adding on to Slothful Soda to have features like HTTP POST requests and other cool stuff I learn how to do in Go, so stay tuned. There is a lot to explore in Go web development, and I gave Slothful Soda the MIT License, so feel free to use Slothful Soda to practice with other Go web development features! Until next time, stay slothful!

*The gopher mascot is the Go Gopher, which was drawn by Renee French.
*The Angular.js shield was made by the Angular.js team and is licensed under the "Creative Commons Attribution-ShareAlike 3.0 Unported License", so my diagram with that logo and the logo of this tutorial are also licensed under that license.

No comments :

Post a Comment