What are these components?

The component approach in angular is simple. Basically, removing all the standalone controllers that were bound to views and treating directives as first class citizens. Creating mini application pieces that are self defined and can be used to compose our application. Angular 2 takes this approach as does React, and web components.

The benefits of this approach are plenty. From better organization and collaboration, to easier re-usability and simpler reasoning about your code. The component approach is here to stay.

  • Components in angular does not mean web components
  • ES6/ES2015 makes it easy
  • You know directives right? Then you're right there

How and why ES6/ES2015?

ES6 is feature complete and is being implemented into browers feature by feature. There will continue to be a development of more features and proposals yearly, so hence the ES2015.

There are many helpful new features that have been needed in JS for years. Some are entirelly new concepts, some are standard implementations of community projects. One thing is for sure, ES2015 is a step forward and makes development a little bit less difficult and more fun if you were getting bored!

However, all the new candy that is ES2015 is fully implemented in all environments just yet so we must transpile it, like CoffeScript. There are two transpilers that have emerged as the goto's for this task.

I recommend babel here ftw. The compiled JS it spits out is more faithful for the original code and it does not require a runtime. The config seems to be more simpler and the community is leaning towards this. Supports more than just ES2015.

A tranpiler does the work conf converting, but we're lazy and we need to automate things, and ES2015 introduces features, that even when compiled aren't compatible in the browers of right now. For that, we need a build process using one or a combo of build tools out there. There are a few favorable ones when talking about ES2015.

Intro to webpack

We will use Gulp and Webpack as our build system. Gulp will orchestrate Webpack and handle things like copying files and serving our app. Webpack will transpile and bundle our code. Note that Webpack can also serve our app. Webpack treats everything as a module, html, css, js, .jpg. This is the future of web development. Lets take a look at how Webpack works.

We need to create a webpack.config.js file on the root of our app. Webpack looks for this file by default, like Gulp and Gulpfile.js

// webpack.config.js
module.exports = {
  context: __dirname + 'client', // the dir to resolve the entry to
  entry: 'path/to/entry/file.js', // entry module file
  output: {
    filename: 'bundle.js', // name of bundled filed created
    path: 'dist' // path to place the output file
  },
  devtool: 'sourcemap', // how to handle debugging
  module: {
    loaders: [ // like gulp plugins and tasks
      // test is a regex that tests a module's name
      // to see if it matches, if it does, it will run the file
      // throigh the associated loader. Use ! for multiple loaders
      // loaders are used right-to-left
      {test: /\.css$/, loader: 'style!css'}
    ]
  }
};
            

Using dependencies

Webpack allows us to use commonjs in the browser, meaning we can use node modules and npm for our dependencies on the front end. Just like on node, we can use require(), but this is ES2015. Let's look at the new module feature.

import _ from 'lodash';
// compiles to
var _ = require('lodash');
          

Using the import and from keywords, we can access other modules. Above is an example of importing a node module, or commonjs module. Because it is such and not an ES2015 module, we can import it with any variable name we'd like. However, when importing an ES2015 module, the naming of the variable is dependent on how it was exported. Also, like commonjs, if we're going to use a module we created we must prefix the path with ./, otherwise node will look for the module in the node_modules directory. Here are a few import and export patterns.

// importing as any var name
// must be exported as default

// @app.js
import config from './config';

// @config.js
// ...
export default config;

///////////////////////////////////////////////////////////////////

// import and export named modules

// @app.js
import {config} from './config';

// @config.js
export var config = {};
// or at the bottom of the file
export {config};
          

The path to the modules is relative the the current file, just like commonjs because webpack compiles it all down using commonjs remember 💯. Working with many imports and exports in the same module is easy. Very similar to commonjs as well.

// no defaults, all named
// @app.js
import {mod1, mod2, mod3} from './myModule';

// @myModule.js
var mod1, mod2, mod3;

export {mod1, mod2, mod3};

// or

export mod1 = {};
export mod2 = {};
export mod3 = {};
          

