//
you're reading...

AngularJS

Part 4 : Web application development using AngularJS, Spring Boot and Maven – BirdLog Application

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"
      }]
      }
  • 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;
              }                   
          }
      } 
      
  • 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.

  • Run your application using mvn spring-boot:run command

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 in ngResource module to interact with RESTful Web Service.
      • List ngResource as a dependency for starterApp.home module
        angular.module("starterApp.main", ["ngResource"])
        
      • Note: To use ngResource, ensure that angular-resource.js is included
        <script src="bower_components/angular-resource/angular-resource.js"></script>
    • Define mySightingsDataFactory that uses $resource to load my bird sightings from the server
      .factory("mySightingsDataFactory", function($resource) {
          return $resource('/sightings');
      })
      
    • Modify HomeCtrl to use mySightingsDataFactory
      .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 the mySightingsDataFactory

  • 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. The ng-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 to ng-model called search.
          <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">
          
  • Support adding new sightings
    • Modify MySightingsController to handle POST request to save new sightings. Sighting data is submitted as part of the request body.
      Spring Boot uses Jackson libraries to handle/parse JSON 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 HomeCtrl in homeCtrl.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">&nbsp;</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 calling addNewSighting function
  • 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

  1. […] Web application development using AngularJS, Spring Boot and Maven […]

Post a Comment


*

Categories