JoshManders

Thoughts, Stories & Ideas

Autoloading Angular With Webpack

↳ November, 3rd, 2015
3 minute read

In my quest to simplify my setup, I have found Webpack and boy is it glorious.

Being a mostly backend developer who is migrating more into the frontend, one of the things I didn't like the most was that I had to manually include the files I wanted. There is no autoloading in frontend. That sucks.

If you've ever used Browserify you are already on the right path with Webpack. You can require() your modules of code and it will bundle it up into a bundle.js file that you can drop on your page and just be done with it.

Webpack takes this a step further and exposes loaders that can do an amazing number of things from loading CSS to Images directly in your JavaScript. I won't go too far into it, but if you don't know already, look into Webpack.

Let me preface this next section that I do know this isn't the "angular way" because of it's move to components, but from my background I found it very helpful to rapidly build my apps. So without further ado.

autoload.js

const req = require.context('./', true, /\.js$/)

const camelize = (str) => {
  return str.replace(/-([a-z])/g, l => l[1].toUpperCase())
}

const titleize = (str) => {
  const camel = camelize(str)
  return camel.charAt(0).toUpperCase() + camel.slice(1)
}

export const mappings = {
  services: {
    fn: 'service',
    transform: (name) => titleize(name)
  },
  controllers: {
    fn: 'controller',
    transform: (name) => titleize(name + 'Controller')
  },
  directives: {
    fn: 'directive',
    transform: (name) => camelize(name)
  },
  filters: {
    fn: 'filter',
    transform: (name) => name
  },
  factories: {
    fn: 'factory',
    transform: (name) => name
  },
  providers: {
    fn: 'provider',
    transform: (name) => titleize(name)
  }
}

export const types = Object.keys(mappings)

export const inits = ['run', 'config']

export function routing ($router) {
  const routes = {}
  req.keys().forEach((file) => {
    if (file.startsWith('./routes/')) {
      const state = file.slice(9, -3).replace('/', '.').replace('.index', '')
      const route = req(file)
      routes[state] = route
      $router.state(state, route)
    }
  })
  return routes
}

export default (app) => {
  req.keys().forEach((file) => {
    const [type, name] = file.slice(2, -3).split('/')
    if ((typeof name !== 'undefined') && (types.indexOf(type) !== -1)) {
      const src = req(file)
      const mapping = mappings[type]
      app[mapping.fn](mapping.transform(name), src)
    } else if (typeof name === 'undefined' && inits.indexOf(type) !== -1) {
      const src = req(file)
      app[type](src)
    }
  })
}

This file is the meat and potatoes. Save this into your project's root directory and it will do it's thing.

So what this does is it uses Webpack's require.context() to autoload files in the directory. It runs through and autoloads services, controllers, directives, filters, factories and providers, maps config.js to app.config() and run.js to app.run() if found. If you use Angular UI Router it will also autoload and map routes to states.

app.js

import angular from 'angular'
import uiRouter from 'angular-ui-router'
import autoload from './autoload'

const app = angular.module('app', [uiRouter])

autoload(app)

angular.bootstrap(document.documentElement, ['app'])

Here we're autoloading all our stuff for our app in the app.js bootstrap file.

Now all you have to do is create ./providers/name.js and return a class and it will get mapped to app.provider('Name', require('./providers/name.js') and same goes for anything else.

All controllers are mapped as {filename}Controller, directives are camelCased, and services and providers are TitleCased.

If you are using Angular UI Router you can create a ./routes folder, and each filename will map to a state, and just export an object that will be passed to $stateProvider.state().

config.js

import { routing } from './autoload'

export default ($urlRouterProvider, $locationProvider) => {
  routing($stateProvider)

  $urlRouterProvider.otherwise('/')
}

routes/hello.js

export default {
  url: '/hello',
  template: require('templates/hello.html'),
  controller: 'HelloController'
}

Now if you $state.go('hello') you will get that state.

It goes a step further and supports nested states also.

So ./routes/hello/index.js gets mapped to hello state, and ./routes/hello/name.js gets mapped to `hello.name state.

I hope this was helpful to you, as much as it was to me.