Angular already has a module system, so how does it work with the ES2015 module system? There are a few approaches. You can have just one angular module and rely completely on ES2015 modules. This is fine but isn't ideal when testing and limits some flexibility. Another approach, what we'll be doing, is to create a new angular module for every component. We can test the angular modules independently and hax some pretty good flexibilty. However, there cant be much more bolilerplate and repeated code with this approach.

There are other patterns like using default modules with named modules and how to handle that. The above is what you'll be using most of the time. Things to remeber are:

  • You can export an assignment expression
  • The brackets on the import and exports are not object literals

Better variables

var used to be the only way to define variables. There are some gotchas with this though around scoping and hoisting. ES2015 introduces two new ways, let and const. Both have local scoping and don't hoist. const is immutable as well.

let val = 1;
const nums = [1,2,3];

if (true) {
  let val = 'heeey';
}

val === 'heeey' // false. val is still 1. same behavior with const too.

nums.push(22); // this is ok, not changing the value.

nums = []; // throws error, you can't change the value of the const

          

We will use const for everything unless we need to change its value, then we'll go back to let. Going back to var when we don't want the local scoping which actually leads to hard to understand code.

Components of a component

A component is widely used term, but we'll be using it as a reference to a composition of functionality, UI, state, and tests.

  • profile/
    • profile.js
    • profile.directive.js
    • profile.controller.js
    • profile.css/less/sass/stylus
    • profile.html
    • profile.spec.js

component.js is our entry file for the component. It creates the angular module and registers the directive to it. Also, if you needed routing, this is where you would do.

component.controller.js this is the controller file where we create the controller for our component's directive.

component.directive.js this is where we create the directive that will be the meat and potatoes of our component. It also has the responsibility of importing the styles and template.

The other files are self explantory. This setup allows for easy testing and flexibilty. Reusablity is on maximum because we're creating a new angular module each time. You could take one component, copy it into another project / app and it would work the same. You must also remember to register the component's angular module onto some parent angular module.

Because webpack allows us to treat anything as a module and has an extensive plugin community, we can import more than just js files. Like our css/stylus/less/sass files. This is great for modularity. Almost like a web component without the css closure. So we don't need link tags or more script tags.

Getting classy

For our components' controllers, we'll be using the ES2015 class feature. Highly contraversial in this functional day and age. Its sugar on top of the sour prototypes.

class Car {
  // function that will be called when
  // class is invoked with new. Also
  // where you would $inject your dependencies
  constructor(maker) {
    this.maker = maker;
  }
}

var myCar = new Car('honda');

class Suv extends Car {
  constructor(maker) {
    super(maker);
    this.mileage = '2mpg'; // 💢
  }
}

          

Object shortcuts

We can take advantage of method and property shortcuts on object literals and classes.

var enable = true;
var conifg = {
  // not a typo. There is an accessable var with the same
  // name as the property so we can omit the key valu pair
  // and just write the key. Has to be the same name as
  // an accessable variable.
  enable,
  // about the same as doing
  // setup: function setup(){}
  setup() {
    // hey I am a function
  },
  someOtherProp: true
};
          

Things to remember

  • When using method shorcuts, the outside context is kept, like .bind()
  • When using method shortcuts on object literals, you must stull obey the comma rule with objects, you don't with classes

Testing components

This is were creating all those angular modules come in handy. Now we can test individual modules without loading the entire app. We'll be using a combo of tools for testing our application. Because our tests will also be written in ES2015, we will need webpack here as well. Tools we will be using are:

  • karma
  • mocha
  • chai

Like with our src code, webpack will create a bundled file of all our specs with a little more configuration. Becuase our components have their own angular modules, we can test them seperately. This is why we are doing alot of repeat code as far as with 3rd party modules like ui-router.

We have some code that was created outside of angular, like our controllers. We can test these things without angular, which just makes testing a little bit easier. We did a great job at isolating everything and can therefor do deeper and more meanignful assertions much easier than with ES5. Things like running a template through a regex are easy now if you fancy that 👌.

