diff --git a/README.md b/README.md index 2c48a9a3..33b6a51f 100644 --- a/README.md +++ b/README.md @@ -380,7 +380,35 @@ const view = (state, prev, send) => { ` } ``` -In this example, when the `Add` button is clicked, the view will dispatch an `add` action that the model’s `add` reducer will receive. [As seen above](#models), the reducer will add an item to the state’s `todos` array. The state change will cause this view to be run again with the new state, and the resulting DOM tree will be used to [efficiently patch the DOM](#does-choo-use-a-virtual-dom). +In this example, when the `Add` button is clicked, the view will dispatch an +`add` action that the model’s `add` reducer will receive. [As seen +above](#models), the reducer will add an item to the state’s `todos` array. The +state change will cause this view to be run again with the new state, and the +resulting DOM tree will be used to [efficiently patch the +DOM](#does-choo-use-a-virtual-dom). + +### Plugins +Sometimes it's necessary to change the way `choo` itself works. For example to +report whenever an action is triggered, handle errors globally or perist state +somewhere. This is done through something called `plugins`. Plugins are objects +that contain `hook` functions and are passed to `app.use()`: + +```js +const log = require('choo-log') +const choo = require('choo') +const app = choo() + +app.use(log()) + +const tree = app.start() +document.body.appendChild(tree) +``` + +Generally people using `choo` shouldn't be too worried about the specifics of +`plugins`, as the internal API is (unfortunatly by necessecity) quite complex. +After all they're the most powerful way to modify a `choo` appliction. If you +want to learn more about creating your own `plugins`, and which `hooks` are +available, head on over to [app.use()](#appusehooks). ## Badges Using `choo` in a project? Show off which version you've used using a badge: @@ -398,26 +426,8 @@ the first time, consider reading through the [handbook][handbook] or [concepts](#concepts) first :sparkles: ### app = choo(opts) -Initialize a new `choo` app. Takes an optional object of handlers. Handlers can -be: -- __onError(err, state, createSend):__ called when an `effect` or - `subscription` emit an error. If no handler is passed, the default handler - will `throw` on each error. -- __onAction(action, state, name, caller, createSend):__ called when an - `action` is fired. -- __onStateChange(action, state, prev, caller, createSend):__ called after a - reducer changes the `state`. - -`createSend()` is a special function that allows the creation of a new named -`send()` function. The first argument should be a string which is the name, the -second argument is a boolean `callOnError` which can be set to `true` to call -the `onError` hook istead of a provided callback. It then returns a -`send(actionName, data?)` function. - -Handlers should be used with care, as they're the most powerful interface into -the state. For application level code it's generally recommended to delegate to -actions inside models using the `send()` call, and only shape the actions -inside the handlers. +Initialize a new `choo` app. Takes an optional object of handlers that is +passed to [app.use()](#appusehooks). ### app.model(obj) Create a new model. Models modify data and perform IO. Takes the following @@ -458,6 +468,32 @@ is URI partials and `send()` can be called to trigger actions. If `defaultRoute` is passed in, that will be called if no paths match. If no `defaultRoute` is specified it will throw instead. +### app.use(hooks) +Register an object of hooks on the application. This is useful to extend the +way `choo` works, adding custom behavior and listeners. Generally returning +objects of `hooks` is done by returning them from functions (which we call +`plugins` throughout the documentation). + +There are several `hooks` that are picked up by `choo`: +- __onError(err, state, createSend):__ called when an `effect` or + `subscription` emit an error. If no handler is passed, the default handler + will `throw` on each error. +- __onAction(action, state, name, caller, createSend):__ called when an + `action` is fired. +- __onStateChange(action, state, prev, caller, createSend):__ called after a + reducer changes the `state`. + +`createSend()` is a special function that allows the creation of a new named +`send()` function. The first argument should be a string which is the name, the +second argument is a boolean `callOnError` which can be set to `true` to call +the `onError` hook istead of a provided callback. It then returns a +`send(actionName, data?)` function. + +Hooks should be used with care, as they're the most powerful interface into +the state. For application level code it's generally recommended to delegate to +actions inside models using the `send()` call, and only shape the actions +inside the hooks. + ### html = app.toString(route, state?) Render the application to a string of HTML. Useful for rendering on the server. First argument is a path that's passed to the router. Second argument is an diff --git a/index.js b/index.js index a047d754..e0206293 100644 --- a/index.js +++ b/index.js @@ -18,17 +18,21 @@ module.exports = choo function choo (opts) { opts = opts || {} - const _store = start._store = barracks(xtend(opts, { onStateChange: render })) + const _store = start._store = barracks() var _router = start._router = null var _defaultRoute = null var _rootNode = null var _routes = null var _frame = null + _store.use({ onStateChange: render }) + _store.use(opts) + start.toString = toString start.router = router start.model = model start.start = start + start.use = use return start @@ -83,10 +87,6 @@ function choo (opts) { // update the DOM after every state mutation // (obj, obj, obj, str, fn) -> null function render (data, state, prev, name, createSend) { - if (opts.onStateChange) { - opts.onStateChange(data, state, prev, name, createSend) - } - if (!_frame) { _frame = nanoraf(function (state, prev) { const newTree = _router(state.location.pathname, state, prev) @@ -109,6 +109,13 @@ function choo (opts) { _store.model(model) } + // register a plugin + // (obj) -> null + function use (hooks) { + assert.equal(typeof hooks, 'object', 'choo.use: hooks should be an object') + _store.use(hooks) + } + // create a new router with a custom `createRoute()` function // (str?, obj, fn?) -> null function createRouter (defaultRoute, routes, createSend) {