In the current code structure of Ionic Starter Project, it has one controller file and one service file under www/js/ folder. During the actual development practice, it is not a good way to keep all controllers and services just in those two files. Instead, the code will be spotted into separate files based on functionality.

However, another pain point brought by this split is every time when a new js file is added, we have to manually add them into index.html file so that it can load while app is running.

In order to solve it, we will need the tool Browserify to help achieve it.

ionic-boilerplate/
    │   
    └── www
        ├── README.md
        ├── css
        │   └── ...
        ├── img
        │   ├── ...
        ├── index.html
        ├── js
        │   ├── app.js
        │   ├── controllers.js
        │   └── services.js
        ├── lib
        │   └── ...
        └── templates
            └── ...

Code Structure

When doing arrangements on code files, generally it has two ways.

  • By Type – Group the files based on types, like controller, service, factory, etc.
  • By Functionality – Group the files based on different features groups.

I would recommend the second approach, since it could provide better readability and accessibility for any new-coming developers – organizing by feature is much more understandable.

Reference: AngularJS Best Practices: Directory Structure

Here is the code structure of Ionic Boilerplate after arrangement.

www/js/
├── account
│   ├── templates
│   │   └── tab-account.html
│   └── account.controller.js
├── chats
│   ├── templates
│   │   ├── chat-detail.html
│   │   └── tab-chats.html
│   ├── chat-detail.controller.js
│   ├── chats.controller.js
│   └── chats.factory.js
├── common
│   └── templates
│       └── tabs.html
├── dash
│   ├── templates
│   │   └── tab-dash.html
│   └── dash.controller.js
├── app.js
├── app.config.js
├── app.constant.js
├── app.router.js
└── app.run.js

And here is how index.html includes these js files.

<!-- your app's js -->
<script src="js/app.js"></script>
<script src="js/app.router.js"></script>
<script src="js/common/app.config.js"></script>
<script src="js/dash/dash.controller.js"></script>
<script src="js/chats/chats.controller.js"></script>
<script src="js/chats/chats.factory.js"></script>
<script src="js/chat-detail/chat-detail.controller.js"></script>
<script src="js/account/account.controller.js"></script>

If there are more, then it will be a long list you need to maintain. And let’s why we need Browserify.

Browserify

By using Browserify, it can help bundle all files into single one and include it in index.html. In order to do this, we need to re-write our code to define required dependencies in the code.

By using Browserify, we can also declare Angular Element (like controller, service, etc.) as a module and do require() at any place that needed.

For example, we have the following Angular module and controller:

// todo.js

'use strict';
   angular
  .module('todo')
  .controller('TodoCtrl', function($scope) {
    ...
  });

By using Browserify, we can declare the “controller” function as a module outside this file and use reauire() to load it here.

// todo.js

'use strict';
var todoModule = require('angular').module('todo');

todoModule.controller('TodoCtrl', require('./todo_ctrl'));

// todo_ctrl.js

function todoCtrl($scope) {
    ...
}

module.exports = todoCtrl;

Please note, the path thatrequire() loads is a relative path.

When doing Browserify, it will help bundle all related js file into a single file. And then you can just load this bundled js file in your index.html.

Luckily, we can still use Gulp to help do the task of Browserify.

Gulp Browserify Task

In order to run the gulp task of Browserify correctly, we need the following NodeJS modules:

  • browserify – The must one.
  • vinyl-source-stream – Recommended by Gulp team to help do task of Browserify.
  • gulp-ng-annotate – Help Add angularjs dependency injection annotations with ng-annotate. See the explanation below.
  • browserify-shim – Help make CommonJS-incompatible modules (like Ionic) browserifyable. By doing this, we can then use require('ionic') to load Ionic framework inside the js code.
  • napa – A helper for installing repos without a package.json with npm. This is also used to importing Ionic as node modules so that they can be bundled with Browserify.

The module napa is recommended to installed as global.

Here is the new adding to package.json.

"devDependencies": {
  ...
  "browserify": "^11.0.1",
  "browserify-shim": "^3.8.8",
  "vinyl-source-stream": "1.1.0",
  "gulp-ng-annotate": "1.1.0"
},
"scripts": {
  "install": "napa"
},
"napa": {
  "ionic": "driftyco/ionic-bower#v1.2.4"
},
"browser": {
  "ionic": "./node_modules/ionic/js/ionic.js"
},
"browserify": {
  "transform": [
    "browserify-shim"
  ]
},
"browserify-shim": {
  "ionic": "ionic"
}

As shown above,

  • Use napa in scripts section to install angular and ionic from Github.
  • Add support of angular and ionic to Browserify.

AngularJS Dependency Injection (DI) – ngAnnotate

AngularJS does Dependency Injection (DI) while loading dependent components. In order to make our coding life easier, usually we use the NodeJS module ngAnnotate to help the AngularJS dependency injection annotations while doing auto code preparation.

