Merge pull request #8 from yoshuawuyts/model-update

model: add namespaces
This commit is contained in:
Yoshua Wuyts
2016-05-23 17:26:47 +09:00
25 changed files with 356 additions and 186 deletions
+10
View File
@@ -0,0 +1,10 @@
## `2.0.0`
### breaking changes
- namespaces are now enforced more strictly
- models now only accept a single argument
- the `namespace` key was introduced inside of models (was prior the leading
string in models)
- namespaced models can now only operate within themselves
## `1.0.0`
- first version of choo
+166 -47
View File
@@ -13,6 +13,7 @@ productive package.
- [Demos](#demos)
- [Usage](#usage)
- [Concepts](#concepts)
- [Models](#models)
- [Effects](#effects)
- [HTTP](#http)
- [Subscriptions](#subscriptions)
@@ -23,6 +24,7 @@ productive package.
- [API](#api)
- [FAQ](#faq)
- [Installation](#installation)
- [See Also](#see-also)
- [License](#license)
## Features
@@ -51,28 +53,31 @@ productive package.
const choo = require('choo')
const app = choo()
app.model('title', {
app.model({
namespace: 'input',
state: {
title: 'my-demo-app'
title: 'my demo app'
},
reducers: {
'update': (action, state) => ({ title: action.payload })
update: (action, state) => ({ title: action.payload })
},
effects: {
'update': (action, state, send) => (document.title = action.payload)
update: (action, state, send) => (document.title = action.payload)
}
})
const mainView = (params, state, send) => choo.view`
<main class="app">
<h1>${state.title}</h1>
<label>Set the title</label>
<input
type="text"
placeholder=${state.title}
oninput=${(e) => send('title:update', { payload: e.target.value })}>
</main>
`
const mainView = (params, state, send) => {
return choo.view`
<main class="app">
<h1>${state.input.title}</h1>
<label>Set the title</label>
<input
type="text"
placeholder=${state.input.title}
oninput=${(e) => send('input:update', { payload: e.target.value })}>
</main>
`
}
app.router((route) => [
route('/', mainView)
@@ -88,8 +93,8 @@ document.body.appendChild(tree)
your browser
- __actions:__ a named event with optional properties attached. Used to call
`effects` and `reducers` that have been registered in `models`
- __model:__ optionally namespaced object containing `subscriptions`, `effects`
and `reducers`
- __model:__ optionally namespaced object containing `subscriptions`,
`effects`, `reducers` and initial `state`
- __subscriptions:__ read-only data sources that emit `actions`
- __effects:__ asynchronous functions that emit an `action` when done
- __reducers:__ synchronous functions that modify `state`
@@ -116,6 +121,41 @@ document.body.appendChild(tree)
└────────┘ └────────┘
```
## Models
`models` are objects that contain initial `state`, `subscriptions`, `effects`
and `reducers`. They're generally grouped around a theme (or domain, if you
like). To provide some sturdiness to your `models`, they can either be
namespaced or not. Namespacing means that only state within the model can be
accessed. Models can still trigger actions on other models, though it's
recommeded to keep that to a minimum.
So say we have a `todos` namespace, an `add` reducer and a `todos` model.
Outside the model they're called by `send('todos:add')` and
`state.todos.todos`. Inside the namespaced model they're called by
`send('todos:add')` and `state.todos`. An example namespaced model:
```js
const app = choo()
app.model({
namespace: 'todos',
model: { todos: [] },
reducers: {
add: (state, action) => ({ todos: state.todos.concat(action.payload) })
}
})
```
In most cases using namespaces is beneficial, as having clear boundries makes
it easier to follow logic. But sometimes you need to call `actions` that
operate over multiple domains (such as a "logout" `action`), or have a
`subscription` that might trigger multiple `reducers` (such as a `websocket`
that calls a different `action` based on the incoming data).
In these cases you probably want to have a `model` that doesn't use namespaces,
and has access to the full application state. Try and keep the logic in these
`models` to a minimum, and declare as few `reducers` as possible. That way the
bulk of your logic will safely shielded, with only a few points touching every
part of your application.
## Effects
Side effects are done through `effects` declared in `app.model()`. Unlike
`reducers` they cannot modify the state by returning objects, but get a
@@ -136,25 +176,49 @@ A typical `effect` flow looks like:
that weighs only `2.4kb`:
```js
const http = require('choo/http')
const choo = require('choo')
const app = choo()
// GET JSON
http.get('/my-endpoint', { json: true }, function (err, res, body) {
if (err) throw err
if (res.statusCode !== 200 || !body) throw new Error('something went wrong')
app.model({
effects: {
'app:error': (state, event_ => console.error(`error: ${event.payload}`)),
'app:print': (state, event) => console.log(`http: ${event.payload}`),
'http:get_json': getJson,
'http:post_json': postJson,
'http:delete': httpDelete
}
})
// POST JSON
const body = { foo: 'bar' }
http.post('/my-endpoint', { json: body }, function (err, res, body) {
if (err) throw err
if (res.statusCode !== 200 || !body) throw new Error('something went wrong')
})
function getJson (state, action, send) {
http.get('/my-endpoint', { json: true }, function (err, res, body) {
if (err) return send('app:error', { payload: err.message })
if (res.statusCode !== 200 || !body) {
return send('app:error', { payload:'something went wrong' })
}
send('app:print', { payload: body })
})
}
// DELETE
http.del('/my-endpoint', function (err, res) {
if (err) throw err
if (res.statusCode !== 200) throw new Error('something went wrong')
})
function postJson (state, action, send) {
const body = { foo: 'bar' }
http.post('/my-endpoint', { json: body }, function (err, res, body) {
if (err) return send('app:error', { payload: err.message })
if (res.statusCode !== 200 || !body) {
return send('app:error', { payload:'something went wrong' })
}
send('app:print', { payload: body })
})
}
function httpDelete (state, action, send) {
const body = { foo: 'bar' }
http.post('/my-endpoint', { json: body }, function (err, res, body) {
if (err) return send('app:error', { payload: err.message })
if (res.statusCode !== 200) {
return send('app:error', { payload:'something went wrong' })
}
})
}
```
Note that `http` only runs in the browser to prevent accidental requests when
rendering in Node. For more details view the [`raynos/xhr`
@@ -165,6 +229,19 @@ Subscriptions are a way of receiving data from a source. For example when
listening for events from a server using `SSE` or `Websockets` for a
chat app, or when catching keyboard input for a videogame.
An example subscription that logs `"dog?"` every second:
```js
const app = choo()
choo.model({
subscriptions: [
(send) => setTimeout(() => send('app:print', { payload: 'dog?' }), 1000)
],
effects: {
'app:print': (state, action) => console.log(action.payload)
}
})
```
### Server Sent Events (SSE)
[Server Sent Events (SSE)][sse] allow servers to push data to the browser.
They're the unidirectional cousin of `websockets` and compliment `HTTP`
@@ -176,17 +253,20 @@ const stream = new document.EventSource('/sse')
app.model({
subscriptions: [
function (send) {
stream.onerror = (e) => send('error', { payload: JSON.stringify(e) })
stream.onmessage = (e) => send('print', { payload: e.data })
stream.onerror = (e) => send('app:error', { payload: JSON.stringify(e) })
stream.onmessage = (e) => send('app:print', { payload: e.data })
}
],
effects: {
'sse:close': () => stream.close()
error: (state, event_ => console.error(`error: ${event.payload}`)),
print: (state, event) => console.log(`pressed key num: ${event.payload}`)
'app:error': (state, event_ => console.error(`error: ${event.payload}`)),
'app:print': (state, event) => console.log(`sse: ${event.payload}`)
}
})
```
This code does not handle reconnects, server timeouts, exponential backoff and
queueing data. You might want to use a package from `npm` or [write your
own][sse-reconnect] if you're building something for production.
### Keyboard
Most browsers have [basic support for keyboard events][keyboard-support]. To
@@ -195,11 +275,11 @@ capture keyboard events, setup a `subscription`:
app.model({
subscriptions: [
function (send) {
keyboard.onkeypress = (e) => send('print', { payload: e.keyCode })
keyboard.onkeypress = (e) => send('app:print', { payload: e.keyCode })
}
],
effects: {
print: (state, event) => console.log(`pressed key num: ${event.payload}`)
'app:print': (state, event) => console.log(`pressed key: ${event.payload}`)
}
})
```
@@ -213,18 +293,21 @@ const socket = new document.WebSocket('ws://localhost:8081')
app.model({
subscriptions: [
function (send) {
socket.onerror = (e) => send('error', { payload: JSON.stringify(e) })
socket.onmessage = (e) => send('print', { payload: e.data })
socket.onerror = (e) => send('app:error', { payload: JSON.stringify(e) })
socket.onmessage = (e) => send('app:print', { payload: e.data })
}
],
effects: {
'ws:close': () => socket.close(),
'ws:send': (state, event) => socket.send(JSON.stringify(event.payload)),
error: (state, event_ => console.error(`error: ${event.payload}`)),
print: (state, event) => console.log(`pressed key num: ${event.payload}`)
'app:error': (state, event_ => console.error(`error: ${event.payload}`)),
'app:print': (state, event) => console.log(`ws: ${event.payload}`)
}
})
```
This code does not handle reconnects, server timeouts, exponential backoff and
queueing data. You might want to use a package from `npm` or [write your
own][ws-reconnect] if you're building something for production.
## Rendering in Node
Sometimes it's necessary to render code inside of Node; for serving first
@@ -273,9 +356,12 @@ else document.body.appendChild(app.start())
### app = choo()
Create a new `choo` app
### app.model(name?, obj)
Create a new model. Models modify data and perform IO. Obj takes the following
### app.model(obj)
Create a new model. Models modify data and perform IO. Takes the following
arguments:
- __namespace:__ optional namespace that prefixes the keys in `state`,
`reducers` and `effects`. Also limits `actions` called by `send()` to
in-namespace only.
- __state:__ object. Key value store of initial values
- __reducers:__ object. Syncronous functions that modify state. Each function
has a signature of `(action, state)`
@@ -283,10 +369,6 @@ arguments:
a signature of `(action, state, send)` where `send` is a reference to
`app.send()`
If a `name` string is passed as a first argument, `reducers` and `signatures`
will be prefixed by the name. So if name is "user" and a reducer called
"update" is registered, it would be accessed as `'user:update'` in `send()`.
### choo.view\`html\`
Tagged template string HTML builder. See
[`yo-yo`](https://github.com/maxogden/yo-yo) for full documentation. Views
@@ -316,6 +398,32 @@ Start the application. Returns a tree of DOM nodes that can be mounted using
accordingly.
## FAQ
### Why did you build this?
`choo` is nothing but a formalization of how I've been building my applications
for the past year. I originally used `virtual-dom` with `virtual-app` and
`wayfarer` where now it's `yo-yo` with `send-action` and `sheet-router`. The
main benefit of using `choo` over these technologies separately is that it
becomes easier for teams to pick up and gather around. The code base for `choo`
itself is super petite (`~150` LOC) and mostly acts to enforce structure around
some excellent npm packages. This is my take on modular frameworks; I hope
you'll find it pleasant.
### Why is it called choo?
Because I thought it sounded cute. All these programs talk about being
"performant", "rigid", "robust" - I like programming to be light, fun and
non-scary. `choo` embraces that.
Also imagine telling some business people you chose to rewrite something
critical to the company using the `choo` framework.
:steam_locomotive::train::train::train:
### Why is it a framework, and not a library?
I love small libraries that do one thing well, but when working in a team,
having an undocumented combination of packages often isn't great. `choo()` is a
small set of packages that work well together, wrapped in an an architectural
pattern. This means you get all the benefits of small packages, but get to be
productive right from the start.
### How does choo compare to X?
Ah, so this is where I get to rant. `choo` (_chugga-chugga-chugga-choo-choo!_)
was built because other options didn't quite cut it for me, so instead of
@@ -382,6 +490,10 @@ transforms:
- [uglifyify](https://github.com/hughsk/uglifyify) - minify your code using
UglifyJS2. Use as a `--global` transform
## Hey, doesn't this look a lot like Elm?
Yup, it's greatly inspired by the `elm` architecture. But contrary to `elm`,
`choo` doesn't introduce a completely new language to build web applications.
### Is it production ready?
Sure.
@@ -390,6 +502,11 @@ Sure.
$ npm install choo
```
## See Also
- [budo](https://github.com/mattdesl/budo) - quick prototyping tool for
`browserify`
- [stack.gl](http://stack.gl/) - open software ecosystem for WebGL
## License
[MIT](https://tldrlegal.com/license/mit-license)
@@ -416,3 +533,5 @@ $ npm install choo
[morphdom]: https://github.com/patrick-steele-idem/morphdom
[morphdom-bench]: https://github.com/patrick-steele-idem/morphdom#benchmarks
[module-parent]: https://nodejs.org/dist/latest-v6.x/docs/api/modules.html#modules_module_parent
[sse-reconnect]: http://stackoverflow.com/questions/24564030/is-an-eventsource-sse-supposed-to-try-to-reconnect-indefinitely
[ws-reconnect]: http://stackoverflow.com/questions/13797262/how-to-reconnect-to-websocket-after-close-connection
+1 -1
View File
@@ -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)
+12 -26
View File
@@ -1,45 +1,31 @@
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, state) {
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' })
return send('app:error', { payload: 'fatal: no body received' })
}
send('api:set', { payload: body.title })
send('api:set', { payload: body.message || body.title })
})
}
+2 -1
View File
@@ -8,6 +8,7 @@
const ERROR_TIMEOUT = 1000
module.exports = {
namespace: 'app',
state: {
error: [],
errorTimeDone: null,
@@ -35,7 +36,7 @@ module.exports = {
error: function (action, state, send) {
const timeout = state.errorTimeDone - Date.now()
setTimeout(function () {
send('error:delete')
send('app:error:delete')
}, timeout)
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ const http = require('http')
const PORT = 8080
const server = http.createServer(createRouter())
server.listen(PORT, () => console.log(`listening on port ${PORT}`))
server.listen(PORT, () => process.stdout.write(`listening on port ${PORT}\n`))
var index = 0
const errors = [
+4 -2
View File
@@ -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`
<section>
<h1>${state['api:title']}</h1>
<h2>Latest error: ${state.error[0]}</h2>
<h1>${title}</h1>
<h2>Latest error: ${error}</h2>
<button onclick=${(e) => send('api:good')}>OK!</button>
<button onclick=${(e) => send('api:bad')}>Naughty</button>
</section>
+3 -3
View File
@@ -6,9 +6,9 @@ sf('tachyons')
const app = choo()
app.model('inbox', require('./models/inbox'))
app.model('spam', require('./models/spam'))
app.model('sent', require('./models/sent'))
app.model(require('./models/inbox'))
app.model(require('./models/spam'))
app.model(require('./models/sent'))
app.router((route) => [
route('/', require('./views/empty')),
+1 -1
View File
@@ -3,7 +3,7 @@ const choo = require('../../../')
module.exports = function (params, state, send) {
const mailbox = params.mailbox
const messages = state[mailbox + ':messages']
const messages = state[mailbox].messages
return choo.view`
<div>
<div class="db cf w-100">
+1 -1
View File
@@ -4,7 +4,7 @@ module.exports = function (params, state, send) {
const mailbox = params.mailbox
const message = params.message
const email = state[mailbox + ':messages'].filter(function (msg) {
const email = state[mailbox].messages.filter(function (msg) {
return String(msg.id) === message
})[0]
+4 -3
View File
@@ -5,9 +5,10 @@ module.exports = function () {
return function (params, state, send) {
const mailbox = params.mailbox
const message = params.message
const messages = state[mailbox].messages
if (message) {
const email = state[mailbox + ':messages'].filter(function (msg) {
const email = state[mailbox].messages.filter(function (msg) {
return String(msg.id) === message
})[0]
@@ -15,7 +16,7 @@ module.exports = function () {
<section class="fl mt4 w-80 db">
<div>
${createHeader()}
${state[mailbox + ':messages'].map(function (msg) {
${messages.map(function (msg) {
return createMessage(msg, mailbox)
})}
</div>
@@ -28,7 +29,7 @@ module.exports = function () {
return choo.view`
<section class="fl mt4 w-80 db">
${createHeader()}
${state[mailbox + ':messages'].map(function (msg) {
${messages.map(function (msg) {
return createMessage(msg, mailbox)
})}
</section>
+3 -4
View File
@@ -3,16 +3,15 @@ const choo = require('../../../')
const mailboxes = [ 'inbox', 'spam', 'sent' ]
module.exports = function (params, state, send) {
const mailbox = params.mailbox
return choo.view`
<aside class="fl mt4 w-20 db">
<ul>
<li>
<h2 class="f4 b lh0">Mailbox</h2>
</li>
${mailboxes.map(function (name) {
return createLi(name, state[name + ':messages'], mailbox)
${mailboxes.map(function (mailbox) {
const messages = mailbox.messages
return createLi(mailbox, messages, mailbox)
})}
</ul>
</aside>
+2 -1
View File
@@ -2,9 +2,10 @@ const pathname = require('pathname-match')
const choo = require('../../../')
module.exports = function (params, state, send) {
const location = state.app.location
return choo.view`
<span class="fl mt4 w-100 f4 b">
URL: ${pathname(state.location) || '/'}
URL: ${pathname(location) || '/'}
</span>
`
}
+1
View File
@@ -1,4 +1,5 @@
module.exports = {
namespace: 'inbox',
state: {
messages: [
{
+1
View File
@@ -1,4 +1,5 @@
module.exports = {
namespace: 'sent',
state: {
messages: [
{
+1
View File
@@ -1,4 +1,5 @@
module.exports = {
namespace: 'spam',
state: {
messages: [
{
+1 -1
View File
@@ -6,7 +6,7 @@ const http = require('http')
const PORT = 8080
const server = http.createServer(createRouter())
server.listen(PORT, () => console.log(`listening on port ${PORT}`))
server.listen(PORT, () => process.stdout.write(`listening on port ${PORT}\n`))
function createRouter () {
const router = serverRouter('/404')
+34
View File
@@ -0,0 +1,34 @@
const serverRouter = require('server-router')
const http = require('http')
const PORT = 8080
const client = require('./client')
// If an incoming request accepts "text/html", render the
// appropriate HTML. Else use the API server
const apiRouter = createRouter()
const server = http.createServer(function (req, res) {
if (/text\/html/.test(req.headers.accept)) handleHtml(req, res)
else apiRouter(req, res)
})
server.listen(PORT, () => process.stdout.write(`listening on port ${PORT}\n`))
// create a new router
// null -> fn
function createRouter () {
const apiRouter = serverRouter('/404')
apiRouter.on('/404', (req, res) => res.end('not found'))
apiRouter.on('/', (req, res) => {
res.end(JSON.stringify({ routes: [ '/', '/404' ] }))
})
return apiRouter
}
// render the client to string
// based on the requested url
// (obj, obj) -> null
function handleHtml (req, res) {
const html = client.toString(req.url, { message: 'hello server!' })
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.end(html)
}
-22
View File
@@ -1,22 +0,0 @@
const serverRouter = require('server-router')
const http = require('http')
const PORT = 8080
const client = require('./client')
const server = http.createServer(createRouter())
server.listen(PORT, () => console.log(`listening on port ${PORT}`))
function createRouter () {
const router = serverRouter('/404')
router.on('/404', (req, res) => res.end('not found'))
router.on('/', function (req, res, params) {
const html = client.toString('/', { message: 'hello server!' })
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.end(html)
})
return router
}
+20 -15
View File
@@ -1,26 +1,31 @@
const choo = require('../')
const choo = require('../../')
const app = choo()
app.model('title', {
state: { title: 'my-demo-app' },
app.model({
namespace: 'input',
state: {
title: 'my demo app'
},
reducers: {
'update': (action, state) => ({ title: action.payload })
update: (action, state) => ({ title: action.payload })
},
effects: {
'update': (action, state, send) => (document.title = action.payload)
update: (action, state, send) => (document.title = action.payload)
}
})
const mainView = (params, state, send) => choo.view`
<main class="app">
<h1>${state.title}</h1>
<label>Set the title</label>
<input
type="text"
placeholder=${state.title}
oninput=${(e) => send('title:update', { payload: e.target.value })}>
</main>
`
const mainView = (params, state, send) => {
return choo.view`
<main class="app">
<h1>${state.input.title}</h1>
<label>Set the title</label>
<input
type="text"
placeholder=${state.input.title}
oninput=${(e) => send('input:update', { payload: e.target.value })}>
</main>
`
}
app.router((route) => [
route('/', mainView)
+87 -56
View File
@@ -3,17 +3,16 @@ 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 (opts) {
opts = opts || {}
const name = opts.name || 'choo'
function choo () {
const _models = []
var _router = null
@@ -25,6 +24,7 @@ function choo (opts) {
return start
// render the application to a string
// (str, obj) -> str
function toString (route, state) {
const initialState = {}
@@ -40,71 +40,102 @@ function choo (opts) {
}
// start the application
// null -> DOMNode
function start () {
// obj -> DOMNode
function start (opts) {
opts = opts || {}
const name = opts.name || 'choo'
const initialState = {}
const reducers = {}
const effects = {}
_models.push(appInit(opts))
_models.forEach(function (model) {
if (model.state) apply(model.name, model.state, initialState)
if (model.reducers) apply(model.name, model.reducers, reducers)
if (model.effects) apply(model.name, model.effects, effects)
if (model.state) apply(model.namespace, model.state, initialState)
if (model.reducers) apply(model.namespace, model.reducers, reducers)
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().location, send.state(), send)
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
if (reducers[action.type]) {
newState = xtend(state, reducers[action.type](action, state))
_reducers = true
// 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.shift()
action.type = arr.join(':')
}
if (effects[action.type]) {
effects[action.type](action, newState || state, send)
newState = newState || state
_effects = true
const _reducers = ns ? reducers[ns] : reducers
if (_reducers && _reducers[action.type]) {
if (ns) {
const newState = _reducers[action.type](action, state[ns])
state[ns] = xtend(state[ns], newState)
} else state = xtend(state, reducers[action.type](action, state))
reducersCalled = true
}
if (!_reducers && !_effects) {
const _effects = ns ? effects[ns] : effects
if (_effects && _effects[action.type]) {
if (ns) _effects[action.type](action, state[ns], send)
else _effects[action.type](action, state, send)
effectsCalled = true
}
if (!reducersCalled && !effectsCalled) {
throw new Error('Could not find action ' + action.type)
}
return newState
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.location, state, send)
const newTree = _router(state.app.location, state, send)
newTree.setAttribute('id', rootId)
yo.update(oldTree, newTree)
}
}
// register all routes
// register all routes on the router
// [obj|fn] -> null
function router (cb) {
_router = sheetRouter(cb)
@@ -113,9 +144,7 @@ function choo (opts) {
// create a new model
// (str?, obj) -> null
function model (name, model) {
if (!model) model = name
if (typeof name === 'string') model.name = name
function model (model) {
_models.push(model)
}
}
@@ -124,45 +153,47 @@ function choo (opts) {
// obj -> obj
function appInit (opts) {
const model = {
state: {
location: document.location.href
},
namespace: 'app',
state: { location: document.location.href },
subscriptions: [],
reducers: {
location: setLocation
},
subscriptions: []
// 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('location', { location: href })
})
})
}
if (opts.history !== false) {
model.subscriptions.push(function (send) {
history(function (href) {
send('location', { location: href })
})
})
}
// enable catching <href a=""></href> 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) {
const location = action.location.replace(/#.*/, '')
return xtend(state, { location: location })
// create a new subscription that modifies
// 'app:location' and push it to be loaded
// 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) target[name + ':' + key] = source[key]
else target[key] = source[key]
if (ns) {
if (!target[ns]) target[ns] = {}
target[ns][key] = source[key]
} else target[key] = source[key]
})
}
+1 -1
View File
@@ -6,7 +6,7 @@ const http = require('http')
const PORT = 8080
const server = http.createServer(createRouter())
server.listen(PORT, () => console.log(`listening on port ${PORT}`))
server.listen(PORT, () => process.stdout.write(`listening on port ${PORT}\n`))
function createRouter () {
const router = serverRouter('/404')