My current single page applications have most of the logic in the browser using AngularJs and limited amount of data injected dynamically via server-side templates. Often there are multiple constants that might be injected by the server into the page: environment, api urls, user object, etc. Let me show how to properly inject these constants into Angular modules without leaking global variables all over the place.
I am using Jade server-side templates with Express server. For clarity I will show single page with all JavaScript embedded inside. In practice, only the constants are embedded, and the rest of the JavaScript is included via external js files.
Bad: global constants object
I will start with all constants rolled into single global object:
1 | doctype html |
Here the express server route with actual values to be injected:
1 | app.get('/example', function (req, res) { |
The page renders when I browse to http://127.0.0.1:3001/example
AngularJs Constants Example
App's API url /debug/api/
The page's source when inspected:
1 | <script> |
Everything is working as normal, but this design suffers from several flaws:
- After a while object
Constants
becomes a dumping ground for all sorts of values, effectively becoming a singleton for the entire application. A red flag is when you see code like this:Constants = Constants || {};
This means someone is about to extend / override something. - It is unclear that module
myApp
depends on the global Constants object. - It is harder to unit test code that depends on global singletons.
Pass 1: inject specific constants via a module
Lets introduce a new module with the sole purpose of being a storage of constants specific to myApp
div(ng-controller='myAppCtrl')
h2 AngularJs Constants Example
p App's API url <strong>{{apiUrl}}</strong>
script.
angular.module('myApp', ['myApp.Constants'])
.controller('myAppCtrl', function ($scope, API_URL) {
$scope.apiUrl = API_URL;
});
script.
angular.module('myApp.Constants', [])
.constant('API_URL', '#{apiUrl}')
.constant('ENVIRONMENT', '#{runEnvironment}');
Again, normally myApp would be in a separate external static javascript file, while myApp.Constants would be inlined javascript in the template.
This is much cleaner design:
- It is clear which constants are used by myApp
- There are no global or leaking variables
- The constants are read-only: if I change something inside the controller', the values in myApp.Constants remain unchanged (more on this later)
- During unit testing we can inject a mock myApp.Constants module, making the testing independent of the global state.
I find this design a good first step, and the only real down side is the number of arguments to the controller function: it is linear to the number of constants. I prefer to keep number of function arguments below 3.
Pass 2: inject single object
Lets inject all our constants into single object
script.
angular.module('myApp', ['myApp.Constants'])
.controller('myAppCtrl', function ($scope, Constants) {
$scope.apiUrl = Constants.apiUrl;
});
script.
angular.module('myApp.Constants', [])
.constant('Constants', {
apiUrl: '#{apiUrl}',
environment: '#{runEnvironment}'
});
I prefer this pattern:
- Keeps the controller function signature clean
- I don't need to use capital letters to signify constants. Writing
Constants.apiUrl
makes it clear enough.
Its only flaw right now is that if passing an object in JavaScript
mean passing by reference. Any object in the chain can modify its local Constants
object changing the shared instance. For example:
script.
angular.module('badApp', ['myApp.Constants'])
.run(function (Constants) {
Constants.apiUrl = 'nowhere';
});
angular.module('myApp', ['badApp', 'myApp.Constants'])
.controller('myAppCtrl', function ($scope, Constants) {
$scope.apiUrl = Constants.apiUrl;
});
script.
angular.module('myApp.Constants', [])
.constant('Constants', {
apiUrl: '#{apiUrl}',
environment: '#{runEnvironment}'
});
Because badApp module runs before myApp, it has a chance to change
the apiUrl
to nowhere
. Luckily, it is easy to solve in modern browsers (> IE8).
Pass 3: inject single constant object
We can inject a frozen object using ES5 Object.freeze method.
script.
angular.module('badApp', ['myApp.Constants'])
.run(function (Constants) {
Constants.apiUrl = 'nowhere';
});
angular.module('myApp', ['badApp', 'myApp.Constants'])
.controller('myAppCtrl', function ($scope, Constants) {
$scope.apiUrl = Constants.apiUrl;
});
script.
angular.module('myApp.Constants', [])
.constant('Constants', Object.freeze({
apiUrl: '#{apiUrl}',
environment: '#{runEnvironment}'
}));
What happens when badApp tries to change the property of the frozen object?
It depends if there is use strict;
statement somewhere in the scope. If not,
the statement Constants.apiUrl = 'nowhere';
fails silently, but the execution
continues, the Constants object is unchanged. That is why I advocate
use strict;
for all browser code: trying to change the frozen object
will generate an exception:
script.
'use strict';
angular.module('badApp', ['myApp.Constants'])
.run(function (Constants) {
Constants.apiUrl = 'nowhere';
});
// generates
Uncaught TypeError: Cannot assign to read only property 'apiUrl' of #<Object>
Update
We have changed our approach to more flexible and intuitive configuration using custom provider, see this blog post.