Build Your First Mobile App With The Ionic Framework - Part 5

It's finally time to write some code in our Ionic project. I'll be using the same code (slightly rewritten) as in the A Little Bit Of Web Scraping In A Hybrid Mobile App post, so you might want to read that first to get some context about the app we're building.

Update: This tutorial is for Ionic 1.x, you can find the Ionic 2 tutorial here.

This post is part of a multi-part series:
Part 1 - Introduction to Hybrid Mobile Apps
Part 2 - Set Up your Development Environment
Part 3 - Mockup with Ionic Creator
Part 4 - Test on Browsers, Emulators and Mobile Devices
Part 5 - Build out the App (this post)
Part 6 - Deploy to Testers with Ionic View

The source code can be found on GitHub.



We'll start by creating an easy-to-maintain folder structure in the www folder of our Ionic project:

index.html  
/app
    app.js
    /login
        login.controller.js
        login.html
    /dinners
        dinners.controller.js
        my-dinners.html
    /services
        dinner.service.js

I moved all the JavaScript code from index.html into app.js and we'll move the views into their own separate .html files as well.

DinnerService

In the original post I used jQuery for parsing the dinners list from the website. Since I'm only using it for some simple parsing I decided to leave out jQuery and just go with vanilla JavaScript.

I added the Moment.js library to parse the dinner dates from the website.

I also added a check to see if the login was successful, because unfortunately, the server returns a 200 status code with both a failed login and a successful login. I didn't want to have to parse the response to see what happened, so I had a look at the headers and there was one header that was only returned when the login was succesful.

// dinner.service.js
angular.module('app').factory('DinnerService', ['$http', '$q', DinnerService])

function DinnerService ($http, $q) {

    return {

        login: function (username, password) {
            var request = {
                method: 'POST',
                url: 'http://www.nerddinner.com/Account/LogOn',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded'
                },
                data: 'UserName=' + username + '&Password=' + password + '&RememberMe=false'
            };

            return $http(request).then(function(response){
                var deferred = $q.defer();

                // this header is only present after we have logged in successfully
                if (response.headers('x-xrds-location')) {
                    deferred.resolve();
                }
                else {
                    deferred.reject();
                } 

                return deferred.promise;   
            });
        },

        getMyDinners: function () {

            var parseDinners = function (response) {

                var tmp = document.implementation.createHTMLDocument();
                tmp.body.innerHTML = response.data;

                var items = tmp.body.getElementsByClassName('upcomingdinners')[0].children;

                var dinners = [];
                for (var i = 0; i < items.length; i++) {
                    var item = items[i];

                    var dateText = item.getElementsByTagName('strong')[0].innerText;
                    dateText = dateText.replace(/\r?\n|\r/g,'').replace(/\t+/, ' ');

                    var dinner = {
                        Name: item.getElementsByTagName('a')[0].innerText,
                        Date: moment(dateText, 'YYYY-MMM-DDhh:mm A').toDate(),
                        Location: item.innerText.split('at')[1]
                    };

                    dinners.push(dinner);
                }

                return dinners;
            }

            return $http.get('http://www.nerddinner.com/Dinners/My')
                        .then(function(response){
                            return parseDinners(response);
                        });
        }
    }
}

LoginController

This is the controller for the Login view, if the login is succesful we'll go to the state my-dinners. If the login is not succesful we'll use the $ionicPopup service to show a popup.

// login.controller.js
angular.module('app').controller('LoginController', ['$state', '$ionicPopup', 'DinnerService', LoginController]);

function LoginController($state, $ionicPopup, dinnerService) {  
    var vm = this;
    vm.doLogin = function () {

        var onSuccess = function () {
            $state.go('my-dinners');    
        };

        var onError = function () {
            $ionicPopup.alert({
                 title: 'Login failed :(',
                 template: 'Please try again.'
               });
        };

        dinnerService.login(vm.username, vm.password)
                     .then(onSuccess, onError);
    }
}

DinnersController

This is the controller for the My Dinners view.

// dinners.controller.js
angular.module('app').controller('DinnersController', ['DinnerService', DinnersController]);

function DinnersController(dinnerService) {  
    var vm = this;

    dinnerService.getMyDinners().then(
            function (dinners) {
                vm.dinners = dinners;
            });

    return vm;
}

Let's take the code for the views out of index.html (completely remove the script tags for the view templates) and move the code into their own files and add the databinding expressions to communicate with the corresponding controllers.

Login View

<!-- login.html -->  
<ion-view title="Login" ng-controller="LoginController as vm">  
    <ion-content padding="true" scroll="false" class="has-header">
        <form>
            <ion-list>
                <label class="item item-input">
                    <span class="input-label">Username</span>
                    <input type="text" placeholder="" ng-model="vm.username">
                </label>
                <label class="item item-input">
                    <span class="input-label">Password</span>
                    <input type="password" placeholder="" ng-model="vm.password">
                </label>
            </ion-list>
            <div class="spacer" style="width: 300px; height: 17px;"></div>
            <a ng-click="vm.doLogin()" class="button button-light button-block">Log in</a>
        </form>
    </ion-content>
</ion-view>  

My Dinners View

<!-- my-dinners.html -->  
<ion-view title="My Dinners" ng-controller="DinnersController as vm">  
    <ion-content padding="true" class="has-header">
        <div class="list card" ng-repeat="dinner in vm.dinners">
            <div class="item item-divider">{{ dinner.Name }}</div>
            <div class="item item-body">
                <div>
                    <div>
                        <i class="icon ion-calendar"></i>{{ dinner.Date }}</div>
                    <div>
                        <i class="icon ion-location"></i>{{ dinner.Location }}</div>
                </div>
            </div>
        </div>
    </ion-content>
</ion-view>  

Make sure you change the routing code in app.js to point to the location of the templates:

// app.js
...
$stateProvider
    .state('login', {
        url: '/login',
        templateUrl: 'app/login/login.html'
    })
    .state('my-dinners', {
        url: '/my-dinners',
        templateUrl: 'app/dinners/my-dinners.html'
    });

// if none of the above states are matched, use this as the fallback
$urlRouterProvider.otherwise('/login');
...

Testing the App

First let's test this app in the desktop browser. As I mentioned before you'll have to disable security in the browser because of the same-origin policy.

So, when you load the app with ionic serve you'll have to open a separate browser with the --disable-security flag and browse to the url in that browser.

On OSX:

$ open -a Google\ Chrome --args --disable-web-security

On Windows:

chrome.exe --user-data-dir="C:/Temp/Chrome" --disable-web-security  

Sidenote: Ionic has an elegant way of enabling you to bypass this restriction by configuring a proxy that will take you to the external URL. It's very easy to use, but in this case I need to send a cookie back to the server so it knows I'm logged in. This happens automatically because withCredentials = true is configured for every $http request. However, when using the proxy it didn't send the cookie back after login and I didn't have time to figure out how to make that work, so I didn't use that.

If everything is working in the desktop browser, have a look at the app on your mobile devices and emulators. The same-origin policy doesn't apply there, so you shouldn't have a problem with that.

The source code can be found on GitHub.


Let me know in the comments if you're having any problems with the code and stay tuned for Part 6 where I'll cover Ionic View for deployment to your testers.

Follow me on Twitter @ashteya and sign up for my weekly emails to get new tutorials.

If you found this article useful, could you hit the share buttons so that others can benefit from it, too? Thanks!

comments powered by Disqus