Environment Specific Configuration in AngularJS Using Grunt

Written by

One aspect of Rails I love is the simplicity of environment specific configuration. Out the box, Rails provides configuration for development and production environments and additions are effortless to implement. Using Angular, I typically create a constants module for the application configuration e.g. api url’s, third party service specific data, etc. All of these can change based on the applications environment with which it is deployed to. This post details an approach I used to configure an AngularJS app constants based on configuration specific to the environment using Grunt tasks.

AngularJS & Grunt

I will use Yeoman as usual to setup the project. Please refer to my previous post on Automating AngularJS With Yeoman, Grunt & Bower to help get started with these tools, be sure to follow through the instructions ensuring Karma is configured for e2e and unit testing.

Plan of action

    • Create an AngularJS app.
    • Create a development config file.
    • Create an Angular constants module.
    • Setup grunt tasks to inject the environment specific data into the Angular app constants.
    • Load the module constants into the app.
    • Configure grunt tasks for app builds based on multiple environments.

Create the app

mkdir env-config-example && cd $_
yo angular
npm install

Setup the env-config generator

I wrote a simple Yeoman generator named env-config that will generate json files which we will name based on the environments we want to configure (by default it will generate a development configuration file).

npm install generator-env-config

Create the development configuration

yo env-config

Create the AngularJS configuration

The env-config generator also has a sub-generator to generate an AngularJS constants module to use with the environment based data.

yo env-config:angular config

Looking at the config.js file for a second you will see it’s basically an AngularJS module that registers a constant service and creates and object of key values. The constant service is a method found on the $provide service but is also exposed on angular Module. It’s within this module that values can be set and used throughout the application by simply injecting the module as required.

Notice the odd syntax for the value @@foo. Also note that this file is outside of the app directory. Both of these will be explained by the grunt-replace (fantastic) plugin.

Setting up grunt-replace

The grunt-replace plugin enables us to link the env-config generated json data, config.js and the angular app together. Using specific patterns the plugin will load configuration data from our environment specific json files, inject them into a copy of config.js and copy the file into our application.

npm install grunt-replace --save-dev

Next load the plugin in the Gruntfile within the module.exports function:

grunt.loadNpmTasks('grunt-replace');

Configure a replace task for the development environment within the initConfig block below the uglify task:

  replace: {
    development: {
      options: {
        patterns: [{
          json: grunt.file.readJSON('./config/environments/development.json')
        }]
      },
      files: [{
        expand: true,
        flatten: true,
        src: ['./config/config.js'],
        dest: '<%= yeoman.app %>/scripts/services/'
      }]
    }

We have declared the development.json file to be used as the pattern to replace content within the config.js file. Then we set the angular app/scripts/services/ directory as the destination for the config.js (within our angular app).

Loading the config with the app

We can now test this by running:

grunt replace:development

You should see the following output:

Running "replace:development" (replace) task
Replace ./config/config.js -> app/scripts/services/config.js

Done, without errors.

…Tidy!

We need to add the file to the script imports in the app, so in index.html add:

<script src="scripts/services/config.js"></script>

And in app.js register the new module plus whilst we are at it, let’s follow the AngularJS recommendation and declare the controller as a module as well:

angular.module('envConfigExampleApp', ['controllers.main', 'services.config'])

Finally let’s use the config value somewhere in our application. Based off the Yeoman generated boilerplate, open main.js and inject the services.config module so it looks like this:

'use strict';

angular.module('controllers.main', [])
  .controller('MainCtrl', ['$scope', 'configuration',
    function ($scope, configuration) {
      $scope.awesomeThings = [
        'HTML5 Boilerplate',
        'AngularJS',
        'Karma',
        configuration.foo
      ];
    }
  ]);

Run grunt server and you should see the value bar added to the list of awesomeThings.

Multiple environments and build tasks

To wrap this all up let’s create multiple development configs:

yo env-config staging
yo env-config production

Update the foo value in each of the generated files e.g. bar-staging and bar-production. Next add the staging and production replace tasks below development:

staging: {
    options: {
      patterns: [{
        json: grunt.file.readJSON('./config/environments/staging.json')
      }]
    },
    files: [{
      expand: true,
      flatten: true,
      src: ['./config/config.js'],
      dest: '<%= yeoman.app %>/scripts/services/'
    }]
  },
  production: {
    options: {
      patterns: [{
        json: grunt.file.readJSON('./config/environments/production.json')
      }]
    },
    files: [{
      expand: true,
      flatten: true,
      src: ['./config/config.js'],
      dest: '<%= yeoman.app %>/scripts/services/'
    }]
  }

Register new staging and production tasks above the default task (at the bottom of the Gruntfile):

grunt.registerTask('staging', [
    'replace:staging'
    // Add further deploy related tasks here
  ]);

  grunt.registerTask('production', [
    'replace:production'
    // Add further deploy related tasks here
  ]);

Update the server task to ensure the configuration defaults to development on running the server:

grunt.registerTask('server', function (target) {
    if (target === 'dist') {
      return grunt.task.run(['build', 'open', 'connect:dist:keepalive']);
    }

    grunt.task.run([
      'clean:server',
      'concurrent:server',
      'autoprefixer',
      'connect:livereload',
      'replace:development',
      'open',
      'watch'
    ]);
  });

Next, stop the server and run:

grunt staging

Open app/scripts/services/config.js and confirm that the value of foo is bar-staging. Then run:

grunt production

Open app/scripts/services/config.js and confirm that the value of foo is bar-production.

Restart the server and the app should be working as expected using development configuration.

Example Source Files

angularjs-env-config-example

Comments