When using ngAnnotate, here is how we do coding.

angular.module("MyMod").controller("MyCtrl", function($scope, $timeout) {
    "ngInject";
    ...
});

As shown, we add a special line "ngInject"; into the definition of Angular element.

In Gulp, we also need the module gulp-ng-annotate.

Changes to IonicBoilerplate

Here is the code structure of the js folder after the changes.

www/js/
├── account
│   ├── templates
│   │   └── tab-account.html
│   ├── account.controller.js
│   └── account.module.js
├── bundles
│   └── app.bundle.js
├── chats
│   ├── templates
│   │   ├── chat-detail.html
│   │   └── tab-chats.html
│   ├── chat-detail.controller.js
│   ├── chats.controller.js
│   ├── chats.factory.js
│   └── chats.module.js
├── common
│   └── templates
│       └── tabs.html
├── dash
│   ├── templates
│   │   └── tab-dash.html
│   ├── dash.controller.js
│   └── dash.module.js
├── app.js
├── app.config.js
├── app.constant.js
├── app.module.processor.js
├── app.router.js
└── app.run.js

Comparing to previous version, here are the changes:

  • Modularize the code based on app functionality like “Dash”, “Chats”, etc.
  • For each module, it includes every piece that this module needs, such as Angular elements (controller, service, etc.), templates, unit tests, etc.
  • Under each module folder, a new module definition file XXX.module.js is used to define the module as well as register all Angular elements.
  • Create a new helper function (in app.module.processor.js) to help process the definition of the modules AUTOMATICALLY. By doing this, every time when you add a new Angular element in a new file, you DON’T need to go back to the module definition file and do the registration.

The following are some details of the code.

“app.js”

In app.js, all the normal pieces (like run(), config(), etc.) have been modularized so that it can be easily maintained outside in separate file.

Also, it uses require() to load all app functional modules so that when when doing Browserify, it knows where to find the next modules.

'use strict';

// General `requires`
require("angular");
require("ionic");

// App `basic` modules
var appConstants = require('./app.constant');
var appConfig = require('./app.config');
var appRouter = require('./app.router');
var appRun = require('./app.run');

// App `functional` modules
require('./dash/dash.module');
require('./chats/chats.module');
require('./account/account.module');

// create and bootstrap application
var app_requires = [
    'ionic',

    // App funcitonal modules
    'app.dash',
    'app.chats',
    'app.account'
];
var appModule = angular.module('ionicBoilerplate', app_requires);

appModule.run(appRun);
appModule.config(appConstants);
appModule.config(appRouter);
appModule.config(appConfig);

module.exports = appModule;

Inside Each Functional Module

Here is the code inside the module definition file.

// Define your `project module`
var angular = require('angular');
var dashModule = angular.module('app.dash',
    []
);

// Use `bulk` to load `all` angular elements under this module folder.
var bulk = require('bulk-require');
// TODO Make the `paths` in `bulk` to be a variable.
//
// For now, `bulk` does not support a variable as the `paths`, since when doing `buuikify` on `browserify`, it will be
// ignored.
var angularElements = bulk(__dirname, ['./**/!(*.module|*.spec).js']);

// Load Angular Module Processor
var processor = require('../app.module.processor');
processor.angularModuleProcessor(dashModule, angularElements);

module.exports = dashModule;

As shown, for doing the registration of all Angular elements, we use a special helper require('../app.module.processor') to help the process.

Also, we use a new NodeJS module bulk to help do require() for multiple files at one time.

After using bulk, the task of browserify needs a new browserify transformbulkify.

Angular Elements Registration Helper

Inside the helper, it exams every Angular elements and does the registration based on the elementType of each one.

switch(element.type) {
    case elementType.CONTROLLER:
        module.controller(element.name, element.func);
        break;
    case elementType.SERVICE:
        module.service(element.name, element.func);
        break;
    case elementType.FACTORY:
        module.factory(element.name, element.func);
        break;
    case elementType.DIRECTIVE:
        module.directive(element.name, element.func);
        break;
    case elementType.FILTER:
        module.filter(element.name, element.func);
        break;
}

Correspondingly, when doing module.exports for each Angular element, you need to define the properties name and func used above.

// dash.controller.js

'use strict';

function dashController($scope) {

    'ngInject';

}

module.exports = {
    name: 'dashController',
    type: "controller",
    func: dashController
};

Re-arrange Gulp Tasks

Our next step is to optimize Gulp tasks, trying to create a good structure which helps streamline the process of doing Gulp related work, such as:

  • Manage Gulp plugins / tasks.
  • Add new Gulp task.
  • Easily let Gulp do all the dirty work, like CSS and JS minification, image/font processing, testing, development/production building, deployment, etc.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>