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.
*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