Building a Google Calendar Booking App with MongoDB, ExpressJS, AngularJS, and Node.js - Part 2

MEAN Stack - MongoDB, ExpressJS, AngularJS, Node.js

If you haven't yet checked it out, please see Part 1 in this series where we walkthrough the setup of the node.js server this app consumes. In Part 2 we are going to go through the AngularJS front-end code that powers our Booking App.

When I was initially creating this app for my buddy there were only 3 requirements:

  • Display a list of upcoming reservations
  • Display detailed information about a reservation
  • Add a New Reservation

So those are the 3 items we will be focusing on in this article.

AngularJS is an amazing framework with a steep learning curve, which is one of the things I love most about it. It is so robust and full of features that I learn something new with every project I undertake, regardless of its size. The community is growing fast and is incredibly helpful. I highly recommend popping into the #AngularJS IRC channel on freenode.net, or asking a question over on StackOverflow.

One tool that has been really helpful in quickly bootstrapping projects has been Yeoman. I read a great article on how to Kickstart Your AngularJS Development with Yeoman, Grunt and Bower which allows you to create a basic app already wired together by simply executing "yo angular" in terminal.

Before we begin you can find the source code for this project on Github, so fork away and follow along.

Now, let's dig into the code.

app.js

This is our main application module which we use to setup up and wire together our application.

angular.module('gcal-bookingapp', [
    'ngRoute',
    'ngAnimate',
    'app.controllers',
    'app.directives',
    'GoogleCalendarService'
])

It is instantiated with the ngApp directive from the html tag in index.html.

<html ng-app="gcal-bookingapp">

config

Configuration blocks - get executed during the provider registrations and configuration phase. Only providers and constants can be injected into configuration blocks. This is to prevent accidental instantiation of services before they have been fully configured. -Module Loading & Dependencies

.config(['$routeProvider', function($routeProvider){
    $routeProvider.when('/', {templateUrl: 'partials/main.html', controller: 'DashboardController'});
    $routeProvider.when('/event/new', {templateUrl: 'partials/newEvent.html', controller: 'AddEventController'});
    $routeProvider.when('/event/:eventId', {templateUrl: 'partials/event.html', controller: 'EventController'});
    $routeProvider.otherwise({redirectTo: '/'});
}]);

In our configuration block we inject the $routeProvider directive and map up our application routes. This directive allows us to change the templateUrl which is what ngView uses to determine which template to display. We also assign different controllers for each route. Although this isn't required, as we can assign a controller to multiple views.

run

Run blocks - get executed after the injector is created and are used to kickstart the application. Only instances and constants can be injected into run blocks. This is to prevent further system configuration during application run time. The run function is executed each time the application is initially setup. -Module Loading & Dependencies

.run(['$rootScope','$location', function($rootScope, $location){
    if($rootScope.eventMap === undefined){
        $location.path('/');
        $location.replace();
    }
}])

In the run block we check if the eventMap property of the $rootScope is undefined, and if so we redirect the application route to "/" where it will be defined upon instantiation of the controller for that view. This prevents any errors from occuring if we navigate directly to the event view without first defining the event map.

controllers.js

"The ngController directive specifies a Controller class; the class contains business logic behind the application to decorate the scope with functions and values"

MainController

.controller('MainController', ['$scope','$rootScope','$googleCalendar', function($scope, $rootScope, $googleCalendar){

    //retrieve event list from google calendar
    $googleCalendar.getEvents().then(onResults, onError);

    function onError(error){
        console.log('error:', error);
    }

    function onResults(results){
        $rootScope.events = $scope.events = results;
        $rootScope.eventMap = {};

        for(var i=0; i<results.length; i++)
        {
            $rootScope.eventMap[results[i].id] = results[i];
        }
    }

}])

This controller is instantiated when the $route "/" is navigated to. Upon instantiation, a call to our $googleCalendar service is made that creates an event map in the $rootScope when the promise is resolved. The map allows other scopes to have access to this event list and lookup an event via id when the rootScope is injected.

EventController

.controller('EventController', ['$scope','$rootScope','$routeParams', function($scope, $rootScope, $routeParams){
    $scope.event = $rootScope.eventMap[$routeParams.eventId];
}])

We assign the scope event property to the event object returned from the map on the $rootScope. This is the same map that was created when the MainController was initially instantiated. The event is then used to populate our view:

partials/event.html

<h3>{{event.summary}}</h3>
<div class="well">
    When: {{event.start.date || event.start.dateTime | date:'MMM d, y @ h:mm a'}}<br/>
    Description: {{event.description}}<br/>
</div>

One of the things I want to point out here is the date filter we are using to format the way the date is displayed on the page. This is one of the many pre-defined filters conveniently baked right into Angulars core.

AddEventController

.controller('AddEventController', ['$scope','$routeParams','$googleCalendar', function($scope, $routeParams, $googleCalendar){

    $scope.durations = [
        {label:'Half Day (4 hours)', hours:4},
        {label:'Full Day (8 hours)', hours:8}
    ];

    $scope.addEvent = function(){
        //format start date/time object in to google format
        var startTime = $scope.event.startTime.split(':');
        var startHours = startTime[0];
        var startMins = startTime[1];

        var startDate = new Date($scope.event.startDate);
        startDate.setHours(startHours);
        startDate.setMinutes(startMins);

        //format end date/time object in to google format
        var endDate = new Date(startDate);
        endDate.setHours(endDate.getHours() + $scope.event.duration.hours);

        //add event to google calendar
        $googleCalendar.addEvent(startDate, endDate, $scope.contactInfo);

    }

}])

