By
Martin Micunda
Edit this page on GitHub

How to start writing apps with ES6, Angular 1.x and JSPM

ES6 development and production workflow with JSPM

Couple weeks ago I have started to work on one of my personal projects (Employee Scheduling) and at that time I didn't think I will write any of my code in ES6 not because I wouldn't want to try one of the cool ES6 features but I was more concerned how to automating my development workflow with ES6 and I also want to get my code to production until I watched Guy Bedford video from JSConf2014 where he was talking about ES6 modules workflow using JSPM. As you may guess JSPM solves a lot of my concerns and today I want to share my ES6 development and production workflow using JSPM. The Employee Scheduling application screenshot: Employee Scheduling Before we start describing my workflow I want to mention that I am using Angular 1.x for my project but you could use similar ES6 workflow with EmberJS or ReactJS.

Project Setup

First clone an employee-scheduling-ui repo and then install dependencies:

git clone git@github.com:martinmicunda/employee-scheduling-ui.git
cd employee-scheduling-ui
npm install

I have set npm postinstall script to run jspm install so when you execute npm install it will also install jspm packages under jspm_packages folder.

Next when you open package.json you can see jspm block that configures packages for jspm. The jspm block can be created manually or through jspm init command and if you want to add a jspm packages just add them manually or through jspm install <package> command:

"jspm": {
    "directories": {
        "lib": "src"
    },
    "configFile": "jspm.conf.js",
    "dependencies": {
        "angular": "1.3.8",
        .....
    },
    "devDependencies": {
      "babel": "npm:babel-core@5.8.24",
      "babel-runtime": "npm:babel-runtime@5.8.24",
      "core-js": "npm:core-js@1.1.4"
    }
}

Next open jspm.config.js which tells SystemJS how to find modules (if you look closer you can see that configuration is similar like in require.js)

System.config({
    baseURL: "./",
    defaultJSExtensions: true,
    transpiler: "babel",
    babelOptions: {
      "optional": [
        "runtime"
      ],
      "stage": 1
    },
    paths: {
      "employee-scheduling-ui/*": "src/*.js",
      "github:*": "jspm_packages/github/*",
      "npm:*": "jspm_packages/npm/*"
    },
    "map": {
      "angular": "github:angular/bower-angular@1.3.8",
      ...
    }
});

As the last thing open src/index.html where we are loading system.js, jspm.config.js files and importing app/bootstrap module with all dependencies:

<script src="jspm_packages/system.js"></script>
<script src="jspm.conf.js"></script>
<script>
    System.import('app/bootstrap')
    .catch(console.error.bind(console));
</script>

The SystemJS is build on top of ES6 Module Loader Polyfill that mean we can use the future Javascript Module Loader today and if you open your browser console you can see how ES6 Module Loader Polyfill load app/bootstrap module with all dependencies. Image of ES6 Module Loader The SystemJS library use Traceur or 6to5 (it depends on your configuration) to compile your ES6 to ES5 code and all this happened in the browser automatically so there is no need for any build steps. If you turn on JavaScript source maps in the browser console you can also see ES6 code. Image of ES6 Source Map

Development

As I mentioned in previous section there is no build step required as all compilation from ES6 to ES5 happened in the browser so only thing that you want to set is to watch for changes and refresh the browser by using gulp or some other tool.

Bootstrap Angular 1.x Application

As we are using ES6 with Angular 1.x we can't use ng-app directive to bootstrap the application as modules are loaded asynchronously instead of that we need to bootstrap the application manually:

import angular from 'angular';
import mainModule from './main';

angular.element(document).ready(function() {
    angular.bootstrap(document, [mainModule.name]);
});

Controllers

class EmployeesAddController {
    constructor(languages, positions, roles, EmployeeResource, $state, $modalInstance) {
        this.$modalInstance = $modalInstance;
        this.EmployeeResource = EmployeeResource;
        this.employee = {};
        this.languages = languages;
        this.positions = positions;
        this.roles = roles;
        this.profileComplete = EmployeeResource.calculateProfileCompleteness({});
        this.$state = $state;
    }

