In my previous posts (Part 1, Part 2 and Part 3), I developed a starter web application using AngularJS, Bootstrap, Spring Boot and Maven. In this post, I am going use this starter app to build the Bird Log app.
What is Bird Log?
The Bird Log is a simple web-based, Single Page Application (SPA), used to record my bird sightings.
This application:
- Displays my recorded sightings in a tabular format
- Supports sorting and filtering
- Provides a way to log new sightings
- Uses eBird APIs to find the latest bird sightings near my current location reported on eBird
- Implements
HTML5 Geocoding APIs
to find my current location - Implements
Google Geocoding APIs
to map latitude/longitude to the location/address - Uses RESTful web services to interact with the server that handles the data (To keep it simple, data is stored in the memory for now)
Implement RESTful Web Service
- Implement RESTful Web Service using Spring Boot
- Spring Boot makes it easy to build a RESTful Web Services. Lets build a service to get the list of user’s sightings from server.
- The server returns
My Sightings
in the JSON format
{
"name" : "John Crow",
"email" : "john@thecrowflies.com",
"sightingDetails" : [{
"birdName" : "Snowy Owl",
"seenOn" : 1395218659868,
"location" : "Salisbury Camp Grounds, MA",
"comments" : "Hunting in the marshes"
},
{
"birdName" : "Eastern Screech Owl",
"seenOn" : 1395218659868,
"location" : "West Newbury, MA",
"comments" : "Roosting in a cavity"
}]
}
- The server returns
- Start by defining the resource model used to record user’s bird sightings.
- Create a POJO
com.rajandesai.proto.springboot.model.MySighting.java
package com.rajandesai.proto.springboot.model; import java.util.Date; import java.util.Set; public class MySighting { private String name; private String email; private Set<SightingDetails> sightingDetails; public final String getName() { return name; } public final void setName(String name) { this.name = name; } public final String getEmail() { return email; } public final void setEmail(String email) { this.email = email; } public final Set<SightingDetails> getSightingDetails() { return sightingDetails; } public final void setSightingDetails(Set<SightingDetails> sightingDetails) { this.sightingDetails = sightingDetails; } public final void addSightingDetails(SightingDetails sightingDetail) { this.sightingDetails.add(sightingDetail); } //------------------------------------------------------------------------- // Inner class to capture actual sighting details //------------------------------------------------------------------------- public static class SightingDetails { private String birdName; private int count; private Date seenOn; private String location; private String comments; public String getBirdName() { return birdName; } public void setBirdName(String birdName) { this.birdName = birdName; } public final Date getSeenOn() { return seenOn; } public final void setSeenOn(Date seenOn) { this.seenOn = seenOn; } public final String getLocation() { return location; } public final void setLocation(String location) { this.location = location; } public final String getComments() { return comments; } public final void setComments(String comments) { this.comments = comments; } public int getCount() { return count; } public void setCount(int count) { this.count = count; } } }
- Create a POJO
- Write a controller identified by
@Controller
annotation- Create a class
com.rajandesai.proto.springboot.controller.MySignhtingsController.java
package com.rajandesai.proto.springboot.controller; import java.util.Date; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import com.rajandesai.proto.springboot.model.MySightings; /** * Controller class that handles HTTP requests for My Sightings */ @Controller public class MySightingsController { private static final Logger log = LoggerFactory.getLogger(MySightingsController.class); //In-memory Data Store private MySightings mySightings = generateMySightings(); @RequestMapping("/sightings") public @ResponseBody MySightings getMySightings() { if (log.isDebugEnabled()) { log.debug("==== in getMySightings ===="); } return mySightings; } private MySightings generateMySightings() { MySightings mySightings = new MySightings(); mySightings.setName("John Crow"); mySightings.setEmail("john@thecrowflies.com"); MySightings.SightingDetails details1 = new MySightings.SightingDetails(); details1.setBirdName("Eastern Screech Owl"); details1.setCount(1); details1.setLocation("West Newbury, MA"); details1.setSeenOn(new Date(System.currentTimeMillis())); details1.setComments("Roosting in a cavity"); mySightings.addSightingDetails(details1); MySightings.SightingDetails details2 = new MySightings.SightingDetails(); details2.setBirdName("Snowy Owl"); details2.setCount(3); details2.setLocation("Salisbury Camp Grounds, MA"); details2.setSeenOn(new Date(System.currentTimeMillis())); details2.setComments("Hunting in the marshes"); mySightings.addSightingDetails(details2); return mySightings; } }
Note:
Spring Boot
scans all the controllers as part of application initialization.
- Create a class
- Run your application using
mvn spring-boot:run
command- Type http://localhost:8080/sightings in your browser. You should get the JSON back.
Implement User Interface
- Modify
index.html
file to change the application name<a class="navbar-brand" href="/">My Bird Log</a>
- Modify
homeCtrl.js
to add logic to interact with the server- Use
$resource
service defined inngResource
module to interact with RESTful Web Service.- List
ngResource
as a dependency forstarterApp.home
moduleangular.module("starterApp.main", ["ngResource"])
- Note: To use
ngResource
, ensure thatangular-resource.js
is included<script src="bower_components/angular-resource/angular-resource.js"></script>
- List
- Define
mySightingsDataFactory
that uses$resource
to load my bird sightings from the server.factory("mySightingsDataFactory", function($resource) { return $resource('/sightings'); })
- Modify
HomeCtrl
to usemySightingsDataFactory
.controller("HomeCtrl", ["$scope", "mySightingsDataFactory", function($scope, mySightingsDataFactory) { $scope.mySightings = {}; $scope.refreshMySightings = function() { mySightingsDataFactory.get(function(data){ $scope.mySightings = data; }); }; $scope.refreshMySightings(); }]);
Note: Since the server returns single object containing the list of my sightings,
get
method is called on themySightingsDataFactory
- Use
-
Implement
My Sightings
retrieved from the server in a tabular format. Use ng-repeat directive to generate the table.- Modify
home.html
file to include table<div class="row"> <div class="panel panel-primary"> <div class="panel-heading"> My Bird Sightings </div> <div class="table-responsive panel-body"> <table class="table table-striped"> <thead> <tr> <td>Name</td> <td>Count</td> <td>Observed On</td> <td>Location</td> <td>Comments</td> </tr> </thead> <tbody> <tr ng-repeat="sighting in mySightings.sightingDetails"> <td>{{sighting.birdName}}</td> <td>{{sighting.count}}</td> <td>{{sighting.seenOn}}</td> <td>{{sighting.location}} </td> <td>{{sighting.comments}}</td> </tr> </tbody> </table> </div> </div> </div>
- Format
seenOn
date field using date filter<td>{{sighting.seenOn | date:'yyyy-MM-dd'}}</td>
- Add support for
Filtering
. Theng-repeat
directive supports filter clause to filter content based on a search term.
NOTE: this is a simple content search in the entire table<div class="row"> <div class="panel panel-primary"> <div class="panel-heading"> My Bird Sightings </div> <div class="panel-body"> <div class="form-group col-sm-4"> <input type="text" class="form-control" id="search" ng-model="search" placeholder="Search My Bird Sightings"> </div> </div> <div class="table-responsive panel-body"> <table class="table table-striped"> <thead> <tr> <td>Name</td> <td>Count</td> <td>Observed On</td> <td>Location</td> <td>Comments</td> </tr> </thead> <tbody> <tr ng-repeat="sighting in mySightings.sightingDetails | filter : search"> <td>{{sighting.birdName}}</td> <td>{{sighting.count}}</td> <td>{{sighting.seenOn | date:'yyyy-MM-dd'}}</td> <td>{{sighting.location}} </td> <td>{{sighting.comments}}</td> </tr> </tbody> </table> </div> </div> </div>
- In the code above, I added a
textfield
to enter the search term. This field is bound tong-model
calledsearch
.<div class="panel-body"> <div class="form-group col-sm-4"> <input type="text" class="form-control" id="search" ng-model="search" placeholder="Search My Bird Sightings"> </div> </div>
ng-repeat
directive is modified to use search term for filtering<tr ng-repeat="sighting in mySightings.sightingDetails | filter : search">
- In the code above, I added a
- Format
- Modify
- Support adding new sightings
- Modify
MySightingsController
to handlePOST
request to save new sightings. Sighting data is submitted as part of the request body.
Spring Boot usesJackson
libraries to handle/parseJSON
data and are included automatically.
Add the following method@RequestMapping(method=RequestMethod.POST, value="/sightings") public @ResponseBody MySightings saveNewSighting(@RequestBody MySightings.SightingDetails sightingDetails) { if (log.isDebugEnabled()) { log.debug("==== in saveNewSighting ==== Bird Name: " + sightingDetails.getBirdName() + ", Count: " + sightingDetails.getCount()); } mySightings.addSightingDetails(sightingDetails); return mySightings; }
This implementation adds the new sighting data to my sightings stored in memory and returns the updated list back to the client.
- Modify
-
Modify
HomeCtrl
inhomeCtrl.js
file to handle Add New Sighting logic- First define a
$scope
object to hold the sighting data$scope.newSighting = {count: 1}; $scope.newSighting.seenOn = currentDate();
NOTE: This just initializes the count to 1 and seenOn date to the current date in
yyyy-MM-dd
format.- Add new $scope level function
addNewSighting
that POSTs the new sighting data to the server$scope.addNewSighting = function() { console.log("New Sighting: " + angular.toJson($scope.newSighting)); //POST user data to the server mySightingsDataFactory.save($scope.newSighting, function(response) { //Handle successful response console.log(response); // Since the server returns updated sightings list back, reset the // $scope level list. This will automatically refresh the UI $scope.mySightings = response; // Reset model used to capture new sightings... This will automatically // clear the fields in the form $scope.newSighting = {count: 1}; $scope.newSighting.seenOn = currentDate(); }); };
- Add a new form in
home.html
that captures new bird sighting.<div class="row"> <div class="panel panel-default"> <div class="panel-heading"> Add New Sightings </div> <div class="panel-body"> <form role="form"> <div class="form-group col-sm-2"> <label for="lat">Name</label> <input type="text" class="form-control" id="name" ng-model="newSighting.birdName" placeholder="Which bird?" required> </div> <div class="form-group col-sm-2"> <label for="lng">Count</label> <input type="number" class="form-control" id="count" ng-model="newSighting.count" placeholder="How many?" required> </div> <div class="form-group col-sm-2"> <label for="dist">Date</label> <input type="date" class="form-control" id="seenOn" ng-model="newSighting.seenOn" placeholder="When (yyyy-MM-dd)?" required> </div> <div class="form-group col-sm-2"> <label for="lng">Location</label> <textarea class="form-control" id="location" ng-model="newSighting.location" placeholder="Where?"></textarea> </div> <div class="form-group col-sm-3"> <label for="lng">Comments</label> <textarea class="form-control" id="comments" ng-model="newSighting.comments" placeholder="Additional info"></textarea> </div> <div class="form-group col-sm-1"> <label class="col-sm-1"> </label> <button type="submit" class="btn btn-default form-control col-sm-1" ng-click="addNewSighting()"> Add </button> </div> </form> </div> </div> </div>
NOTES
- Type of the button is
submit
, this triggers HTML5 validations on clicking Add button. This ensures that all required fields are entered before callingaddNewSighting
function
- Type of the button is
- Add new $scope level function
- First define a
- Run and test the application
Sum it up
I now have a working Bird Log application developed using AngularJS, Spring Boot and Maven. Next, I plan to use eBird APIs to display the latest bird sightings, near my current location, reported on eBird.org.
Discussion
Trackbacks/Pingbacks
[…] Web application development using AngularJS, Spring Boot and Maven […]