Angular Component Cutter Using Es2015 Gulp And Webpack

Angular Component Cutter Using ES2015, Gulp and Webpack

Cookie Cutter to get base cookie.

We have components everywhere. Our software is no exception. We have them in cutting edge stuffs like Bower, NPM, Web Components, Ember, and now in Angular 2. Components are like building blocks in Lego game which does one thing and does it very well. And they’re so generic/reusable that we can fit them together to build stuffs that we want.

Now think about them other way around. We can split our huge application into smaller, independent, reusable and testable components based on their functionality which can then be packaged together to build the entire application. These components can be business objects like product, user, shopping cart, etc. or even technical things like header, footer, menu, etc. These can then be reused in other applications of similar nature requiring similar business and technical components.

With ES2015 and component router in Angular 1.5 coming soon which is going to ease the transition to Angular 2, it’s worth exploring the newest way of writing Angular apps using this super awesome combo and component mindset. And then we can take it to next level by automating component making process and make it as simple as a cookie cutter.

Here are the 2 tools that we need:

1. Webpack:

  • Transpiles ES2015 (ES6) to ES5 (JavaScript that current browsers know).
  • Loads HTML files and dependent files as modules.
  • Transpiles stylesheets and appends them to HTML.
  • Bundles the entire app.
  • Loads the modules on demand to keep initial load time low.

2. Gulp:

Gulp is used as orchestrator for

  • Starting and calling Webpack.
  • Starting a development server.
  • Refreshing the browser and rebuilding on file changes.

File Organization of Angular App:

With components in mind, let’s have file organization as show below:

client
⋅⋅app/
⋅⋅⋅⋅app.js * app entry file
⋅⋅⋅⋅app.html * app template
⋅⋅⋅⋅common/ * functionality pertinent to several components
⋅⋅⋅⋅components/ * where components live
⋅⋅⋅⋅⋅⋅components.js * components entry file
⋅⋅⋅⋅⋅⋅home/ * home component
⋅⋅⋅⋅⋅⋅⋅⋅home.js * entry file (routes, configurations, declarations)
⋅⋅⋅⋅⋅⋅⋅⋅home.component.js * home "directive"
⋅⋅⋅⋅⋅⋅⋅⋅home.controller.js * home controller
⋅⋅⋅⋅⋅⋅⋅⋅home.styl * home styles
⋅⋅⋅⋅⋅⋅⋅⋅home.html * home template
⋅⋅⋅⋅⋅⋅⋅⋅home.spec.js * home specs

Let’s look at how home component can be implemented in these individual files using new features like import, export, class, object shortcut notation, arrow function from ES2015:

home.js:

This is the first file we should look at as it holds the definition of the component, what url it is reachable at and what’s its template which acts as skin cover for the component. The template should use a custom directive, home in this case, which is imported as homeComponent from home.component.js file.

import angular from 'angular';
import uiRouter from 'angular-ui-router';
import homeComponent from './home.component';

let homeModule = angular.module('home', [
  uiRouter
])

.config(($stateProvider, $urlRouterProvider) => {
  $urlRouterProvider.otherwise('/');

  $stateProvider
    .state('home', {
      url: '/',
      template: '<home></home>'
    });
})

.directive('home', homeComponent);

export default homeModule;

Note: Above is the only component file that has angular in it. The following files have plain JavaScript making Angular’s footprint smaller in out code base. Thanks to ES2015's import/export feature to make this possible.

home.component.js:

This is the center piece that imports and knits the component’s HTML, controller and stylesheet together. Since this custom directive is used in the template of home component, home.html gets rendered on the screen with home.styl applied to it.

import template from './home.html';
import controller from './home.controller';
import './home.styl';

let homeComponent = function () {
  return {
    restrict: 'E',
    scope: {},
    template,
    controller,
    controllerAs: 'vm',
    bindToController: true
  };
};

export default homeComponent;

home.controller.js:

Next comes the controller that makes all the JavaScript properties and methods defined on HomeController class accessible in the home.html template via controller’s alias name vm declared in above file.

class HomeController {
  constructor() {
    this.name = 'home';
  }
}

export default HomeController;

home.styl:

Stylesheet written using Stylus gets converted to CSS using Webpack’s stylus loader.

.home
  color red

home.html:

This holds the UI of the component, binding the data and methods defined in the controller class via vm object.

<navbar></navbar>
<header>
  <hero></hero>
</header>
<main>
  <div>
    <h1>Found in .html</h1>
  </div>
</main>

home.spec.js:

With spec file for individual components, we can write well focused and isolated test cases for all the features we add in our component. Also packing spec file in here ensures that it will always move along with the component if it is reused in other applications.

import HomeModule from './home'
import HomeController from './home.controller';
import HomeComponent from './home.component';
import HomeTemplate from './home.html';

