@flipflop/core

Just Another IOC Library. This is the core flipflop library.

Stats

StarsIssuesVersionUpdatedCreatedSize
@flipflop/core
0.0.95 years ago5 years agoMinified + gzip package size for @flipflop/core in KB

Readme

flipflop/core

Just Another IOC Library

flipflops

flipflop core is flipflop's dependency injection library. Use it in Node.js or in the browser, it doesn't matter.

Build Status

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);
  });

If you find any bugs or have a feature request, please open an issue on github!

The npm package download data comes from npm's download counts api and package details come from npms.io.