@flipflop/core
v0.0.9
Published
Just Another IOC Library. This is the core flipflop library.
Downloads
8
Maintainers
Readme
flipflop/core
Just Another IOC Library
flipflop core is flipflop's dependency injection library. Use it in Node.js or in the browser, it doesn't matter.
Install
npm install --save @flipflop/core
Usage
Use flipflop core to register and request modules.
let ff = new (require('@flipflop/core'))();
ff.module({
name: 'my-awesome-module',
value: "my-awesome-module's value"
});
flipflop core loads your modules and takes care of supplying your (a)sync dependencies when they're ready.
List your dependencies and they'll be provided to you.
ff.module({
name: 'my-awesome-module',
// use '@' in front of you dependencies to let flipflop know that you want a
// module instead of a collection
dependencies: ['@my-other-module'],
provider: (otherModule)=>{
console.log(otherModule); // -> 'other module value!'
}
});
ff.module({
name: 'my-other-module',
value: 'other module value!'
});
Make your module's value an A+ compliant thenable (a promise) if your module has to do some async stuff
ff.module({
dependencies: ['@my-awesome-module'],
provider: (awesomeModule)=>{
console.log(awesomeModule);
// after 4 seconds we get 'This is otherModule's value: my async value!'
}
});
ff.module({
name: 'my-awesome-module',
dependencies: ['@my-other-module'],
provider: (otherModule)=>{
return new Promise((resolve, reject)=>{
setTimeout(()=>{
resolve(`This is otherModule's value: ${otherModule}`);
}, 2000);
});
}
});
ff.module({
name: 'my-other-module',
value: new Promise((resolve, reject)=>{
setTimeout(()=>{
resolve('my async value');
}, 2000);
});
});
Why?
There are plenty of great IOC libraries but they don't handle dependencies that aren't ready right away. It's not too difficult to build in your own event emitter or promise based logic but you shouldn't have to write that stuff.
Features
Asyncronous Dependencies
As long as your module provides an A+ compliant thenable then any modules that depend on it won't load until it resolves. This is really useful for things like database connections or remote configurations.
In the browser
Making an ajax call
ff.module({
dependencies: ['@config'],
provider: (config)=>{
useConfigToBootstrapSidebar(config);
}
});
ff.module({
name: 'config',
value: new Promise((resolve, reject)=>{
console.log('Fetching config!');
function reqListener () {
resolve(this.responseText);
}
var oReq = new XMLHttpRequest();
oReq.addEventListener("load", reqListener);
oReq.upload.addEventListener("error", reject);
oReq.open('GET', 'http://config.com/my-config');
oReq.send();
});
});
On the server
Creating a mongodb conneciton
ff.module({
dependencies: ['@mongo'],
provider: (mongo)=>{
mongo
.collection('documents')
.insert({
key: 'value'
}, (err, result)=>{
console.log(err);
console.log(result);
});
}
});
ff.module({
name: 'mongo',
provider: ()=>{
let MongoClient = require('mongodb').MongoClient;
let url = 'mongodb://localhost:27017/myproject';
console.log('Connecting to Mongo!');
MongoClient.connect(url, function(err, db) {
if(err){
return reject(err);
}
return resolve(db);
});
}
});
Providing anything but a promise lets flipflop core know that your module is ready right away.
Collections
With flipflop core you register and depend on collections. This is useful when you want to create multiple modules that share something in common
ff.module({
name: 'express',
// use '#' in front of you dependencies to let flipflop know that you want a
// collection instead of a module
dependencies: ['#controllers'],
provider: (controllers)=>{
let app = require('express')();
console.log(controllers.names); // -> { controller1: Function }
console.log(controllers.items); // -> [ Function, Function ]
controllers.items.forEach(controller=>{
app.use(controller);
});
}
});
ff.module({
name: 'controller1',
memberOf: ['controllers'],
value: (req, res, next)=>{
res.send('controller1 with a name');
}
});
ff.module({
memberOf: ['controllers'],
value: (req, res, next)=>{
res.send('controller2 with no name');
}
});
API
new FlipFlop(options) {Object} | FlipFlop
const FlipFlop = require('@flipflop/core');
const ff = new FlipFlop();
options.ignoreEmptyCollections | boolean
If a module depends on a collection that is empty then flipflop will throw an exceptions unless this is true.
options.moduleFallback | Function
flipflop can be configured to use a custom module loader if neither a value nor provider is given when defining a module. This can be useful when you want to lazy load modules in the browser or use a native module loader like in Node.js.
Lazy loading in the browser using jsonp
const FlipFlop = require('@flipflop/core');
const ff = new FlipFlop({
// Everytime a module is requested that doesn't exist we can fetch it from
// the server using jsonp.
moduleFallback: (()=>{
let count = 0;
return (module)=>{
return new Promise((resolve, reject)=>{
let callback = `jsonpCallback${count++}`;
window[callback] = (data)=>{
resolve(data);
}
let script = document.createElement('script');
script.type = 'text/javascript';
script.src = `http://myjsonpservice?callback=${callback}`;
document.getElementsByTagName('head')[0].appendChild(script);
});
}
})()
});
Loading native modules in Node
const FlipFlop = require('@flipflop/core');
const ff = new FlipFlop({
moduleFallback: (module)=>{
return require(module); // use Node's module loader.
}
});
new FlipFlop().module(options) {Object} | FlipFlop
const FlipFlop = require('@flipflop/core');
const ff = new FlipFlop();
ff
.module({
name: 'my-module',
dependencies: ['@mongo', '#controllers'],
value: 123,
provider: (mongo, controllers)=>{
console.log(mongo, controllers);
},
memberOf: ['important-modules'],
meta: {
random: 'data'
}
})
.module({
dependencies: ['@mongo', '#controllers', '#important-modules'],
provider: (mongo, controllers)=>{
console.log(mongo, controllers, importantModules);
}
});
options.name {string} (optional)
The name of the module. This is the name that you'll use in your dependency list.
ff
.module({
name: 'my-module',
value: 123
})
.module({
dependencies: ['@my-module'],
provider: (myModule)=>{
console.log(myModule);
}
});
options.dependencies {string[]} (optional)
Define the (a)sync dependencies that your module needs before it can load.
ff
.module({
name: 'my-module',
memberOf: ['my-collection'],
value: 123
})
.module({
dependencies: ['@my-module', '#my-collection'],
provider: (myModule, myCollection)=>{
console.log(myModule, myCollection);
}
});
Prefixing your dependencies with @
means you want a module.
ff
.module({
name: 'my-module',
value: 123
})
.module({
dependencies: ['@my-module'],
provider: (myModule)=>{
console.log(myModule);
}
});
Prefixing your dependencies with #
means you want a collection.
ff
.module({
value: 123,
memberOf: ['my-collection']
})
.module({
dependencies: ['#my-collection'],
provider: (myCollection)=>{
console.log(myCollection);
}
});
options.value {Any} (optional)
Define a value for your module.
ff
.module({
name: 'my-module',
value: 123
})
.module({
dependencies: ['@my-module'],
provider: (myModule)=>{
console.log(myModule); // 123
}
});
options.provider {Function} (optional)
Supply a provider function for your module. When your module's dependencies are ready this function is called with those dependencies as arguments. Your module's value will be the return value of your provider function.
If both a value and provider are given the provider will take precedence for the module's value.
ff
.module({
name: 'my-module',
provider: ()=>{
return 123;
}
})
.module({
dependencies: ['@my-module'],
provider: (myModule)=>{
console.log(myModule); // 123
}
});
Modules' values will be passed as arguments to your provider function but
collections are a little different. Collections passed to your function will be
an object with two properties, items
and names
.
The items key is an array that will contain all of the values of the collection's modules. The names key will be an object containing the names of modules in the collection which have names defined. These module names will map to the modules' values.
ff
.module({
name: 'my-module',
memberOf: ['my-collection'],
value: 123
})
.module({
memberOf: ['my-collection'],
value: 456
})
.module({
dependencies: ['@my-collection'],
provider: (myCollection)=>{
console.log(myModule); //->
// {
// items: [123, 456],
// names: { 'my-module': 123 }
// }
}
});
options.memberOf {string[]} (optional)
Tell flipflop which collections your module is a member of.
ff
.module({
value: 123,
memberOf: ['my-collection']
})
.module({
dependencies: ['#my-collection'],
provider: (myCollection)=>{
console.log(myCollection.items[0]); // 123
}
});
new FlipFlop().mock(modules) | FlipFlop
A short hand way of defining modules for testing. Modules defined using the mock function will take preference over modules defined in other ways.
Mocking modules
Mocking modules is pretty straight forward. Just prefix your mock object key with
@
and a module will be created using the name of the key after @
and the
corresponding value
ff
.module({
name: 'my-module',
value: 123
})
.module({
dependencies: ['@my-module'],
provider: (myModule)=>{
console.log(myModule); // 456
}
})
.mock({
'@my-module': 456
});
Mocking Collections
There are two ways to mock a collection, by defining an array with anonymous
module values or defining an object with named
and anonymous
value.
Using an array
ff
.module({
name: 'my-module',
value: 123
})
.module({
dependencies: ['@my-module'],
memberOf: ['my-collection'],
provider: (myModule)=>{
console.log(myModule); // { items: [1,2,3], names: {}}
}
})
.mock({
'@my-module': 456,
'#my-collection': [1,2,3]
});
Using an object
Keep in mind that when using the object syntax the anonymous modules will be added to the collection first
ff
.module({
name: 'my-module',
value: 123
})
.module({
dependencies: ['@my-module'],
memberOf: ['my-collection'],
provider: (myModule)=>{
console.log(myModule);
{
items: [1,2,3,'b'],
names: {
a: 'b'
}
}
}
})
.mock({
'@my-module': 456,
'#my-collection': {
named: {
a: 'b'
},
anonymous: [1,2,3]
}
});
new FlipFlop().load() | Promise
After the dependency tree has been built, start loading all of the modules and collections. Provider functions won't be called until this stage.
ff
.module({
name: 'my-module1'
value: 123
})
.module({
name: 'my-module2'
value: 123
})
.load()
.then(()=>{
console.log('All modules loaded successfully!');
})
.catch(err=>{
console.log(err.stack || err);
});
new FlipFlop().getModules() | Module[]
This method is for debugging modules. You'll get a list of all of the modules that
have been registered with flipflop. The modules in the array returned by getModules()
can be inspected to reveal all of the meta data that flipflop uses to load them.
new FlipFlop().getCollections() | Collection[]
This is equivalent to getModules()
above but for collections
new FlipFlop().merge(new FlipFlop()) | FlipFlop
Merge modules and collections from one flipflop instance into another.
The FlipFlop instance that .merge()
is called on will have any named modules
or collections overridden that collide with the target FlipFlop instance.
let flipflop1 = new FlipFlop();
let flipflop2 = new FlipFlop();
flipflop1.module({
name: 'a',
value: 1
});
flipflop2.module({
name: 'b',
value: 2
});
flipflop1.module({
dependencies: ['@a', '@b'],
provider: (a, b)=>{
console.log(a); // 1
console.log(b); // 2
}
});
flipflop1.merge(flipflop2);
flipflop1.load()
.then(modules=>{
console.log(modules);
})
.catch(err=>{
console.log(err.stack || err);
});