diff --git a/examples/http/client.js b/examples/http/client.js
index 31370a2..c1473a5 100644
--- a/examples/http/client.js
+++ b/examples/http/client.js
@@ -5,7 +5,7 @@ const mainView = require('./views/main')
const app = choo()
app.model(require('./models/error'))
-app.model('api', require('./models/api'))
+app.model(require('./models/api'))
app.router((route) => [
route('/', mainView)
diff --git a/examples/http/models/api.js b/examples/http/models/api.js
index 37907e3..73daf8a 100644
--- a/examples/http/models/api.js
+++ b/examples/http/models/api.js
@@ -1,45 +1,32 @@
const http = require('../../../http')
module.exports = {
+ namespace: 'api',
state: {
title: 'Button pushing machine 3000'
},
reducers: {
- set: function (action, state) {
- return { 'api:title': action.payload }
- }
+ set: (action, state) => ({ 'title': action.payload })
},
effects: {
- good: performGoodRequest,
- bad: performBadRequest
+ good: (action, state, send) => request('/good', send),
+ bad: (action, state, send) => request('/bad', send)
}
}
-function performGoodRequest (action, state, send) {
- http('/good', { json: true }, function (err, res, body) {
- if (err) return send('error', { payload: 'HTTP error' })
- if (res.statusCode !== 200) {
- return send('error', { payload: body.payload })
- }
- if (!body) {
- return send('error', { payload: 'fatal: no body received' })
- }
- send('api:set', { payload: body.message })
- })
-}
-
-function performBadRequest (action, state, send) {
- http('/bad', { json: true }, function (err, res, body) {
- if (err) return send('error', { payload: 'HTTP error' })
+function request (uri, send) {
+ http(uri, { json: true }, function (err, res, body) {
+ if (err) return send('app:error', { payload: 'HTTP error' })
if (res.statusCode !== 200) {
const message = (body && body.message)
- ? body.message
- : 'unknown server error'
- return send('error', { payload: message })
+ ? body.message
+ : 'unknown server error'
+ return send('app:error', { payload: message })
}
if (!body) {
- return send('error', { payload: 'fatal: no body received' })
+ console.log('req made!')
+ return send('app:error', { payload: 'fatal: no body received' })
}
- send('api:set', { payload: body.title })
+ send('api:set', { payload: body.message || body.title })
})
}
diff --git a/examples/http/models/error.js b/examples/http/models/error.js
index 4bc3694..7d05ce5 100644
--- a/examples/http/models/error.js
+++ b/examples/http/models/error.js
@@ -8,6 +8,7 @@
const ERROR_TIMEOUT = 1000
module.exports = {
+ namespace: 'app',
state: {
error: [],
errorTimeDone: null,
@@ -33,9 +34,9 @@ module.exports = {
},
effects: {
error: function (action, state, send) {
- const timeout = state.errorTimeDone - Date.now()
+ const timeout = state.app.errorTimeDone - Date.now()
setTimeout(function () {
- send('error:delete')
+ send('app:error:delete')
}, timeout)
}
}
diff --git a/examples/http/views/main.js b/examples/http/views/main.js
index 22c5414..4d0d435 100644
--- a/examples/http/views/main.js
+++ b/examples/http/views/main.js
@@ -1,10 +1,12 @@
const choo = require('../../../')
module.exports = function (params, state, send) {
+ const error = state.app.error[0]
+ const title = state.api.title
return choo.view`
- ${state['api:title']}
- Latest error: ${state.error[0]}
+ ${title}
+ Latest error: ${error}
diff --git a/index.js b/index.js
index aad8e4e..3e012dd 100644
--- a/index.js
+++ b/index.js
@@ -3,13 +3,14 @@ const sheetRouter = require('sheet-router')
const document = require('global/document')
const href = require('sheet-router/href')
const sendAction = require('send-action')
+const assert = require('assert')
const xtend = require('xtend')
const yo = require('yo-yo')
choo.view = yo
module.exports = choo
-// A framework for creating sturdy web applications
+// framework for creating sturdy web applications
// null -> fn
function choo () {
const _models = []
@@ -23,6 +24,7 @@ function choo () {
return start
// render the application to a string
+ // (str, obj) -> str
function toString (route, state) {
const initialState = {}
@@ -53,61 +55,79 @@ function choo () {
if (model.effects) apply(model.namespace, model.effects, effects)
})
+ // send() is used to trigger actions inside
+ // views, effects and subscriptions
const send = sendAction({
onaction: handleAction,
onchange: onchange,
state: initialState
})
+ // subscriptions are loaded after sendAction() is called
+ // because they both need access to send() and can't
+ // react to actions (read-only)
_models.forEach(function (model) {
if (model.subscriptions) {
+ assert.ok(Array.isArray(model.subscriptions, 'subs must be an array'))
model.subscriptions.forEach(function (sub) {
sub(send)
})
}
})
+ // the rootId is determined to find the application root
+ // on update. Since the DOM nodes change between updates,
+ // we must call document.querySelector() to find the root.
+ // Use different names when loading multiple choo applications
+ // on the same page
const rootId = name + '-root'
const tree = _router(send.state().app.location, send.state(), send)
tree.setAttribute('id', rootId)
return tree
+ // handle an action by either reducers, effects
+ // or both - return the new state when done
+ // (obj, obj, fn) -> obj
function handleAction (action, state, send) {
- var _reducers = false
- var _effects = false
- var newState = null
+ var reducersCalled = false
+ var effectsCalled = false
+ // validate if a namespace exists. Namespaces
+ // are delimited by the first ':'. Perhaps
+ // we'll allow recursive namespaces in the
+ // future - who knows
if (/:/.test(action.type)) {
const arr = action.type.split(':')
- var ns = arr[0]
- action.type = arr[1]
+ var ns = arr.shift()
+ action.type = arr.join(':')
}
- const nsReducers = ns ? reducers[ns] : reducers
- if (nsReducers && nsReducers[action.type]) {
+ const _reducers = ns ? reducers[ns] : reducers
+ if (_reducers && _reducers[action.type]) {
if (ns) {
- state[ns] = reducers[ns][action.type](action, state[ns])
- newState = state
+ const newState = reducers[ns][action.type](action, state[ns])
+ state[ns] = xtend(state[ns], newState)
} else {
- newState = xtend(state, reducers[action.type](action, state))
+ state = xtend(state, reducers[action.type](action, state))
}
- _reducers = true
+ reducersCalled = true
}
- const nsEffects = ns ? effects[ns] : effects
- if (nsEffects && nsEffects[action.type]) {
- nsEffects[action.type](action, newState || state)
- _effects = true
+ const _effects = ns ? effects[ns] : effects
+ if (_effects && _effects[action.type]) {
+ _effects[action.type](action, state, send)
+ effectsCalled = true
}
- if (!_reducers && !_effects) {
+ if (!reducersCalled && !effectsCalled) {
throw new Error('Could not find action ' + action.type)
}
- return newState || state
+ return state
}
- // update on every change
+ // update the DOM after every state mutation
+ // (obj, obj) -> null
function onchange (action, state) {
const oldTree = document.querySelector('#' + rootId)
const newTree = _router(state.app.location, state, send)
@@ -116,7 +136,7 @@ function choo () {
}
}
- // register all routes
+ // register all routes on the router
// [obj|fn] -> null
function router (cb) {
_router = sheetRouter(cb)
@@ -136,42 +156,45 @@ function appInit (opts) {
const model = {
namespace: 'app',
state: { location: document.location.href },
- reducers: { location: setLocation },
- subscriptions: []
+ subscriptions: [],
+ reducers: {
+ // handle href links
+ location: function setLocation (action, state) {
+ return {
+ location: action.location.replace(/#.*/, '')
+ }
+ }
+ }
}
- if (opts.href !== false) {
- model.subscriptions.push(function (send) {
- href(function (href) {
- send('app:location', { location: href })
- })
- })
- }
-
- if (opts.history !== false) {
- model.subscriptions.push(function (send) {
- history(function (href) {
- send('app:location', { location: href })
- })
- })
- }
+ // enable catching links
+ // enable HTML5 history API
+ if (opts.history !== false) pushLocationSub(href)
+ if (opts.history !== false) pushLocationSub(history)
return model
- // handle href links
- function setLocation (action, state) {
- return { location: action.location.replace(/#.*/, '') }
+ // create a new subscription that modifies
+ // 'app:location' and push it to the subs
+ // fn -> null
+ function pushLocationSub (cb) {
+ model.subscriptions.push(function (send) {
+ cb(function (href) {
+ send('app:location', { location: href })
+ })
+ })
}
}
// compose an object conditionally
+// optionally contains a namespace
+// which is used to nest properties.
// (str, obj, obj) -> null
-function apply (name, source, target) {
+function apply (ns, source, target) {
Object.keys(source).forEach(function (key) {
- if (name) {
- if (!target[name]) target[name] = {}
- target[name][key] = source[key]
- target[name][key].namespace = name
+ if (ns) {
+ if (!target[ns]) target[ns] = {}
+ target[ns][key] = source[key]
} else target[key] = source[key]
})
}