diff --git a/README.md b/README.md
index 73254c8..a527068 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,7 @@ productive package.
- [keyboard](#keyboard)
- [websockets](#websockets)
- [Rendering in Node](#rendering-in-node)
+ - [Rehydration](#rehydration)
- [API](#api)
- [FAQ](#faq)
- [Installation](#installation)
@@ -310,9 +311,10 @@ 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
-requests, testing or other purposes. Applications that are capable of being
-rendered in both Node and the browser are called _[isomorphic][isomorphic]_.
+Sometimes it's necessary to render code inside of Node; for serving hyper fast
+first requests, testing or other purposes. Applications that are capable of
+being rendered in both Node and the browser are called
+_[isomorphic][isomorphic]_.
Rendering in Node is slightly different than in the browser. First off, to
maintain performance all calls to `subscriptions`, `effects`, and `reducers`
@@ -352,6 +354,45 @@ if (module.parent) module.exports = app
else document.body.appendChild(app.start())
```
+### Rehydration
+Now that your application is succesfully rendering in Node, the next step would
+be to make it load a JavaScript bundle once has loaded the HTML. To do this we
+will use a technique called _rehydration_.
+
+_Rehydration_ is when you take the static, server-rendered version of your
+application (static HTML, _dehydrated_ because it has no logic) and _rehydrate_
+it by booting up the JS and attaching event handlers on the DOM to make it
+dynamic again. It's like restoring flavor to cup noodles by adding hot water.
+
+Because we're using something called `morphdom` under the hood, all we need is
+point at an `id` at the root of the application. The syntax for this is
+slightly different from what we've seen so far, because we're _updating_ a
+dehydrated DOM nodes to make them dynamic, rather than a new DOM tree and
+attaching it to the DOM.
+```js
+const choo = require('choo')
+const app = choo()
+
+app.router((route) => [
+ route('/', (params, state, send) => choo.view`
+
${state.message}
+ `)
+])
+
+if (module.parent) module.exports = app
+else app.start('#app-root'))
+```
+
+When the JS is booted on top of the dehydrated application, it will look for
+the `#app-root` id and load on top of it. You can choose any name you like for
+the id, but __make sure it's the same on every possible top level DOM node__,
+or else things might break. Furthermore to ensure things go smoothly, try and
+keep the initial state identical on both the server and the client.
+
+And that's it! If you want to go down the route of mad performance, consider
+make all first request static and caching them using something like [bl][bl],
+[nginx][nginx], [varnish][varnish] or a global CDN.
+
## API
### app = choo()
Create a new `choo` app
@@ -404,7 +445,7 @@ 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
+itself is super petite (`~200` 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.
@@ -535,3 +576,6 @@ $ npm install choo
[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
+[bl]: https://github.com/rvagg/bl
+[varnish]: https://varnish-cache.org
+[nginx]: http://nginx.org/
diff --git a/examples/server-rendering/client.js b/examples/server-rendering/client.js
index 16d09b9..7e8283c 100644
--- a/examples/server-rendering/client.js
+++ b/examples/server-rendering/client.js
@@ -4,6 +4,14 @@ const mainView = require('./views/main')
const app = choo()
+app.model({
+ namespace: 'message',
+ state: {
+ server: 'rehydration has kicked in, server data was tossed',
+ client: 'hello client!'
+ }
+})
+
app.router((route) => [
route('/', mainView)
])
@@ -11,6 +19,5 @@ app.router((route) => [
if (module.parent) {
module.exports = app
} else {
- const tree = app.start()
- document.body.appendChild(tree)
+ app.start('#app-root')
}
diff --git a/examples/server-rendering/package.json b/examples/server-rendering/package.json
index 687cd91..5c6d6f5 100644
--- a/examples/server-rendering/package.json
+++ b/examples/server-rendering/package.json
@@ -7,7 +7,9 @@
},
"dependencies": {
"bankai": "^2.0.2",
+ "bl": "^1.1.2",
"browserify": "^13.0.1",
+ "hyperstream": "^1.2.2",
"server-router": "^2.1.0"
}
}
diff --git a/examples/server-rendering/server.js b/examples/server-rendering/server.js
index 93e138f..2a22276 100644
--- a/examples/server-rendering/server.js
+++ b/examples/server-rendering/server.js
@@ -1,4 +1,7 @@
const serverRouter = require('server-router')
+const hyperstream = require('hyperstream')
+const browserify = require('browserify')
+const bankai = require('bankai')
const http = require('http')
const PORT = 8080
@@ -17,18 +20,26 @@ server.listen(PORT, () => process.stdout.write(`listening on port ${PORT}\n`))
// 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' ] }))
- })
+
+ apiRouter.on('/404', (req, res) => res.end('404 not found'))
+ apiRouter.on('/', (req, res) => res.end('nothing to be found here'))
+
+ const js = bankai.js(browserify, require.resolve('./client.js'))
+ apiRouter.on('/bundle.js', (req, res) => js(req, res).pipe(res))
+
return apiRouter
}
// render the client to string
// based on the requested url
// (obj, obj) -> null
+const createIndex = bankai.html({ favicon: false, css: false })
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)
+
+ const state = { message: { server: 'hello server!' } }
+ const inner = client.toString(req.url, state)
+ const hs = hyperstream({ 'body': { _appendHtml: inner } })
+
+ createIndex(req, res).pipe(hs).pipe(res)
}
diff --git a/examples/server-rendering/views/main.js b/examples/server-rendering/views/main.js
index c0dd659..c038c87 100644
--- a/examples/server-rendering/views/main.js
+++ b/examples/server-rendering/views/main.js
@@ -1,10 +1,25 @@
+const assert = require('assert')
const choo = require('../../../')
module.exports = function (params, state, send) {
- const message = state.message
+ const serverMessage = state.message.server
+ const clientMessage = state.message.client
+
+ assert.equal(typeof serverMessage, 'string', 'server should be a string')
+ assert.equal(typeof clientMessage, 'string', 'server should be a string')
+
return choo.view`
-
- ${message}
+
+ server message: ${serverMessage}
+ client message: ${clientMessage}
+ ${`
+ The first message is passed in by the server on compile time,
+ the second message was set by the client.
+ The more static the data you pass in, the more cachable your site
+ beocmes (and thus performant). Try and keep the amount of properties
+ you pass in on the server to a minimum for most applications - it'll
+ make life a lot easier in the long run, hah.
+ `}
`
}
diff --git a/index.js b/index.js
index 62e0718..67f4f50 100644
--- a/index.js
+++ b/index.js
@@ -25,14 +25,23 @@ function choo () {
// render the application to a string
// (str, obj) -> str
- function toString (route, state) {
+ function toString (route, serverState) {
const initialState = {}
+ const nsState = {}
_models.forEach(function (model) {
- if (model.state) apply(model.name, model.state, initialState)
+ const ns = model.namespace
+ if (ns) {
+ if (!nsState[ns]) nsState[ns] = {}
+ apply(ns, model.state, nsState)
+ nsState[ns] = xtend(nsState[ns], serverState[ns])
+ } else {
+ apply(model.namespace, model.state, initialState)
+ }
})
- const tree = _router(route, xtend(initialState, state), function () {
+ const state = xtend(initialState, xtend(serverState, nsState))
+ const tree = _router(route, state, function () {
throw new Error('send() cannot be called on the server')
})
@@ -40,8 +49,9 @@ function choo () {
}
// start the application
- // obj -> DOMNode
- function start (opts) {
+ // (str?, obj?) -> DOMNode
+ function start (rootId, opts) {
+ if (!opts) opts = rootId
opts = opts || {}
const name = opts.name || 'choo'
const initialState = {}
@@ -75,15 +85,32 @@ function choo () {
}
})
- // the rootId is determined to find the application root
+ // If an id is provided, the application will rehydrate
+ // on the node. If no id is provided it will return
+ // a tree that's ready to be appended to the DOM.
+ //
+ // 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
+ if (rootId) {
+ document.addEventListener('DOMContentLoaded', function (event) {
+ rootId = rootId.replace(/^#/, '')
+
+ const oldTree = document.querySelector('#' + rootId)
+ assert.ok(oldTree, 'could not find node #' + rootId)
+
+ const newTree = _router(send.state().app.location, send.state(), send)
+
+ yo.update(oldTree, newTree)
+ })
+ } else {
+ 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
diff --git a/test.js b/test.js
deleted file mode 100644
index 0fe45ac..0000000
--- a/test.js
+++ /dev/null
@@ -1,7 +0,0 @@
-const test = require('tape')
-const choo = require('./')
-
-test('should assert input types', function (t) {
- t.plan(1)
- t.throws(choo)
-})
diff --git a/tests/index.js b/tests/index.js
index e71a307..e9bbe8f 100644
--- a/tests/index.js
+++ b/tests/index.js
@@ -30,7 +30,7 @@ tape('should render on the server', function (t) {
t.equal(html, expected, 'strings are equal')
})
- t.test('should extend existing models', function (t) {
+ t.test('should extend flat existing models', function (t) {
t.plan(1)
const app = choo()
@@ -47,6 +47,33 @@ tape('should render on the server', function (t) {
t.equal(html, expected, 'strings are equal')
})
+ t.test('should extend namespaced existing models', function (t) {
+ t.plan(1)
+
+ const app = choo()
+ app.model({
+ namespace: 'hello',
+ state: { bin: 'baz', beep: 'boop' }
+ })
+ app.router((route) => [
+ route('/', function (params, state) {
+ return choo.view`
+ ${state.hello.foo} ${state.hello.bin} ${state.hello.beep}
+ `
+ })
+ ])
+
+ const state = {
+ hello: {
+ foo: 'bar!',
+ beep: 'beep'
+ }
+ }
+ const html = app.toString('/', state)
+ const expected = 'bar! baz beep
'
+ t.equal(html, expected, 'strings are equal')
+ })
+
t.test('should throw if called without route', function (t) {
t.plan(1)