Organize AngularJS code (2/4 steps to properly set mobile app project using Ionic framework)
This is second step of organising Ionic mobile project in this blog series and we’ll take a look at some more interesting things and will dive into Angular more deeply. In last two blog posts of series we described how to prepare blank project from Ionic built in templates and now it’s time to reorganise it a little bit and give it our personal touch.
- Four steps to properly set mobile app project using Ionic framework – Introduction
- Blank Ionic mobile project setup (1/4 steps to properly set mobile app project using Ionic framework)
For now Angular code is organized in very simple and primitive way, which is ok for current application purpose. Since our plan is to build large and complicated mobile apps we need more organized architecture so we can be adding more and more functionality easily without making a spaghetti code or have controllers with thousands of lines of code.
As you can see we have js and templates folder separated. js folder contains of app.js
and controllers.js
files. app.js
file is application entry point with main module definition, run block (contains code which is being executed first) and routes definition. It also includes two dependency modules (ionic and starter.controllers module). ionic module comes from lib/ionic/js/ionic.bundle.js
file which we have installed using gulp install command in previous blog post. starter.controllers module comes from previously mentioned controllers.js
file.
Modularization
We don’t want to have all the code in two files. We want to split the code, in separate files, based on functionality.
Now the angular module structure comes in place. When talking about modularizing applications built on AngularJS, there are two independent elements to talk about: splitting the code into logical modules on one hand, and into multiple physical files on the other hand. Logical modules gives us the ability to split the code into separate functional units so we can maintain the code more easier, and reuse it in many different projects. Also if you want to be able to find a part of the code you want to change quickly, then multiple physical files are necessary when app gets bigger and bigger.
Given that you now know that you can split your application into different modules, your next question might be: what’s the best boundary for this split? Should you split your application into separate parts for directives, for controllers, filters, etc? Or should you group your modules based on functionality (“sort by feature”)?
Sort by feature means that when you are looking for the code that makes a feature work, it is located in one place. So controllers, views and related services are grouped into one folder which represents a specific feature in the application.
You could also organize it “by type” but we found it more useful this way since it’s easier to locate your file when you want to find something in the code.
Sort by type means that all controllers are located in one folder, all view in other, services in their own folder and so on.
Sort by feature allows you to create reusable components at the base of your application (or even at the base of multiple applications), to reuse existing modules which have been created by others, and then to add modules which group functional parts of your application.
On the other hand, the code organization structure is up to you. There are many right ways of doing this, and consistency in your project is the key.
Sort by feature
js/
---- common/
-------- templates/
------------ menu.html
-------- common.js
-------- config.js
---- utility/
-------- api.js
-------- helpers.js
---- playlist/
--------- templates
------------ playlist.html
-------------- playlistDetails.html
--------- playlist.js
--------- playlistDetails.js
----app.js
App structure
There’s some important things we want to point out. app.js is the application entry point, where we define our main module, routes and run block (which is being executed first and where we put all the logic which prepares data and do common setup used through the whole application). We’ll put this file in js root folder, above all other angular modules.
We also want to have some common logic which we can inject and use in every other controller and service so we don’t have to repeat ourselves and keep duplicated code. I will separate this part in two pieces.
The first one is common controller which is being executed before all other controllers and only once per application load. This controller will contain all variables and functions available in the $scope object, so it will be accessible in every template (view). With this have set up we can reduce the code in all other controllers by preparing some commonly used properties in one place. For example, we’ll put click handlers for menu template, if we have a logic for login popup etc. We’ll put this controller in js/common folder. Also, we want to have a common configuration module where we can define constants and variables, like API endpoint, commonly used messages, folder paths… Ones you move from API v1 to v2, you’ll need to change the url only in one place and not in dozens services where you perform http requests. We will put this module in common folder as well because it will be specific for one application only.
The second part of common logic is the code which doesn’t manipulate with $scope object. This means that these functions will not be visible in your view. Here we keep some utility and helper methods which we can call from other services and controllers. Basically these are the functions for some calculations, or handling the http response (display popup notification by default, unless we don’t specify it other way), or manipulations over arrays and objects (iterations, functions for splitting…) etc.These files will be put in utility folder. We also put a common api module which we call for every http request in the app.
Once we prepared a common logic we can move to more specific parts of the app. In the current Ionic template, we started from, there’s a list of items and their details (playlist). This functionality unit we can put in playlist folder so we can easily find it in the project. If one functionality unit consists of more detailed sections (list of items, item details, item CRUD operation etc.) we will have more controllers for each sub unit. If unit requires to have API requests or some code which needs to be separated from controllers, we’ll create an Angular service. In this case we can call it playlistService.js
so we can differentiate it from the controllers. Also, all templates and directives goes into /playlist/templates
and /playlist/directives
folder.
One last thing to point out is route configuration and how to define common.js
controller. We mentioned earlier that common.js
controller needs to be executed before all others and basically it needs to be a parent controller. To achieve this we need to define an abstract state.
An abstract state can have child states but cannot get activated itself. An ‘abstract’ state is simply a state that can’t be transitioned to. It is activated implicitly when one of its descendants are activated. Abstract states still need their own for their children to plug into, which is menu.html
in our case.
appModule.config([
'$stateProvider',
'$urlRouterProvider',
function($stateProvider, $urlRouterProvider) {
$stateProvider
.state('app', {
url: "/app",
abstract: true,
templateUrl: '/js/common/templates/menu.html',
controller: 'CommonCtrl'
})
.state('app.playlists', {
url: '/playlists',
views: {
'menuContent' :{
templateUrl: '/js/playlist/templates/playlist.html',
controller: 'PlaylistCtrl'
}
}
})
.state('app.single', {
url: '/playlists/:playlistId',
views: {
'menuContent': {
templateUrl: '/js/playlist/templates/playlistDetails.html',
controller: 'PlaylistDetailsCtrl'
}
}
});
$urlRouterProvider.otherwise('/app/playlists');
}
]);
Include Browserify
Given that you now know how to structure your app in theory let’s see what needs to be done to revive it and see the modules in action. When speaking about modules and multiple physical files the main question is how to connect it together and make them work as one single app. Since we don’t want to be including every single javascript controller, service etc. In index.html
file and manage dependencies and file order manually we’ll introduce Browserify. This way we’ll define required dependencies in the code, bundle all files into single one and include it in index.html
. So let’s start from app.js
.
(function() {
'use strict';
// Instantiate all dependencies modules
angular.module('App.Config', []);
angular.module('App.Helpers', []);
angular.module('App.Common', []);
angular.module('App.Api', []);
angular.module('App.Playlist', []);
angular.module('templates', []);
// Include dependency modules components via Browserify
require('./common/config');
require('./utility/helpers');
require('./utility/api');
require('./common/common');
require('./playlist/playlist');
require('./utility/templates');
// Instantiate main App module and include all previously defined dependency modules
var appModule = angular.module('App', [
// Ionic and angular modules
'ionic',
// Project modules
'App.Config',
'App.Helpers',
'App.Common',
'App.Api',
'App.Playlist',
'templates'
]);
appModule.config([
// App config code
]);
module.exports = appModule;
})();
Our goal is to have main App module, inject all dependency modules we need (App.Config, App. Helpers…) and bootstrap the application. Before including all dependencies, we need to instantiate them and import module files via Browserify. This way App module will know they exist and it will be able to inject them via Angular dependency system. In order to use Browserify system we need to export module at the end of the file module.exports = appModule;
It’s important to notify that we don’t have to import all functionality unit module files here in app.js file. For example App.Playlist
module consists of two files (playlist.js
and playlistDetails.js
controller). We’ll define one main controller for each functional unit and import all functional dependencies there. So App.Playlist
module will take care of all it’s dependencies by itself and App module doesn’t need to know how many files other modules have, so app.js
file will stay clean. When other developer opens app.js file he/she will clearly see which modules the application consists of and diving into each of them he/she will find what files the each module consists of.
(function() {
'use strict';
angular.module('App.Playlist')
.controller('PlaylistCtrl', [
'$scope',
playlistCtrl
]);
function playlistCtrl($scope) {
console.debug('Playlist Controller: ', $scope.commonArray);
$scope.playlists = [
{ title: 'Reggae', id: 1 },
{ title: 'Chill', id: 2 },
{ title: 'Dubstep', id: 3 },
{ title: 'Indie', id: 4 },
{ title: 'Rap', id: 5 },
{ title: 'Cowbell', id: 6 }
];
}
require('./playlistDetails');
module.exports = angular.module('App.Playlist');
})();
Now we can say that our code is organized. We can easily take App.Helpers
or App.Api
module and import them into other project. They represent an independent standalone units we can share between the projects.
Next step is to minify our well prepared code to reduce network traffic and speed up the application.
Stay tuned and see you in the next blog post in this series.
Got a project?
Let's talk.
Like what you see?
Or have a project, idea, requirement or scope.