From e0462dd6d50c9a6dab873b5e4428f39d98a860a0 Mon Sep 17 00:00:00 2001 From: Diogo Cunha Date: Wed, 14 Mar 2018 15:16:32 +0000 Subject: [PATCH 1/6] Asynchronous render --- html/index.js | 13 ++++++++++++- index.js | 26 +++++++++++++------------- package-lock.json.328094334 | 0 3 files changed, 25 insertions(+), 14 deletions(-) create mode 100644 package-lock.json.328094334 diff --git a/html/index.js b/html/index.js index e25543b4..b6c82e95 100644 --- a/html/index.js +++ b/html/index.js @@ -1 +1,12 @@ -module.exports = require('bel') +var bel = require('bel') + +async function html (strings, ...keys) { + const promises = keys.map(key => Array.isArray(key) + ? Promise.all(key) + : Promise.resolve(key) + ) + const resolved = await Promise.all(promises) + return bel(strings, ...resolved) +} + +module.exports = html diff --git a/index.js b/index.js index c29ab82d..fef19d31 100644 --- a/index.js +++ b/index.js @@ -85,7 +85,7 @@ Choo.prototype.use = function (cb) { }) } -Choo.prototype.start = function () { +Choo.prototype.start = async function () { assert.equal(typeof window, 'object', 'choo.start: window was not found. .start() must be called in a browser, use .toString() if running in Node') var self = this @@ -132,13 +132,9 @@ 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 () { + this.emitter.prependListener(self._events.RENDER, nanoraf(async function () { var renderTiming = nanotiming('choo.render') - var newTree = self._prerender(self.state) + var newTree = await self._prerender(self.state) 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 <' + @@ -152,6 +148,10 @@ Choo.prototype.start = function () { renderTiming() })) + this._matchRoute() + this._tree = await this._prerender(this.state) + assert.ok(this._tree, 'choo.start: no valid DOM node returned for location ' + this.state.href) + documentReady(function () { self.emitter.emit(self._events.DOMCONTENTLOADED) self._loaded = true @@ -171,9 +171,9 @@ Choo.prototype.mount = function mount (selector) { var self = this - documentReady(function () { + documentReady(async function () { var renderTiming = nanotiming('choo.render') - var newTree = self.start() + var newTree = await self.start() if (typeof selector === 'string') { self._tree = document.querySelector(selector) } else { @@ -193,7 +193,7 @@ Choo.prototype.mount = function mount (selector) { }) } -Choo.prototype.toString = function (location, state) { +Choo.prototype.toString = async function (location, state) { this.state = xtend(this.state, state || {}) assert.notEqual(typeof window, 'object', 'choo.mount: window was found. .toString() must be called in Node, use .start() or .mount() if running in the browser') @@ -206,7 +206,7 @@ Choo.prototype.toString = function (location, state) { }) this._matchRoute(location) - var html = this._prerender(this.state) + var html = await 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() @@ -230,9 +230,9 @@ Choo.prototype._matchRoute = function (locationOverride) { return this.state } -Choo.prototype._prerender = function (state) { +Choo.prototype._prerender = async function (state) { var routeTiming = nanotiming("choo.prerender('" + state.route + "')") - var res = this._handler(state, this.emit) + var res = await this._handler(state, this.emit) routeTiming() return res } diff --git a/package-lock.json.328094334 b/package-lock.json.328094334 new file mode 100644 index 00000000..e69de29b From 2c734b20922181a7da74f1b20d42c4cb539e4441 Mon Sep 17 00:00:00 2001 From: Diogo Cunha Date: Wed, 14 Mar 2018 16:54:12 +0000 Subject: [PATCH 2/6] async/await removed --- html/index.js | 10 ++---- index.js | 92 ++++++++++++++++++++++++++++----------------------- 2 files changed, 53 insertions(+), 49 deletions(-) diff --git a/html/index.js b/html/index.js index b6c82e95..ec6799ad 100644 --- a/html/index.js +++ b/html/index.js @@ -1,12 +1,8 @@ var bel = require('bel') -async function html (strings, ...keys) { - const promises = keys.map(key => Array.isArray(key) - ? Promise.all(key) - : Promise.resolve(key) - ) - const resolved = await Promise.all(promises) - return bel(strings, ...resolved) +function html (strings, ...keys) { + const promises = keys.map(key => Array.isArray(key) ? Promise.all(key) : Promise.resolve(key)) + return Promise.all(promises).then(resolved => bel(strings, ...resolved)) } module.exports = html diff --git a/index.js b/index.js index fef19d31..8a71dceb 100644 --- a/index.js +++ b/index.js @@ -85,7 +85,7 @@ Choo.prototype.use = function (cb) { }) } -Choo.prototype.start = async function () { +Choo.prototype.start = function () { assert.equal(typeof window, 'object', 'choo.start: window was not found. .start() must be called in a browser, use .toString() if running in Node') var self = this @@ -132,32 +132,37 @@ Choo.prototype.start = async function () { initStore() }) - this.emitter.prependListener(self._events.RENDER, nanoraf(async function () { + this.emitter.prependListener(self._events.RENDER, nanoraf(function () { var renderTiming = nanotiming('choo.render') - var newTree = await self._prerender(self.state) - assert.ok(newTree, 'choo.render: no valid DOM node returned for location ' + self.state.href) + self._prerender(self.state).then(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() + }) })) + var self = this + this._matchRoute() - this._tree = await this._prerender(this.state) - assert.ok(this._tree, 'choo.start: no valid DOM node returned for location ' + this.state.href) + return this._prerender(this.state).then(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 - }) + documentReady(function () { + self.emitter.emit(self._events.DOMCONTENTLOADED) + self._loaded = true + }) - return this._tree + return self._tree + }) } Choo.prototype.mount = function mount (selector) { @@ -171,29 +176,30 @@ Choo.prototype.mount = function mount (selector) { var self = this - documentReady(async function () { + documentReady(function () { var renderTiming = nanotiming('choo.render') - var newTree = await self.start() - if (typeof selector === 'string') { - self._tree = document.querySelector(selector) - } else { - self._tree = selector - } + self.start().then(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() + }) }) } -Choo.prototype.toString = async function (location, state) { +Choo.prototype.toString = function (location, state) { this.state = xtend(this.state, state || {}) assert.notEqual(typeof window, 'object', 'choo.mount: window was found. .toString() must be called in Node, use .start() or .mount() if running in the browser') @@ -206,10 +212,11 @@ Choo.prototype.toString = async function (location, state) { }) this._matchRoute(location) - var html = await 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() + return this._prerender(this.state).then(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) { @@ -230,9 +237,10 @@ Choo.prototype._matchRoute = function (locationOverride) { return this.state } -Choo.prototype._prerender = async function (state) { +Choo.prototype._prerender = function (state) { var routeTiming = nanotiming("choo.prerender('" + state.route + "')") - var res = await this._handler(state, this.emit) - routeTiming() - return res + return this._handler(state, this.emit).then(function (res) { + routeTiming() + return res + }) } From 8079dd514c125abfeca26f1b4f08a4c598ee5135 Mon Sep 17 00:00:00 2001 From: Diogo Cunha Date: Wed, 14 Mar 2018 17:13:57 +0000 Subject: [PATCH 3/6] fix tests --- index.js | 4 +--- package-lock.json.328094334 | 0 test.js | 18 ++++++++++-------- 3 files changed, 11 insertions(+), 11 deletions(-) delete mode 100644 package-lock.json.328094334 diff --git a/index.js b/index.js index 8a71dceb..9b6dd849 100644 --- a/index.js +++ b/index.js @@ -149,8 +149,6 @@ Choo.prototype.start = function () { }) })) - var self = this - this._matchRoute() return this._prerender(this.state).then(function (tree) { self._tree = tree @@ -239,7 +237,7 @@ Choo.prototype._matchRoute = function (locationOverride) { Choo.prototype._prerender = function (state) { var routeTiming = nanotiming("choo.prerender('" + state.route + "')") - return this._handler(state, this.emit).then(function (res) { + return Promise.resolve(this._handler(state, this.emit)).then(function (res) { routeTiming() return res }) diff --git a/package-lock.json.328094334 b/package-lock.json.328094334 deleted file mode 100644 index e69de29b..00000000 diff --git a/test.js b/test.js index cb748542..113d49b9 100644 --- a/test.js +++ b/test.js @@ -13,10 +13,11 @@ tape('should render on the server with bel', function (t) {

${raw(strong)}

` }) - var res = app.toString('/') - var exp = '

Hello filthy planet

' - t.equal(res.toString().trim(), exp, 'result was OK') - t.end() + app.toString('/').then(function (res) { + var exp = '

Hello filthy planet

' + t.equal(res.toString().trim(), exp, 'result was OK') + t.end() + }) }) tape('should render on the server with hyperscript', function (t) { @@ -24,8 +25,9 @@ tape('should render on the server with hyperscript', function (t) { app.route('/', function (state, emit) { return h('p', h('strong', 'Hello filthy planet')) }) - var res = app.toString('/') - var exp = '

Hello filthy planet

' - t.equal(res.toString().trim(), exp, 'result was OK') - t.end() + app.toString('/').then(function (res) { + var exp = '

Hello filthy planet

' + t.equal(res.toString().trim(), exp, 'result was OK') + t.end() + }) }) From 03624f64078746d27270b65576aa9e138880e15f Mon Sep 17 00:00:00 2001 From: Diogo Cunha Date: Fri, 16 Mar 2018 18:07:26 +0000 Subject: [PATCH 4/6] Fully backward compatible, shim only required if used --- html/index.js | 30 ++++++++++++++++++++++++++---- index.js | 22 +++++++++++++++++----- test.js | 23 ++++++++++++++++++----- 3 files changed, 61 insertions(+), 14 deletions(-) diff --git a/html/index.js b/html/index.js index ec6799ad..48739453 100644 --- a/html/index.js +++ b/html/index.js @@ -1,8 +1,30 @@ -var bel = require('bel') +const bel = require('bel') -function html (strings, ...keys) { - const promises = keys.map(key => Array.isArray(key) ? Promise.all(key) : Promise.resolve(key)) - return Promise.all(promises).then(resolved => bel(strings, ...resolved)) +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 9b6dd849..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 || {} @@ -134,7 +141,8 @@ Choo.prototype.start = function () { this.emitter.prependListener(self._events.RENDER, nanoraf(function () { var renderTiming = nanotiming('choo.render') - self._prerender(self.state).then(function (newTree) { + 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 <' + @@ -150,7 +158,8 @@ Choo.prototype.start = function () { })) this._matchRoute() - return this._prerender(this.state).then(function (tree) { + 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) @@ -176,7 +185,8 @@ Choo.prototype.mount = function mount (selector) { documentReady(function () { var renderTiming = nanotiming('choo.render') - self.start().then(function (newTree) { + var pNewTree = self.start() + resolve(pNewTree, function (newTree) { if (typeof selector === 'string') { self._tree = document.querySelector(selector) } else { @@ -210,7 +220,8 @@ Choo.prototype.toString = function (location, state) { }) this._matchRoute(location) - return this._prerender(this.state).then(function (html) { + 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() @@ -237,7 +248,8 @@ Choo.prototype._matchRoute = function (locationOverride) { Choo.prototype._prerender = function (state) { var routeTiming = nanotiming("choo.prerender('" + state.route + "')") - return Promise.resolve(this._handler(state, this.emit)).then(function (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 113d49b9..5e8f808d 100644 --- a/test.js +++ b/test.js @@ -13,6 +13,20 @@ tape('should render on the server with bel', function (t) {

${raw(strong)}

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

Hello filthy planet

' + t.equal(res.toString().trim(), exp, 'result was OK') + 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') @@ -25,9 +39,8 @@ tape('should render on the server with hyperscript', function (t) { app.route('/', function (state, emit) { return h('p', h('strong', 'Hello filthy planet')) }) - app.toString('/').then(function (res) { - var exp = '

Hello filthy planet

' - t.equal(res.toString().trim(), exp, 'result was OK') - t.end() - }) + var res = app.toString('/') + var exp = '

Hello filthy planet

' + t.equal(res.toString().trim(), exp, 'result was OK') + t.end() }) From f476400660802f1741a673c27a348e1f38d4215a Mon Sep 17 00:00:00 2001 From: Diogo Cunha Date: Fri, 16 Mar 2018 18:17:51 +0000 Subject: [PATCH 5/6] fix typo --- html/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/html/index.js b/html/index.js index 48739453..c539c1e0 100644 --- a/html/index.js +++ b/html/index.js @@ -1,4 +1,4 @@ -const bel = require('bel') +var bel = require('bel') function isPromise (elem) { return elem && typeof elem.then === 'function' From 919d5d3a3c76515b1c48470e9adf4c2c6d95fe1b Mon Sep 17 00:00:00 2001 From: Diogo Cunha Date: Mon, 26 Mar 2018 18:34:03 +0100 Subject: [PATCH 6/6] few more tests --- test.js | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/test.js b/test.js index 5e8f808d..7e8867a2 100644 --- a/test.js +++ b/test.js @@ -34,6 +34,64 @@ tape('should render async on the server with bel', function (t) { }) }) +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) {