A script loader for AngularJS that perform AMD-like script injection without the need for special syntax.
Say you are like me and love having a Javascript file for every module. And you have a lot of scripts with specific tasks; directives, factories, utility functions. You name it.
One solution that we used here at Coders at Work was RequireJS, but I grew tired of it; it is becoming too heavy and doesn't integrate that well with AngularJS.
So I decided to go ahead and build this little script. Here's how it works:
File
index.html
:<html> <head> <script type="text/javascript" src="path/to/angular.js"></script> <script type="text/javascript" src="path/to/angularjs-loader.js" app="test_app"></script> </head> <body> <div ng-controller="TestCtrl as test"> {{test.value}} </div> </body> </html>
File
test_app.js
:angular.module('test_app', ['test_ctrl']);
File
test_ctrl.js
:angular.module('test_ctrl', []).controller('TestCtrl', function() { this.value = 'Hello World!'; });
Angularjs-loader automatically sees that angular.module() has a dependency to 'test_ctrl', and loads the script automatically when it's ready. When all angular.module()
calls are done, it automatically bootstrap angular.
As long as you stick to AngularJS, there's no need for shims, paths or configurations. A blocking module makes it simple to include other libraries as well.
The script tag for loading this script needs to be inserted after AngularJS, but before the document is ready.
It takes 3 arguments:
app
. The main module to load. This is the name of the first script to load.root
. A root to prepend to every path (except absolute paths). By default, no path is prepended.timeout
. A timeout for loading scripts, in milliseconds. If a script hasn't fired anonload
event, an error will be thrown. By default, 30 seconds.onbootstrap
. Code to evaluate when the Angular is finished loading.noinit
. Any non-empty string to prevent the AngularJS-Loader from automatically starting the initialization process. See Tests.
The loader overrides angular.module()
and does two things:
-
When creating a new module, it takes the list of module dependencies and load the Javascript associated with it (using <script> tags).
-
It then uses an atomic lock for bootstrapping and will unlock once the module is created. When all locks are unlocked, it bootstrap AngularJS by calling
angular.bootstrap(document)
.
As you use angular.module()
to create a module, it will load its dependencies recursively. Using angular.module()
to only access the module itself is okay and has no side effect.
When you need to load custom scripts you should use the new function angular.loader()
. It takes 2 arguments and returns a simplified promise object.
The first argument is either a filename or an array of filename to load. Remember that these will be transformed using the path transform.
The second argument is an object of named options:
sequence
. Set totrue
to have files loaded in a sequence instead of all at once. Loader will wait for each file to be loaded (onload
event) before starting the next one.checker
. A function, a string representing a variable name or an hash map of ScriptName to function or string.
Thefunction
is called at regular intervals after the script is loaded and only if it returnstrue
will the script considered fully loaded (releasing the lock on bootstrapping Angular).
If astring
is specified, it checks for that variable name inwindow
and will unlock the bootstrapping when it exists.
If ahash map
is specified, it will take each scripts and check them using the either thefunction
or thestring
value in the same manner as above.
Omitting this option performs no checks at all and unlock the bootstrap when theonload
event is fired.timeout
. A timeout function for the checker. An error will be thrown when this timeout is reached and checker does not returntrue
. By default, it is equivalent to the timeout specified in the script tag, or 30 seconds.
The object returned can be used as a simpler Angular deferred promise object, but there's no finally
method on it, only catch
and then
. The promise will be fulfilled after the checker has returned true
for all the scripts.
You can lock bootstrapping of AngularJS with angular.loader.lock()
(and unlock it with angular.loader.unlock()
). The functions take a name as parameter. Locking the same name twice, unlocking an unlocked or unknown name are all errors. The only limit on locks are that you cannot lock/unlock once the bootstrap happened. Anything after bootstrapping angular is considered an error.
Locking and unlocking before the end of loading all the modules and their dependencies will simply result in waiting for the dependencies to finish loading.
Configuration is set by calling angular.loader.config()
. The function takes an object as argument and update the internal configuration.
The list of configuration options is as follow:
path
. A map of name to full path (ornull
). If a module name is present in this map, the loader will use this path instead of the name to load. If the full path in the map isnull
, the script will returnpathTransform
. A list of functions that take a path and return a path ornull
. This is to allow transforming a module or script name to its file path. The transform chain will not be executed if the path is part of thepath
option above or is a full URI.checker
. A checker map for modules. See loading custom scripts above. This is to simplify the calls toangular.loader()
.error
. An error handler. If specified, NO error will be thrown or outputted, they will all be passed to the handler.
The process of getting a script path from a module name is as follow:
var name; // Original name for the module.
var path = name in config.path ? config.path[name] : name; // Path to load.
if (path === null) return;
if (isUri(path)) return path;
for (var i = 0; i < config.transformPath.length; i++) {
old_path = path;
path = config.transformPath[i](path, name);
if (path === null) {
path = old_path;
break;
}
}
if (isUri(path)) return path;
if (path[0] != '/') path = '/' + path;
if (!/\.js$/.test(path)) path += '.js';
return path;
Some Karma tests are included to cover the most basis cases. Tests are added regularly for better coverage.
To run the tests, simply run karma run
in the angularjs-loader folder.
For version 1.0:
- Minification pre-step. Having a script that takes all
angular.module()
calls and merge the scripts into a single file (or multiple).
Future:
- Nothing really.
If you think of something, create an issue!
None for now. I want to add some cookbook for this as it can be quite powerful but hard to grasp. I figure that for now the simple case will be enough for most uses.
A mixture of reasons, really. RequireJS has a couple of devs behind it and more bandwidth. Also, I wanted to keep the size really low (AJS-Loader is currently under 3650 bytes minified and uncompressed, while RequireJS sits at 15kb) and performance really high (benchmarks coming later).
Some stuff had to be sacrificed. Mainly, there's no plugin for AJS-Loader, you can't load things that are not scripts (but can load stuff outside of AngularJS modules, of course), it is mainly meant as a developer only script (meaning that production code should use the concat
script I'm working on), and while path transforms and checkers can be quite powerful, they're also more complex than other solutions.
None for now.