Transition Time

We're in the process of transitioning one of our application's front ends from a collection of independent scripts containing IIFEs to ES6-style exportable components. To correspond with the new importing scheme, we needed to convert one of the project's depdencies, our Angular waiting button component, from a Bower project to an NPM project. After diving in to try and find other articles explaining this process and coming up empty-handed, I decided it would be helpful to others to document building out a Angular module using Webpack and NPM.

We will be building out a very simple button component that we can use within any Angular 1 application by including it in our app's dependencies. The functionality of the button will be rudimentary, as the focus of this article will be on getting development setup and providing a module that is importable.

Step 1: Dependencies

First we need to create a directory to hold our project and run npm init to create the boilerplate package.json file. Next we need to install our dev dependencies in order to work with webpack and angular. To do this you can run:

npm install --save-dev angular babel-core babel-loader babel-polyfill babel-preset-es2015 css-loader node-sass rimraf sass-loader style-loader webpack

With our dependencies present we're ready to set up our build script. We can add a new "build" target to package.json like so:

"scripts": {
  "build": "rimraf dist && webpack --bail --progress --profile"
}

The build script will first remove the directory dest using rimraf and then run webpack.

Also notice from the json below that our name for the module is angular-surprise-button and we have the main entry point option pointed to dist/angular-surprise-button.js. This value gives NPM instructions on where to find the starting point when importing NPM packages.

// package.json

{
  "name": "angular-surprise-button",
  "version": "1.0.0",
  "description": "Angular 1 Surprise Button",
  "main": "dist/angular-surprise-button.js",
  "dependencies": {},
  "devDependencies": {
    "angular": "^1.6.3",
    "babel-core": "^6.24.0",
    "babel-loader": "^6.2.4",
    "babel-polyfill": "^6.9.1",
    "babel-preset-es2015": "^6.9.0",
    "css-loader": "^0.23.1",
    "node-sass": "^3.10.1",
    "rimraf": "^2.6.1",
    "sass-loader": "^3.2.3",
    "style-loader": "^0.13.1",
    "webpack": "^2.2.1"
  },
  "scripts": {
    "build": "rimraf dist && webpack --bail --progress --profile"
  },
  "repository": {
    "type": "git",
    "url": ""
  },
  "keywords": [],
  "author": "",
  "license": "MIT",
  "bugs": {
    "url": ""
  },
  "homepage": ""
}

Step 2: Configure Webpack

The next step is to create a webpack.config.js file to define the build phase.

// webpack.config.js

var webpack = require("webpack");
var path = require("path");

var BUILD_DIR = path.resolve(path.join(__dirname, "dist"));
var APP_DIR = path.resolve(path.join(__dirname, "src", "js"));

var config = {
  devtool: "source-map",
  entry: path.join(APP_DIR + "/surprise-button.module.js"),
  output: {
    path: BUILD_DIR,
    filename: "angular-surprise-button.js",
    library: "mbmSurpriseButton",
    libraryTarget: "commonjs2"
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        loaders: ["babel-loader"],
        exclude: /node_modules/
      },
      {
        test: /\.scss$/,
        loaders: ["style-loader", "css-loader", "sass-loader"]
      }
    ]
  },
  resolve: {
    alias: {
      angular: path.resolve(path.join(__dirname, "node_modules", "angular"))
    }
  }
};

module.exports = config;

Breaking this down we can see that our project has two primary directories: the APP_DIR (src/js) will contain the project's source scripts and styles. The BUILD_DIR will contain the bundled module after it has been compiled by webpack. We make sure our paths will work platforms by creating the final paths with the help of path.join.

Looking at the config object it's important to take note of the entry point, as that is were webpack will start from. In the /src/js/ directory we need to create a surprise-button.module.js file.

One of the most important steps is making sure that our output config is setup correct:

  output: {
    path: BUILD_DIR,
    filename: "angular-surprise-button.js",
    libraryTarget: "commonjs2"
  }

The path option will point to the directory that will contain our compiled javascript. The filename is the name of the file generated. The libraryTarget has many options which format to export the library:

"var" - Export by setting a variable: var Library = xxx (default)

"this" - Export by setting a property of this: this["Library"] = xxx

