Bly - Flux app framework with hapi like interface
Bly is a tiny framework to help you write web apps using a Flux architecture. It's designed to give help you structure and organise your code in anyway that works for your app. Simple enough to work on small prototypes, elegant enough to work on bigger projects too citation needed. It's designed to empower you and then get out of your way.
Stability: experimental. So the claims above might not yet be met, be ready for bugs and be careful using it in production.
Browser support
At a glance
Simple app interface
- Create an app using
new Bly.App()
- Define what stores are available to the rest of the app using
app.stores
(we'll let you decide how to implement them) - Register handlers for actions using
app.action
(again, letting you decide how to implement stores) - Render your app by passing a function to
app.render
(called every time after an action was dispatched) - Start your app using
app.start
(from now on actions can be injected) - Inject an action
app.inject
Plugins for code organisation
- Register plugins using
app.register
- Define plugins with a register function with the signature
plugin, options, next
- Use the plugin interface to access
plugin.action
, plugin.stores
, etc. - Nest plugins in plugins using
plugin.register
- Report results to the
render
function using plugin.results
Quick example
var app = new Bly.App();
var pageStore = yourPageStore();
app.stores('pages', pageStore);
app.action({
name: 'navigate',
handler: function(waitFor, payload) {
pageStore.navigate(payload);
}
});
app.render(function() {
React.renderComponent({
App({
bly: app
});
})
});
app.start();
Background
Making it easier to create great UI's and app experiences for the end user, and keeping it that way through the course of the project. That's the most important thing; if I can't imagine how something would actually benefit the end user I probably won't put it in. This doesn't mean that these benefits can sometimes be quite indirect. For example, if something just greatly simplifies the developing experience on my side, that'll make it easier for me to be creative, try different things, pay attention to details, feel happy about my project. All of which I believe in the end contribute to the quality of the final product.
Especially the keeping it that way is important, as that's where I see a lot of approaches go down the drain. If in any way possible I highly favour approaches that will make my codebase grow linearly relative to the complexity of the app. There are plenty of frameworks / architectures out there that are very clean and easy to start off with, but as soon as you step outside the bounds of it, doing anything becomes very complex. It should be relatively easily to get started, but more importantly, it should be easy to keep going.
Using with React
Bly and React make for a really good couple, it's what Bly was designed with in mind.
The bly-react-mixin module is the glue to their relationship, letting you access stores and dispatch actions from your React components.
The rendering on every dispatched action really comes into it's own when used with immutable stores. Combining state defined with something like immutable-js with a simple shouldComponentUpdate implementation can make for efficient rendering of Bly apps.
Examples
Bly is still pretty experimental and not used in many places yet. The best example is an experimental repo in which the idea and first code of Bly was developed, which is a port of Facebook's chat example. It also features stores written with immutable-js and rendering with [React.
Built anything? Add it here and send a pull request!
API
App interface
var app = new App()
Create a new Bly App, of which generally one should exist. If you find yourself passing the app object around alot, consider using plugins to organise your code instead.
var Bly = require('bly');
var app = new Bly.App();
var ref = app.action(options)
Adds an action handler where:
options
- the action options object.
Returns a reference (string) to which to identify this handler with.
Action options
name
- (required) the name of the action, the identifying string. For example RECEIVE_MESSAGES
. Can already be used before to register other handlers.handler
- (required) a function with the signature function (waitFor, payload)
used to generate the state mutations in stores.
ref
- an optional reference for this particular handler, which other handlers can use to waitFor
this handler. Defaults to be randomly generated.
app.action({
name: 'RECEIVE_MESSAGES',
handler: function(waitFor, payload) {
waitFor('other-handler');
}
});
app.action({
name: 'RECEIVE_MESSAGES',
ref: 'other-handler'
handler: function(waitFor, payload) {
}
});
var refs = app.action(actions)
Same as var ref = app.action(options)
but where actions
is an array of Action options. Returns an array of references to the handlers registered.
app.inject(name, payload)
Inject an action into the system to be dispatched. The dispatching of an action is synchronous and only one action can be dispatched at a time. App has to be started with app.start
before actions can be injected.
name
- (required) the name of the action you want to inject. For example RECEIVE_MESSAGES
.payload
- the payload of the action, can be any value. Defaults to an empty object {}
var app = new Bly.App();
app.action({
name: 'RECEIVE_MESSAGES',
handler: myActionHandler
});
app.start();
app.inject('RECEIVE_MESSAGES');
app.inject(actionObject)
Same as app.inject(name, payload)
but but with the name
and payload
arguments as props of an object.
var app = new Bly.App();
app.action({
name: 'RECEIVE_MESSAGES',
handler: myActionHandler
});
app.start();
app.inject({
name: 'RECEIVE_MESSAGES'
});
app.inject(actionCreator)
Functions, often referred to as 'action creators' in Flux lingo, are a great way to compose and orchestrate the injection of actions, especially when dealing with asynchronous code. When passing one to app.inject, it will be called with an instance of app, allowing the action creators to be decoupled from the app more easily. This article on the evolution of flux frameworks describes the pattern quite well.
actionCreator
- (required) a function with signature function(app)
, the returned value of which will be fed back into app.inject(actionObject)
. If nothing is returned no further action is taken.
var app = new Bly.App();
app.action({
name: 'RECEIVE_MESSAGES',
handler: myActionHandler
});
app.start();
var saveMessage = function(message) {
return function(appInstance) {
message.save(function(err, savedMessage) {
if (err) return;
appInstance.inject({
name: 'RECEIVE_MESSAGES',
payload: [savedMessage]
});
});
};
};
var newMessage = new Message();
app.inject(saveMessage(newMessage));
Injected action lifecycle
Each injected action goes through a pre-defined life cycle constrained by the ideas of a Flux architecture.
onPreDispatch
event emitted.- action dispatched to handlers registered with
app.action
or plugin.action
. - results gathered from any functions registered with
app.results
or plugin.results
. - render functions registered with
app.render
called with the gathered results. onPostDispatch
event emitted with the gathered results.
app.plugins
Object where each key is a plugin name and the value are the exposed properties by that plugin using plugin.expose()
.
app.register(plugins, [options,] callback)
Register one or more plugins.
plugins
- (required) a plugin object or array of plugin objects, either manually constructed or plugin module.options
- optional options for registering, used by Bly to register the plugin and not passed to the plugin. Currently there are no options available, but reserved for future use.callback
- (required) function with signature function(err)
to be called once plugins have registered or failed to do so. Failure to register should be considered an unrecoverable event.
Register plugin object
To register a plugin this object is (or array of objects are) required containing the following:
pluginName
or name
- (required) name of the plugin, which must be unique. If using a module, using the name of the package is a pretty solid way to ensure it is doesn't conflict with others. pluginName
can be used if the plugin object is a function to prevent conflicts with Function.name
.multiple
- a boolean that indicates whether a plugin can be registered more than once. For safety defaults to false
.register
- (required) a function with signature function(plugin, options, next)
that is responsible for registering the plugin
Passing options to plugin's register function
To pass options to the plugin's register function, wrap the plugin object into an object containing:
plugin
- (required) plugin objectoptions
- object of options to be passed to the plugin
app.results(resultFn)
Register a function with signature function(report)
that allow for values to be passed to app.render
functions. Proven to be especially useful for rendering individual sections of apps in plugins, which can then be stitched together during the actual rendering of the app.
resultFn
- function with signature function(report)
where:
report
- function with signature function(key, value)
used to expose results using a where:
key
- (required) string by which made accessible on results
object
value
- value to be exposed on results object
app.render(renderFunction)
Register a function that is to be called with the results of each dispatched actions, which is most useful for rendering your views to reflect the possible state changes made by the stores. If the app was started before registering the render function the render function will be called once straight away.
renderFunction
- (required) function with signature function(results)
where:
results
- object with results generated by functions registered with app.results
app.start()
Start the app. For now this basically means actions can from then on be injected, in order to make sure all plugins and stores had a chance to set themselves up in working order. Would also be the point where in the future we can add more safety regarding the correct configuration of an app, like validating that stores are only listening for actions that the system knows off.
var store = app.stores(storeName)
Retrieve a reference to a store instance
storeName
- name of the store to retrieve the instance for.
var stores = app.stores()
Retrieve a reference to the the global stores object, which contains all stores instance indexed by storeName.
app.stores(storeName, instance)
Register a store instance to be available to the rest of the app.
storeName
- (required) name of the store it can be referenced by.instance
- (required) value which represents your store instance.
app.stores(storeMap [, options])
Set an entire object of store instances at once, indexed by store names.
storeMap
- (required) object of store instances, indexed by store names.options
- object of options containing any of the following:
merge
- whether to merge the store map with the existing register of stores instead of completely replacing it. Defaults to true
.
Plugin interface
Plugins, inspired by Hapi's plugins provide a way to organise your application's business logic, as well as extend apps with general purpose utilities (allthough many of the hooks for the latter still have to be discovered / determined). At the present it mostly enables you to think of your app as composition of various logical units, each covering their own domains, instead of one monolothic app.
A plugin consists of:
name
- (required) the plugin name used as an unique key to identify the plugin. When publishing plugins on npm it's a good idea to use their package name as name of the plugin in order to prevent conflicts.register
- (required) single entry point into the plugin's functionality. This is where a plugin declares what it should be doing.multiple
- a boolean that indicates whether a plugin can be registered more than once. For safety defaults to false
.
Example of a plugin implementing a very simple store.
exports.name = 'messages';
exports.register = function(plugin, options, next) {
var messages = [];
plugin.action({
name: 'RECEIVE_MESSAGE',
handler: function(waitFor, message) {
messages.push(message);
}
});
plugin.stores('messages', messages);
next();
}
register(plugin, options, next)
Register the plugin where:
plugin
- the registration interface to the app.options
- options object passed in app.register
.next
- function with signature function(err)
which should be called once registration of the plugin is complete. While this allows for asynchronous registration it also means that if next
is never called, the app will not configure itself properly. Any errors passed should be considered unrecoverable events and should trigger application termination.
plugin.bly
A reference to the Bly
module used to create the app, so the plugin doesn't need to have Bly
as a dependency itself.
plugin.version
The version the Bly
module used.
plugin.action(options)
Adds a handler for an action as described by app.action
plugin.after(afterFunction)
Adds a function to be called after the app was started.
afterFunction
- (required) function which will be executed after the app starts (no arguments passed)
plugin.expose(key, value)
Exposes a property to the app.plugins[pluginName]
object.
key
- (required) the key for which to expose the valuevalue
plugin.inject(action, payload)
Inject an action into the app to be dispatched as described by app.inject
.
plugin.register(plugins, [options ,], callback)
Register plugins with this app as described by app.register
.
plugin.render(renderFn)
Register a function that is to be called with the results of each dispatched actions. As described by app.render
.
plugin.results(resultsFn)
Register a function with signature function(report)
that allow for values to be passed to app.render
functions. As described by app.results
.
plugin.stores(...)
Interface to setting and getting store as described by app.stores
.