Two sides of web application. Part 3: Communication Layer
Foreword
In this section we will implement the communication layer for our application. It’ll handle all the requests to/from our web server. Have no worries - we will create server application in the next section!
First resource
Let’s create a Session
resource. Since we have no backend part, we should stub
the data. We’ll use Angular Services. That’s easy: a service defines a
function, returning, say, an object. That object will be used every time you
call a service. And you may use not only objects - you may return functions,
constants or literally anything from your services.
We’ll return an object with two methods: get(account)
and create(account)
.
Both methods will take an object, containing user account details - email
,
password
, and, in case of create()
, name
and password_confirmation
, additionally.
You might’ve guessed already: the get(account)
method will log the user in and
create(account)
will register a new account for the user.
ourStatsControllers.factory('Session', [
() => {
var testAccount = {
name: 'Artem',
email: 'artem@ourstats.com'
};
return {
get: (account) => {
var params = {
email: account.email,
password: account.password
};
},
create: (account) => {
var params = {
name: account.name,
email: account.email,
password: account.password
};
}
}
}
]);
When you inject it into your controller, you may use it via
Session.get({ email: '', password: '' });
The thing here is that we want to keep user data somewhere to simulate a
database work. So when user “signs up”, he will have his data saved in the memory
not to loose it when changing a page. We’ll use another service for that purpose.
But contrary to our sessions
service, this one will be used in the future and even
barely changed.
Now, here’s one more trick: we defined our Session
factory in the ourStatsControllers
module.
Why did we do this? Why not just use ourStatsApp
module? This is done mostly to keep things
in one place, so everything related to controllers and UI processing is kept inside one module.
Doing so we can later separate our modules into separate files and keep our main application
module clean from unnecessary code (in terms of the whole application).
To store user account data we’ll use cookies. And there is a plugin for this
already! It is called ngCookies
. And we’ll install it right now:
bower install --save angular-cookies
This one’s configuration is a bit more complicated. But just a bit: it needs to be added to application dependency list:
var ourStatsApp = angular.module('ourStatsApp', [ 'ngRoute', 'ngCookies', 'ourStatsControllers' ]);
But since we use cookies directly in controllers only, we may move the ngCookies
dependency to
the ourStatsControllers
module:
var ourStatsControllers = angular.module('ourStatsControllers', [ 'ngCookies' ]);
// ...
var ourStatsApp = angular.module('ourStatsApp', [ 'ngRoute', 'ourStatsControllers' ]);
Now we need to define our AccountData
service. It’ll also require a ngCookies
dependency:
ourStatsControllers.factory('AccountData', [ '$cookies',
($cookies) => {
return {
get: () => {
return $cookies.getObject('account');
},
set: (account) => {
$cookies.putObject('account', account);
},
reset: () => {
$cookies.remove('account');
}
}
}
]);
And we need to modify our Session
service a bit:
ourStatsControllers.factory('Session', [ '$http', 'AccountData',
($http, AccountData) => {
var testAccount = {
name: 'Artem',
email: 'artem@ourstats.com'
};
return {
get: (account) => {
var params = {
email: account.email,
password: account.password
};
AccountData.set(testAccount);
},
create: (account) => {
var params = {
name: account.name,
email: account.email,
password: account.password
};
AccountData.set(testAccount);
}
}
}
]);
And now we can implement the controller to pick up the newly added service:
ourStatsControllers.controller('NewSessionCtrl', [ '$scope', 'Session', 'AccountData',
($scope, Session) => {
$scope.newAccount = {};
$scope.existingAccount = {};
$scope.signIn = () => {
Session.get($scope.existingAccount);
};
$scope.signUp = () => {
Session.create($scope.newAccount);
};
$scope.signOut = () => {
AccountData.reset();
};
}
]);
But here’s just one small detail: we need both Session
and AccountData
services to perform
similar actions - managing session. We can refactor our code to keep all the session management
tasks in one place - Session
service:
ourStatsControllers.factory('Session', [ '$http', 'AccountData',
($http, AccountData) => {
var testAccount = {
name: 'Artem',
email: 'artem@ourstats.com'
};
return {
get: (account) => {
var params = {
email: account.email,
password: account.password
};
AccountData.set(testAccount);
},
create: (account) => {
var params = {
name: account.name,
email: account.email,
password: account.password
};
AccountData.set(testAccount);
},
reset: () => {
AccountData.reset();
}
}
}
]);
Now our NewSessionCtrl
could be refactored too:
ourStatsControllers.controller('NewSessionCtrl', [ '$scope', 'Session',
($scope, Session) => {
$scope.newAccount = {};
$scope.existingAccount = {};
$scope.signIn = () => {
Session.get($scope.existingAccount);
};
$scope.signUp = () => {
Session.create($scope.newAccount);
};
$scope.signOut = () => {
Session.reset();
};
}
]);
Looks better, huh?
So now we have our first member of our Model layer. It works with stubbed data right now,
but we’ll be changing that later. It is not bound to the ViewModel, because the values
of our $scope.newAccount
and $scope.existingAccount
are constant and don’t depend on
the template. To change this, we need to modify our new_session.jade
template:
.row
.col-xs-12.col-md-4.col-md-offset-1.well
h3 Sign up
form(ng-submit="signUp()")
fieldset.form-group
input.form-control(type="text" placeholder="Your name" ng-model="newAccount.name")
fieldset.form-group
input.form-control(type="email" placeholder="Email" ng-model="newAccount.email")
fieldset.form-group
input.form-control(type="password" placeholder="Password" ng-model="newAccount.password")
fieldset.form-group
input.form-control(type="password" placeholder="Password confirmation")
fieldset.form-group.text-center
button.btn.btn-primary(type="submit") Sign up
.col-md-2
.col-xs-12.col-md-4.well
h3 Sign in
form(ng-submit="signIn()")
fieldset.form-group
input.form-control(type="email" placeholder="Email" ng-model="existingAccount.name")
fieldset.form-group
input.form-control(type="password" placeholder="Password" ng-model="existingAccount.password")
fieldset.form-group.text-center
button.btn.btn-success(type="submit") Sign in
Note the ng-model
directive: it performs two-way data binding. It means, when user changes the
value of our, say, name
input, the corresponding value inside controller, namely $scope.newAccount.name
,
will also change. What’s more, when the value of $scope.newAccount.name
will be changed from
the JavaScript (or, from the controller itself), the value within the input in a user’s browser,
will also be changed.
Now, what’s about the ng-submit
directive. It is added to form tags only. And it works exactly as
you might’ve guessed: when form is submitted, the corresponding expression within that directive is called.
Making fake calls
Now that we have our stubbed signing in and up routines, we may redirect user to the right page
when he signed in or up successfully. This is the job for $location
service. It’s available
in Angular core module, ng
, so we don’t need to add any new module-wise dependencies.
Just use it in our NewSessionCtrl
:
ourStatsControllers.controller('NewSessionCtrl', [ '$scope', '$location', 'Session',
($scope, $location, Session) => {
$scope.newAccount = {};
$scope.existingAccount = {};
$scope.signIn = () => {
Session.get($scope.existingAccount);
$location.path('/applications');
};
$scope.signUp = () => {
Session.create($scope.newAccount);
$location.path('/applications');
};
$scope.signOut = () => {
Session.reset();
$location.path('/');
};
}
]);
Here comes new refactoring-able piece of code: we now have our routes declared in both
ourStatsApp
module and ourStatsControllers
. So if we decide to change, let’s say,
route /new-session
to /login
, we will need to change it in both modules. We can easily
extract those URLs into a single service and use it in both ourStatsControllers
and ourStatsApp
modules, because everything we define in ourStatsControllers
will be
available in the ourStatsApp
thanks to dependency injection. And since our URLs will
always be the same (except, maybe, the parametrised ones), we may define a constant
instead of factory:
ourStatsControllers.constant('Url', {
landing: '/',
newSession: '/new-session',
editAccount: '/edit-account',
listApplications: '/applications',
newApplication: '/applications/new',
showApplication: (id) => {
if (!id)
id = ':id';
return `/applications/${id}`;
},
editApplication: (id) => {
if (!id)
id = ':id';
return `/applications/${id}/edit`;
}
})
Application startup phases
As you can see, there are a couple of entities you can create with Angular - we’ve already dealt with controllers, factories, modules and constants. They all are slighty different from each other and the difference lies under Angular startup process.
When your application “starts up” or initializes, there are two kingds of methods, which are
ran first: config
and run
. They are user-defined and your application can have more than just
one of those. Those methods define the initialization behaviour of your application. They both
can deal with dependency injection. But their run order is different: config
blocks are executed
first, at the very beginning of the whole application initialization process. Thus you can inject
only providers ($routeProvider
for instance) and constants into config
blocks.
Whilst run
blocks can only handle instances (for example, $scope
) and constants.
Since we have our routes defined at the config
stage, we need to use constant to name our
routes. And then use them like this:
ourStatsApp
.config(['$routeProvider', 'Url',
($routeProvider, Url) => {
$routeProvider
.when(Url.landing, {
templateUrl: 'landing-page.html',
controller: 'LandingPageCtrl'
})
.when(Url.newSession, {
templateUrl: 'new-session.html',
controller: 'NewSessionCtrl'
})
.when(Url.editAccount, {
templateUrl: 'edit-account.html',
controller: 'EditAccountCtrl'
})
.when(Url.listApplications, {
templateUrl: 'list-applications.html',
controller: 'ListApplicationsCtrl'
})
.when(Url.newApplication, {
templateUrl: 'new-application.html',
controller: 'NewApplicationCtrl'
})
.when(Url.editApplication(), {
templateUrl: 'edit-application.html',
controller: 'EditApplicationCtrl'
})
.when(Url.showApplication(), {
templateUrl: 'show-application.html',
controller: 'ShowApplicationCtrl'
})
.otherwise({
redirectTo: Url.landing
});
}]);
ourStatsControllers
.controller('NewSessionCtrl', [ '$scope', '$location', 'Session', 'Url',
($scope, $location, Session, Url) => {
$scope.newAccount = {};
$scope.existingAccount = {};
$scope.signIn = () => {
Session.get($scope.existingAccount);
$location.path(Url.listApplications);
};
$scope.signUp = () => {
Session.create($scope.newAccount);
$location.path(Url.listApplications);
};
$scope.signOut = () => {
AccountData.reset();
$location.path(Url.landing);
};
}
])
To show the difference in practice, let’s add the authentication verification. We now have our stubbed account and session management service, so why not? Angular provides us with two options:
- the
resolve
attribute for the$routeProvider.when()
method - the
$routeChangeStart
event
The first approach is nice, but since we set up routes in the config
section, we are not allowed
to use services. And that is the problem, because we’ve got our Session
service, handling the
tasks we need. And one more drawback of this method: one should set it for each route, which requires
verification:
var verifyAuthentication = () => {
// ...
};
ourStatsApp
.config(['$routeProvider', 'Url',
($routeProvider, Url) => {
$routeProvider
.when(Url.landing, {
templateUrl: 'landing-page.html',
controller: 'LandingPageCtrl'
})
.when(Url.newSession, {
templateUrl: 'new-session.html',
controller: 'NewSessionCtrl'
})
.when(Url.editAccount, {
templateUrl: 'edit-account.html',
controller: 'EditAccountCtrl',
resolve: verifyAuthentication
})
.when(Url.listApplications, {
templateUrl: 'list-applications.html',
controller: 'ListApplicationsCtrl',
resolve: verifyAuthentication
})
.when(Url.newApplication, {
templateUrl: 'new-application.html',
controller: 'NewApplicationCtrl',
resolve: verifyAuthentication
})
.when(Url.editApplication(), {
templateUrl: 'edit-application.html',
controller: 'EditApplicationCtrl',
resolve: verifyAuthentication
})
.when(Url.showApplication(), {
templateUrl: 'show-application.html',
controller: 'ShowApplicationCtrl',
resolve: verifyAuthentication
})
.otherwise({
redirectTo: Url.landing
});
}]);
Handling $routeChangeStart
event suits us, and what’s more interesting: it shows the use of run
section. But as a drawback to this, we’ll need the list of routes, which need to be checked. So
I modified the definition of Url
constant like this:
ourStatsApp
.constant('Url', (() => {
var urls = {
landing: '/',
newSession: '/new-session',
editAccount: '/edit-account',
listApplications: '/applications',
newApplication: '/applications/new',
showApplication: (id) => {
if (!id)
id = ':id';
return `/applications/${id}`;
},
editApplication: (id) => {
if (!id)
id = ':id';
return `/applications/${id}/edit`;
}
};
urls.authRequired = [
urls.editAccount,
urls.listApplications,
urls.newApplication,
urls.showApplication(),
urls.editApplication()
];
return urls;
})());
A bit tricky, does not it? And here’s the handler for the $routeChangeStart
event:
ourStatsApp
.run(['$rootScope', '$location', 'Session', 'AccountData', 'Url',
($rootScope, $location, Session, AccountData, Url) => {
$rootScope.$on('$routeChangeStart', (event, current, next) => {
// check if user is authenticated for selected URLs only
// first, try to restore his session
// if this failed because user has no AccountData cookie - redirect him to newSession page
if (Url.authRequired.indexOf($location.path()) > -1 && !Session.get() && !AccountData.get())
$location.path(Url.newSession);
});
}]);
Rocking on
Other services and controllers are much like those we already have, so I’ll just put in the code
in the end of the page. The interesting thing here is the MockData
service that I’ve used to keep
data, used while we have no server side.
ourStatsApp
.constant('MockData', {
account: {
name: 'Artem',
email: 'artem@ourstats.com',
password: 'abc123'
},
applications: [
{
id: 0,
name: 'App #1',
token: 'APP1TOK',
stats: {
byCountry: [
{country: 'Poland', amount: 10},
{country: 'USA', amount: 190},
{country: 'Algeria', amount: 5}
]
}
}, {
id: 1,
name: 'App #2',
token: 'APP2TOK',
stats: {
byCountry: [
{country: 'Vietnam', amount: 7},
{country: 'New Zeland', amount: 19},
{country: 'USA', amount: 15}
]
}
}, {
id: 2,
name: 'App #3',
token: 'APP3TOK',
stats: {
byCountry: [
{country: 'USA', amount: 95}
]
}
}
]
});
// ...
ourStatsApp
.factory('Application', ['$http', 'MockData', ($http, MockData) => {
return {
create: (application) => {
// send data to the server and check if response status == 200; return application (arg)
return application;
},
get: (id, fromDate, toDate) => {
// fromDate and toDate are used for stats filtering
// send data to the server and return response
return MockData.applications[id];
},
update: (id, application) => {
// send data to the server and check if response status == 200; return application (arg)
return application;
},
all: () => {
// send request to the server and return the response
return MockData.applications;
}
};
}]);
And here’s the demo of what we’ve done up until now:
See the Pen Simple web analytics. Communication layer. v2 by Artem Shoobovych (@shybovycha) on CodePen.