Write a post
Published May 15, 2017

How to: Modernized AngularJS 1.5+ with ES6, Webpack, Mocha, SASS, and Components

How to: Modernized AngularJS 1.5+ with ES6, Webpack, Mocha, SASS, and Components

There are many reasons why you might want to keep working with AngularJS 1.x — I will simply assume you have your reasons.

Angular ≠ AngularJS. This site and all of its contents are referring to AngularJS (version 1.x), if you are looking for the latest Angular, please visit angular.io — angularjs.org

For new projects, I would recommend using React because this is where the momentum is in front-end development.

Or at least, this person thinks it is, and I agree with him

I made a GitHub repo you can fork/clone to start your own project

jsdoc_output // where docs are generated
node_modules // where your vendor stuff goes                 
.gitignore
mocha-webpack.opts // specify a different webpack config for testing
package.json 
README.md
webpack.config.base.js
webpack.config.js // extends base config
webpack.config.test.js // extends base config
public 
|   index-bundle.js // webpack generated bundle
|   index.html
|   index.js // webpack 
|   
\---superAwesomeComponent
        componentStylez.sass
        componentTemplate.html
        fancyJsModule.js
        theComponent.js
        theComponent.spec.js
        theComponentController.js

Generated using tree /a /f on windows

Let’s check index.html

<body
  ng-app="theWholeApp"
  ng-controller="IndexController as IndexCtrl"
  ng-cloak>
  <super-awesome-component
    some-input="93"
    some-output="IndexCtrl.fancyValue = value">
  </super-awesome-component>
  <super-awesome-component
    some-input="2"
    some-output="IndexCtrl.fancyValue = IndexCtrl.fancyValue + IndexCtrl.addValue">
  </super-awesome-component>
  <p>
    A variable on the controller above the components: {{IndexCtrl.fancyValue}}
  </p>
</body>
<tail>
  <script src="index-bundle.js"></script>
</tail>


No action has been done yet

You can see here, that our two buttons are the two super-awesome-component elements. These are Angular 1.5 components.

Angular 1.5 components

Angular 1.5 components are just directives with better default values. They are always elements, there is a default “Controller as $ctrl”, and they have isolate scopes. Most of what I've learned about components, I learned them here.

The components have two bindings, some-input and some-output.

These components are useful because they allow us to encapsulate a combination of view and controller functionality. Let’s look at the component file:

import template from './componentTemplate.html'
import componentStylez from './componentStylez.sass'
import {ComponentController} from './theComponentController.js'
const bindings = {
  someInput: '<',
  someOutput: '&'
}
export const theComponent = {
  controller: ComponentController,
  template,
  bindings
}

Notice how each element of the controller can be re-used. The controller can be specific to this component, or it could be a controller that is used elsewhere.

Furthermore, this file contains references to everything you need to know about the component. The component is totally self-contained, you don’t need to worry about how it is being used in the larger application in order to make it.

The controller makes use of normal ES6 features — I won’t go into how it works, but take note of how the class structure is used, and the lack of \$scope. The result is a framework-agnostic controller, minus the component lifecycle event (\$onInit).

import fancyFunction from './fancyJsModule.js'
/**
 * Provides handlers for theComponent
 */
class ComponentController {
  /**
   * Announces that input bindings aren't defined
   * @return {undefined} undefined
   */
  constructor () {
    console.log('input bindings arent defined!', this.someInput)
  }
  /**
   * Calls someOutput with the value of someInput put in fancyFunction
   * @return {undefined} undefined
   */
  doSuperThings () {
    console.log('doing super things')
    this.someOutput({value: fancyFunction(this.someInput, 3)})
  }
  /**
   * Announces that input bindings are defined
   * @return {undefined} undefined
   */
  $onInit () {
    console.log('input bindings are defined!', this.someInput)
  }
}
export { ComponentController }

StandardJS formatting

The obvious difference is the lack of semicolons. I personally believe this provides cleaner looking code and the StandardJS linter/formatter neatly solves the issues around semicolon usage, which will prevent you from encountering weird issues there.

Webpack (which can be confusing)

Notice how all we have to do is to import index.bundle.js in index.html. This is because we are using Webpack, which bundles all of our assets into a single file. This includes our templates, JavaScript, CSS, and anything you can imagine needing in there.