Using services

The term service is loosely used in Angular 1.x. All the below are all methods on angular.module() and they are all "services".

  • .service()
  • .factory()
  • .constant()
  • .value()

.serive() and .factory() are synonymous. The differences are simple. A service is a "newable" function and uses this. Therefore it will always return an object. A factory can return whatever we'd like.

angular.module('app', [])
// an actual service
service('myService', function(){ // is a "newable" function so no arrows please
  this.action = ()=>{};
})
// a "service" but technically a factory
.factory('myFactory', ()=> {
  return 'heeeeey';
});
          

We shouldn't store state in our components for the same reason we wouldn't store state in our controllers before. Angular has services for this. The location of the definition of the service is dependent on what component(s) need it. If there is just one component that needs the service, you can define the service in the component's directory. If there are more than one component, then you need to register it at a common parent.

The big idea about services, especially with components, is to hold our application state and define an api to interact with that state and expose it to a controller or another service.

angular.module('app', [])
.factory('Todos', () => {
  // the actual state, where we keep our todos
  // keep private, only expose with methods
  let todos = [];

  // define some CRUD for this state
  const get = (id) => {
    if (id) {
      return _.find(todos, {id});
    } else {
      return todos;
    }
  };

  const create = (todo) => {
    todos.push(todo);
  }
  // using a revealing module design pattern
  return {get, create};
});
          

Above is an example of creating an inline factory. What we'll do instead is create our services outside of angular and register them later, just like we do with controllers and directives. Because of that, we could easily bypass angular completely and just import the services and use them wherever we wanted. We should stick to registering them with angular though.

Destructuring

A common pattern we use often is plucking properties off an object and creating accessable variables from them. We have a shorcut for that now.

const action = (options) => {
  // typical
  const enable = options.enable;
  const id = options.id;

  // instead do this, is the same as above
  const {enable, id} = options;
}
            

We can't use destructuring in the signature of injectable functions like controllers and directives.

Decorators

There is a proposal out that will add support to decorators. Decorators are basically mixins with a sweeter syntax similar to Python. Make sure your transpiler supports it, and then enable it. Right now, support for decortators are limited to classes and object literals.

class PageController {

  @throttle(300)
  handleScroll() {
    animate();
  }
}         

Mentions

Below are some comparisons of components in angular 1.x and other frameworks / technolgies

Angular 2 use decorators with typescript extensively to create components. There exist and API if you don't want to use typescript.

@Component({selector: 'our-app'})
@View({template: '<div>{{ greeting }}</div>'})
class OurAppComponent {
  gretting: string;

  constructor() {
    this.greeting = 'Hey';
  }
}
          

React is just a component library and takes a different approach. Allowing us to access lifecyle hooks into our components. React uses JSX which is a way to write some XML like html in our JS.

class myComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      greeting: 'Heey'
    }
  }

  render() {
    return (
      <div>{this.state.greeting}</div>
    );
  }
}
          

Polymer is a framework that sits on top of web components. Adds polyfils and creates awesome apis to make things easier.

<dom-module id="my-component">
  <template>
    <div> {{ greeting }} </div>
  </template>

  <script>
    Polymer({
      is: 'my-component',
      properties: {
        greeting: {
          type: String,
          value: 'HeeeEy'
        }
      }
    });
  </script>
</dom-module">
          

Recap

A few things to remember when taking the component approach with angular 1.x

  • pick a file naming convention before you start and stick to it
  • look at JSPM, it's awesome (harder config)
  • the new router will only work with components so get used to them
  • decide if you're going to use named exports or not and stick with with
  • if you forget to isolate your scopes on your components you may have unexpected side effects

There is no standard way to adopt a component architecture in angular 1.x yet and may never be. The approach we learned is a collection on best practices, trial and error, and community collections on what works best. Feel frree to adjust and chagne for what works for you. Just be sure you're still having fun.