Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Asynchronous render #646

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion html/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,30 @@
module.exports = require('bel')
var bel = require('bel')

function isPromise (elem) {
return elem && typeof elem.then === 'function'
}

function pWrap (array) {
var wrap = false
var result = array.map(function (elem) {
if (Array.isArray(elem)) { elem = pWrap(elem) }
if (isPromise(elem)) { wrap = true }
return elem
})
if (wrap) {
// NOTE: `Promise` will only be used if there is a promise within the tree, up to the dev to shim it
return Promise.all(result)
}
return result
}

function html (strings) {
var keys = Array.prototype.slice.call(arguments, 1)
var pKeys = pWrap(keys)
if (isPromise(pKeys)) {
return pKeys.then(function (rKeys) { return bel.apply(null, [strings].concat(rKeys)) })
}
return bel.apply(null, [strings].concat(pKeys))
}

module.exports = html
96 changes: 57 additions & 39 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ module.exports = Choo

var HISTORY_OBJECT = {}

function resolve (p, cb) {
if (p && typeof p.then === 'function') {
return p.then(cb)
}
return cb(p)
}

function Choo (opts) {
if (!(this instanceof Choo)) return new Choo(opts)
opts = opts || {}
Expand Down Expand Up @@ -132,32 +139,37 @@ Choo.prototype.start = function () {
initStore()
})

this._matchRoute()
this._tree = this._prerender(this.state)
assert.ok(this._tree, 'choo.start: no valid DOM node returned for location ' + this.state.href)

this.emitter.prependListener(self._events.RENDER, nanoraf(function () {
var renderTiming = nanotiming('choo.render')
var newTree = self._prerender(self.state)
assert.ok(newTree, 'choo.render: no valid DOM node returned for location ' + self.state.href)
var pNewTree = self._prerender(self.state)
resolve(pNewTree, function (newTree) {
assert.ok(newTree, 'choo.render: no valid DOM node returned for location ' + self.state.href)

assert.equal(self._tree.nodeName, newTree.nodeName, 'choo.render: The target node <' +
self._tree.nodeName.toLowerCase() + '> is not the same type as the new node <' +
newTree.nodeName.toLowerCase() + '>.')
assert.equal(self._tree.nodeName, newTree.nodeName, 'choo.render: The target node <' +
self._tree.nodeName.toLowerCase() + '> is not the same type as the new node <' +
newTree.nodeName.toLowerCase() + '>.')

var morphTiming = nanotiming('choo.morph')
nanomorph(self._tree, newTree)
morphTiming()
var morphTiming = nanotiming('choo.morph')
nanomorph(self._tree, newTree)
morphTiming()

renderTiming()
renderTiming()
})
}))

documentReady(function () {
self.emitter.emit(self._events.DOMCONTENTLOADED)
self._loaded = true
})
this._matchRoute()
var pTree = this._prerender(this.state)
return resolve(pTree, function (tree) {
self._tree = tree
assert.ok(self._tree, 'choo.start: no valid DOM node returned for location ' + self.state.href)

documentReady(function () {
self.emitter.emit(self._events.DOMCONTENTLOADED)
self._loaded = true
})

return this._tree
return self._tree
})
}

Choo.prototype.mount = function mount (selector) {
Expand All @@ -173,23 +185,25 @@ Choo.prototype.mount = function mount (selector) {

documentReady(function () {
var renderTiming = nanotiming('choo.render')
var newTree = self.start()
if (typeof selector === 'string') {
self._tree = document.querySelector(selector)
} else {
self._tree = selector
}
var pNewTree = self.start()
resolve(pNewTree, function (newTree) {
if (typeof selector === 'string') {
self._tree = document.querySelector(selector)
} else {
self._tree = selector
}

assert.ok(self._tree, 'choo.mount: could not query selector: ' + selector)
assert.equal(self._tree.nodeName, newTree.nodeName, 'choo.mount: The target node <' +
self._tree.nodeName.toLowerCase() + '> is not the same type as the new node <' +
newTree.nodeName.toLowerCase() + '>.')
assert.ok(self._tree, 'choo.mount: could not query selector: ' + selector)
assert.equal(self._tree.nodeName, newTree.nodeName, 'choo.mount: The target node <' +
self._tree.nodeName.toLowerCase() + '> is not the same type as the new node <' +
newTree.nodeName.toLowerCase() + '>.')

var morphTiming = nanotiming('choo.morph')
nanomorph(self._tree, newTree)
morphTiming()
var morphTiming = nanotiming('choo.morph')
nanomorph(self._tree, newTree)
morphTiming()

renderTiming()
renderTiming()
})
})
}

Expand All @@ -206,10 +220,12 @@ Choo.prototype.toString = function (location, state) {
})