Webpack is a finicky beast, and a beast it is. It’s complicated enough that people put it on their resumes. It moves a lot of complexity from various parts of your application, into your webpack.config.js file.

Evidence of this complexity can be found in the fact that we have cause for 3 webpack.config*.js files. One provides a base, the second is to accomodate our testing setup, and the third is for splitting code in to vendor chunks (which we don’t want to do in our test setup do to strange interactions with the CommonsChunkPlugin).

var path = require('path')
var webpack = require('webpack')
module.exports = {
  entry: {
    'index': path.join(__dirname, '/public/index.js')
  },
  output: {
    filename: '[name]-bundle.js',
    path: path.join(__dirname, '/public/'),
    devtoolLineToLine: true,
    pathinfo: true,
    sourceMapFilename: '[name].js.map',
    publicPath: path.join(__dirname, '/src/main/webapp/')
  },
  module: {
    loaders: [
      { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ },
      { test: /\.css$/, loader: 'style-loader!css-loader' },
      { test: /\.sass$/, loaders: ['style-loader', 'css-loader', 'sass-loader'] },
      { test: /\.html$/, loader: 'raw-loader' },
      // inline base64 URLs for <=8k images, direct URLs for the rest
      { test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192' },
      // helps to load bootstrap's css.
      { test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
        loader: 'url?limit=10000&minetype=application/font-woff' },
      { test: /\.woff2$/,
        loader: 'url?limit=10000&minetype=application/font-woff' },
      { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
        loader: 'url?limit=10000&minetype=application/octet-stream' },
      { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
        loader: 'file' },
      { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
        loader: 'url?limit=10000&minetype=image/svg+xml' }
    ]
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ],
  devServer: {
    publicPath: '/',
    contentBase: path.join(__dirname, '/public'),
    compress: true
  },
  devtool: 'eval'
}

I’m not going to explain everything here, because that’s what the webpack docs are for (this link is for Webpack 1 even though we’re using Webpack 2. The Webpack 2 docs are thorough only in their incompleteness, but do see the migrations documentation).

To give an overview, you must specify:

  • Where your application starts
  • Where the bundle goes
  • How you’re going to magically import things
  • What plugins you’re using
  • Your webpack-dev-server setup
  • How your source maps are set up.

What? Plugins? Source maps? Why do I need another server?

Plugins

Here, we’re just using the HotModuleReplacement (HMR) plugin. It allows our browser to automatically reload when a file is changed. This magically removes one step of the normal iteration of write, save, test.

There are tons of other plugins out there — (here's one that stands out but I haven’t gotten around to trying!)

Here’s a list of popular Webpack plugins (why does Webpack do so many things!)

Source maps

Source maps are products of ES6 and bundling. I haven’t figured out how to get them perfect yet — there is an unfortunate speed/quality tradeoff that occurs with sourcemaps, as the perfect ones can be rather slow to create. Our ES6 conversion is achieved through a babel loader.

If we look back at theComponent.js, this contains most of our Webpack:

import template from './componentTemplate.html'
import componentStylez from './componentStylez.sass'
import {ComponentController} from './theComponentController.js'
const bindings = {
  someInput: '<',
  someOutput: '&'
}
export const theComponent = {
  controller: ComponentController,
  template,
  bindings
}

Note how we are import’ing html, SASS, and ES6 here. This is accomplished through our loaders. Which loader is used is based on the file name.

Webpack-dev-server

Webpack-dev-server is an amazing thing, regardless of whether or not you have a real back-end. It supports HMR and is a static file server, which makes your development fast. In addition, using webpack-dev-server will force you to de-couple your front-end and back-end.

Being able to do front-end development without needing a “real” server is amazing for a lot of reasons. It will force you to create practical mock data, know exactly what functionality belongs to the back-end vs. the front-end, give you HMR, and make your front-end hostable on just about any server, with a clear contract between the front-end and the back-end.

In this setup, webpack-dev-server, along with everything else needed for front-end development, is run by a single npm run dev command, as specified in package.json:

{
  "name": "modern-angularjs-starter",
  "version": "0.0.1",
  "description": "Base project",
  "main": "index.js",
  "scripts": {
    "dev": "concurrently --kill-others \"webpack-dev-server --host 0.0.0.0\" \"npm run docs\"",
    "docs_gen": "jsdoc -r -d jsdoc_output/ public/",
    "docs_watch": "watch \"npm run docs_gen\" public",
    "docs_serve": "echo Docs are being served on port 8082! && live-server -q --port=8082 --no-browser jsdoc_output/",
    "docs": "concurrently --kill-others \"npm run docs_serve\" \"npm run docs_watch\"",
    "postinstall": "bower install",
    "webpack": "webpack",
    "test": "mocha-webpack public/**/*.spec.js"
  },
  "devDependencies": { /* hidden for space */  }
  "dependencies": { /* hidden for space */ }
}

Notice the use of concurrently.

This allows us to run 2 blocking commands in parallel.

Notice there are also testing and documentation commands. The documentation commands generate JSDoc pages and then host them on a small server, which auto-refreshes (similar to HMR) the browser when there is a change. This way, you can watch your docs update as you write them if you save often.

It is not demonstrated in this project, however, specifying types in JSDoc is a good way to specify data-contracts between front-end/back-end. Alternatively, you could just use typescript (there are loaders for that).

Unit Testing (because it’s worth the effort)

Testing with ES6 + AngularJS + Webpack is tricky to get right. Each of these causes complications. For unit testing, I ended up settling on very small units, testing my AngularJS controllers as functions in Node. Karma is quite popular, but in my opinion the tests aren’t really unit tests. Nonetheless, it would be useful to have both.

Thus, we have mocha-webpack. This allows us to use imports in our tests, without specifying an entrypoint for each one.

The hardest part about testing here is mocking out ES6 imports. There are a few different ways to do that, but the only one that doesn’t require modifying the file being tested is inject-loader.

This is particularly useful for writing tests where mocking things inside your module-under-test is sometimes necessary before execution — inject-loader.

/* eslint-disable */
import chai from 'chai'
import sinon from 'sinon'
const theControllerInjector = require('inject-loader!./theComponentController.js')
let {expect, should, assert} = chai
describe('superAwesomeComponent', function() {
  let stub 
  let theComponentController
  let controller
beforeEach(function setupComponent () {
    stub = sinon.stub().returns(1)
    theComponentController = theControllerInjector({
      // The module is really simple, so it's not really necessary to mock it
      // In a real app, it could be much more complex (ie, something that makes API calls)
      './fancyJsModule.js': stub
    }).ComponentController
    controller = new theComponentController()
    controller.someOutput = sinon.stub()
    controller.someInput = 1
  })
  describe('doSuperThings', function() {
    it('calls fancyFunction', function() {
      controller.doSuperThings()
      assert(stub.calledOnce)
    })
  })
})

To use inject-loader, we use the old require + webpack loader syntax because there isn’t a wildcard filename check we can do for the import (we don’t want all js files to get passed into the inject loader all the time). The return of this require gives us a function that we can call with an object stubbing out various imports:

theComponentController = theControllerInjector({
  './fancyJsModule.js': stub
}).ComponentController

Here, we stub out fancyJsModule from our controller’s imports. This allows us to return a mock value, subverting all the logic that module might do, so we can isolate any problems that occur in the test.

We use Chai as our assertion library, Sinon.js for mocking/spying, and Mocha for running the tests.

This test doesn’t attempt to be a good example of what to test, it’s simply to show how testing can be set up with ES6+Webpack+Mocha+Angular.

The goal of this is to force the developer into focusing on writing AngularJS handlers as actual functions. There is a strong tendency for these handlers to be executed purely for side-effects, and creating these tests will highlight that fact.

Soo…

This architecture provides a way of modernizing AngularJS front-end without making a framework jump. One of the biggest benefits of this approach is that it abstracts away a lot of the AngularJS-specific code.

One of the trickiest elements of this approach is deciding what to use AngularJS modules for vs. what to use ES6 modules for. I try to use ES6 as much as possible. This should make it easier to port an application using this architecture to another framework.

AngularJS still has a fair amount of life to it, but there is no doubt that its prime time has passed. ES6/7, however, are still on the rise.

Long live AngularJS!

By the way, check out my previous rants on JS


This post was originally published by the author here. This version has been edited for clarity and may appear different from the original post.

Discover and read more posts from Nicholas Arthur
get started
Enjoy this post?

Leave a like and comment for Nicholas