diff --git a/api/autoredraw.js b/api/autoredraw.js index 280be765..f3b88c03 100644 --- a/api/autoredraw.js +++ b/api/autoredraw.js @@ -6,7 +6,7 @@ module.exports = function(root, renderer, pubsub, callback) { var run = throttle(callback) if (renderer != null) { renderer.setEventCallback(function(e) { - if (e.redraw !== false) run() + if (e.redraw !== false) pubsub.publish() }) } diff --git a/api/router.js b/api/router.js index 3bb05f07..ea848a36 100644 --- a/api/router.js +++ b/api/router.js @@ -23,7 +23,7 @@ module.exports = function($window, renderer, pubsub) { renderer.render(root, Node(payload, null, args, undefined, undefined, undefined)) } }, function() { - router.setPath(defaultRoute) + router.setPath(defaultRoute, null, {replace: true}) }) autoredraw(root, renderer, pubsub, replay) } @@ -31,6 +31,6 @@ module.exports = function($window, renderer, pubsub) { route.prefix = router.setPrefix route.set = router.setPath route.get = router.getPath - + return route } diff --git a/api/tests/test-router.js b/api/tests/test-router.js index 98c1a8e9..627ffc34 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -49,6 +49,45 @@ o.spec("route", function() { }) }) + o("default route doesn't break back button", function(done) { + $window.location.href = "http://google.com" + route(root, "/a", { + "/a" : { + view: function() { + return m("div") + } + } + }) + + setTimeout(function() { + o(root.firstChild.nodeName).equals("DIV") + + $window.history.back() + + o($window.location.pathname).equals("/") + + done() + }, FRAME_BUDGET) + }) + + o("default route does not inherit params", function(done) { + $window.location.href = "/invalid?foo=bar" + route(root, "/a", { + "/a" : { + oninit: init, + view: function() { + return m("div") + } + } + }) + + function init(vnode) { + o(vnode.attrs).deepEquals({}) + + done() + } + }) + o("redraws when render function is executed", function(done) { var onupdate = o.spy() var oninit = o.spy() diff --git a/docs/prop.md b/docs/prop.md index 00a8bae2..c720c386 100644 --- a/docs/prop.md +++ b/docs/prop.md @@ -4,6 +4,7 @@ - [Static members](#static-members) - [prop.combine](#prop-combine) - [prop.reject](#prop-reject) + - [prop.merge](#prop-merge) - [prop.HALT](#prop-halt) - [Instance members](#static-members) - [stream.run](#stream-run) @@ -85,6 +86,19 @@ Argument | Type | Required | Description [How to read signatures](signatures.md) +##### prop.merge + +Creates a stream whose value is the array of values from an array of streams + +`stream = m.prop.merge(streams)` + +Argument | Type | Required | Description +------------ | -------------------- | -------- | --- +`streams` | `Array` | Yes | A list of streams +**returns** | `Stream` | | Returns a stream whose value is an array of input stream values + +[How to read signatures](signatures.md) + ##### prop.HALT A special value that can be returned to stream callbacks to halt execution of downstreams diff --git a/docs/v1.x-migration.md b/docs/v1.x-migration.md index c0dd5476..ba4b0c14 100644 --- a/docs/v1.x-migration.md +++ b/docs/v1.x-migration.md @@ -11,6 +11,7 @@ - [Reading/writing the current route](#readingwriting-the-current-route) - [Accessing route params](#accessing-route-params) - [Setting route prefix](#setting-route-prefix) +- [m.request](#mrequest) ## `config` function @@ -270,3 +271,57 @@ m.route.mode = "pathname"; ```js m.route.prefix(""); ``` + +## m.request + +[m.request](request.md) now returns an [m.prop stream](prop.md) instead of a promise. The main difference is you'll have to use `.run` to get similar functionality as a promise's `.then`: + +### `v0.2.x` + +```js +m.request({ method: 'GET', url: 'https://api.github.com/' }) + .then(function (responseBody) { + return m.request({ method: 'GET', url: responseBody.emojis_url }); + }) + .then(function (emojis) { + console.log("+1 url:", emojis['+1']); + }); +``` + +### `v1.x` + +```js +m.request({ method: 'GET', url: 'https://api.github.com/' }) + .run(function (responseBody) { + return m.request({ method: 'GET', url: responseBody.emojis_url }); + }) + .run(function (emojis) { + console.log("+1 url:", emojis['+1']); + }); +``` + +The equivalent of `m.sync` is now `m.prop.sync`: + +### `v0.2.x` + +```js +m.sync([ + m.request({ method: 'GET', url: 'https://api.github.com/users/lhorie' }), + m.request({ method: 'GET', url: 'https://api.github.com/users/isiahmeadows' }), +]) + .then(function (users) { + console.log("Contributors:", users[0].name, "and", users[1].name); + }); +``` + +### `v1.x` + +```js +m.prop.sync([ + m.request({ method: 'GET', url: 'https://api.github.com/users/lhorie' }), + m.request({ method: 'GET', url: 'https://api.github.com/users/isiahmeadows' }), +]) + .run(function (users) { + console.log("Contributors:", users[0].name, "and", users[1].name); + }); +``` diff --git a/docs/vnodes.md b/docs/vnodes.md index ec1bcf7c..d363bd2d 100644 --- a/docs/vnodes.md +++ b/docs/vnodes.md @@ -84,7 +84,7 @@ The `tag` property of a vnode determines its type. There are five vnode types: Vnode type | Example | Description ------------ | ------------------------------ | --- Element | `{tag: "div"}` | Represents a DOM element. -Fragment | `{tag: "[", children: []}` | Represents a list of DOM elements whose parent DOM element may also contain other elements that are not in the fragment. +Fragment | `{tag: "[", children: []}` | Represents a list of DOM elements whose parent DOM element may also contain other elements that are not in the fragment. When using the [`m()`](hyperscript.md) helper function, fragment vnodes can only be created by nesting arrays into the `children` parameter of `m()`. `m("[")` does not create a valid vnode. Text | `{tag: "#", children: ""}` | Represents a DOM text node. Trusted HTML | `{tag: "<", children: "
"}` | Represents a list of DOM elements from an HTML string. Component | `{tag: ExampleComponent}` | If `tag` is a Javascript object with a `view` method, the vnode represents the DOM generated by rendering the component. diff --git a/index.js b/index.js index 139be0be..8ee79ef4 100644 --- a/index.js +++ b/index.js @@ -19,6 +19,7 @@ m.trust = require("./render/trust") m.prop = Stream.stream m.prop.combine = Stream.combine m.prop.reject = Stream.reject +m.prop.merge = Stream.merge m.prop.HALT = Stream.HALT m.withAttr = require("./util/withAttr") m.render = renderService.render diff --git a/mithril.js b/mithril.js index ee63e707..4f664859 100644 --- a/mithril.js +++ b/mithril.js @@ -7,18 +7,15 @@ function createStream() { return stream._state.value } initStream(stream, arguments) - if (arguments.length > 0) updateStream(stream, arguments[0], undefined) - return stream } function initStream(stream, args) { stream.constructor = createStream stream._state = {id: guid++, value: undefined, error: undefined, state: 0, derive: undefined, recover: undefined, deps: {}, parents: [], errorStream: undefined, endStream: undefined} stream.map = map, stream.ap = ap, stream.of = createStream - stream.valueOf = valueOf, stream.toJSON = toJSON + stream.valueOf = valueOf, stream.toJSON = toJSON, stream.toString = valueOf stream.run = run, stream.catch = doCatch - Object.defineProperties(stream, { error: {get: function() { if (!stream._state.errorStream) { @@ -58,7 +55,10 @@ function updateState(stream, value, error) { if (recovered === HALT) return updateValues(stream, recovered, undefined) } - catch (e) {updateValues(stream, undefined, e)} + catch (e) { + updateValues(stream, undefined, e) + reportUncaughtError(stream, e) + } } else updateValues(stream, value, error) stream._state.changed = true @@ -81,6 +81,7 @@ function updateDependency(stream, mustSync) { } catch (e) { updateState(stream, undefined, e) + reportUncaughtError(stream, e) } } } @@ -96,6 +97,13 @@ function finalize(stream) { stream._state.changed = false for (var id in stream._state.deps) stream._state.deps[id]._state.changed = false } +function reportUncaughtError(stream, e) { + if (Object.keys(stream._state.deps).length === 0) { + setTimeout(function() { + if (Object.keys(stream._state.deps).length === 0) console.error(e) + }, 0) + } +} function run(fn) { var self = createStream(), stream = this return initDependency(self, [stream], function() { @@ -130,10 +138,8 @@ function initDependency(dep, streams, derive, recover) { state.derive = derive state.recover = recover state.parents = streams.filter(notEnded) - registerDependency(dep, state.parents) updateDependency(dep, true) - return dep } function registerDependency(stream, parents) { @@ -168,7 +174,12 @@ function reject(e) { stream.error(e) return stream } -var Stream = {stream: createStream, combine: combine, reject: reject, HALT: HALT} +function merge(streams) { + return combine(function () { + return streams.map(function (s) {return s()}) + }, streams) +} +var Stream = {stream: createStream, merge: merge, combine: combine, reject: reject, HALT: HALT} function Node(tag, key, attrs, children, text, dom) { return {tag: tag, key: key, attrs: attrs, children: children, text: text, dom: dom, domSize: undefined, state: {}, events: undefined, instance: undefined} } @@ -328,10 +339,13 @@ var renderService = function($window) { if (vnode.instance != null) { var element = createNode(vnode.instance, hooks, ns) vnode.dom = vnode.instance.dom - vnode.domSize = vnode.instance.domSize + vnode.domSize = vnode.dom != null ? vnode.instance.domSize : 0 return element } - else return $emptyFragment + else { + vnode.domSize = 0 + return $emptyFragment + } } //update function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) { @@ -341,12 +355,12 @@ var renderService = function($window) { else { var recycling = isRecyclable(old, vnodes) if (recycling) old = old.concat(old.pool) - + var oldStart = 0, start = 0, oldEnd = old.length - 1, end = vnodes.length - 1, map while (oldEnd >= oldStart && end >= start) { var o = old[oldStart], v = vnodes[start] if (o === v) oldStart++, start++ - else if (o != null && v != null && o.key === v.key) { + else if (o != null && v != null && o.key === v.key && o.tag === v.tag) { oldStart++, start++ updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), recycling, ns) if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) @@ -354,7 +368,7 @@ var renderService = function($window) { else { var o = old[oldEnd] if (o === v) oldEnd--, start++ - else if (o != null && v != null && o.key === v.key) { + else if (o != null && v != null && o.key === v.key && o.tag === v.tag) { updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns) insertNode(parent, toFragment(o), getNextSibling(old, oldStart, nextSibling)) oldEnd--, start++ @@ -365,10 +379,10 @@ var renderService = function($window) { while (oldEnd >= oldStart && end >= start) { var o = old[oldEnd], v = vnodes[end] if (o === v) oldEnd--, end-- - else if (o != null && v != null && o.key === v.key) { + else if (o != null && v != null && o.key === v.key && o.tag === v.tag) { updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns) if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) - nextSibling = o.dom + if (o.dom != null) nextSibling = o.dom oldEnd--, end-- } else { @@ -380,7 +394,7 @@ var renderService = function($window) { updateNode(parent, movable, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns) insertNode(parent, toFragment(movable), nextSibling) old[oldIndex].skip = true - nextSibling = movable.dom + if (movable.dom != null) nextSibling = movable.dom } else { var dom = createNode(v, hooks, undefined) @@ -440,7 +454,7 @@ var renderService = function($window) { if (children != null) { for (var i = 0; i < children.length; i++) { var child = children[i] - if (child != null) { + if (child != null && child.dom != null) { if (vnode.dom == null) vnode.dom = child.dom domSize += child.domSize || 1 } @@ -479,7 +493,12 @@ var renderService = function($window) { } else if (old.instance != null) { removeNode(parent, old.instance, null, false) - vnode.dom = vnode.domSize = undefined + vnode.dom = undefined + vnode.domSize = 0 + } + else { + vnode.dom = old.dom + vnode.domSize = old.domSize } } function isRecyclable(old, vnodes) { @@ -506,7 +525,7 @@ var renderService = function($window) { } function toFragment(vnode) { var count = vnode.domSize - if (count != null) { + if (count != null || vnode.dom == null) { var fragment = $doc.createDocumentFragment() if (count > 0) { var dom = vnode.dom @@ -519,7 +538,7 @@ var renderService = function($window) { } function getNextSibling(vnodes, i, nextSibling) { for (; i < vnodes.length; i++) { - if (vnodes[i] != null) return vnodes[i].dom + if (vnodes[i] != null && vnodes[i].dom != null) return vnodes[i].dom } return nextSibling } @@ -572,11 +591,14 @@ var renderService = function($window) { function onremove(vnode) { if (vnode.attrs && vnode.attrs.onremove) vnode.attrs.onremove.call(vnode.state, vnode) if (typeof vnode.tag !== "string" && vnode.tag.onremove) vnode.tag.onremove.call(vnode.state, vnode) - var children = vnode.children - if (children instanceof Array) { - for (var i = 0; i < children.length; i++) { - var child = children[i] - if (child != null) onremove(child) + if (vnode.instance != null) onremove(vnode.instance) + else { + var children = vnode.children + if (children instanceof Array) { + for (var i = 0; i < children.length; i++) { + var child = children[i] + if (child != null) onremove(child) + } } } } @@ -699,12 +721,12 @@ var renderService = function($window) { function copy(data) { if (data instanceof Array) { var output = [] - for (var i = 0; i < data.length; i++) output[i] = copy(data[i]) + for (var i = 0; i < data.length; i++) output[i] = data[i] return output } else if (typeof data === "object") { var output = {} - for (var i in data) output[i] = copy(data[i]) + for (var i in data) output[i] = data[i] return output } return data @@ -1030,7 +1052,7 @@ var autoredraw = function(root, renderer, pubsub, callback) { var run = throttle(callback) if (renderer != null) { renderer.setEventCallback(function(e) { - if (e.redraw !== false) run() + if (e.redraw !== false) pubsub.publish() }) } if (pubsub != null) { @@ -1058,7 +1080,7 @@ m.route = function($window, renderer, pubsub) { renderer.render(root, Node(payload, null, args, undefined, undefined, undefined)) } }, function() { - router.setPath(defaultRoute) + router.setPath(defaultRoute, null, {replace: true}) }) autoredraw(root, renderer, pubsub, replay) } @@ -1083,6 +1105,7 @@ m.trust = function(html) { m.prop = Stream.stream m.prop.combine = Stream.combine m.prop.reject = Stream.reject +m.prop.merge = Stream.merge m.prop.HALT = Stream.HALT m.withAttr = function(attrName, callback, context) { return function(e) { diff --git a/mithril.min.js b/mithril.min.js new file mode 100644 index 00000000..7bb133aa --- /dev/null +++ b/mithril.min.js @@ -0,0 +1,38 @@ +(function(){function y(){function a(){0=B&&r>=w;){var v=c[B],x=g[w];if(v===x)B++,w++;else if(null!=v&&null!=x&&v.key===x.key)B++,w++,k(a,v,x,e,d(c,B,f),t,p),t&&v.tag===x.tag&&m(a,l(v),f);else if(v=c[u],v===x)u--,w++;else if(null!=v&&null!=x&&v.key===x.key)k(a,v,x,e,d(c,u+1,f),t,p),m(a,l(v),d(c,B,f)),u--,w++;else break}for(;u>=B&&r>=w;){v=c[u];x=g[r];if(v===x)u--;else if(null!=v&&null!= +x&&v.key===x.key)k(a,v,x,e,d(c,u+1,f),t,p),t&&v.tag===x.tag&&m(a,l(v),f),f=v.dom,u--;else{if(!n){n=c;var v=u,q={},y;for(y=0;ya.indexOf("?")?"?":"&";a+=f+e}return a}function e(a){try{return""!==a?JSON.parse(a):null}catch(b){throw Error(a);}}function f(a){return a.responseText}var k=0,l;return{xhr:function(d){var m=z.stream();void 0!==d.initialValue&&m(d.initialValue);var k="boolean"===typeof d.useBody?d.useBody:"GET"!==d.method&&"TRACE"!==d.method;"function"!== +typeof d.serialize&&(d.serialize=JSON.stringify);"function"!==typeof d.deserialize&&(d.deserialize=e);"function"!==typeof d.extract&&(d.extract=f);d.url=b(d.url,d.data);k?d.data=d.serialize(d.data):d.url=h(d.url,d.data);var p=new a.XMLHttpRequest;p.open(d.method,d.url,"boolean"===typeof d.async?d.async:!0,"string"===typeof d.user?d.user:void 0,"string"===typeof d.password?d.password:void 0);d.serialize===JSON.stringify&&k&&p.setRequestHeader("Content-Type","application/json; charset=utf-8");d.deserialize=== +e&&p.setRequestHeader("Accept","application/json, text/*");"function"===typeof d.config&&(p=d.config(p,d)||p);p.onreadystatechange=function(){if(4===p.readyState){try{var a=d.deserialize(d.extract(p,d));if(200<=p.status&&300>p.status){if("function"===typeof d.type)if(a instanceof Array)for(var b=0;b= oldStart && end >= start) { var o = old[oldStart], v = vnodes[start] if (o === v) oldStart++, start++ - else if (o != null && v != null && o.key === v.key) { + else if (o != null && v != null && o.key === v.key && o.tag === v.tag) { oldStart++, start++ updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), recycling, ns) if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) @@ -125,7 +128,7 @@ module.exports = function($window) { else { var o = old[oldEnd] if (o === v) oldEnd--, start++ - else if (o != null && v != null && o.key === v.key) { + else if (o != null && v != null && o.key === v.key && o.tag === v.tag) { updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns) insertNode(parent, toFragment(o), getNextSibling(old, oldStart, nextSibling)) oldEnd--, start++ @@ -136,10 +139,10 @@ module.exports = function($window) { while (oldEnd >= oldStart && end >= start) { var o = old[oldEnd], v = vnodes[end] if (o === v) oldEnd--, end-- - else if (o != null && v != null && o.key === v.key) { + else if (o != null && v != null && o.key === v.key && o.tag === v.tag) { updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns) if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling) - nextSibling = o.dom + if (o.dom != null) nextSibling = o.dom oldEnd--, end-- } else { @@ -151,7 +154,7 @@ module.exports = function($window) { updateNode(parent, movable, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns) insertNode(parent, toFragment(movable), nextSibling) old[oldIndex].skip = true - nextSibling = movable.dom + if (movable.dom != null) nextSibling = movable.dom } else { var dom = createNode(v, hooks, undefined) @@ -211,7 +214,7 @@ module.exports = function($window) { if (children != null) { for (var i = 0; i < children.length; i++) { var child = children[i] - if (child != null) { + if (child != null && child.dom != null) { if (vnode.dom == null) vnode.dom = child.dom domSize += child.domSize || 1 } @@ -250,7 +253,12 @@ module.exports = function($window) { } else if (old.instance != null) { removeNode(parent, old.instance, null, false) - vnode.dom = vnode.domSize = undefined + vnode.dom = undefined + vnode.domSize = 0 + } + else { + vnode.dom = old.dom + vnode.domSize = old.domSize } } function isRecyclable(old, vnodes) { @@ -277,7 +285,7 @@ module.exports = function($window) { } function toFragment(vnode) { var count = vnode.domSize - if (count != null) { + if (count != null || vnode.dom == null) { var fragment = $doc.createDocumentFragment() if (count > 0) { var dom = vnode.dom @@ -290,7 +298,7 @@ module.exports = function($window) { } function getNextSibling(vnodes, i, nextSibling) { for (; i < vnodes.length; i++) { - if (vnodes[i] != null) return vnodes[i].dom + if (vnodes[i] != null && vnodes[i].dom != null) return vnodes[i].dom } return nextSibling } @@ -346,12 +354,14 @@ module.exports = function($window) { function onremove(vnode) { if (vnode.attrs && vnode.attrs.onremove) vnode.attrs.onremove.call(vnode.state, vnode) if (typeof vnode.tag !== "string" && vnode.tag.onremove) vnode.tag.onremove.call(vnode.state, vnode) - - var children = vnode.children - if (children instanceof Array) { - for (var i = 0; i < children.length; i++) { - var child = children[i] - if (child != null) onremove(child) + if (vnode.instance != null) onremove(vnode.instance) + else { + var children = vnode.children + if (children instanceof Array) { + for (var i = 0; i < children.length; i++) { + var child = children[i] + if (child != null) onremove(child) + } } } } @@ -479,12 +489,12 @@ module.exports = function($window) { function copy(data) { if (data instanceof Array) { var output = [] - for (var i = 0; i < data.length; i++) output[i] = copy(data[i]) + for (var i = 0; i < data.length; i++) output[i] = data[i] return output } else if (typeof data === "object") { var output = {} - for (var i in data) output[i] = copy(data[i]) + for (var i in data) output[i] = data[i] return output } return data diff --git a/render/tests/test-component.js b/render/tests/test-component.js index 828585c7..c6078469 100644 --- a/render/tests/test-component.js +++ b/render/tests/test-component.js @@ -640,10 +640,11 @@ o.spec("component", function() { }) }) o.spec("state", function() { - o("deep copies state", function() { + o("copies state", function() { var called = 0 + var data = {a: 1} var component = { - data: [{a: 1}], + data: data, oninit: init, view: function() { return "" @@ -653,7 +654,27 @@ o.spec("component", function() { render(root, [{tag: component}]) function init(vnode) { - o(vnode.state.data).deepEquals([{a: 1}]) + o(vnode.state.data).deepEquals(data) + o(vnode.state.data).equals(data) + } + }) + o("state copy is shallow", function() { + var called = 0 + var body = {a: 1} + var data = [body] + var component = { + data: data, + oninit: init, + view: function() { + return "" + } + } + + render(root, [{tag: component}]) + + function init(vnode) { + o(vnode.state.data).equals(data) + o(vnode.state.data[0]).equals(body) } }) }) diff --git a/render/tests/test-onremove.js b/render/tests/test-onremove.js index 4f59fd7d..1605cae1 100644 --- a/render/tests/test-onremove.js +++ b/render/tests/test-onremove.js @@ -79,6 +79,39 @@ o.spec("onremove", function() { o(remove.this).equals(vnode.state) o(remove.args[0]).equals(vnode) }) + o("calls onremove on nested component", function() { + var spy = o.spy() + var comp = { + view: function() {return m(outer)} + } + var outer = { + view: function() {return m(inner)} + } + var inner = { + onremove: spy, + view: function() {return m("div")} + } + render(root, {tag: comp}) + render(root, null) + + o(spy.callCount).equals(1) + }) + o("calls onremove on nested component child", function() { + var spy = o.spy() + var comp = { + view: function() {return m(outer)} + } + var outer = { + view: function() {return m(inner, m("a", {onremove: spy}))} + } + var inner = { + view: function(vnode) {return m("div", vnode.children)} + } + render(root, {tag: comp}) + render(root, null) + + o(spy.callCount).equals(1) + }) o("does not set onremove as an event handler", function() { var remove = o.spy() var vnode = {tag: "div", attrs: {onremove: remove}, children: []} diff --git a/render/tests/test-updateNodes.js b/render/tests/test-updateNodes.js index 29ea9fe9..598f0910 100644 --- a/render/tests/test-updateNodes.js +++ b/render/tests/test-updateNodes.js @@ -813,4 +813,36 @@ o.spec("updateNodes", function() { o(root.childNodes[0].nodeName).equals("A") o(root.childNodes[1].nodeName).equals("B") }) + o("fragment child toggles from null when followed by null component then tag", function() { + var component = {view: function() {return null}} + var vnodes = [{tag: "[", children: [{tag: "a"}, {tag: component}, {tag: "b"}]}] + var temp = [{tag: "[", children: [null, {tag: component}, {tag: "b"}]}] + var updated = [{tag: "[", children: [{tag: "a"}, {tag: component}, {tag: "b"}]}] + + render(root, vnodes) + render(root, temp) + render(root, updated) + + o(root.childNodes.length).equals(2) + o(root.childNodes[0].nodeName).equals("A") + o(root.childNodes[1].nodeName).equals("B") + }) + o("fragment child toggles from null in component when followed by null component then tag", function() { + var flag = true + var a = {view: function() {return flag ? {tag: "a"} : null}} + var b = {view: function() {return null}} + var vnodes = [{tag: "[", children: [{tag: a}, {tag: b}, {tag: "s"}]}] + var temp = [{tag: "[", children: [{tag: a}, {tag: b}, {tag: "s"}]}] + var updated = [{tag: "[", children: [{tag: a}, {tag: b}, {tag: "s"}]}] + + render(root, vnodes) + flag = false + render(root, temp) + flag = true + render(root, updated) + + o(root.childNodes.length).equals(2) + o(root.childNodes[0].nodeName).equals("A") + o(root.childNodes[1].nodeName).equals("S") + }) }) diff --git a/util/stream.js b/util/stream.js index 1897f72b..d8555852 100644 --- a/util/stream.js +++ b/util/stream.js @@ -7,18 +7,18 @@ function createStream() { return stream._state.value } initStream(stream, arguments) - + if (arguments.length > 0) updateStream(stream, arguments[0], undefined) - + return stream } function initStream(stream, args) { stream.constructor = createStream stream._state = {id: guid++, value: undefined, error: undefined, state: 0, derive: undefined, recover: undefined, deps: {}, parents: [], errorStream: undefined, endStream: undefined} stream.map = map, stream.ap = ap, stream.of = createStream - stream.valueOf = valueOf, stream.toJSON = toJSON + stream.valueOf = valueOf, stream.toJSON = toJSON, stream.toString = valueOf stream.run = run, stream.catch = doCatch - + Object.defineProperties(stream, { error: {get: function() { if (!stream._state.errorStream) { @@ -58,7 +58,10 @@ function updateState(stream, value, error) { if (recovered === HALT) return updateValues(stream, recovered, undefined) } - catch (e) {updateValues(stream, undefined, e)} + catch (e) { + updateValues(stream, undefined, e) + reportUncaughtError(stream, e) + } } else updateValues(stream, value, error) stream._state.changed = true @@ -81,6 +84,7 @@ function updateDependency(stream, mustSync) { } catch (e) { updateState(stream, undefined, e) + reportUncaughtError(stream, e) } } } @@ -96,6 +100,13 @@ function finalize(stream) { stream._state.changed = false for (var id in stream._state.deps) stream._state.deps[id]._state.changed = false } +function reportUncaughtError(stream, e) { + if (Object.keys(stream._state.deps).length === 0) { + setTimeout(function() { + if (Object.keys(stream._state.deps).length === 0) console.error(e) + }, 0) + } +} function run(fn) { var self = createStream(), stream = this @@ -132,10 +143,10 @@ function initDependency(dep, streams, derive, recover) { state.derive = derive state.recover = recover state.parents = streams.filter(notEnded) - + registerDependency(dep, state.parents) updateDependency(dep, true) - + return dep } function registerDependency(stream, parents) { @@ -174,4 +185,10 @@ function reject(e) { return stream } -module.exports = {stream: createStream, combine: combine, reject: reject, HALT: HALT} +function merge(streams) { + return combine(function () { + return streams.map(function (s) {return s()}) + }, streams) +} + +module.exports = {stream: createStream, merge: merge, combine: combine, reject: reject, HALT: HALT} diff --git a/util/tests/test-stream.js b/util/tests/test-stream.js index fabbf771..458016ed 100644 --- a/util/tests/test-stream.js +++ b/util/tests/test-stream.js @@ -11,24 +11,24 @@ o.spec("stream", function() { var initialValue = stream() stream(2) var newValue = stream() - + o(initialValue).equals(1) o(newValue).equals(2) }) o("has undefined value by default", function() { var stream = Stream.stream() - + o(stream()).equals(undefined) }) o("can update to undefined", function() { var stream = Stream.stream(1) stream(undefined) - + o(stream()).equals(undefined) }) o("can be stream of streams", function() { var stream = Stream.stream(Stream.stream(1)) - + o(stream()()).equals(1) }) }) @@ -36,41 +36,41 @@ o.spec("stream", function() { o("transforms value", function() { var stream = Stream.stream() var doubled = Stream.combine(function(s) {return s() * 2}, [stream]) - + stream(2) - + o(doubled()).equals(4) }) o("transforms default value", function() { var stream = Stream.stream(2) var doubled = Stream.combine(function(s) {return s() * 2}, [stream]) - + o(doubled()).equals(4) }) o("transforms multiple values", function() { var s1 = Stream.stream() var s2 = Stream.stream() var added = Stream.combine(function(s1, s2) {return s1() + s2()}, [s1, s2]) - + s1(2) s2(3) - + o(added()).equals(5) }) o("transforms multiple default values", function() { var s1 = Stream.stream(2) var s2 = Stream.stream(3) var added = Stream.combine(function(s1, s2) {return s1() + s2()}, [s1, s2]) - + o(added()).equals(5) }) o("transforms mixed default and late-bound values", function() { var s1 = Stream.stream(2) var s2 = Stream.stream() var added = Stream.combine(function(s1, s2) {return s1() + s2()}, [s1, s2]) - + s2(3) - + o(added()).equals(5) }) o("combines atomically", function() { @@ -82,9 +82,9 @@ o.spec("stream", function() { count++ return b() + c() }, [b, c]) - + a(3) - + o(d()).equals(15) o(count).equals(1) }) @@ -97,7 +97,7 @@ o.spec("stream", function() { count++ return b() + c() }, [b, c]) - + o(d()).equals(15) o(count).equals(1) }) @@ -108,10 +108,10 @@ o.spec("stream", function() { var c = Stream.combine(function(a, b, changed) { streams = changed }, [a, b]) - + a(3) b(5) - + o(streams.length).equals(1) o(streams[0]).equals(b) }) @@ -122,9 +122,9 @@ o.spec("stream", function() { var c = Stream.combine(function(a, b, changed) { streams = changed }, [a, b]) - + a(7) - + o(streams.length).equals(1) o(streams[0]).equals(a) }) @@ -133,7 +133,7 @@ o.spec("stream", function() { var b = Stream.combine(function(a) { return undefined }, [a]) - + o(b()).equals(undefined) }) o("combine can return stream", function() { @@ -141,7 +141,7 @@ o.spec("stream", function() { var b = Stream.combine(function(a) { return Stream.stream(2) }, [a]) - + o(b()()).equals(2) }) o("combine can return pending stream", function() { @@ -149,7 +149,7 @@ o.spec("stream", function() { var b = Stream.combine(function(a) { return Stream.stream() }, [a]) - + o(b()()).equals(undefined) }) o("combine can halt", function() { @@ -162,48 +162,73 @@ o.spec("stream", function() { count++ return 1 }) - + o(b()).equals(undefined) }) }) + o.spec("merge", function() { + o("transforms an array of streams to an array of values", function() { + var all = Stream.merge([ + Stream.stream(10), + Stream.stream("20"), + Stream.stream({value: 30}), + ]) + + o(all()).deepEquals([10, "20", {value: 30}]) + }) + o("remains pending until all streams are active", function() { + var straggler = Stream.stream() + + var all = Stream.merge([ + Stream.stream(10), + Stream.stream("20"), + straggler, + ]) + + o(all()).equals(undefined) + + straggler(30) + o(all()).deepEquals([10, "20", 30]) + }) + }) o.spec("end", function() { o("end stream works", function() { var stream = Stream.stream() var doubled = Stream.combine(function(stream) {return stream() * 2}, [stream]) - + stream.end(true) - + stream(3) - + o(doubled()).equals(undefined) }) o("end stream works with default value", function() { var stream = Stream.stream(2) var doubled = Stream.combine(function(stream) {return stream() * 2}, [stream]) - + stream.end(true) - + stream(3) - + o(doubled()).equals(4) }) o("cannot add downstream to ended stream", function() { var stream = Stream.stream(2) stream.end(true) - + var doubled = Stream.combine(function(stream) {return stream() * 2}, [stream]) stream(3) - + o(doubled()).equals(undefined) }) o("upstream does not affect ended stream", function() { var stream = Stream.stream(2) var doubled = Stream.combine(function(stream) {return stream() * 2}, [stream]) - + doubled.end(true) - + stream(4) - + o(doubled()).equals(4) }) }) @@ -211,16 +236,16 @@ o.spec("stream", function() { o("error() works", function() { var stream = Stream.stream() var errored = Stream.combine(function(stream) {throw new Error("error")}, [stream]) - + stream(3) - + o(errored()).equals(undefined) o(errored.error().message).equals("error") }) o("error() works with default value", function() { var stream = Stream.stream(3) var errored = Stream.combine(function(stream) {throw new Error("error")}, [stream]) - + o(errored()).equals(undefined) o(errored.error().message).equals("error") }) @@ -230,9 +255,9 @@ o.spec("stream", function() { if (typeof stream() !== "number") throw new Error("error") else return stream() * 2 }, [stream]) - + stream(3) - + o(doubled()).equals(6) o(doubled.error()).equals(undefined) }) @@ -243,9 +268,9 @@ o.spec("stream", function() { count++ return 2 }) - + stream.error(new Error("error")) - + o(handled()).equals(2) o(handled.error()).equals(undefined) o(count).equals(1) @@ -262,7 +287,7 @@ o.spec("stream", function() { count++ return value * 3 }) - + o(stream()).equals(undefined) o(stream.error().message).equals("error") o(count).equals(0) @@ -279,7 +304,7 @@ o.spec("stream", function() { return value * 3 }) stream.error(new Error("error")) - + o(mapped()).equals(undefined) o(mapped.error().message).equals("error") o(count).equals(0) @@ -287,13 +312,13 @@ o.spec("stream", function() { o("error.map works", function() { var stream = Stream.stream(1) var mappedFromError = stream.error.map(function(value) { - return "from" + value.message + if (value) return "from" + value.message }) - + o(mappedFromError()).equals(undefined) - + stream.error(new Error("error")) - + o(mappedFromError()).equals("fromerror") }) o("error from error.map propagates", function() { @@ -304,11 +329,11 @@ o.spec("stream", function() { .map(function(value) { return "a" + value }) - + o(mappedFromError()).equals(undefined) - + stream.error(new Error("error")) - + o(mappedFromError()).equals("afromerror") }) o("error thrown from error.map propagates downstream", function() { @@ -317,15 +342,15 @@ o.spec("stream", function() { var mappedFromError = stream.error.map(function(value) { throw new Error("b") }) - + var downstream = mappedFromError.map(function() { count++ }) - + o(mappedFromError()).equals(undefined) - + stream.error(new Error("a")) - + o(mappedFromError()).equals(undefined) o(mappedFromError.error().message).equals("b") o(downstream()).equals(undefined) @@ -341,7 +366,7 @@ o.spec("stream", function() { count++ return 1 }) - + o(stream()).equals(undefined) o(count).equals(0) }) @@ -350,15 +375,15 @@ o.spec("stream", function() { var error = stream.error.map(function(value) { return Stream.stream(1) }) - + o(error()()).equals(1) }) o("combined stream of two errored streams adopts error from first", function() { var a = Stream.stream(1) var b = Stream.combine(function(a) {throw new Error("error from b")}, [a]) var c = Stream.combine(function(a) {throw new Error("error from c")}, [a]) - var d = Stream.combine(function(a, b) {return 2}, [a, b]) - + var d = Stream.combine(function(b, c) {return 2}, [b, c]) + o(d()).equals(undefined) o(d.error().message).equals("error from b") }) @@ -366,7 +391,7 @@ o.spec("stream", function() { o.spec("reject", function() { o("reject works", function() { var stream = Stream.reject(new Error("error")) - + o(stream()).equals(undefined) o(stream.error().message).equals("error") }) @@ -381,7 +406,7 @@ o.spec("stream", function() { count++ return value * 3 }) - + o(stream()).equals(undefined) o(stream.error().message).equals("error") }) @@ -390,9 +415,9 @@ o.spec("stream", function() { var doubled = stream.map(function(value) { return value * 2 }) - + stream(1) - + o(doubled()).equals(2) o(stream.error()).equals(undefined) }) @@ -404,7 +429,7 @@ o.spec("stream", function() { count++ return a() + b() }, [a, b]) - + o(combined()).equals(undefined) o(combined.error().message).equals("a") o(count).equals(0) @@ -414,29 +439,29 @@ o.spec("stream", function() { o("works", function() { var stream = Stream.stream() var doubled = stream.run(function(value) {return value * 2}) - + stream(3) - + o(doubled()).equals(6) }) o("works with default value", function() { var stream = Stream.stream(3) var doubled = stream.run(function(value) {return value * 2}) - + o(doubled()).equals(6) }) o("works with undefined value", function() { var stream = Stream.stream() var mapped = stream.run(function(value) {return String(value)}) - + stream(undefined) - + o(mapped()).equals("undefined") }) o("works with default undefined value", function() { var stream = Stream.stream(undefined) var mapped = stream.run(function(value) {return String(value)}) - + o(mapped()).equals("undefined") }) o("works with stream that throws", function() { @@ -447,7 +472,7 @@ o.spec("stream", function() { count++ return value }) - + o(errored()).equals(undefined) o(errored.error().message).equals("error") o(mapped()).equals(undefined) @@ -462,20 +487,20 @@ o.spec("stream", function() { count++ return value }) - + o(mapped()).equals(undefined) o(count).equals(0) }) o("works with active stream", function() { var stream = Stream.stream(undefined) var mapped = stream.run(function(value) {return Stream.stream(1)}) - + o(mapped()).equals(1) }) o("works with errored stream", function() { var stream = Stream.stream(undefined) var mapped = stream.run(function(value) {return Stream.reject(new Error("error"))}) - + o(mapped()).equals(undefined) o(mapped.error().message).equals("error") }) @@ -486,36 +511,36 @@ o.spec("stream", function() { ended.end(true) return ended }) - + stream(3) - + o(mapped()).equals(2) }) o("works when active stream updates", function() { var stream = Stream.stream(undefined) var absorbed = Stream.stream(1) var mapped = stream.run(function(value) {return absorbed}) - + absorbed(2) - + o(mapped()).equals(2) - + absorbed(3) - + o(mapped()).equals(3) }) o("works when updating stream to errored state", function() { var stream = Stream.stream(undefined) var absorbed = Stream.stream(1) var mapped = stream.run(function(value) {return absorbed}) - + absorbed.error(new Error("error")) - + o(mapped()).equals(undefined) o(mapped.error().message).equals("error") - + absorbed.error(new Error("another error")) - + o(mapped()).equals(undefined) o(mapped.error().message).equals("another error") }) @@ -523,18 +548,18 @@ o.spec("stream", function() { var stream = Stream.stream(undefined) var absorbed = Stream.stream() var mapped = stream.run(function(value) {return absorbed}) - + absorbed(2) - + o(mapped()).equals(2) }) o("works when updating pending stream to errored state", function() { var stream = Stream.stream(undefined) var absorbed = Stream.stream() var mapped = stream.run(function(value) {return absorbed}) - + absorbed.error(new Error("error")) - + o(mapped()).equals(undefined) o(mapped.error().message).equals("error") }) @@ -542,14 +567,14 @@ o.spec("stream", function() { var stream = Stream.stream(undefined) var absorbed = Stream.stream(1) var mapped = stream.run(function(value) {return absorbed}) - + absorbed.error(new Error("error")) - + o(mapped()).equals(undefined) o(mapped.error().message).equals("error") - + absorbed(2) - + o(mapped()).equals(2) o(mapped.error()).equals(undefined) }) @@ -564,7 +589,7 @@ o.spec("stream", function() { .map(function(value) { return value + "mapped" }) - + o(count).equals(1) o(stream()).equals("noerrormapped") o(stream.error()).equals(undefined) @@ -578,7 +603,7 @@ o.spec("stream", function() { .map(function(value) { return value + "mapped" }) - + o(count).equals(1) o(stream()).equals("noerrormapped") o(stream.error()).equals(undefined) @@ -593,9 +618,9 @@ o.spec("stream", function() { .map(function(value) { return value + "mapped" }) - + stream("a") - + o(count).equals(0) o(handled()).equals("aamapped") o(handled.error()).equals(undefined) @@ -609,7 +634,7 @@ o.spec("stream", function() { .map(function(value) { return value + "mapped" }) - + o(count).equals(0) o(stream()).equals("aamapped") o(stream.error()).equals(undefined) @@ -619,7 +644,7 @@ o.spec("stream", function() { throw new Error("b") }) var mapped = stream.map(function(value) {return value + "ok"}) - + o(stream()).equals(undefined) o(stream.error().message).equals("b") o(mapped()).equals(undefined) @@ -627,7 +652,7 @@ o.spec("stream", function() { }) o("catch can return undefined", function() { var stream = Stream.reject(new Error("b")).catch(function(e) {}).map(function(value) {return String(value)}) - + o(stream()).equals("undefined") o(stream.error()).equals(undefined) }) @@ -641,7 +666,7 @@ o.spec("stream", function() { count++ return String(value) }) - + o(mapped()).equals(undefined) o(count).equals(0) }) @@ -651,7 +676,7 @@ o.spec("stream", function() { return stream }) .map(function(value) {return String(value)}) - + o(mapped()).equals("1") }) o("catch absorbs errored stream", function() { @@ -660,7 +685,7 @@ o.spec("stream", function() { return stream }) .map(function(value) {return String(value)}) - + o(mapped()).equals(undefined) o(mapped.error().message).equals("a") }) @@ -669,7 +694,7 @@ o.spec("stream", function() { var b = a.map(function(value) {return value + "b"}).catch(function(e) {}) var c = a.map(function(value) {return value + "c"}) var d = Stream.combine(function(b, c) {return b() + c()}, [b, c]) - + o(d()).equals(undefined) o(d.error().message).equals("a") }) @@ -685,7 +710,7 @@ o.spec("stream", function() { .map(function(value) { return value + "mapped" }) - + o(stream()).equals("noerrormapped") }) o("catches nested wrapped rejected stream", function() { @@ -702,7 +727,7 @@ o.spec("stream", function() { .map(function(value) { return value + "mapped" }) - + o(stream()).equals("noerrormapped") }) }) @@ -717,6 +742,19 @@ o.spec("stream", function() { o(Stream.stream([1, 2, 3]).valueOf()).deepEquals([1, 2, 3]) o(Stream.stream().valueOf()).equals(undefined) }) + o("allows implicit value access in mathematical operations", function() { + o(Stream.stream(1) + Stream.stream(1)).equals(2) + }) + }) + o.spec("toString", function() { + o("aliases valueOf", function() { + var stream = Stream.stream(1) + + o(stream.toString).equals(stream.valueOf) + }) + o("allows implicit value access in string operations", function() { + o(Stream.stream("a") + Stream.stream("b")).equals("ab") + }) }) o.spec("toJSON", function() { o("works", function() { @@ -734,35 +772,35 @@ o.spec("stream", function() { o("works", function() { var stream = Stream.stream() var doubled = stream.map(function(value) {return value * 2}) - + stream(3) - + o(doubled()).equals(6) }) o("works with default value", function() { var stream = Stream.stream(3) var doubled = stream.map(function(value) {return value * 2}) - + o(doubled()).equals(6) }) o("works with undefined value", function() { var stream = Stream.stream() var mapped = stream.map(function(value) {return String(value)}) - + stream(undefined) - + o(mapped()).equals("undefined") }) o("works with default undefined value", function() { var stream = Stream.stream(undefined) var mapped = stream.map(function(value) {return String(value)}) - + o(mapped()).equals("undefined") }) o("works with pending stream", function() { var stream = Stream.stream(undefined) var mapped = stream.map(function(value) {return Stream.stream()}) - + o(mapped()()).equals(undefined) }) }) @@ -771,26 +809,26 @@ o.spec("stream", function() { var apply = Stream.stream(function(value) {return value * 2}) var stream = Stream.stream(3) var applied = apply.ap(stream) - + o(applied()).equals(6) - + apply(function(value) {return value / 3}) - + o(applied()).equals(1) - + stream(9) - + o(applied()).equals(3) }) o("works with undefined value", function() { var apply = Stream.stream(function(value) {return String(value)}) var stream = Stream.stream(undefined) var applied = apply.ap(stream) - + o(applied()).equals("undefined") - + apply(function(value) {return String(value) + "a"}) - + o(applied()).equals("undefineda") }) }) @@ -799,18 +837,18 @@ o.spec("stream", function() { o("identity", function() { var stream = Stream.stream(3) var mapped = stream.map(function(value) {return value}) - + o(stream()).equals(mapped()) }) o("composition", function() { function f(x) {return x * 2} function g(x) {return x * x} - + var stream = Stream.stream(3) - + var mapped = stream.map(function(value) {return f(g(value))}) var composed = stream.map(g).map(f) - + o(mapped()).equals(18) o(mapped()).equals(composed()) }) @@ -820,7 +858,7 @@ o.spec("stream", function() { var a = Stream.stream(function(value) {return value * 2}) var u = Stream.stream(function(value) {return value * 3}) var v = Stream.stream(5) - + var mapped = a.map(function(f) { return function(g) { return function(x) { @@ -828,9 +866,9 @@ o.spec("stream", function() { } } }).ap(u).ap(v) - + var composed = a.ap(u.ap(v)) - + o(mapped()).equals(30) o(mapped()).equals(composed()) }) @@ -839,7 +877,7 @@ o.spec("stream", function() { o("identity", function() { var a = Stream.stream().of(function(value) {return value}) var v = Stream.stream(5) - + o(a.ap(v)()).equals(5) o(a.ap(v)()).equals(v()) }) @@ -847,7 +885,7 @@ o.spec("stream", function() { var a = Stream.stream(0) var f = function(value) {return value * 2} var x = 3 - + o(a.of(f).ap(a.of(x))()).equals(6) o(a.of(f).ap(a.of(x))()).equals(a.of(f(x))()) }) @@ -855,10 +893,10 @@ o.spec("stream", function() { var u = Stream.stream(function(value) {return value * 2}) var a = Stream.stream() var y = 3 - + o(u.ap(a.of(y))()).equals(6) o(u.ap(a.of(y))()).equals(a.of(function(f) {return f(y)}).ap(u)()) }) }) }) -}) \ No newline at end of file +})