"commonjs" - Export by setting a property of exports: exports["Library"] = xxx

"commonjs2" - Export by setting module.exports: module.exports = xxx

"amd" - Export to AMD (optionally named - set the name via the library option)

"umd" - Export to AMD, CommonJS2 or as property in root

We could use commonjs and name the Library to export or additionally leave out the Library and just use ES6 destructuring to pull out the exports we want from the module. However our Angular code is intended to be imported as a single angular.module, so this is unnecessary.

The module section of the webpack config defines the different loaders to be used depending on a regex match of file types. We are using babel-loader for all javascript files and for the Sass we are first running it through sass-loader, then css-loader, and finally style-loader so that it can be in-lined in the javascript and won't need to be a separate file to depend on.

module: {
    loaders: [
      {
        test: /\.js$/,
        loaders: ["babel-loader"],
        exclude: /node_modules/
      },
      {
        test: /\.scss$/,
        loaders: ["style-loader", "css-loader", "sass-loader"]
      }
    ]
  },

The last part of the webpack config is important so that we can keep our dependencies out of the bundle that we know the consumer of the application will already have. For example we know the consumer will be using Angular so we can let webpack load Angular through it's resolve. This makes angular accessible within any of our javascript files without needing to import it and Angular will not be included when we build the distributable module.

resolve: {
  alias: {
    angular: path.resolve(path.join(__dirname, "node_modules", "angular"))
  }
}
Step 3: Angular Time

Now that we have the build steps setup and the dependencies installed we can start to create the module. Within the file surprise-button-module.js we need to create our initial Angular module.

// /src/js/surprise-button.module.js

import SurpriseButton from "./surprise-button.component";
import "../sass/surprise-button.scss";

export default angular
  .module("mbmSurpriseButton", [])
  .component("mbmSurpriseButton", SurpriseButton).name;

This is just basic Angular module creation and we are using the new component syntax as well. Take note that we are using .name at the end of our angular module in order to get back the module name for importing.

At the top of this file we are importing our component from /src/js/surprise-button.component.js, as well as our styles from /src/sass/surprise-button.scss.

// /src/js/surprise-button.component.js

class controller {
  constructor () {}
}

export default {
  transclude: true,
  controller: controller,
  template: `
    <a class="mbm-surprise-button" role="button">
      <ng-transclude></ng-transclude>
    </a>
  `
};

The component defines an object literal with several properties, configuring transclusion support, the component's controller, and the template we will be using. For this example it's just a basic component with an anchor tag as a button.

The important part is to understand that we will be able to use this component on any project by simply importing it. In addition if you publish the module to NPM you can simply install it with npm install --save NAME_OF_PACKAGE in order to start using it.

Once all of this is in place we can run npm run build in order to kick off the build script and produce our bundled, importable file.

Developing within another project

Something that I recently found out is that you can easily link packages using npm link so that you can include an under-development module in a project. The module will be included in the project's dependencies, and any changes to the under-development module will be automatically include in the project.

For example, if I wanted to use this button in an application and still make changes to the button I could run npm link within the module directory and then change into the application that I wanted to use it in and run npm link angular-surprise-button. Then I would be able to use this package within the other application. Even better if I run webpack --watch I can make changes to this button and see them reflected immediately within the other application.

Allowing for modularity within javascript has opened up a wide world of opportunities. We are no longer stuck in the days of throwing script tag after script tag on our html file and worry about the order they are loaded in. This is the future and the future is looking very bright!

You can find the full example application at angular-surprise-button

You can also find an example WebpackBin using this module with the component webpackbin

More posts
  • Commit with Intent

    The success of git has brought a new generation of committers to open source projects. That being said, it also makes us take a look at how we commit our code and what we can do to improve our commit messaging.

    Read More
  • Debugging Angular from the Browser Console

    No matter how well-designed your AngularJS application is, when components start interacting with the real world--and each other--unforeseen issues can arise. Being able to dive into the app's components, inspecting individual slices of behavior and state, is critical to efficiently resolving those issues.

    Read More
  • InVision's Design Disruptors

    DESIGN DISRUPTORS is a full-length documentary by InVision featuring design leaders and product designers from 15+ industry-toppling companies — valued at more than $1 trillion dollars combined.

    Read More