    cancel() {
        this.$modalInstance.dismiss('cancel');
    }
}
EmployeesAddController.$inject = ['languages', 'positions', 'roles', 'EmployeeResource', '$state', '$modalInstance'];

export default EmployeesAddController;

Services

class RoleService {
    constructor(Restangular) {
        this.Restangular = Restangular;
    }

    getList(query) {
        return this.Restangular
            .all('roles')
            .withHttpConfig({cache: true})
            .getList(query);
    }
}
RoleService.$inject = ['Restangular'];

export default RoleService;

Routes

function employeesRoute($stateProvider) {
    $stateProvider
        .state('employees', {
            url: '/employees',
            templateUrl: 'app/states/employees/employees.html',
            controller: 'EmployeesController as vm',
            resolve: {
                employees: ['EmployeeResource', EmployeeResource => EmployeeResource.getList()],
                languages: ['LanguageResource', LanguageResource => LanguageResource.getList()],
                positions: ['PositionResource', PositionResource => PositionResource.getList({lang: 'en'})],
                roles: ['RoleResource', RoleResource => RoleResource.getList({lang: 'en'})]
            }
        });
}
employeesRoute.$inject = ['$stateProvider'];

export default employeesRoute;

Directives

function mmScrollUp($location, $anchorScroll) {
    let directive = {
        restrict: 'AC',
        link: link
    };
    return directive;

    function link(scope, element, attrs) {
        element.on('click', function() {
            $location.hash(attrs.uiScroll);
            $anchorScroll();
        });
    }
}
mmScrollUp.$inject = ['$location', '$anchorScroll'];

export default angular.module('mmScrollUp', [])
    .directive('mmScrollUp', mmScrollUp);

Configs

export function onConfig($locationProvider, $urlRouterProvider, RestangularProvider) {

    // set restful base API Route
    RestangularProvider.setBaseUrl('/api/v1');

    // use the HTML5 History API
    $locationProvider.html5Mode(true);

    // for any unmatched url, send to 404 page (Not page found)
    $urlRouterProvider.otherwise('/employees');

    // the `when` method says if the url is `/` redirect to `/dashboard` what is basically our `home` for this application
    $urlRouterProvider.when('/', '/employees');
}
onConfig.$inject = ['$locationProvider', '$urlRouterProvider', 'RestangularProvider'];

Production

If you run employee-scheduling app and open the browser console you can see there are over 100 requests and with so many requests you probably don't want to go to production so let's create production bundle with systemjs-builder by running gulp bundle. This task will create self-executing bundle with all the modules imported by src/app/bootstrap.js that can run without needing SystemJS by adding a micro-loader implementation and traceur-runtime into one bundle.

gulp.task('bundle', 'Create JS production bundle', ['jshint'], function (cb) {
    const Builder = require('systemjs-builder');
    const builder = new Builder();
    const inputPath = 'src/app/bootstrap';
    const outputFile = `${path.tmp.scripts}build.js`;
    const outputOptions = { sourceMaps: true, config: {sourceRoot: path.tmp.scripts} };
    
    builder.loadConfig(`${path.root}/jspm.conf.js`)
         .then(() => {
             builder.buildStatic(inputPath, outputFile, outputOptions)
                 .then(() => cb())
                 .catch((ex) => cb(new Error(ex)));
         });
});

If you want to run my production code just run npm start -- --env=PROD that will create production code under build/dist directory and it will run code from this directory.

Conclusion

I think by now you have at least some idea how to start writing apps with ES6, JSPM and how you can start preparing for Angular 2 as it will be written in ES6. I have still plan to continue on this app and add some tests and slowly migrate to Angular 2 in the future. You can find the complete app code here: https://github.com/martinmicunda/employee-scheduling-ui

Written by
Full Stack Software Engineer

Martin Micunda

Martin Micunda Full Stack Software Engineer

Martin Micunda is a Full Stack Software Engineer based in Dublin, Ireland.