syringejs
v0.6.5
Published
An ultra-lightweight dependency injection framework for JavaScript
Downloads
53
Maintainers
Readme
syringe.js
Syringe is a small JavaScript library that allows you to dynamically assign data contracts to your functions, constructor functions, and methods.
No more worrying about passing information directly, indirectly, or relying on the lexical scope as Syringe can vaccinate your operations ahead of time.
Now, let's roll up our sleeves and begin!
Table of Contents
Installation
Browser
Just download syringe.min.js and add it to your to your environment.
Dependencies
Syringe uses JSON.parse
and also the following ECMAScript 5 / JavaScript 1.6 methods:
Array.filter
Array.map
Array.reduce
Function.bind
Object.keys
Object.create
String.trim
All of the above methods are available natively on modern browsers. If you need to support older browsers, the polyfills for these methods are provided in lib/polyfill.min.js
Compatibility
Syringe has been tested on the following browsers:
- Firefox 2+
- Chrome 11+
- Safari 3+
- Opera 9+
- Internet Explorer 7+
Node
Ensure that you have installed the latest version of node.js and run the following from the command prompt:
npm install syringejs
Bower
Ensure that you have installed the latest version of Bower and run the following from the command prompt:
bower install syringe --save
NuGet
Run the following command in the Package Manager Console:
Install-Package syringe.js
Overview
Syringe works by taking a function and binding it with deep or shallow references to data items located within a data registry. When this pre-bound function executes, the references are reconciled against the registry and the corresponding data items are passed into the function automatically. This technique is known as dependency injection.
Tutorial
In the following tutorial we're going to demonstrate how dependency injection works by using Syringe to build a small set of pre-bound functions that allow us to interact in various ways with a simple datastore.
Defining the Data
Let's start by creating a Syringe object that holds some departmental information for a fictional organization. Syringe.create()
is used to generate a new instance of an empty Syringe object, but can also be initialized with a payload of fresh data if you pass in an object map like this:
var syr = Syringe.create({
depts: {
'sales' : {id: 'A1', name: 'Sales'},
'finance' : {id: 'B2', name: 'Finance'},
'marketing' : {id: 'C3', name: 'Marketing'}
}
});
Now we'll check our new syr
object instance to ensure that it does indeed contain some data:
syr.get('depts.sales.id'); // Returns: "A1"
syr.get('depts.finance.name'); // Returns: "Finance"
The first important thing to note here is that the syr
object's get
method uses a dot-delimited string to retrieve data.
Unlike using dot-notation to fetch items directly from a JavaScript object (e.g., obj.foo.bar
), this method of retrieval will not cause the system to throw an exception if you attempt to access the data of a property that doesn't exist. Instead, if you execute something like syr.get('depts.engineering.id')
you'll get a return value of false
.
Okay, so we've confirmed that we have now got a brand new syr
object that holds some basic data. Let's use Syringe's binding capabilities to create a retrieval function called report
that receives the depts
object data automatically when it executes.
To do this we use the on
method to create a new bound function:
var report = syr.on(['depts'], function (data, key) {
return data[key] || false;
});
Now we can execute our report
function like so:
report('sales'); // Returns: {id: "A1", name: "Sales"}
Notice how we only passed in one argument: the name of the department we're interested in. Arguments passed by the function's caller are known as free arguments. The depts
argument is pre-bound to the function and passed in automatically.
When defining a pre-bound function, data registry item names and argument names (the depts
array item and the data
argument respectively in the above example) do not have to match. However, there must be at least as many pre-bound arguments as registry items.
To make this a little clearer, consider the following example:
var func = syr.on(['foo', 'bar.fly', 'buzz.lightyear'], function (a, b, c, key) {
// ...
});
In the above example the a
, b
, and c
arguments are bound to their corresponding data registry items foo
, bar.fly
, and buzz.lightyear
, with key
being the only free (unbound) argument.
Bound arguments always match the array order of their corresponding data registry items and precede the free arguments (if any) in the function signature.
Storing Bound Functions
Syringe's on
method allows us the create ad hoc pre-bound functions. However the report
function is useful, so let's add it to the Syringe data registry as a utility method in its own right so it can be used throughout our system.
The code for adding pre-bound functions to the registry looks like this:
syr.add('utils.report', function (data, key) {
return data[key] || false;
}, ['depts']);
The first argument specifies where we want our method to reside inside the Syringe data registry. It doesn't matter that the registry does not yet have a utils
property as it is created automatically when we add the utils.report
item.
Note: If a utils
property containing a report
property was already present in the registry, Syringe would throw an error and suggest that we first use the remove
method to unregister the report
property.
The second argument is the function definition, and the third argument is the array of items we want to pull from the Syringe data registry and inject directly into our utils.report
function when it executes.
The third argument is optional. You don't have to store a pre-bound function; you could just store a regular function (however, in this case we want to).
Unlike on
, the add
operation returns the entire Syringe object, so it isn't useful to assign it to a variable. If we want to test our newly added method (or any a method contained within the Syringe registry) we can execute it like this:
syr.exec('report', ['sales']); // Returns: {id: "A1", name: "Sales"}
However, apart from calling newly-defined pre-bound methods directly for testing purposes, you probably won't be using the exec
method very often. The real value of adding pre-bound methods to the Syringe data registry is that they too can be injected into other functions. And that's what we're going to look at next.
Injecting Bound Functions
So far we've created a Syringe object that contains some basic departmental information, and defined a getter-like report
function that grants any executor access to a specific named item within the depts
data object.
Let's enhance report
by improving its data-retrieval capabilities. We'll expand it to accept either a name or an ID as a lookup key in order to locate departmental information.
Because utils.report
already exists, we must use the set
method instead of the add
method to change the function definition:
syr.set('utils.report', function(data, key) {
if (data[key]) {
return data[key];
} else {
var str = Object.keys(data).filter(function(item) {
return data[item].id === key;
});
return str ? data[str] : false;
}
}, ['depts']);
As before, we can use the exec
method test our enhanced utils.report
function:
syr.exec('utils.report', ['sales']); // Returns: {id: "A1", name: "Sales"}
syr.exec('utils.report', ['B2']); // Returns: {id: "B2", name: "Finance"}
We are now going to extend the Syringe registry to include some members of staff:
syr.add({
personnel: {
'smith_r': {id: '001', name: 'Robert Smith', dept: 'A1'},
'jones_t': {id: '002', name: 'Edward Jones', dept: 'B2'},
'coope_a': {id: '003', name: 'Andrea Coope', dept: 'C3'}
}
});
It would be useful to create a function that uses what we've created so far in order to provide us with profile data about a member of staff that also includes information about their department.
Like report
, this function should be able to accept either a name or an ID as a lookup key in order to locate the corresponding profile.
We'll call this new method utils.profile
. When it's executed, the method gets passed the utils.report
function as its first argument:
syr.add('utils.profile', function(fn, key) {
var
personnel = this.copy(['personnel'], fn),
employee = personnel(key);
if (employee && employee.dept) {
return {
id : employee.id,
name: employee.name,
dept: fn(employee.dept)
};
} else {
return false;
}
}, ['utils.report']);
Syringe provides a method called copy
that allows you to duplicate an existing pre-bound function but specify a new injection payload.
We can use this method to create a new internal function called personnel
that uses the same mechanism as utils.report
but is pre-bound to a different data payload.
As before, we can test this new function by executing the following:
syr.exec('utils.profile', 'coope_a'); // Returns:
// {
// "id" : "003",
// "name": "Andrea Coope",
// "dept": {
// "id" : "C3",
// "name": "Marketing"
// }
// }
syr.exec('utils.profile', '002'); // Returns:
// {
// "id" : "002",
// "name": "Edward Jones",
// "dept": {
// "id" : "B2",
// "name": "Finance"
// }
// }
Wrappers
When our utils.profile
method is called, we want some way to indicate that this activity has taken place by logging a message to the console.
One way we can do this is by wrapping the utils.profile
method in another method that itself receives pre-bound arguments upon execution.
First, let's create a boilerplate alert message in our data registry:
syr.add('data.messages.access', 'Profile accessed: {0}');
Next, create a logger function that is pre-bound with the data.messages
object and can receive both the original utils.profile
function and also a lookup key (name or ID) to pass to it:
var log = syr.on(['data.messages'], function (msg, fn, key) {
var profile = fn(key);
console.log(msg.access.replace('{0}', profile.name));
return profile;
});
We can now use Syringe's wrap
method to wrap our log
function around the utils.profile
method. Then we'll use the result to update the original utils.profile
method in the registry:
syr.set('utils.profile', syr.wrap(syr.get('utils.profile'), log));
As before, we can use the exec
method to test our freshly wrapped utils.report
function:
syr.exec('utils.profile', ['coope_a']) // Returns:
// {
// "id" : "003",
// "name": "Andrea Coope",
// "dept": {
// "id" : "C3",
// "name": "Marketing"
// }
// }
// Logs:
// Profile accessed: "Andrea Coope"
Listeners
In much the same way that you might want to be alerted when a stored function executes, you may also want to be notified when the registry itself changes. For this purpose, Syringe provides listeners that can execute when they detect a particular registry event.
Listeners can be created to detect operations that use the get
, set
, add
, remove
, listops
methods (or all of the above). In addition, they can be mapped to specific registry items, or to general items using the *
wildcard. Example:
syr.listen('add:data.personnel.*', function (name, value, data) {
console.log('The following data was added to "%s": %o', value, data);
});
The above binding will fire if any additions are made to the data.personnel
object. So, now if we do this:
syr.add('data.personnel.moore_l', {id: '004', name: 'Leanna Moore', dept: 'D4'});
... the following is logged to the console:
The following data was added to "data.personnel.moore_l": {
dept: 'D4',
id : '004',
name: 'Leanna Moore'
}
Questions
"Does injection work with constructor functions?"
Yes it does, and we can demonstrate this with another simple example. Create a data object:
var syr2 = Syringe.create({
'data': {
'A00': {
'name' : 'Slothrop, Tyrone',
'rank' : 'Lieutenant',
'locale' : 'GB',
'division' : 'ACHTUNG'
},
'A01': {
'name' : 'Mucker-Maffick, Oliver',
'rank' : 'Lieutenant',
'locale' : 'GB',
'division' : 'ACHTUNG'
}
}
});
Create a simple constructor that automatically adds data
to its context and bind the data
object to the constructor:
StaffObj = syr2.on(['data'], function (data, id) {
data = data || {};
if (({}).toString.call(data[id]) === '[object Object]') {
for (var prop in data[id]) {
if (data[id].hasOwnProperty(prop)) {
this[prop] = data[id][prop];
}
}
}
});
... and create a couple of new objects:
var slothrop = new StaffObj('A00'); // Creates:
// {
// "name" : "Slothrop, Tyrone",
// "rank" : "Lieutenant",
// "locale" : "GB",
// "division": "ACHTUNG"
// }
var tantivy = new StaffObj('A01'); // Creates:
// {
// "name" : "Mucker-Maffick, Oliver",
// "rank" : "Lieutenant",
// "locale" : "GB",
// "division": "ACHTUNG"
// }
"Can I see a more complex example?"
Here's a Todos application+ that uses Syringe dependency injection to construct collection and view objects and manage controller operations. You can view the source code for this app in the syringe-todos repo.
+ CSS and images courtesy of the awesome TodoMVC project
"Aren't we just making a curry?"
When you curry a function you typically have some values in your hand before you create a version of the function that has some (or all) of those values partially applied to it. With Syringe, instead of actual values we bind pointers to a registry which is interrogated at execution time when the bound method is invoked.
This is very convenient because you can arbitrarily change the registry values for a parameter so that completely different data gets passed the next time your bound function gets called. To further labor the medical theme, it's as if the flu shot you received last Winter could be remotely updated throughout the year. Only minus the Nobel Prize, obviously.
Currying does take place, just at a different point. Syringe curries your bound function into a factory that examines the passed parameters and applies the corresponding registry values to your function when it is called.
"What's this about a registry?"
The registry is a closured dependency map unique to each Syringe object instance that holds all of the data items you're interested in automatically provisioning to your bound functions on invocation. You can provision objects, arrays, values, functions, strings, numbers, anything really. You can map to their values directly, or by reference.
Note: The free arguments you pass to a bound function don't have to match the signature; this is consistent with ordinary JavaScript functions. However, the bound parameters are expected to exist in the registry when the bound function is invoked.
"Why doesn't Syringe just use the function signature?"
Some JavaScript dependency injection tutorials and libraries out there describe or provide ways of deriving function dependencies by inference - that is, by scraping the contents of the bound function's signature:
var f = function ($dep1, $dep2, freearg1, frearg2) { ... };
Injection.bind(f); // The library uses RegEx to figure out the parameters
// of `f` in order to pull them from the data registry
// and apply them to `f` when the function is executed
There are a number of reasons why Syringe does not work this way, the main one being that parameters often get renamed when run through compression / obfuscation systems such as Google Closure or UglifyJS. This makes any subsequent reconciliation of the parameters against named items impossible.
In addition, unless you namespace the dependencies it is impossible to disambiguate them from the free arguments. Also, dot-notation is not allowed in parameter names so you end up using something goofy like $leve1_level2_level3
to retrieve deep items.
API
This following table describes the methods provided by the Syringe
object:
Method | Parameter(s) | Description
---------|--------------|-------------
create | [map]
| Create a new syringe object. Example: var syr = Syringe.create();
add | name, value [, bindings]
| Register an item with the dependency map, where name
is the dependency name and value
is any valid JavaScript value. Alias: register. Example: syr.add('data', {'name': 'Mike'});
If value
is a function that you want to automatically bind as a Syringe method, set the bindings
property to the array of properties you want to inject. Alias: register. Example: syr.add('data', function (props) {...}, ['props']);
add | map
| Register a map of dependencies, where map
is an object. Alias: register. Example: syr.add({'data': {'name': 'Mike'}});
add | array
| Register an array of map of dependencies, where each map is an object. Useful for asserting order when additions are functions that are side-effectful. Alias: register. Example: syr.add([{'data.name': 'Mike'}, {'data.age': '39'}]);
remove | name
| Remove a named item from the dependency map. Alias: unregister. Example: syr.remove('data');
remove | array
| Remove an array of named items from the dependency map. Alias: unregister. Example: syr.remove(['data', 'foo.bar']);
on | bindings, fn [, ctx]
| Return a bound function that can access the dependency map. An optional ctx
parameter makes the bound function execute in a specific context. Alias: bind. Example: var f = syr.on(['data'], function (data) {...});
If you want to bind the current Syringe object, use the keyword this
instead of a keyname in the bindings array. Example: var f = syr.on(['this'], function (syr) {...});
If you want to bind the entire dependency map, use an asterisk (*
) instead of a keyname in the bindings array. Example: var f = syr.on(['*'], function (map) {...});
If you want to bind a shallow or deep item located outside of the dependency map in the global object, use the prefix global:
before the keyname in the bindings array. Example: var f = syr.on(['global:jQuery'], function ($) {..});
on | name, bindings, fn [, ctx]
| Bind a named function. The name
string can be a character-delimited path; if the path doesn't exist it will be created dynamically as a nested object structure in the global context.An optional ctx
parameter makes the bound function execute in a specific context. Alias: bind. Example: syr.on('f', ['data'], function (data) {...}, this);
get | name
| Returns the named value from dependency map object. Dot-notation is permitted. Passing no argument returns the dependency map object. Example: syr.get('data');
set | name, value [, bindings]
| Directly sets the value of a named key in the dependency map, if it exists. Example: syr.set('data.name', 'Bob');
If value
is a function that you want to automatically bind as a Syringe method, set the bindings
property to the array of properties you want to inject.Example: syr.set('get', function (name) {...}, ['data.name']);
exec | name, args [, ctx]
| Directly execute a method within the dependency map. Provided as a convenience for occasions where binding isn't possible. An optional ctx
parameter executes the method against a specified context. Example: syr.exec('f', ['Mike', '39']);
fetch | array, props
| Retrieve array-defined items asynchronously. Each array item is an object that contains a path
property and a bind
property. The path
property is a string containing the (local) URI of the resource. The bind
property specifies the Syringe key you want to associate with the JSON object retrieved from the resource.Note: This method is only available in the browser.Example: See below
wrap | fn, wrapper [, ctx]
| Wrap a bound method with another method in order to develop middleware. An optional ctx
parameter adds the bound function to a specified context.Example: See below
copy | bindings, fn [, ctx]
| Create a new bound function from an existing one using a new dependency map binding. Example: var f2 = syr.copy(['data2'], f);
mixin | map
| Add mixin methods to the Syringe object prototype. Example: syr.mixin({'f': function () { return this; }});
separator | value
| Change the name separator character used to create, retrieve, and bind objects. The default character is a period (.
). The character must be non-alphanumeric. Example: syr.separator('#');
listen | name, fn
| Binds a listener to a named Syringe method (get
, set
, add
, remove
, listops
, or all
). Shallow or deep path namespacing is also supported. Example: syr.listen('add', function (name, value) {...});
Example: syr.listen('set:name', function (name, value) {...});
Example: syr.listen('remove:data.name', function (name) {...});
listops | name, fn
| A convenience function that allows you to directly perform operations on stored arrays, raising an event on completion. Example: syr.listops('data.names', function (arr) { ... });
Register Items Asynchronously
You can retrieve JSON data items from a remote source and add them to the Syringe registry using the fetch
method:
syr.fetch([{
path: '/syringe/test1',
bind: 'data1'
}, {
path: '/syringe/test2',
bind: 'data2'
}], {
'success': function () {
console.log(this.get('data1'));
console.log(this.get('data2'));
}
});
Additional Examples
The following generic examples show how some of the API methods provided by Syringe might be used to manage function operations.
FizzBuzz (add, bind, exec, remove)
A FizzBuzz is a program that prints the numbers from 1 to 100. But for multiples of three it prints Fizz
instead of the number. For multiples of five it prints Buzz
. For numbers which are multiples of both three and five it prints FizzBuzz
. It's also a drinking game, and a surprisingly tricky one.
Syringe.add('fn', function (syr, fn, lg, n) {
// Examine the passed integer and log the evaluation
n = n || 1, lg((n % 3?'':'Fizz')+(n % 5?'':'Buzz') || n);
// Execute against self with current integer or remove self
return (100 >= ++n ? fn(n) : syr.remove('fn'));
}, ['this', 'fn', 'global:console.log']);
// Execute the function:
Syringe.exec('fn');
The above code shows how a simple functional FizzBuzz can be created with Syringe, and illustrates a number of different qualities. The first is obviously recursion - a function can inject itself into itself when executed.
The bound function also receives its action (in this case a logger from the global:
context) as a passed parameter, which decouples side-effectful operations from the main operation slightly. When the function completes, it removes itself from the dependency map and returns the Syringe object.
Sieve Of Eratosthenes (add, bind, on, copy)
The sieve of Eratosthenes is a simple, ancient algorithm for finding all prime numbers up to any given limit. It does so by iteratively marking as composite (that is, not prime) the multiples of each prime, starting with all multiples of 2.
For this example we're going to implement two slightly different ways of recursively processing the data and see which one is more performant. First we create a processor that sieves the data using reduce
:
Syringe.add('reduce', function (proc, arr, cnt) {
if ((cnt = cnt || 1) > Math.sqrt(arr.length)) {
return 1 === arr[0] && arr.shift(), arr;
}
return proc(arr.reduce(function (prv, cur, idx, lst) {
0 === cur % lst[cnt] && cur !== lst[cnt] || prv.push(cur);
return prv;
}, []), ++cnt);
}, ['reduce']);
Next we create an alternate processor that sieves the data using an incrementing while
loop:
Syringe.add('while', function (proc, arr, cnt) {
var len = 0, newarr = [];
if ((cnt = cnt || 1) > Math.sqrt(arr.length)) {
return 1 === arr[0] && arr.shift(), arr;
}
while (len < arr.length) {
arr[len] % arr[cnt] === 0
&& arr[len] !== arr[cnt] || newarr.push(arr[len])
len++;
}
return proc(newarr, ++cnt);
}, ['while']);
Now we create a sieving function that builds the seed array and passes it to the while
processor:
var sieve_while = Syringe.on(['while'], function (proc, to) {
to = to || 10;
var n = 1, arr = [];
while (to--) arr[to] = n++;
return proc(arr.reverse());
});
We can copy sieve_while
and create an alternate sieving function that uses the reduce
processor:
var sieve_reduce = Syringe.copy(['reduce'], sieve_while);
Finally, let's ask both to return all prime numbers between 1 and 10,000 and see what happens:
console.time('sieve while');
console.log(sieve_while(10000)); // Logs:
// [ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41 ... ]
console.timeEnd('sieve while'); // Logs:
// sieve while: 80ms
console.time('sieve reduce');
console.log(sieve_reduce(10000)); // Logs:
// [ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41 ... ]
console.timeEnd('sieve reduce'); // Logs:
// sieve reduce: 120ms
The while
processor appears to be considerably more performant!
Fibonacci Number (add, on, remove, wrap, get)
The Fibonacci sequence is a numeric list where the first two numbers are 0 and 1 and each subsequent number is the sum of the previous two. The following example describes a simple function that calculates the Fibonacci value of a specific number.
var fib = function () {
var fn = Syringe.on(['store'], function (store, a) {
var res = store[a];
if ('number' !== typeof res) {
res = fn(a - 1) + fn(a - 2);
store[a] = res;
}
return res;
});
return fn;
}();
To improve performance, the storage of each preceding number is held inside the dependency map:
Syringe.add('store', [0, 1]);
In the Sieve Of Eratosthenes example we bookended the bound function with a console timer to determine the overall performance of the operation. In this example we'll use Syringe to wrap our bound function with a timer in order to produce a new function:
fib = Syringe.wrap(fib, function (fn, num, name) {
var start, stop, ret;
start = (new Date()).getTime();
ret = fn();
stop = (new Date()).getTime();
this.remove('log.' + name)
.add('log.' + name, 'The ' + name + ' function took ' + (stop - start) + 'ms');
return ret;
});
Now we execute the function, providing an argument that specifies the number for which we want know the Fibonacci value, and the name of the function for logging purposes:
fib(100, 'fib'); // Returns:
// 55
Syringe.get('log'); // Returns:
// {
// fib: "The fib function took 11ms"
// }
License
Syringe is freely distributable under the terms of the MIT license.