describe('Home', () => {
  let $rootScope, makeController;

  beforeEach(window.module(HomeModule.name));
  beforeEach(inject((_$rootScope_) => {
    $rootScope = _$rootScope_;
    makeController = () => {
      return new HomeController();
    };
  }));

  describe('Module', () => {
    // top-level specs: i.e., routes, injection, naming
  });

  describe('Controller', () => {
    // controller specs
    it('has a name property [REMOVE]', () => { // erase if removing this.name from the controller
      let controller = makeController();
      expect(controller).to.have.property('name');
    });
  });

  describe('Template', () => {
    // template specs
    // tip: use regex to ensure correct bindings are used e.g.,
    it('has name in template [REMOVE]', () => {
      expect(HomeTemplate).to.match(//g);
    });
  });

  describe('Component', () => {
      // component/directive specs
      let component = HomeComponent();

      it('includes the intended template',() => {
        expect(component.template).to.equal(HomeTemplate);
      });

      it('uses `controllerAs` syntax', () => {
        expect(component).to.have.property('controllerAs');
      });

      it('invokes the right controller', () => {
        expect(component.controller).to.equal(HomeController);
      });
  });
});

Webpack Config:

It’s worth mentioning about Webpack module loaders configuration that makes importing .html, .styl, .css, .js and even .png, .jpg, .jpeg possible at component level. Here is how loaders are configured:

webpack.config.js:

/*
config for webpack. Will be used in
the Gulpfile for building our app.
Does not need gulp in order to do so,
but we use gulp to orchestrate
 */
module.exports = {
  output: {
    filename: 'bundle.js'
  },

  devtool: 'sourcemap',

  module: {
    loaders: [
      { test: /\.html$/, loader: 'raw' },
      { test: /\.styl$/, loader: 'style!css!stylus' },
      { test: /\.css/, loader: 'style!css' },
      { test: /\.(png|jpg|jpeg)$/, loader: 'file' },
      { test: /\.js$/, loader: 'babel?stage=1', exclude: [/client\/lib/, /node_modules/, /\.spec\.js/] }
    ]
  },

  stylus: {
    use: [require('jeet')(), require('rupture')()]
  }
};

Automation of Component Creation:

Writing these individual files for creating components is tedious. How about automating it? Let’s take copy of all the home component files and name them as temp and parameterize name of the component in these files for automating component creation.

temp.js:

import angular from 'angular';
import uiRouter from 'angular-ui-router';
import <%= name %>Component from './<%= name %>.component';

let <%= name %>Module = angular.module('<%= name %>', [
  uiRouter
])

.directive('<%= name %>', <%= name %>Component);

export default <%= name %>Module;

temp.component.js:

import template from './<%= name %>.html';
import controller from './<%= name %>.controller';
import './<%= name %>.styl';

let <%= name %>Component = function () {
  return {
    restrict: 'E',
    scope: {},
    template,
    controller,
    controllerAs: 'vm',
    bindToController: true
  };
};

export default <%= name %>Component;

temp.controller.js:

class <%= upCaseName %>Controller {
  constructor() {
    this.name = '<%= name %>';
  }
}

export default <%= upCaseName %>Controller;

temp.styl:

.<%= name %>
  color red

temp.html:

<div>
  <h1></h1>
</div>

temp.spec.js:

import <%= upCaseName %>Module from './<%= name %>'
import <%= upCaseName %>Controller from './<%= name %>.controller';
import <%= upCaseName %>Component from './<%= name %>.component';
import <%= upCaseName %>Template from './<%= name %>.html';

describe('<%= upCaseName %>', () => {
  let $rootScope, makeController;

  beforeEach(window.module(<%= upCaseName %>Module.name));
  beforeEach(inject((_$rootScope_) => {
    $rootScope = _$rootScope_;
    makeController = () => {
      return new <%= upCaseName %>Controller();
    };
  }));

  describe('Module', () => {
    // top-level specs: i.e., routes, injection, naming
  });

  describe('Controller', () => {
    // controller specs
    it('has a name property [REMOVE]', () => { // erase if removing this.name from the controller
      let controller = makeController();
      expect(controller).to.have.property('name');
    });
  });

  describe('Template', () => {
    // template specs
    // tip: use regex to ensure correct bindings are used e.g.,
    it('has name in template [REMOVE]', () => {
      expect(<%= upCaseName %>Template).to.match(//g);
    });
  });

  describe('Component', () => {
      // component/directive specs
      let component = <%= upCaseName %>Component();

      it('includes the intended template',() => {
        expect(component.template).to.equal(<%= upCaseName %>Template);
      });

      it('uses `controllerAs` syntax', () => {
        expect(component).to.have.property('controllerAs');
      });

      it('invokes the right controller', () => {
        expect(component.controller).to.equal(<%= upCaseName %>Controller);
      });
  });
});

Command To Cut A Component:

gulp component --name componentName

And you get a brand new component cut out with the given name. Isn’t this as simple as a cookie cutter? This is all it takes to setup this command as a gulp task in gulpfile.js:

gulp.task('component', () => {
  let cap = (val) => {
    return val.charAt(0).toUpperCase() + val.slice(1);
  };
  let name = yargs.argv.name;
  let parentPath = yargs.argv.parent || '';
  let destPath = path.join(resolveToComponents(), parentPath, name);

  return gulp.src(paths.blankTemplates)
    .pipe(template({
      name: name,
      upCaseName: cap(name)
    }))
    .pipe(rename((path) => {
      path.basename = path.basename.replace('temp', name);
    }))
    .pipe(gulp.dest(destPath));
});

We need to have all the temp files listed above inside a directory and point paths.blankTemplates variable to it to seed all these files in this task. It will take name parameter that we will enter in the command after — name and use it to replace name and upCaseName in all these files that we have parameterized between <%= and %>

That’s is it! If you want to try this out yourself, fork and play around with this tiny seed called NG6-starter.

Bake some cookies the way you like. Enjoy.

Cookie Cutter to get base cookie.

Leave a Reply

Your email address will not be published. Required fields are marked *