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 |
|
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 |
|
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 | angular.module('MyApp.Constants', []) |
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 |
|
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 |
|
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 |
|
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 | angular.module('AppConfig', []) |
Any module that needs configuration can inject AppConfig
(which calls the special $get
method)
1 | angular.module('App', ['AppConfig']) // AppConfig is module name |
During testing we can easily swap values in the config module
1 | beforeEach(function () { |
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 | angular.module('AppConfig', []) |
Any module injecting AppConfig
should expect a promise
1 | angular.module('App', ['AppConfig']) // AppConfig is module name |
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 | angular.module('AppConfig', []) |
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 | (function setConfigAndStart() { |
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