The select element in the newEvent.html partial uses the ngOptions attribute to populate its options. These options are derived from the $scope.durations array we created in our controller above.

<select class="form-control" ng-model="event.duration" ng-options="d.label for d in durations" style="width:auto" required>

We also create an addEvent function that is called when the button in our form is clicked.

<div class="col-sm-2">
    <button class="btn btn-primary" ng-disabled="contactForm.$invalid" ng-click="addEvent()">Add Reservation</button>
</div>

You will also notice that the button is disabled, and unclickable until the all the required fields in the form are filled out and valid.

directives.js

Directives are the core of what makes Angular tick. These articles really helped me understand best practices and setup of custom directives within Angular. You should definitely check them out:

inputfield

The custom inputfield directive outputs different input fields for a bootstrap form based on the type attribute passed into the element.

angular.module('app.directives', [])

.directive('inputfield', ['$compile', function($complile) {
    return {
        restrict: 'E',
        scope: {
            ngModel: '=',
            label: '@',
            type: '@',
            placeholder: '@',
            id: '@',
            required: '@',
        },
        transclude: true,
        templateUrl: 'partials/formField.html',
        replace: true,
    };
}])

;

This allows us to simplify each bootstrap form-group down to a single line of code:

<inputfield ng-model="contactInfo.firstName" type="text" label="First Name" placeholder="Enter first name..." id="firstName" required>

animations.css

Inside this CSS file we have CSS3 animations that will couple with the ngAnimate directive to apply transitions to the element with ngView.

.container {
  position:relative;
  height:1000px!important;
  position:relative;
  overflow:hidden;
}

.view-animate {
}

.view-animate.ng-enter, .view-animate.ng-leave {
  -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) .75s;
  transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) .75s;

  display:block;
  width:100%;

  position:absolute;
  top:0;
  left:0;
  right:0;
  bottom:0;
}

.view-animate.ng-enter {
  left:100%;
}
.view-animate.ng-enter.ng-enter-active {
  left:0;
}
.view-animate.ng-leave.ng-leave-active {
  left:-100%;
}

We see these animations applied to our ngView element in the main.html partial.

<div class="container">
  <div ng-view class="view-animate"></div>
</div>

Animations are one of the newly revamped features in the latest 1.2 release of Angular.

GoogleCalendarService.js

This is the service we create to handle the communication with our node.js application from Part 1.

"The Factory recipe constructs a new service using a function with zero or more arguments (these are dependencies on other services). The return value of this function is the service instance created by this recipe."

Note: All services in Angular are singletons. That means that the injector uses each recipe at most once to create the object. The injector then caches the reference for all future needs.

angular.module('GoogleCalendarService', [], function($provide){

    $provide.factory('$googleCalendar', function($http, $q){

        var $scope = angular.element(document).scope();

        //the url where our node.js application is located
        var baseUrl = 'http://localhost:5000';

        return {
            getEvents: function(){
                var defer = $q.defer();

                $http.get(baseUrl+'/events').then(function(response){
                    if(response.status == 200){
                        $scope.$broadcast('GoogleEventsReceived', response.data.items);
                        defer.resolve(response.data.items);
                    }

                    else{
                        $scope.$broadcast('GoogleError', response.data);
                        defer.reject(response.data);
                    }

                });

                return defer.promise;
            },
            addEvent: function(scheduledDate, endDate, contactInfo){
                var defer = $q.defer();

                var postData = {
                    startdate: scheduledDate,
                    enddate: endDate,
                    contact: contactInfo
                };

                $http.post(baseUrl+'/event', postData, {'Content-Type':  'application/json'}).then(function(response){
                    console.log('Add Event Response:', response);

                    if(response.status == 200){
                        $scope.$broadcast('eventAddedSuccess', response.data);
                        defer.resolve(response.data);
                    }
                    else{
                        $scope.$broadcast('GoogleError', response.data);
                        defer.reject(response.data)
                    }
                });

                return defer.promise;
            }
        };

    });

})

You can see that there are two functions implemented to map up to the two methods we created in our node.js app:

We will now be able to inject this service into directives, controllers, etc. via "$googleCalendar".

You will also notice inside each function that upon success or failure we both $broadcast an event and resolve/reject our javascript promise. I did this to demonstrate multiple ways of having your service communicate with external code. It is not necessary to do both by any means though.

With broadcasting, we could apply an event listener to a controller $scope to handle updating the DOM in some way when that event is received.

$scope.$on('eventName', function(event){
    //do something when event is received
});

While with promises we can handle success/failure with the Promise.then method.

That wraps up our walkthrough of the code for this app. Again, you can find the source code for this application on Github. Hope this information was helpful and please let me know if you have any questions or suggestions on anything can be improved!

Vote on HN


comments powered by Disqus