this._matchRoute(location)
var html = this._prerender(this.state)
assert.ok(html, 'choo.toString: no valid value returned for the route ' + location)
assert(!Array.isArray(html), 'choo.toString: return value was an array for the route ' + location)
return typeof html.outerHTML === 'string' ? html.outerHTML : html.toString()
var pHtml = this._prerender(this.state)
return resolve(pHtml, function (html) {
assert.ok(html, 'choo.toString: no valid value returned for the route ' + location)
assert(!Array.isArray(html), 'choo.toString: return value was an array for the route ' + location)
return typeof html.outerHTML === 'string' ? html.outerHTML : html.toString()
})
}

Choo.prototype._matchRoute = function (locationOverride) {
Expand All @@ -232,7 +248,9 @@ Choo.prototype._matchRoute = function (locationOverride) {

Choo.prototype._prerender = function (state) {
var routeTiming = nanotiming("choo.prerender('" + state.route + "')")
var res = this._handler(state, this.emit)
routeTiming()
return res
var pRes = this._handler(state, this.emit)
return resolve(pRes, function (res) {
routeTiming()
return res
})
}
73 changes: 73 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,79 @@ tape('should render on the server with bel', function (t) {
t.end()
})

tape('should render async on the server with bel', function (t) {
var app = choo()
app.route('/', function (state, emit) {
var strong = '<strong>Hello filthy planet</strong>'
return html`
<p>${Promise.resolve(raw(strong))}</p>
`
})
app.toString('/').then(function (res) {
var exp = '<p><strong>Hello filthy planet</strong></p>'
t.equal(res.toString().trim(), exp, 'result was OK')
t.end()
})
})

tape('should render composition of sync and async on the server', function (t) {
var app = choo()
var async = function (state, emit) {
return new Promise(function (resolve) {
resolve(html`<p>Hello!</p>`)
})
}
var hoc = function (child) {
return function (state, emit) {
return html`<div>${child(state, emit)}</div>`
}
}
var component = hoc(async)
app.route('/', function (state, emit) {
return html`<div>${component(state, emit)}</div>`
})
app.toString('/').then(function (res) {
var exp = '<div><div><p>Hello!</p></div></div>'
t.equal(res.toString().trim(), exp, 'result was OK')
t.end()
})
})

tape('should render async with custom promise resolution strategy', function (t) {
t.plan(5)
var order = 3
var app = choo()
var component1 = function (state, emit) {
return new Promise(function (resolve) { resolve(html`<p>hEllo!</p>`) })
}
var component2 = function (state, emit) {
return new Promise(function (resolve) { resolve(html`<p>heLlo!</p>`) })
}
var component3 = function (state, emit) {
return new Promise(function (resolve) { resolve(html`<p>helLo!</p>`) })
}
app.route('/', function (state, emit) {
// resolve component3 first then component2 and then component1
var pC3 = component3(state, emit).then(function (result) { t.equal(order, 3); order--; return result })
var pC2 = pC3.then(function () { return component2(state, emit).then(function (result) { t.equal(order, 2); order--; return result }) })
var pC1 = pC2.then(function () { return component1(state, emit).then(function (result) { t.equal(order, 1); order--; return result }) })
return html`
<div>
${pC1}
${pC2}
${pC3}
${'HELLO'}
</div>
`
})
app.toString('/').then(function (res) {
var exp = '<div>\n <p>hEllo!</p>\n <p>heLlo!</p>\n <p>helLo!</p>\n HELLO\n </div>'
t.equal(order, 0, 'order was OK')
t.equal(res.toString().trim(), exp, 'result was OK')
t.end()
})
})

tape('should render on the server with hyperscript', function (t) {
var app = choo()
app.route('/', function (state, emit) {
Expand Down