Merge pull request #8 from yoshuawuyts/model-update
model: add namespaces
This commit is contained in:
@@ -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
|
||||||
@@ -13,6 +13,7 @@ productive package.
|
|||||||
- [Demos](#demos)
|
- [Demos](#demos)
|
||||||
- [Usage](#usage)
|
- [Usage](#usage)
|
||||||
- [Concepts](#concepts)
|
- [Concepts](#concepts)
|
||||||
|
- [Models](#models)
|
||||||
- [Effects](#effects)
|
- [Effects](#effects)
|
||||||
- [HTTP](#http)
|
- [HTTP](#http)
|
||||||
- [Subscriptions](#subscriptions)
|
- [Subscriptions](#subscriptions)
|
||||||
@@ -23,6 +24,7 @@ productive package.
|
|||||||
- [API](#api)
|
- [API](#api)
|
||||||
- [FAQ](#faq)
|
- [FAQ](#faq)
|
||||||
- [Installation](#installation)
|
- [Installation](#installation)
|
||||||
|
- [See Also](#see-also)
|
||||||
- [License](#license)
|
- [License](#license)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
@@ -51,28 +53,31 @@ productive package.
|
|||||||
const choo = require('choo')
|
const choo = require('choo')
|
||||||
|
|
||||||
const app = choo()
|
const app = choo()
|
||||||
app.model('title', {
|
app.model({
|
||||||
|
namespace: 'input',
|
||||||
state: {
|
state: {
|
||||||
title: 'my-demo-app'
|
title: 'my demo app'
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
'update': (action, state) => ({ title: action.payload })
|
update: (action, state) => ({ title: action.payload })
|
||||||
},
|
},
|
||||||
effects: {
|
effects: {
|
||||||
'update': (action, state, send) => (document.title = action.payload)
|
update: (action, state, send) => (document.title = action.payload)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const mainView = (params, state, send) => choo.view`
|
const mainView = (params, state, send) => {
|
||||||
|
return choo.view`
|
||||||
<main class="app">
|
<main class="app">
|
||||||
<h1>${state.title}</h1>
|
<h1>${state.input.title}</h1>
|
||||||
<label>Set the title</label>
|
<label>Set the title</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder=${state.title}
|
placeholder=${state.input.title}
|
||||||
oninput=${(e) => send('title:update', { payload: e.target.value })}>
|
oninput=${(e) => send('input:update', { payload: e.target.value })}>
|
||||||
</main>
|
</main>
|
||||||
`
|
`
|
||||||
|
}
|
||||||
|
|
||||||
app.router((route) => [
|
app.router((route) => [
|
||||||
route('/', mainView)
|
route('/', mainView)
|
||||||
@@ -88,8 +93,8 @@ document.body.appendChild(tree)
|
|||||||
your browser
|
your browser
|
||||||
- __actions:__ a named event with optional properties attached. Used to call
|
- __actions:__ a named event with optional properties attached. Used to call
|
||||||
`effects` and `reducers` that have been registered in `models`
|
`effects` and `reducers` that have been registered in `models`
|
||||||
- __model:__ optionally namespaced object containing `subscriptions`, `effects`
|
- __model:__ optionally namespaced object containing `subscriptions`,
|
||||||
and `reducers`
|
`effects`, `reducers` and initial `state`
|
||||||
- __subscriptions:__ read-only data sources that emit `actions`
|
- __subscriptions:__ read-only data sources that emit `actions`
|
||||||
- __effects:__ asynchronous functions that emit an `action` when done
|
- __effects:__ asynchronous functions that emit an `action` when done
|
||||||
- __reducers:__ synchronous functions that modify `state`
|
- __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
|
## Effects
|
||||||
Side effects are done through `effects` declared in `app.model()`. Unlike
|
Side effects are done through `effects` declared in `app.model()`. Unlike
|
||||||
`reducers` they cannot modify the state by returning objects, but get a
|
`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`:
|
that weighs only `2.4kb`:
|
||||||
```js
|
```js
|
||||||
const http = require('choo/http')
|
const http = require('choo/http')
|
||||||
|
const choo = require('choo')
|
||||||
|
const app = choo()
|
||||||
|
|
||||||
// GET JSON
|
app.model({
|
||||||
http.get('/my-endpoint', { json: true }, function (err, res, body) {
|
effects: {
|
||||||
if (err) throw err
|
'app:error': (state, event_ => console.error(`error: ${event.payload}`)),
|
||||||
if (res.statusCode !== 200 || !body) throw new Error('something went wrong')
|
'app:print': (state, event) => console.log(`http: ${event.payload}`),
|
||||||
|
'http:get_json': getJson,
|
||||||
|
'http:post_json': postJson,
|
||||||
|
'http:delete': httpDelete
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// POST JSON
|
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 })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function postJson (state, action, send) {
|
||||||
const body = { foo: 'bar' }
|
const body = { foo: 'bar' }
|
||||||
http.post('/my-endpoint', { json: body }, function (err, res, body) {
|
http.post('/my-endpoint', { json: body }, function (err, res, body) {
|
||||||
if (err) throw err
|
if (err) return send('app:error', { payload: err.message })
|
||||||
if (res.statusCode !== 200 || !body) throw new Error('something went wrong')
|
if (res.statusCode !== 200 || !body) {
|
||||||
|
return send('app:error', { payload:'something went wrong' })
|
||||||
|
}
|
||||||
|
send('app:print', { payload: body })
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// DELETE
|
function httpDelete (state, action, send) {
|
||||||
http.del('/my-endpoint', function (err, res) {
|
const body = { foo: 'bar' }
|
||||||
if (err) throw err
|
http.post('/my-endpoint', { json: body }, function (err, res, body) {
|
||||||
if (res.statusCode !== 200) throw new Error('something went wrong')
|
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
|
Note that `http` only runs in the browser to prevent accidental requests when
|
||||||
rendering in Node. For more details view the [`raynos/xhr`
|
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
|
listening for events from a server using `SSE` or `Websockets` for a
|
||||||
chat app, or when catching keyboard input for a videogame.
|
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)
|
||||||
[Server Sent Events (SSE)][sse] allow servers to push data to the browser.
|
[Server Sent Events (SSE)][sse] allow servers to push data to the browser.
|
||||||
They're the unidirectional cousin of `websockets` and compliment `HTTP`
|
They're the unidirectional cousin of `websockets` and compliment `HTTP`
|
||||||
@@ -176,17 +253,20 @@ const stream = new document.EventSource('/sse')
|
|||||||
app.model({
|
app.model({
|
||||||
subscriptions: [
|
subscriptions: [
|
||||||
function (send) {
|
function (send) {
|
||||||
stream.onerror = (e) => send('error', { payload: JSON.stringify(e) })
|
stream.onerror = (e) => send('app:error', { payload: JSON.stringify(e) })
|
||||||
stream.onmessage = (e) => send('print', { payload: e.data })
|
stream.onmessage = (e) => send('app:print', { payload: e.data })
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
effects: {
|
effects: {
|
||||||
'sse:close': () => stream.close()
|
'sse:close': () => stream.close()
|
||||||
error: (state, event_ => console.error(`error: ${event.payload}`)),
|
'app:error': (state, event_ => console.error(`error: ${event.payload}`)),
|
||||||
print: (state, event) => console.log(`pressed key num: ${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
|
### Keyboard
|
||||||
Most browsers have [basic support for keyboard events][keyboard-support]. To
|
Most browsers have [basic support for keyboard events][keyboard-support]. To
|
||||||
@@ -195,11 +275,11 @@ capture keyboard events, setup a `subscription`:
|
|||||||
app.model({
|
app.model({
|
||||||
subscriptions: [
|
subscriptions: [
|
||||||
function (send) {
|
function (send) {
|
||||||
keyboard.onkeypress = (e) => send('print', { payload: e.keyCode })
|
keyboard.onkeypress = (e) => send('app:print', { payload: e.keyCode })
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
effects: {
|
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({
|
app.model({
|
||||||
subscriptions: [
|
subscriptions: [
|
||||||
function (send) {
|
function (send) {
|
||||||
socket.onerror = (e) => send('error', { payload: JSON.stringify(e) })
|
socket.onerror = (e) => send('app:error', { payload: JSON.stringify(e) })
|
||||||
socket.onmessage = (e) => send('print', { payload: e.data })
|
socket.onmessage = (e) => send('app:print', { payload: e.data })
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
effects: {
|
effects: {
|
||||||
'ws:close': () => socket.close(),
|
'ws:close': () => socket.close(),
|
||||||
'ws:send': (state, event) => socket.send(JSON.stringify(event.payload)),
|
'ws:send': (state, event) => socket.send(JSON.stringify(event.payload)),
|
||||||
error: (state, event_ => console.error(`error: ${event.payload}`)),
|
'app:error': (state, event_ => console.error(`error: ${event.payload}`)),
|
||||||
print: (state, event) => console.log(`pressed key num: ${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
|
## Rendering in Node
|
||||||
Sometimes it's necessary to render code inside of Node; for serving first
|
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()
|
### app = choo()
|
||||||
Create a new `choo` app
|
Create a new `choo` app
|
||||||
|
|
||||||
### app.model(name?, obj)
|
### app.model(obj)
|
||||||
Create a new model. Models modify data and perform IO. Obj takes the following
|
Create a new model. Models modify data and perform IO. Takes the following
|
||||||
arguments:
|
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
|
- __state:__ object. Key value store of initial values
|
||||||
- __reducers:__ object. Syncronous functions that modify state. Each function
|
- __reducers:__ object. Syncronous functions that modify state. Each function
|
||||||
has a signature of `(action, state)`
|
has a signature of `(action, state)`
|
||||||
@@ -283,10 +369,6 @@ arguments:
|
|||||||
a signature of `(action, state, send)` where `send` is a reference to
|
a signature of `(action, state, send)` where `send` is a reference to
|
||||||
`app.send()`
|
`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\`
|
### choo.view\`html\`
|
||||||
Tagged template string HTML builder. See
|
Tagged template string HTML builder. See
|
||||||
[`yo-yo`](https://github.com/maxogden/yo-yo) for full documentation. Views
|
[`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.
|
accordingly.
|
||||||
|
|
||||||
## FAQ
|
## 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?
|
### How does choo compare to X?
|
||||||
Ah, so this is where I get to rant. `choo` (_chugga-chugga-chugga-choo-choo!_)
|
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
|
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
|
- [uglifyify](https://github.com/hughsk/uglifyify) - minify your code using
|
||||||
UglifyJS2. Use as a `--global` transform
|
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?
|
### Is it production ready?
|
||||||
Sure.
|
Sure.
|
||||||
|
|
||||||
@@ -390,6 +502,11 @@ Sure.
|
|||||||
$ npm install choo
|
$ 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
|
## License
|
||||||
[MIT](https://tldrlegal.com/license/mit-license)
|
[MIT](https://tldrlegal.com/license/mit-license)
|
||||||
|
|
||||||
@@ -416,3 +533,5 @@ $ npm install choo
|
|||||||
[morphdom]: https://github.com/patrick-steele-idem/morphdom
|
[morphdom]: https://github.com/patrick-steele-idem/morphdom
|
||||||
[morphdom-bench]: https://github.com/patrick-steele-idem/morphdom#benchmarks
|
[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
|
[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
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const mainView = require('./views/main')
|
|||||||
const app = choo()
|
const app = choo()
|
||||||
|
|
||||||
app.model(require('./models/error'))
|
app.model(require('./models/error'))
|
||||||
app.model('api', require('./models/api'))
|
app.model(require('./models/api'))
|
||||||
|
|
||||||
app.router((route) => [
|
app.router((route) => [
|
||||||
route('/', mainView)
|
route('/', mainView)
|
||||||
|
|||||||
+10
-24
@@ -1,45 +1,31 @@
|
|||||||
const http = require('../../../http')
|
const http = require('../../../http')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
namespace: 'api',
|
||||||
state: {
|
state: {
|
||||||
title: 'Button pushing machine 3000'
|
title: 'Button pushing machine 3000'
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
set: function (action, state) {
|
set: (action, state) => ({ 'title': action.payload })
|
||||||
return { 'api:title': action.payload }
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
effects: {
|
effects: {
|
||||||
good: performGoodRequest,
|
good: (action, state, send) => request('/good', send),
|
||||||
bad: performBadRequest
|
bad: (action, state, send) => request('/bad', send)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function performGoodRequest (action, state, send) {
|
function request (uri, send, state) {
|
||||||
http('/good', { json: true }, function (err, res, body) {
|
http(uri, { json: true }, function (err, res, body) {
|
||||||
if (err) return send('error', { payload: 'HTTP error' })
|
if (err) return send('app: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' })
|
|
||||||
if (res.statusCode !== 200) {
|
if (res.statusCode !== 200) {
|
||||||
const message = (body && body.message)
|
const message = (body && body.message)
|
||||||
? body.message
|
? body.message
|
||||||
: 'unknown server error'
|
: 'unknown server error'
|
||||||
return send('error', { payload: message })
|
return send('app:error', { payload: message })
|
||||||
}
|
}
|
||||||
if (!body) {
|
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 })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
const ERROR_TIMEOUT = 1000
|
const ERROR_TIMEOUT = 1000
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
namespace: 'app',
|
||||||
state: {
|
state: {
|
||||||
error: [],
|
error: [],
|
||||||
errorTimeDone: null,
|
errorTimeDone: null,
|
||||||
@@ -35,7 +36,7 @@ module.exports = {
|
|||||||
error: function (action, state, send) {
|
error: function (action, state, send) {
|
||||||
const timeout = state.errorTimeDone - Date.now()
|
const timeout = state.errorTimeDone - Date.now()
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
send('error:delete')
|
send('app:error:delete')
|
||||||
}, timeout)
|
}, timeout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const http = require('http')
|
|||||||
const PORT = 8080
|
const PORT = 8080
|
||||||
|
|
||||||
const server = http.createServer(createRouter())
|
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
|
var index = 0
|
||||||
const errors = [
|
const errors = [
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
const choo = require('../../../')
|
const choo = require('../../../')
|
||||||
|
|
||||||
module.exports = function (params, state, send) {
|
module.exports = function (params, state, send) {
|
||||||
|
const error = state.app.error[0]
|
||||||
|
const title = state.api.title
|
||||||
return choo.view`
|
return choo.view`
|
||||||
<section>
|
<section>
|
||||||
<h1>${state['api:title']}</h1>
|
<h1>${title}</h1>
|
||||||
<h2>Latest error: ${state.error[0]}</h2>
|
<h2>Latest error: ${error}</h2>
|
||||||
<button onclick=${(e) => send('api:good')}>OK!</button>
|
<button onclick=${(e) => send('api:good')}>OK!</button>
|
||||||
<button onclick=${(e) => send('api:bad')}>Naughty</button>
|
<button onclick=${(e) => send('api:bad')}>Naughty</button>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ sf('tachyons')
|
|||||||
|
|
||||||
const app = choo()
|
const app = choo()
|
||||||
|
|
||||||
app.model('inbox', require('./models/inbox'))
|
app.model(require('./models/inbox'))
|
||||||
app.model('spam', require('./models/spam'))
|
app.model(require('./models/spam'))
|
||||||
app.model('sent', require('./models/sent'))
|
app.model(require('./models/sent'))
|
||||||
|
|
||||||
app.router((route) => [
|
app.router((route) => [
|
||||||
route('/', require('./views/empty')),
|
route('/', require('./views/empty')),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const choo = require('../../../')
|
|||||||
|
|
||||||
module.exports = function (params, state, send) {
|
module.exports = function (params, state, send) {
|
||||||
const mailbox = params.mailbox
|
const mailbox = params.mailbox
|
||||||
const messages = state[mailbox + ':messages']
|
const messages = state[mailbox].messages
|
||||||
return choo.view`
|
return choo.view`
|
||||||
<div>
|
<div>
|
||||||
<div class="db cf w-100">
|
<div class="db cf w-100">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ module.exports = function (params, state, send) {
|
|||||||
const mailbox = params.mailbox
|
const mailbox = params.mailbox
|
||||||
const message = params.message
|
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
|
return String(msg.id) === message
|
||||||
})[0]
|
})[0]
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ module.exports = function () {
|
|||||||
return function (params, state, send) {
|
return function (params, state, send) {
|
||||||
const mailbox = params.mailbox
|
const mailbox = params.mailbox
|
||||||
const message = params.message
|
const message = params.message
|
||||||
|
const messages = state[mailbox].messages
|
||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
const email = state[mailbox + ':messages'].filter(function (msg) {
|
const email = state[mailbox].messages.filter(function (msg) {
|
||||||
return String(msg.id) === message
|
return String(msg.id) === message
|
||||||
})[0]
|
})[0]
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ module.exports = function () {
|
|||||||
<section class="fl mt4 w-80 db">
|
<section class="fl mt4 w-80 db">
|
||||||
<div>
|
<div>
|
||||||
${createHeader()}
|
${createHeader()}
|
||||||
${state[mailbox + ':messages'].map(function (msg) {
|
${messages.map(function (msg) {
|
||||||
return createMessage(msg, mailbox)
|
return createMessage(msg, mailbox)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -28,7 +29,7 @@ module.exports = function () {
|
|||||||
return choo.view`
|
return choo.view`
|
||||||
<section class="fl mt4 w-80 db">
|
<section class="fl mt4 w-80 db">
|
||||||
${createHeader()}
|
${createHeader()}
|
||||||
${state[mailbox + ':messages'].map(function (msg) {
|
${messages.map(function (msg) {
|
||||||
return createMessage(msg, mailbox)
|
return createMessage(msg, mailbox)
|
||||||
})}
|
})}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -3,16 +3,15 @@ const choo = require('../../../')
|
|||||||
const mailboxes = [ 'inbox', 'spam', 'sent' ]
|
const mailboxes = [ 'inbox', 'spam', 'sent' ]
|
||||||
|
|
||||||
module.exports = function (params, state, send) {
|
module.exports = function (params, state, send) {
|
||||||
const mailbox = params.mailbox
|
|
||||||
|
|
||||||
return choo.view`
|
return choo.view`
|
||||||
<aside class="fl mt4 w-20 db">
|
<aside class="fl mt4 w-20 db">
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<h2 class="f4 b lh0">Mailbox</h2>
|
<h2 class="f4 b lh0">Mailbox</h2>
|
||||||
</li>
|
</li>
|
||||||
${mailboxes.map(function (name) {
|
${mailboxes.map(function (mailbox) {
|
||||||
return createLi(name, state[name + ':messages'], mailbox)
|
const messages = mailbox.messages
|
||||||
|
return createLi(mailbox, messages, mailbox)
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ const pathname = require('pathname-match')
|
|||||||
const choo = require('../../../')
|
const choo = require('../../../')
|
||||||
|
|
||||||
module.exports = function (params, state, send) {
|
module.exports = function (params, state, send) {
|
||||||
|
const location = state.app.location
|
||||||
return choo.view`
|
return choo.view`
|
||||||
<span class="fl mt4 w-100 f4 b">
|
<span class="fl mt4 w-100 f4 b">
|
||||||
URL: ${pathname(state.location) || '/'}
|
URL: ${pathname(location) || '/'}
|
||||||
</span>
|
</span>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
|
namespace: 'inbox',
|
||||||
state: {
|
state: {
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
|
namespace: 'sent',
|
||||||
state: {
|
state: {
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
|
namespace: 'spam',
|
||||||
state: {
|
state: {
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const http = require('http')
|
|||||||
const PORT = 8080
|
const PORT = 8080
|
||||||
|
|
||||||
const server = http.createServer(createRouter())
|
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 () {
|
function createRouter () {
|
||||||
const router = serverRouter('/404')
|
const router = serverRouter('/404')
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -1,26 +1,31 @@
|
|||||||
const choo = require('../')
|
const choo = require('../../')
|
||||||
|
|
||||||
const app = choo()
|
const app = choo()
|
||||||
app.model('title', {
|
app.model({
|
||||||
state: { title: 'my-demo-app' },
|
namespace: 'input',
|
||||||
|
state: {
|
||||||
|
title: 'my demo app'
|
||||||
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
'update': (action, state) => ({ title: action.payload })
|
update: (action, state) => ({ title: action.payload })
|
||||||
},
|
},
|
||||||
effects: {
|
effects: {
|
||||||
'update': (action, state, send) => (document.title = action.payload)
|
update: (action, state, send) => (document.title = action.payload)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const mainView = (params, state, send) => choo.view`
|
const mainView = (params, state, send) => {
|
||||||
|
return choo.view`
|
||||||
<main class="app">
|
<main class="app">
|
||||||
<h1>${state.title}</h1>
|
<h1>${state.input.title}</h1>
|
||||||
<label>Set the title</label>
|
<label>Set the title</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder=${state.title}
|
placeholder=${state.input.title}
|
||||||
oninput=${(e) => send('title:update', { payload: e.target.value })}>
|
oninput=${(e) => send('input:update', { payload: e.target.value })}>
|
||||||
</main>
|
</main>
|
||||||
`
|
`
|
||||||
|
}
|
||||||
|
|
||||||
app.router((route) => [
|
app.router((route) => [
|
||||||
route('/', mainView)
|
route('/', mainView)
|
||||||
|
|||||||
@@ -3,17 +3,16 @@ const sheetRouter = require('sheet-router')
|
|||||||
const document = require('global/document')
|
const document = require('global/document')
|
||||||
const href = require('sheet-router/href')
|
const href = require('sheet-router/href')
|
||||||
const sendAction = require('send-action')
|
const sendAction = require('send-action')
|
||||||
|
const assert = require('assert')
|
||||||
const xtend = require('xtend')
|
const xtend = require('xtend')
|
||||||
const yo = require('yo-yo')
|
const yo = require('yo-yo')
|
||||||
|
|
||||||
choo.view = yo
|
choo.view = yo
|
||||||
module.exports = choo
|
module.exports = choo
|
||||||
|
|
||||||
// A framework for creating sturdy web applications
|
// framework for creating sturdy web applications
|
||||||
// null -> fn
|
// null -> fn
|
||||||
function choo (opts) {
|
function choo () {
|
||||||
opts = opts || {}
|
|
||||||
const name = opts.name || 'choo'
|
|
||||||
const _models = []
|
const _models = []
|
||||||
var _router = null
|
var _router = null
|
||||||
|
|
||||||
@@ -25,6 +24,7 @@ function choo (opts) {
|
|||||||
return start
|
return start
|
||||||
|
|
||||||
// render the application to a string
|
// render the application to a string
|
||||||
|
// (str, obj) -> str
|
||||||
function toString (route, state) {
|
function toString (route, state) {
|
||||||
const initialState = {}
|
const initialState = {}
|
||||||
|
|
||||||
@@ -40,71 +40,102 @@ function choo (opts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// start the application
|
// start the application
|
||||||
// null -> DOMNode
|
// obj -> DOMNode
|
||||||
function start () {
|
function start (opts) {
|
||||||
|
opts = opts || {}
|
||||||
|
const name = opts.name || 'choo'
|
||||||
const initialState = {}
|
const initialState = {}
|
||||||
const reducers = {}
|
const reducers = {}
|
||||||
const effects = {}
|
const effects = {}
|
||||||
|
|
||||||
_models.push(appInit(opts))
|
_models.push(appInit(opts))
|
||||||
_models.forEach(function (model) {
|
_models.forEach(function (model) {
|
||||||
if (model.state) apply(model.name, model.state, initialState)
|
if (model.state) apply(model.namespace, model.state, initialState)
|
||||||
if (model.reducers) apply(model.name, model.reducers, reducers)
|
if (model.reducers) apply(model.namespace, model.reducers, reducers)
|
||||||
if (model.effects) apply(model.name, model.effects, effects)
|
if (model.effects) apply(model.namespace, model.effects, effects)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// send() is used to trigger actions inside
|
||||||
|
// views, effects and subscriptions
|
||||||
const send = sendAction({
|
const send = sendAction({
|
||||||
onaction: handleAction,
|
onaction: handleAction,
|
||||||
onchange: onchange,
|
onchange: onchange,
|
||||||
state: initialState
|
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) {
|
_models.forEach(function (model) {
|
||||||
if (model.subscriptions) {
|
if (model.subscriptions) {
|
||||||
|
assert.ok(Array.isArray(model.subscriptions, 'subs must be an array'))
|
||||||
model.subscriptions.forEach(function (sub) {
|
model.subscriptions.forEach(function (sub) {
|
||||||
sub(send)
|
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 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)
|
tree.setAttribute('id', rootId)
|
||||||
return tree
|
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) {
|
function handleAction (action, state, send) {
|
||||||
var _reducers = false
|
var reducersCalled = false
|
||||||
var _effects = false
|
var effectsCalled = false
|
||||||
var newState = null
|
|
||||||
|
|
||||||
if (reducers[action.type]) {
|
// validate if a namespace exists. Namespaces
|
||||||
newState = xtend(state, reducers[action.type](action, state))
|
// are delimited by the first ':'. Perhaps
|
||||||
_reducers = true
|
// 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]) {
|
const _reducers = ns ? reducers[ns] : reducers
|
||||||
effects[action.type](action, newState || state, send)
|
if (_reducers && _reducers[action.type]) {
|
||||||
newState = newState || state
|
if (ns) {
|
||||||
_effects = true
|
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)
|
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) {
|
function onchange (action, state) {
|
||||||
const oldTree = document.querySelector('#' + rootId)
|
const oldTree = document.querySelector('#' + rootId)
|
||||||
const newTree = _router(state.location, state, send)
|
const newTree = _router(state.app.location, state, send)
|
||||||
newTree.setAttribute('id', rootId)
|
newTree.setAttribute('id', rootId)
|
||||||
yo.update(oldTree, newTree)
|
yo.update(oldTree, newTree)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// register all routes
|
// register all routes on the router
|
||||||
// [obj|fn] -> null
|
// [obj|fn] -> null
|
||||||
function router (cb) {
|
function router (cb) {
|
||||||
_router = sheetRouter(cb)
|
_router = sheetRouter(cb)
|
||||||
@@ -113,9 +144,7 @@ function choo (opts) {
|
|||||||
|
|
||||||
// create a new model
|
// create a new model
|
||||||
// (str?, obj) -> null
|
// (str?, obj) -> null
|
||||||
function model (name, model) {
|
function model (model) {
|
||||||
if (!model) model = name
|
|
||||||
if (typeof name === 'string') model.name = name
|
|
||||||
_models.push(model)
|
_models.push(model)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,45 +153,47 @@ function choo (opts) {
|
|||||||
// obj -> obj
|
// obj -> obj
|
||||||
function appInit (opts) {
|
function appInit (opts) {
|
||||||
const model = {
|
const model = {
|
||||||
state: {
|
namespace: 'app',
|
||||||
location: document.location.href
|
state: { location: document.location.href },
|
||||||
},
|
subscriptions: [],
|
||||||
reducers: {
|
reducers: {
|
||||||
location: setLocation
|
// handle href links
|
||||||
},
|
location: function setLocation (action, state) {
|
||||||
subscriptions: []
|
return {
|
||||||
|
location: action.location.replace(/#.*/, '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.href !== false) {
|
// enable catching <href a=""></href> links
|
||||||
model.subscriptions.push(function (send) {
|
// enable HTML5 history API
|
||||||
href(function (href) {
|
if (opts.history !== false) pushLocationSub(href)
|
||||||
send('location', { location: href })
|
if (opts.history !== false) pushLocationSub(history)
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.history !== false) {
|
|
||||||
model.subscriptions.push(function (send) {
|
|
||||||
history(function (href) {
|
|
||||||
send('location', { location: href })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return model
|
return model
|
||||||
|
|
||||||
// handle href links
|
// create a new subscription that modifies
|
||||||
function setLocation (action, state) {
|
// 'app:location' and push it to be loaded
|
||||||
const location = action.location.replace(/#.*/, '')
|
// fn -> null
|
||||||
return xtend(state, { location: location })
|
function pushLocationSub (cb) {
|
||||||
|
model.subscriptions.push(function (send) {
|
||||||
|
cb(function (href) {
|
||||||
|
send('app:location', { location: href })
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// compose an object conditionally
|
// compose an object conditionally
|
||||||
|
// optionally contains a namespace
|
||||||
|
// which is used to nest properties.
|
||||||
// (str, obj, obj) -> null
|
// (str, obj, obj) -> null
|
||||||
function apply (name, source, target) {
|
function apply (ns, source, target) {
|
||||||
Object.keys(source).forEach(function (key) {
|
Object.keys(source).forEach(function (key) {
|
||||||
if (name) target[name + ':' + key] = source[key]
|
if (ns) {
|
||||||
else target[key] = source[key]
|
if (!target[ns]) target[ns] = {}
|
||||||
|
target[ns][key] = source[key]
|
||||||
|
} else target[key] = source[key]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ const http = require('http')
|
|||||||
const PORT = 8080
|
const PORT = 8080
|
||||||
|
|
||||||
const server = http.createServer(createRouter())
|
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 () {
|
function createRouter () {
|
||||||
const router = serverRouter('/404')
|
const router = serverRouter('/404')
|
||||||
|
|||||||
Reference in New Issue
Block a user