diff --git a/html/index.js b/html/index.js index e25543b4..c539c1e0 100644 --- a/html/index.js +++ b/html/index.js @@ -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 diff --git a/index.js b/index.js index c29ab82d..6e3b76ce 100644 --- a/index.js +++ b/index.js @@ -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 || {} @@ -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) { @@ -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() + }) }) } @@ -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) { @@ -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 + }) } diff --git a/test.js b/test.js index cb748542..7e8867a2 100644 --- a/test.js +++ b/test.js @@ -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 = 'Hello filthy planet' + return html` +

${Promise.resolve(raw(strong))}

+ ` + }) + app.toString('/').then(function (res) { + var exp = '

Hello filthy planet

' + 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`

Hello!

`) + }) + } + var hoc = function (child) { + return function (state, emit) { + return html`
${child(state, emit)}
` + } + } + var component = hoc(async) + app.route('/', function (state, emit) { + return html`
${component(state, emit)}
` + }) + app.toString('/').then(function (res) { + var exp = '

Hello!

' + 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`

hEllo!

`) }) + } + var component2 = function (state, emit) { + return new Promise(function (resolve) { resolve(html`

heLlo!

`) }) + } + var component3 = function (state, emit) { + return new Promise(function (resolve) { resolve(html`

helLo!

`) }) + } + 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` +
+ ${pC1} + ${pC2} + ${pC3} + ${'HELLO'} +
+ ` + }) + app.toString('/').then(function (res) { + var exp = '
\n

hEllo!

\n

heLlo!

\n

helLo!

\n HELLO\n
' + 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) {