So You Want to Build an App in ES6, Part Four
In the last article we talked about using concepts of fractal design to build our application architecture. This means separating your application into features instead of into models, views and controllers. This seems at odds with the MVC architecture as a whole, but the reality is that it really works well with MVC, and especially well with ES6.
Application Structure
In a standard MVC environment, you'd split out your code like this:
/root
/controllers
/mainController.js
/loginController.js
/services
/restService.js
/eventService.js
/directives
/headerDirective.js
/footerDirective.js
This works reasonably well for small applications where you only have a few controllers, services, and directives. What happens when you end up with a large application with hundreds of controllers and services? It quickly loses its maintainability. A better way to handle your architecture is to use the concepts from fractal design:
/root
/shared
/ajax
/restService.js
/eventing
/eventServices.js
/page
/header
/headerDirective.js
/headerController.js
/headerTemplate.html
/footer
/footerDirective.js
/footerController.js
/footerTemplate.html
/application
/home
/mainController.js
This brings me to my next topic: using modules to organize your code. There is no hard and fast rule that says you have to put everything into your base module. I see a lot of people coming to AngularJS and building everything into one module:
var app = angular.module('myModule', []);
app.controller('MyController', [function(){}]);
app.controller('MyController2', [function(){}]);
app.controller('MyController3', [function(){}]);
app.controller('MyController4', [function(){}]);
app.controller('MyController5', [function(){}]);
For small applications, this is alright, but it really is not optimal. What we want to really accomplish using the build tools is to allow us to throw things into separate modules and worry about the details outside of the calling application. For instance, the main module of your application should not care how the authentication is implemented, only that it is and has a concrete interface.
main.js
var app = angular.module('myModule', ['mainControllers', 'otherControllers']);
mainControllers.js
var mainModule = angular.module('mainControllers', []);
mainModule.controller('MyController', [function() {}]);
mainModule.controller('MyOtherController', [function() {}]);
otherControllers.js
var otherControllers = angular.module('otherControllers', []);
otherControllers.controller('AuthenticationController', [function() {}]);
otherControllers.controller('LoginController', [function() {}]);
AngularJS and ES6
If you've been working with my baseline build system, then you already have everything installed and ready to go to start building an AngularJS application, so let's get started.
/source/index.js
This is the root of our application. This file is going to import everything we need in order to get our application running.
import angular from 'angular';
angular.module('OurTestApplication', []);
We start with a simple application base. By using import angular from 'angular';
we are telling Browserify that we want it to load our local copy of AngularJS so that we do not have to actually load it from a separate location or a CDN.
A Simple Controller
Next, we are going to want to build a simple controller. This controller is going to just have a simple list of items that it is going to display on the page.
/source/items/itemsController.js
var ItemsControllerModule = angular.module('OurItemsModule.ItemsController', []);
class ItemsController {
constructor () {
this.items = [
{ label: "Item 1", value: 1 },
{ label: "Item 2", value: 2 },
{ label: "Item 3", value: 3 },
{ label: "Item 4", value: 4 },
{ label: "Item 5", value: 5 }
];
}
}
ItemsController.$inject = [];
ItemsControllerModule.controller('ItemsController', ItemsController);
export default ItemsControllerModule;
Building a controller in ES6 is pretty easy. You can write this as a class, inject dependencies, and then export the module this class now belongs to. Everything is hidden away from consuming modules, so you can replace this controller with a different controller, and as long as they share the same interface, no one will ever know the difference.
A Custom Module
Because we want to include this in a separate module, we are going to create a custom module specifically for this controller.
/source/items/index.js
import angular from 'angular';
import ItemsControllerModule from './itemsController';
var OurItemsModule = angular.module('OurItemsModule', [
ItemsControllerModule.name
]);
export default OurItemsModule;
A couple of syntax items we want to discuss here. First, we are loading angular from 'angular', but we are loading ItemsController from './itemsController'. the reason for this is that our build system knows where 'angular' resides, but not where itemController resides. Since the controller's file is in the same directory, we need to use the correct directory structure calls. Second, notice that we did not add '.js' to itemsController. Browserify handles this for us automatically when attempting to import another file.
Updating our Application
After building this little controller, we are going to need to also update our main application module:
/source/index.js
import angular from 'angular';
import OurItemsModule from './items';
angular.module('OurTestApplication', [OurItemsModule.name]);
In this snippet, notice that we are not including index.js
at all. Again, Browserify handles our entry script for us automatically. In fact, every directory can have an index.js file that is used as the entry point for anything requiring it. Think of it as the __init__.py
of ES6.
Another thing to pay attention to is how we are calling our module. We are including it as a dependency by using OurItemsModule.name
. The reason we want to do this, and not hard-code a string in there, is that we can quickly and easily replace the OurItemsModule with a different module in a different namespace, and our test application won't actually care. As long as it implements the same interfaces.
Building
At this point, we can start making sure we are building correctly. You can either run the gulp watch task from the first article or you can run this command in your CLI:
gulp
Additionally, if you want to manually run your build step to get started, you can run this command in your CLI:
gulp browserify
You should see a file in /build/index.js
. This file will contain AngularJS, your application module, your items module, and your items controller.
A Simple Service
One thing you need to consider when building any application in AngularJS is that you will need to encapsulate services in order to allow controllers to access different shared data without caring where that data comes from or how to actually call that data. Additionally, services are great for sharing data between controllers. We are going to build a super simple event bus service that will let you inject event handlers into the system.
/source/eventBus/index.js
var EventBusModule = angular.module('EventBus', []);
class EventBus {
constructor ($rootScope) {
this.$scope = $rootScope;
this.registry = [];
this.index = 0;
}
Register (event, callback) {
this.index++;
this.registry[this.index] = this.$scope.$on(event, function (e, p) {
callback(p);
});
return this.index;
}
Deregister (eventId) {
if (this.registry[eventId] && typeof this.registry[eventId] === 'function') {
this.registry[eventId]();
delete this.registry[eventId];
}
}
}
EventBus.$inject = ['$rootScope'];
EventBusModule.service('EventBus', EventBus);
export default EventBusModule;
We are going to need to update our main application file to now include this new service:
/source/index.js
import angular from 'angular';
import OurItemsModule from './items';
import EventBusModule from './eventBus';
angular.module('OurTestApplication', [
OurItemsModule.name,
EventBusModule.name
]);
A Simple Directive
Another thing that you are going to have to build on a regular basis is a directive. Directives allow you to re-use components without having to copy/paste them everywhere. Additionally, you can change them in one location, and know that they will be updated everywhere that they are used. Let's build a simple button that fires a callback from the parent's scope.
/source/buttonDirective/index.js
var buttonDirectiveModule = angular.module('ButtonDirectiveModule', []);
var buttonTemplate = require('buttonDirectiveTemplate.html');
class ButtonDirectiveController {
constructor () {
}
}
ButtonDirectiveController.$inject = [];
var buttonDirectiveDefinition = function () {
return {
restrict: 'E',
scope: {
fn: '&myCustomFunction',
label: '@myCustomLabel'
},
bindToController: true,
controller: ButtonDirectiveController,
controllerAs: 'Button'
};
};
buttonDirectiveModule.directive('myCustomButton', buttonDirectiveDefinition);
export default buttonDirectiveModule;
/source/buttonDirective/buttonDirectiveTemplate.html
<button ng-click="Button.fn()">
{{Button.label}}
</button>
Calling this in your regular HTML template is as easy as this:
<my-custom-button my-custom-function="Controller.SomeFunction()" my-custom-label="Click Me!"></my-custom-button>
Don't forget, we also need to update our main application file in order to use this directive:
/source/index.js
import angular from 'angular';
import OurItemsModule from './items';
import EventBusModule from './eventBus';
import ButtonDirectiveModule from './buttonDirective';
angular.module('OurTestApplication', [
OurItemsModule.name,
EventBusModule.name,
ButtonDirectiveModule.name
]);
Next Steps
In the next, and final, article, we are going to go over how to set up unit tests using Karma and Jasmine.