Inject valid constants into Angular

Using providers to inject runtime values into modules.

This is an update to "Server-side constants injection into Angular modules" blog post I wrote earlier. After using the server side injection in several apps by several developers, we have decided to change our approach. We still keep the spirit of the original post: server-side constants are injected into the angular app. We have extended our approach with default values, validation and removing separate module just to keep the constants.

Step 1 - the base

You can see an example of the way we used to inject constants in this jsbin example and the in code below

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.min.js"></script>
<meta charset="utf-8">
<title>Validated constants</title>
<script>
// 1 - declare dependency on the constants module, but keep it undefined
angular.module('MyApp', ['MyApp.Constants'])
.run(function ($rootScope, username) {
// 3 - start using constants module
$rootScope.username = username;
});
</script>
</head>
<body ng-app="MyApp">
<script>
// 2 - create constants module in the page and inject values
angular.module('MyApp.Constants', [])
.constant('username', 'World');
</script>
<p>Hello, {{ username }}</p>
</body>
</html>

We declared the application module dependent on a nonexistent module MyApp.Constants (line // 1), and we only defined this module in the HTML template rendered by the server-side engine (line // 2). The constants were then dependency-injected by Angular framework into the actual module (line // 3).

This approach worked ok, but has certain downsides

  • We have decoupled a module from its constants, moving the two modules very far away.
  • When bringing several modules together into a single app, a developer often had to create dummy empty constants modules just to make angular module resolution happy.
  • We could not provide any default values to the constants
  • We could not validate the constants passed to the module until much later, or by making dummy .run call just to validate them.

The next several steps show how our approach has changed.

Step 2 - move constants module closer

We have moved the creation of the constants module closer to the module using it, while leaving constant injection in the HTML template. See jsbin and below

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.min.js"></script>
<meta charset="utf-8">
<title>Validated constants</title>
<script>
angular.module('MyApp.Constants', []);
angular.module('MyApp', ['MyApp.Constants'])
.run(function ($rootScope, username) {
// start using constants module
$rootScope.username = username;
});
</script>
</head>
<body ng-app="MyApp">
<script>
// inject actual values when needed, the module already exists
angular.module('MyApp.Constants').constant('username', 'World');
</script>
<p>Hello, {{ username }}</p>
</body>
</html>

Notice that the we still do not declare or validate constants, or provide default values.

Step 3 - FAILED decorating constant service

We tried decorating module.constant service using the $provide mechanism, but unfortunately it cannot be done due to Angular restrictions.

You can see our attempt here. It is mainly doing this

1
2
3
4
5
6
7
8
9
angular.module('MyApp.Constants', [])
.config(function ($provide) {
$provide.decorator('constant', function ($delegate) {
return function(name, value) {
// validate name/value
return $delegate(name, value);
};
});
});

I even thought about overriding module.constant method directly, as I have done in stop angular overrides but decided against introducing any 3rd party code.

Step 4 - using custom provider

Finally we determined a better approach: defining a custom provider that would return a configuration object. The configuration object can keep defaults and perform validation, and can be injected into controller and other angular functions. Based on this example we have written a simple object that just supplied the defaults, see jsbin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js"></script>
<meta charset="utf-8">
<title>Validated constants</title>
<script>
angular.module('MyApp', [])
.provider('MyAppConstants', function () {
// default values
var values = {
username: 'World'
};
return {
$get: function () {
return values;
}
};
})
.run(function ($rootScope, MyAppConstants) {
$rootScope.username = MyAppConstants.username;
});
</script>
</head>
<body ng-app="MyApp">
<p>Hello, {{ username }}</p>
</body>
</html>

This example only shows using the default values.

Step 5 - overriding default values

Let us override default values with values provided by the HTML injection, for example. This jsbin uses the provider to set extend default values with new ones

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js"></script>
<meta charset="utf-8">
<title>Validated constants</title>
<script>
angular.module('MyApp', [])
.provider('MyAppConfiguration', function () {
// default values
var values = {
username: 'World'
};
return {
set: function (constants) { // 1
angular.extend(values, constants);
},
$get: function () { // 2
return values;
}
};
})
.run(function ($rootScope, MyAppConfiguration) { // 3
$rootScope.username = MyAppConfiguration.username;
});
</script>
</head>
<body ng-app="MyApp">
<script>
// override default values
angular.module('MyApp').config(function (MyAppConfigurationProvider) {
MyAppConfigurationProvider.set({
username: 'Angular'
});
});
</script>
<p>Hello, {{ username }}</p>
</body>
</html>

Note: method name set inside the returned object (line // 1) is arbitrary, you can have multiple methods named whatever you like. Only required name is $get that will return the object to be injected into any function via dependency injection (line // 3).

Step 6 - lock down the configuration

Let us add configuration validation using a custom function added to my favorite assert library check-types. See jsbin or the code below

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.js"></script>
<script src="https://cdn.rawgit.com/philbooth/check-types.js/master/src/check-types.js"></script>
<title>Validated constants</title>
<script>
check.verify.myConstants = function (values) { // 1
check.verify.object(values, 'missing constants');
check.verify.unemptyString(values.username, 'need username');
};
angular.module('MyApp', [])
.provider('MyAppConstants', function () {
// default values
var values = {
username: 'World'
};
return {
set: function (constants) {
check.verify.myConstants(constants); // 2
angular.extend(values, constants);
values = Object.freeze(values); // 4
},
$get: function () {
return values;
}
};
})
.run(function ($rootScope, MyAppConstants) {
check.verify.myConstants(MyAppConstants, 'MyApp is configured incorrectly'); // 3
$rootScope.username = MyAppConstants.username;
});
</script>
</head>
<body ng-app="MyApp">
<script>
// override default values
angular.module('MyApp').config(function (MyAppConstantsProvider) {
MyAppConstantsProvider.set({
username: 'Angular'
});
});
</script>
<p>Hello, {{ username }}</p>
</body>
</html>

We use custom assertion function (defined in line // 1) when we set config object (line // 2) and use constants at run time (line // 3). We can be flexible how we merge constants with defaults, but I like freezing the result (line // 4) to prevent accidental configuration errors.

Update 1 - the final approach

We finally settled on the following pattern for injecting settings (not limited to constants)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
angular.module('AppConfig', [])
// config module has provider with same name
.provider('AppConfig', function () {
// initial / default config
var config = {
bar: 'bar'
};
return {
set: function (settings) {
config = settings;
},
$get: function () {
return config;
}
};
});

Any module that needs configuration can inject AppConfig (which calls the special $get method)

1
2
3
4
5
6
angular.module('App', ['AppConfig']) // AppConfig is module name
.service('foo', function (AppConfig) { // AppConfig is provider name
return function foo() {
return AppConfig.bar;
};
});

During testing we can easily swap values in the config module

1
2
3
4
5
6
7
8
9
beforeEach(function () {
angular.mock.module('App');
angular.module('AppConfig').config(function (AppConfigProvider) {
AppConfigProvider.set({
bar: 'test bar value'
});
});
});
// App foo service will return 'test bar value' after injection

We follow the same approach when injecting values from the server template. In the code Jade template engine will inject runtime.value variable.

// index.jade
<script>
  angular.module('AppConfig').config(function (AppConfigProvider) {
    AppConfigProvider.set({
      bar: #{runtime.value}
    });
  });
</script>

Update 2 - async config

Several people have asked if the config could be fetched asynchronously by the AppConfig provider. Sure, although I think this will greatly delay the application's startup. Remember every module that injects the config object will have to wait.

In details: just return a promise from the $get method. You can use $http service to fetch the config, or in the example below I am using $timeout. You can only inject these dependencies in to the $get method itself, not into the provider function (// 1).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
angular.module('AppConfig', [])
// config module has provider with same name
.provider('AppConfig', function () { // 1
// initial / default config
var config = {
bar: 'bar'
};
return {
set: function (settings) {
config = settings;
},
$get: function ($timeout) {
console.log('AppConfig.$get');
return $timeout(function () {
console.log('resolving AppConfig after 2 seconds');
return config;
}, 2000);
}
};
});

Any module injecting AppConfig should expect a promise

1
2
3
4
5
6
angular.module('App', ['AppConfig']) // AppConfig is module name
.run(function (AppConfig) { // AppConfig is provider name
AppConfig.then(function (config) {
console.log('App has async config.bar', config.bar);
});
});

You can see this in action at plnkr.co - just open the DevTools console to see the output. I copied the output with timestamps to better show the delay of 2 seconds.

2015-01-27 11:21:09.608 AppConfig.$get
2015-01-27 11:21:11.612 resolving AppConfig after 2 seconds
2015-01-27 11:21:11.615 App has async config.bar bar

Update 3 - delayed bootstrap

If you really want to keep object syntax without promises, then you can delay the application bootstrapping until the config has been loaded. Let us switch back to simple module with constant configuration object. By default it has hardcoded values. The application uses explicit bootstrap command.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
angular.module('AppConfig', [])
// constant object with default values
.constant('AppConfig', {
bar: 'bar'
});
angular.module('App', ['AppConfig'])
.run(function (AppConfig) {
console.log('App has async config.bar', AppConfig.bar);
});
// explicit application bootstrap
function startApp() {
angular.element(document).ready(function () {
console.log('starting application');
angular.bootstrap(document, ['App']);
});
}
startApp(); to use default values

If we want to fetch the config first, then start the application, we can even avoid using anything but Angular (inspired by this StackOverflow answer).

1
2
3
4
5
6
7
8
9
10
(function setConfigAndStart() {
var $timeout = angular.injector(['ng']).get('$timeout');
// or grab $http service, etc.
$timeout(function () {
console.log('overwriting the entire config module after 2 seconds');
var newConfig = { bar: 'foo' };
angular.module('AppConfig', []).constant('AppConfig', newConfig);
startApp();
}, 2000);
}());

Notice that we explicitly overwrite the entire AppConfig module in order to override the AppConfig constant. The output in the browser console:

2015-01-27 13:07:40.652 overwriting entire config module after 2 seconds
2015-01-27 13:07:40.657 starting application
2015-01-27 13:07:40.659 App has async config.bar foo

You can find this code at plnkr.co

Update 4

I described how to use config provider to configure a module to be distributed as a 3rd party module in Configuring AngularJS 3rd party module