Modularity in Single Page Applications

modularity
I recently joined tutum in an effort to re-engineer the dashboard web interface.

The initial/planning stage of any medium to large project shouldn’t be taken lightly unless you want to end up buried in technical debt 3 months in. Defining the correct structure and building it piece by piece in a modular way becomes almost mandatory if you want to keep your job.

Modularity is not a new concept, but it’s probably the single most important thing for building large-scale applications:

“The secret to building large apps is never build large apps. Break your applications into small pieces. Then, assemble those testable, bite-sized pieces into your big application” – Justin Meyer, author JavaScriptMVC

Defining a structure

Regardless of the fancy Javascript framework we’ll use and whatnot, we need to have a convention. A proper way of doing things. Although it’s often overlooked, this is very important for several reasons, namely to ensure predictability. Once you understand how things are done you no longer need to think about it (eg: How do I name this file? where do I put this?) and you can focus on what really matters which is building your awesome app.

With that in mind, we need to define our modular structure – so what is a module?

$ tree node-cluster/
.
├── index.js
├── node-cluster-ctrl.js
├── node-cluster.less
└── node-cluster.tpl.html

We have an entrypoint index.js, which is the one that is imported/required externally. The rest of the files are local to the component itself and shouldn’t be accessed externally (except maybe for testing purposes). Files within the component may require each other if necessary. As a rule of thumb, if a component inside a module begins to have a lot of dependencies it’s very likely it needs to become a separate module. This way components will always have a small size (3 to 4 files on average).

Grouping modules

We often need to group modules logically. You may have views, components, etc; having these grouped in separate folders would help to further modularize our app.

Also, modules may need to be nested together where a sub-module is local to the parent module and it’s only exposed through it. This makes more sense for things like views and where the parent view controls the behavior of the child view.

So following our previous example we now have:

.
└── views
└── node-cluster
├── node-cluster-list
│ ├── index.js
│ └── ...
├── node-cluster-detail
│ ├── index.js
│ ├── node-cluster-detail-ctrl.js
│ ├── node-cluster-detail.less
│ └── node-cluster-detail.tpl.html
├── index.js
├── node-cluster-ctrl.js
├── node-cluster.less
└── node-cluster.tpl.html
└── components
└── list
├── index.js
├── list-ctrl.js
├── list.less
└── list.tpl.html

Module Anatomy

Taking a closer a look, we can already see how the views/ start to resemble what the routing will look like. We have:

  • /node-cluster
  • /node-cluster/list
  • /node-cluster/:id

If for whatever reason we need to define, for example, a /node-cluster/:id/overview sub-view, it’s pretty obvious where it should go.

At Tutum, we use Angular and the Angular UI router. This modular approach goes very well with both since we can define routes (states) in each module without having a centralized router on which the view depends upon. Also, every module we define exports an Angular module that can be a dependency of another Angular module. We could also easily enable lazy loading of modules and routes in the future without a major refactor.

We also use ES6 which is a newer version of Javascript that adds a ton of features to the language. Even though it will take a while until browsers support it natively and tools like Babel are required to compile it down to ES5, I can’t stress enough how important this is for building modular components while making it forward compatible and forgetting about AMD and CommonJS modules altogether.

With that in mind, show me the code!

node-cluster/index.js:

import angular from 'angular';
import ngmodTemplate from './nodecluster.tpl';
import { Nodecluster } from './nodecluster-ctrl';
import ngmodList from './nodecluster-list/index';

import './nodecluster.css!';
import 'angular-ui-router';

var nodeclusterModule = angular.module('nodecluster',[ 'ui.router', ngmodTemplate.name, ngmodList.name ]);

nodeclusterModule.config(['$stateProvider', '$urlRouterProvider', ($stateProvider, $urlRouterProvider) => {
$stateProvider
.state('nodecluster', {
abstract: true,
url: '/node-cluster',
templateUrl: ngmodTemplate.name,
controller: Nodecluster,
controllerAs: 'nodeclusterCtrl'
});

$urlRouterProvider.when('/nodeclusters','/nodeclusters/list');
}]);

export default nodeclusterModule;

node-cluster-ctrl.js:

export class Nodecluster {
constructor () {
this.name = 'Node Clusters';
// ...
}
}

node-cluster.tpl.html:

<div class="'node-cluster-view'"></div>

node-cluster.less:

.node-cluster-view{
// All styles wrapped in here...
}

The node-cluster module only cares about node-cluster. It’s children can then define their own dependencies:

node-cluster/node-cluster-list/index.js:

import angular from 'angular';
import ngmodTemplate from './nodecluster-list.tpl';
import { NodeclusterList } from './nodecluster-list-ctrl';
import listComponent from 'app/components/list/index';

// my module here...

Modular CSS and HTML

The gulp watch task does a lot of the heavy lifting by adding the html templates to the angular $templateCache and inlining the CSS with the SystemJS CSS plugin.

The SystemJS CSS plugin allows us to define CSS locally in the component. This is huge since we can now define specific styles directly in the component regardless of the CSS framework of choice. The only caveat is that the order of the CSS injection is not guaranteed so we need to increase specificity in local selectors, but it’s a good trade-off.

Components

Coming back to predictability, we now know what our module looks like. This lets us further improve our workflow, and for example, create a module programatically:

#! /bin/bash

# Script to create a new reusable component.

# $1: [required]

if [ -z "$1" ]; then echo 'component name is required required'; exit 1; fi

# Components folder location
folder=src/app/components

# pcomponent name
name=$1

# component name capitalized
Name="$(tr '[:lower:]' '[:upper:]' <<< ${name:0:1})${name:1}"

mkdir $folder/$name

# Entry point file (index.js)
cat << EOF | sed 's/*//' > $folder/$name/index.js
import angular from 'angular';
import template from './$name.tpl';
import { $Name*Ctrl } from './$name-ctrl';
import './$name.css!';

var ngModule = angular.module('tt.$name',[ template.name ]);

$Name*Ctrl.$inject = [];

ngModule.directive('tt*$Name',[() => {
return {
scope: true,
restrict: 'A',
controllerAs: '$name*Ctrl',
controller: $Name*Ctrl,
bindToController: {},
templateUrl: template.name
};
}]);

export default ngModule;
EOF

# LESS base file
cat << EOF > $folder/$name/$name.less
@import (less) '../../less/utilities';

.$name {

}
EOF

# Controller file
cat << EOF | sed 's/*//' > $folder/$name/$name-ctrl.js
export class $Name*Ctrl {
constructor(){

}
}
EOF

# Template file
cat << EOF > $folder/$name/$name.tpl.html
<div class="'$name'"></div>
EOF

Conclusion

If you consider your UI a state machine with nested routes hierarchies you might find this architecture useful. Also having a project structure that resembles the routing makes logical sense, making it easier to work with.

Tagged with: , ,
Posted in Design

Leave a Comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Categories
%d bloggers like this: