- [What is Mithril?](#what-is-mithril)
-- [Getting started](#getting-started)
-- [Hello world](#hello-world)
-- [DOM elements](#dom-elements)
-- [Components](#components)
-- [Routing](#routing)
-- [XHR](#xhr)
+- [Installation](#installation)
+- [Documentation](#documentation)
+- [Getting Help](#getting-help)
+- [Contributing](#contributing)
+
+## What is Mithril?
+
+A modern client-side Javascript framework for building Single Page Applications. It's small (8.87 KB gzipped), fast and provides routing and XHR utilities out of the box.
+
+Mithril is used by companies like Vimeo and Nike, and open source platforms like Lichess 👍.
+
+Browsers all the way back to IE9 are supported, no polyfills required 👌.
+
+## Installation
+
+### CDN
+
+```html
+
+```
+
+### npm
+
+```bash
+$ npm install mithril
+```
+
+The ["Getting started" guide](https://mithril.js.org/#getting-started) is a good place to start learning how to use mithril.
+
+## Documentation
+
+Documentation lives on [mithril.js.org](https://mithril.js.org).
+
+You may be interested in the [API Docs](https://mithril.js.org/api.html), a [Simple Application](https://mithril.js.org/simple-application.html), or perhaps some [Examples](https://mithril.js.org/examples.html).
+
+## Getting Help
+
+Mithril has an active & welcoming community on [Gitter](https://gitter.im/mithriljs/mithril.js), or feel free to ask questions on [Stack Overflow](https://stackoverflow.com/questions/tagged/mithril.js) using the `mithril.js` tag.
+
+## Contributing
+
+There's a [Contributing FAQ](https://mithril.js.org/contributing.html) on the mithril site that hopefully helps, but if not definitely hop into the [Gitter Room](https://gitter.im/mithriljs/mithril.js) and ask away!
---
-### What is Mithril?
+Thanks for reading!
-Mithril is a modern client-side Javascript framework for building Single Page Applications.
-It's small (< 8kb gzip), fast and provides routing and XHR utilities out of the box.
-
-
-
-Mithril is used by companies like Vimeo and Nike, and open source platforms like Lichess.
-
-If you are an experienced developer and want to know how Mithril compares to other frameworks, see the [framework comparison](http://mithril.js.org/framework-comparison.html) page.
-
-Mithril supports browsers all the way back to IE9, no polyfills required.
-
----
-
-### Getting started
-
-The easiest way to try out Mithril is to include it from a CDN, and follow this tutorial. It'll cover the majority of the API surface (including routing and XHR) but it'll only take 10 minutes.
-
-Let's create an HTML file to follow along:
-
-```markup
-
-
-
-
-```
-
----
-
-### Hello world
-
-Let's start as small as we can: render some text on screen. Copy the code below into your file (and by copy, I mean type it out - you'll learn better)
-
-```javascript
-var root = document.body
-
-m.render(root, "Hello world")
-```
-
-Now, let's change the text to something else. Add this line of code under the previous one:
-
-```javascript
-m.render(root, "My first app")
-```
-
-As you can see, you use the same code to both create and update HTML. Mithril automatically figures out the most efficient way of updating the text, rather than blindly recreating it from scratch.
-
----
-
-### DOM elements
-
-Let's wrap our text in an `
` tag.
-
-```javascript
-m.render(root, m("h1", "My first app"))
-```
-
-The `m()` function can be used to describe any HTML structure you want. So if you need to add a class to the `
`:
-
-```javascript
-m("h1", {class: "title"}, "My first app")
-```
-
-If you want to have multiple elements:
-
-```javascript
-[
- m("h1", {class: "title"}, "My first app"),
- m("button", "A button"),
-]
-```
-
-And so on:
-
-```javascript
-m("main", [
- m("h1", {class: "title"}, "My first app"),
- m("button", "A button"),
-])
-```
-
-Note: If you prefer `` syntax, [it's possible to use it via a Babel plugin](http://mithril.js.org/jsx.html).
-
-```jsx
-// HTML syntax via Babel's JSX plugin
-
-
My first app
-
-
-```
-
----
-
-### Components
-
-A Mithril component is just an object with a `view` function. Here's the code above as a component:
-
-```javascript
-var Hello = {
- view: function() {
- return m("main", [
- m("h1", {class: "title"}, "My first app"),
- m("button", "A button"),
- ])
- }
-}
-```
-
-To activate the component, we use `m.mount`.
-
-```javascript
-m.mount(root, Hello)
-```
-
-As you would expect, doing so creates this markup:
-
-```markup
-
-
My first app
-
-
-```
-
-The `m.mount` function is similar to `m.render`, but instead of rendering some HTML only once, it activates Mithril's auto-redrawing system. To understand what that means, let's add some events:
-
-```javascript
-var count = 0 // added a variable
-
-var Hello = {
- view: function() {
- return m("main", [
- m("h1", {class: "title"}, "My first app"),
- // changed the next line
- m("button", {onclick: function() {count++}}, count + " clicks"),
- ])
- }
-}
-
-m.mount(root, Hello)
-```
-
-We defined an `onclick` event on the button, which increments a variable `count` (which was declared at the top). We are now also rendering the value of that variable in the button label.
-
-You can now update the label of the button by clicking the button. Since we used `m.mount`, you don't need to manually call `m.render` to apply the changes in the `count` variable to the HTML; Mithril does it for you.
-
-If you're wondering about performance, it turns out Mithril is very fast at rendering updates, because it only touches the parts of the DOM it absolutely needs to. So in our example above, when you click the button, the text in it is the only part of the DOM Mithril actually updates.
-
----
-
-### Routing
-
-Routing just means going from one screen to another in an application with several screens.
-
-Let's add a splash page that appears before our click counter. First we create a component for it:
-
-```javascript
-var Splash = {
- view: function() {
- return m("a", {href: "#!/hello"}, "Enter!")
- }
-}
-```
-
-As you can see, this component simply renders a link to `#!/hello`. The `#!` part is known as a hashbang, and it's a common convention used in Single Page Applications to indicate that the stuff after it (the `/hello` part) is a route path.
-
-Now that we going to have more than one screen, we use `m.route` instead of `m.mount`.
-
-```javascript
-m.route(root, "/splash", {
- "/splash": Splash,
- "/hello": Hello,
-})
-```
-
-The `m.route` function still has the same auto-redrawing functionality that `m.mount` does, and it also enables URL awareness; in other words, it lets Mithril know what to do when it sees a `#!` in the URL.
-
-The `"/splash"` right after `root` means that's the default route, i.e. if the hashbang in the URL doesn't point to one of the defined routes (`/splash` and `/hello`, in our case), then Mithril redirects to the default route. So if you open the page in a browser and your URL is `http://localhost`, then you get redirected to `http://localhost/#!/splash`.
-
-Also, as you would expect, clicking on the link on the splash page takes you to the click counter screen we created earlier. Notice that now your URL will point to `http://localhost/#!/hello`. You can navigate back and forth to the splash page using the browser's back and next button.
-
----
-
-### XHR
-
-Basically, XHR is just a way to talk to a server.
-
-Let's change our click counter to make it save data on a server. For the server, we'll use [REM](http://rem-rest-api.herokuapp.com), a mock REST API designed for toy apps like this tutorial.
-
-First we create a function that calls `m.request`. The `url` specifies an endpoint that represents a resource, the `method` specifies the type of action we're taking (typically the `PUT` method [upserts](https://en.wiktionary.org/wiki/upsert)), `data` is the payload that we're sending to the endpoint and `withCredentials` means to enable cookies (a requirement for the REM API to work)
-
-```javascript
-var count = 0
-var increment = function() {
- m.request({
- method: "PUT",
- url: "//rem-rest-api.herokuapp.com/api/tutorial/1",
- data: {count: count + 1},
- withCredentials: true,
- })
- .then(function(data) {
- count = parseInt(data.count)
- })
-}
-```
-
-Calling the increment function [upserts](https://en.wiktionary.org/wiki/upsert) an object `{count: 1}` to the `/api/tutorial/1` endpoint. This endpoint returns an object with the same `count` value that was sent to it. Notice that the `count` variable is only updated after the request completes, and it's updated with the response value from the server now.
-
-Let's replace the event handler in the component to call the `increment` function instead of incrementing the `count` variable directly:
-
-```javascript
-var Hello = {
- view: function() {
- return m("main", [
- m("h1", {class: "title"}, "My first app"),
- m("button", {onclick: increment}, count + " clicks"),
- ])
- }
-}
-```
-
-Clicking the button should now update the count.
-
----
-
-We covered how to create and update HTML, how to create components, routes for a Single Page Application, and interacted with a server via XHR.
-
-This should be enough to get you started writing the frontend for a real application. Now that you are comfortable with the basics of the Mithril API, [be sure to check out the simple application tutorial](http://mithril.js.org/simple-application.html), which walks you through building a realistic application.
+🎁
diff --git a/api/mount.js b/api/mount.js
index 2178505a..7203bf7c 100644
--- a/api/mount.js
+++ b/api/mount.js
@@ -16,6 +16,6 @@ module.exports = function(redrawService) {
redrawService.render(root, Vnode(component))
}
redrawService.subscribe(root, run)
- redrawService.redraw()
+ run()
}
}
diff --git a/api/redraw.js b/api/redraw.js
index a05b6c88..a6549975 100644
--- a/api/redraw.js
+++ b/api/redraw.js
@@ -4,26 +4,23 @@ var coreRenderer = require("../render/render")
function throttle(callback) {
//60fps translates to 16.6ms, round it down since setTimeout requires int
- var time = 16
+ var delay = 16
var last = 0, pending = null
var timeout = typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout
return function() {
- var now = Date.now()
- if (last === 0 || now - last >= time) {
- last = now
- callback()
- }
- else if (pending === null) {
+ var elapsed = Date.now() - last
+ if (pending === null) {
pending = timeout(function() {
pending = null
callback()
last = Date.now()
- }, time - (now - last))
+ }, delay - elapsed)
}
}
}
-module.exports = function($window) {
+
+module.exports = function($window, throttleMock) {
var renderService = coreRenderer($window)
renderService.setEventCallback(function(e) {
if (e.redraw === false) e.redraw = undefined
@@ -31,18 +28,24 @@ module.exports = function($window) {
})
var callbacks = []
+ var rendering = false
+
function subscribe(key, callback) {
unsubscribe(key)
- callbacks.push(key, throttle(callback))
+ callbacks.push(key, callback)
}
function unsubscribe(key) {
var index = callbacks.indexOf(key)
if (index > -1) callbacks.splice(index, 2)
}
- function redraw() {
- for (var i = 1; i < callbacks.length; i += 2) {
- callbacks[i]()
- }
+ function sync() {
+ if (rendering) throw new Error("Nested m.redraw.sync() call")
+ rendering = true
+ for (var i = 1; i < callbacks.length; i+=2) try {callbacks[i]()} catch (e) {if (typeof console !== "undefined") console.error(e)}
+ rendering = false
}
+
+ var redraw = (throttleMock || throttle)(sync)
+ redraw.sync = sync
return {subscribe: subscribe, unsubscribe: unsubscribe, redraw: redraw, render: renderService.render}
}
diff --git a/api/router.js b/api/router.js
index 93f2e074..4d893acc 100644
--- a/api/router.js
+++ b/api/router.js
@@ -11,9 +11,14 @@ module.exports = function($window, redrawService) {
var render, component, attrs, currentPath, lastUpdate
var route = function(root, defaultRoute, routes) {
if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined")
- var run = function() {
+ function run() {
if (render != null) redrawService.render(root, render(Vnode(component, attrs.key, attrs)))
}
+ var redraw = function() {
+ run()
+ redraw = redrawService.redraw
+ }
+ redrawService.subscribe(root, run)
var bail = function(path) {
if (path !== defaultRoute) routeService.setPath(defaultRoute, null, {replace: true})
else throw new Error("Could not resolve default route " + defaultRoute)
@@ -24,7 +29,7 @@ module.exports = function($window, redrawService) {
component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div"
attrs = params, currentPath = path, lastUpdate = null
render = (routeResolver.render || identity).bind(routeResolver)
- run()
+ redraw()
}
if (payload.view || typeof payload === "function") update({}, payload)
else {
@@ -36,7 +41,6 @@ module.exports = function($window, redrawService) {
else update(payload, "div")
}
}, bail)
- redrawService.subscribe(root, run)
}
route.set = function(path, data, options) {
if (lastUpdate != null) {
@@ -48,7 +52,7 @@ module.exports = function($window, redrawService) {
}
route.get = function() {return currentPath}
route.prefix = function(prefix) {routeService.prefix = prefix}
- route.link = function(vnode) {
+ var link = function(options, vnode) {
vnode.dom.setAttribute("href", routeService.prefix + vnode.attrs.href)
vnode.dom.onclick = function(e) {
if (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2) return
@@ -56,9 +60,13 @@ module.exports = function($window, redrawService) {
e.redraw = false
var href = this.getAttribute("href")
if (href.indexOf(routeService.prefix) === 0) href = href.slice(routeService.prefix.length)
- route.set(href, undefined, undefined)
+ route.set(href, undefined, options)
}
}
+ route.link = function(args) {
+ if (args.tag == null) return link.bind(link, args)
+ return link({}, args)
+ }
route.param = function(key) {
if(typeof attrs !== "undefined" && typeof key !== "undefined") return attrs[key]
return attrs
diff --git a/api/tests/index.html b/api/tests/index.html
index 37d313f4..e85bd4b7 100644
--- a/api/tests/index.html
+++ b/api/tests/index.html
@@ -13,8 +13,8 @@
-
-
+
+
@@ -28,7 +28,6 @@
-
diff --git a/api/tests/test-mount.js b/api/tests/test-mount.js
index 53a35640..7385e43d 100644
--- a/api/tests/test-mount.js
+++ b/api/tests/test-mount.js
@@ -3,25 +3,29 @@
var o = require("../../ospec/ospec")
var components = require("../../test-utils/components")
var domMock = require("../../test-utils/domMock")
+var throttleMocker = require("../../test-utils/throttleMock")
var m = require("../../render/hyperscript")
var apiRedraw = require("../../api/redraw")
var apiMounter = require("../../api/mount")
o.spec("mount", function() {
- var FRAME_BUDGET = Math.floor(1000 / 60)
- var $window, root, redrawService, mount, render
+ var $window, root, redrawService, mount, render, throttleMock
o.beforeEach(function() {
$window = domMock()
+ throttleMock = throttleMocker()
root = $window.document.body
-
- redrawService = apiRedraw($window)
+ redrawService = apiRedraw($window, throttleMock.throttle)
mount = apiMounter(redrawService)
render = redrawService.render
})
+ o.afterEach(function() {
+ o(throttleMock.queueLength()).equals(0)
+ })
+
o("throws on invalid component", function() {
var threw = false
try {
@@ -46,7 +50,7 @@ o.spec("mount", function() {
o(threw).equals(true)
})
- o("renders into `root`", function() {
+ o("renders into `root` synchronoulsy", function() {
mount(root, createComponent({
view : function() {
return m("div")
@@ -68,7 +72,37 @@ o.spec("mount", function() {
o(root.childNodes.length).equals(0)
})
- o("redraws on events", function(done) {
+ o("Mounting a second root doesn't cause the first one to redraw", function() {
+ var view = o.spy(function() {
+ return m("div")
+ })
+
+ render(root, [
+ m("#child0"),
+ m("#child1")
+ ])
+
+ mount(root.childNodes[0], createComponent({
+ view : view
+ }))
+
+ o(root.firstChild.nodeName).equals("DIV")
+ o(view.callCount).equals(1)
+
+ mount(root.childNodes[1], createComponent({
+ view : function() {
+ return m("div")
+ }
+ }))
+
+ o(view.callCount).equals(1)
+
+ throttleMock.fire()
+
+ o(view.callCount).equals(1)
+ })
+
+ o("redraws on events", function() {
var onupdate = o.spy()
var oninit = o.spy()
var onclick = o.spy()
@@ -96,17 +130,12 @@ o.spec("mount", function() {
o(onclick.args[0].type).equals("click")
o(onclick.args[0].target).equals(root.firstChild)
- // Wrapped to give time for the rate-limited redraw to fire
- setTimeout(function() {
- o(onupdate.callCount).equals(1)
+ throttleMock.fire()
- done()
- }, FRAME_BUDGET)
+ o(onupdate.callCount).equals(1)
})
- o("redraws several mount points on events", function(done, timeout) {
- timeout(60)
-
+ o("redraws several mount points on events", function() {
var onupdate0 = o.spy()
var oninit0 = o.spy()
var onclick0 = o.spy()
@@ -153,26 +182,26 @@ o.spec("mount", function() {
o(onclick0.callCount).equals(1)
o(onclick0.this).equals(root.childNodes[0].firstChild)
- setTimeout(function() {
- o(onupdate0.callCount).equals(1)
- o(onupdate1.callCount).equals(1)
+ throttleMock.fire()
- root.childNodes[1].firstChild.dispatchEvent(e)
- o(onclick1.callCount).equals(1)
- o(onclick1.this).equals(root.childNodes[1].firstChild)
+ o(onupdate0.callCount).equals(1)
+ o(onupdate1.callCount).equals(1)
- setTimeout(function() {
- o(onupdate0.callCount).equals(2)
- o(onupdate1.callCount).equals(2)
+ root.childNodes[1].firstChild.dispatchEvent(e)
- done()
- }, FRAME_BUDGET)
- }, FRAME_BUDGET)
+ o(onclick1.callCount).equals(1)
+ o(onclick1.this).equals(root.childNodes[1].firstChild)
+ throttleMock.fire()
+
+ o(onupdate0.callCount).equals(2)
+ o(onupdate1.callCount).equals(2)
})
- o("event handlers can skip redraw", function(done) {
- var onupdate = o.spy()
+ o("event handlers can skip redraw", function() {
+ var onupdate = o.spy(function(){
+ throw new Error("This shouldn't have been called")
+ })
var oninit = o.spy()
var e = $window.document.createEvent("MouseEvents")
@@ -194,15 +223,12 @@ o.spec("mount", function() {
o(oninit.callCount).equals(1)
- // Wrapped to ensure no redraw fired
- setTimeout(function() {
- o(onupdate.callCount).equals(0)
+ throttleMock.fire()
- done()
- }, FRAME_BUDGET)
+ o(onupdate.callCount).equals(0)
})
- o("redraws when the render function is run", function(done) {
+ o("redraws when the render function is run", function() {
var onupdate = o.spy()
var oninit = o.spy()
@@ -220,17 +246,12 @@ o.spec("mount", function() {
redrawService.redraw()
- // Wrapped to give time for the rate-limited redraw to fire
- setTimeout(function() {
- o(onupdate.callCount).equals(1)
+ throttleMock.fire()
- done()
- }, FRAME_BUDGET)
+ o(onupdate.callCount).equals(1)
})
- o("throttles", function(done, timeout) {
- timeout(200)
-
+ o("throttles", function() {
var i = 0
mount(root, createComponent({view: function() {i++}}))
var before = i
@@ -242,12 +263,11 @@ o.spec("mount", function() {
var after = i
- setTimeout(function(){
- o(before).equals(1) // mounts synchronously
- o(after).equals(1) // throttles rest
- o(i).equals(2)
- done()
- },40)
+ throttleMock.fire()
+
+ o(before).equals(1) // mounts synchronously
+ o(after).equals(1) // throttles rest
+ o(i).equals(2)
})
})
})
diff --git a/api/tests/test-redraw.js b/api/tests/test-redraw.js
index f13c2d3f..65831bb0 100644
--- a/api/tests/test-redraw.js
+++ b/api/tests/test-redraw.js
@@ -2,6 +2,7 @@
var o = require("../../ospec/ospec")
var domMock = require("../../test-utils/domMock")
+var throttleMocker = require("../../test-utils/throttleMock")
var apiRedraw = require("../../api/redraw")
o.spec("redrawService", function() {
@@ -17,25 +18,39 @@ o.spec("redrawService", function() {
redrawService.redraw()
})
+ o("honours throttleMock", function() {
+ var throttleMock = throttleMocker()
+ redrawService = apiRedraw(domMock(), throttleMock.throttle)
+ var spy = o.spy()
+
+ redrawService.subscribe(root, spy)
+
+ o(spy.callCount).equals(0)
+
+ redrawService.redraw()
+
+ o(spy.callCount).equals(0)
+
+ throttleMock.fire()
+
+ o(spy.callCount).equals(1)
+ })
+
o("should run a single renderer entry", function(done) {
var spy = o.spy()
redrawService.subscribe(root, spy)
o(spy.callCount).equals(0)
-
- redrawService.redraw()
-
- o(spy.callCount).equals(1)
redrawService.redraw()
redrawService.redraw()
redrawService.redraw()
- o(spy.callCount).equals(1)
+ o(spy.callCount).equals(0)
setTimeout(function() {
- o(spy.callCount).equals(2)
-
+ o(spy.callCount).equals(1)
+
done()
}, 20)
})
@@ -54,27 +69,29 @@ o.spec("redrawService", function() {
redrawService.redraw()
- o(spy1.callCount).equals(1)
- o(spy2.callCount).equals(1)
- o(spy3.callCount).equals(1)
+ o(spy1.callCount).equals(0)
+ o(spy2.callCount).equals(0)
+ o(spy3.callCount).equals(0)
redrawService.redraw()
- o(spy1.callCount).equals(1)
- o(spy2.callCount).equals(1)
- o(spy3.callCount).equals(1)
+ o(spy1.callCount).equals(0)
+ o(spy2.callCount).equals(0)
+ o(spy3.callCount).equals(0)
setTimeout(function() {
- o(spy1.callCount).equals(2)
- o(spy2.callCount).equals(2)
- o(spy3.callCount).equals(2)
-
+ o(spy1.callCount).equals(1)
+ o(spy2.callCount).equals(1)
+ o(spy3.callCount).equals(1)
+
done()
}, 20)
})
- o("should stop running after unsubscribe", function() {
- var spy = o.spy()
+ o("should stop running after unsubscribe", function(done) {
+ var spy = o.spy(function() {
+ throw new Error("This shouldn't have been called")
+ })
redrawService.subscribe(root, spy)
redrawService.unsubscribe(root, spy)
@@ -82,9 +99,33 @@ o.spec("redrawService", function() {
redrawService.redraw()
o(spy.callCount).equals(0)
+ setTimeout(function() {
+ o(spy.callCount).equals(0)
+
+ done()
+ }, 20)
})
- o("does nothing on invalid unsubscribe", function() {
+ o("should stop running after unsubscribe, even if it occurs after redraw is requested", function(done) {
+ var spy = o.spy(function() {
+ throw new Error("This shouldn't have been called")
+ })
+
+ redrawService.subscribe(root, spy)
+
+ redrawService.redraw()
+
+ redrawService.unsubscribe(root, spy)
+
+ o(spy.callCount).equals(0)
+ setTimeout(function() {
+ o(spy.callCount).equals(0)
+
+ done()
+ }, 20)
+ })
+
+ o("does nothing on invalid unsubscribe", function(done) {
var spy = o.spy()
redrawService.subscribe(root, spy)
@@ -92,6 +133,39 @@ o.spec("redrawService", function() {
redrawService.redraw()
- o(spy.callCount).equals(1)
+ setTimeout(function() {
+ o(spy.callCount).equals(1)
+
+ done()
+ }, 20)
+ })
+
+ o("redraw.sync() redraws all roots synchronously", function() {
+ var el1 = $document.createElement("div")
+ var el2 = $document.createElement("div")
+ var el3 = $document.createElement("div")
+ var spy1 = o.spy()
+ var spy2 = o.spy()
+ var spy3 = o.spy()
+
+ redrawService.subscribe(el1, spy1)
+ redrawService.subscribe(el2, spy2)
+ redrawService.subscribe(el3, spy3)
+
+ o(spy1.callCount).equals(0)
+ o(spy2.callCount).equals(0)
+ o(spy3.callCount).equals(0)
+
+ redrawService.redraw.sync()
+
+ o(spy1.callCount).equals(1)
+ o(spy2.callCount).equals(1)
+ o(spy3.callCount).equals(1)
+
+ redrawService.redraw.sync()
+
+ o(spy1.callCount).equals(2)
+ o(spy2.callCount).equals(2)
+ o(spy3.callCount).equals(2)
})
})
diff --git a/api/tests/test-router.js b/api/tests/test-router.js
index f0defd22..f5336625 100644
--- a/api/tests/test-router.js
+++ b/api/tests/test-router.js
@@ -3,6 +3,7 @@
var o = require("../../ospec/ospec")
var callAsync = require("../../test-utils/callAsync")
var browserMock = require("../../test-utils/browserMock")
+var throttleMocker = require("../../test-utils/throttleMock")
var m = require("../../render/hyperscript")
var callAsync = require("../../test-utils/callAsync")
@@ -14,19 +15,23 @@ o.spec("route", function() {
void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}].forEach(function(env) {
void ["#", "?", "", "#!", "?!", "/foo"].forEach(function(prefix) {
o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() {
- var FRAME_BUDGET = Math.floor(1000 / 60)
- var $window, root, redrawService, route
+ var $window, root, redrawService, route, throttleMock
o.beforeEach(function() {
$window = browserMock(env)
+ throttleMock = throttleMocker()
root = $window.document.body
- redrawService = apiRedraw($window)
+ redrawService = apiRedraw($window, throttleMock.throttle)
route = apiRouter($window, redrawService)
route.prefix(prefix)
})
+ o.afterEach(function() {
+ o(throttleMock.queueLength()).equals(0)
+ })
+
o("throws on invalid `root` DOM node", function() {
var threw = false
try {
@@ -50,7 +55,7 @@ o.spec("route", function() {
o(root.firstChild.nodeName).equals("DIV")
})
- o("routed mount points can redraw synchronously (POJO component)", function() {
+ o("routed mount points only redraw asynchronously (POJO component)", function() {
var view = o.spy()
$window.location.href = prefix + "/"
@@ -60,11 +65,14 @@ o.spec("route", function() {
redrawService.redraw()
- o(view.callCount).equals(2)
+ o(view.callCount).equals(1)
+ throttleMock.fire()
+
+ o(view.callCount).equals(2)
})
- o("routed mount points can redraw synchronously (constructible component)", function() {
+ o("routed mount points only redraw asynchronously (constructible component)", function() {
var view = o.spy()
var Cmp = function(){}
@@ -77,11 +85,14 @@ o.spec("route", function() {
redrawService.redraw()
- o(view.callCount).equals(2)
+ o(view.callCount).equals(1)
+ throttleMock.fire()
+
+ o(view.callCount).equals(2)
})
- o("routed mount points can redraw synchronously (closure component)", function() {
+ o("routed mount points only redraw asynchronously (closure component)", function() {
var view = o.spy()
function Cmp() {return {view: view}}
@@ -93,8 +104,11 @@ o.spec("route", function() {
redrawService.redraw()
- o(view.callCount).equals(2)
+ o(view.callCount).equals(1)
+ throttleMock.fire()
+
+ o(view.callCount).equals(2)
})
o("default route doesn't break back button", function(done) {
@@ -160,11 +174,12 @@ o.spec("route", function() {
o(oninit.callCount).equals(1)
redrawService.redraw()
+ throttleMock.fire()
o(onupdate.callCount).equals(1)
})
- o("redraws on events", function(done) {
+ o("redraws on events", function() {
var onupdate = o.spy()
var oninit = o.spy()
var onclick = o.spy()
@@ -194,12 +209,9 @@ o.spec("route", function() {
o(onclick.args[0].type).equals("click")
o(onclick.args[0].target).equals(root.firstChild)
- // Wrapped to give time for the rate-limited redraw to fire
- callAsync(function() {
- o(onupdate.callCount).equals(1)
- done()
- })
+ throttleMock.fire()
+ o(onupdate.callCount).equals(1)
})
o("event handlers can skip redraw", function(done) {
@@ -269,6 +281,36 @@ o.spec("route", function() {
o($window.location.href).equals(env.protocol + "//" + (env.hostname === "/" ? "" : env.hostname) + slash + (prefix ? prefix + "/" : "") + "test")
})
+ o("passes options on route.link", function() {
+ var opts = {}
+ var e = $window.document.createEvent("MouseEvents")
+
+ e.initEvent("click", true, true)
+ $window.location.href = prefix + "/"
+
+ route(root, "/", {
+ "/" : {
+ view: function() {
+ return m("a", {
+ href: "/test",
+ oncreate: route.link(opts)
+ })
+ }
+ },
+ "/test" : {
+ view : function() {
+ return m("div")
+ }
+ }
+ })
+ route.set = o.spy(route.set)
+
+ root.firstChild.dispatchEvent(e)
+
+ o(route.set.callCount).equals(1)
+ o(route.set.args[2]).equals(opts)
+ })
+
o("accepts RouteResolver with onmatch that returns Component", function(done) {
var matchCount = 0
var renderCount = 0
@@ -502,7 +544,10 @@ o.spec("route", function() {
o(oninit.callCount).equals(1)
route.set("/def")
callAsync(function() {
+ throttleMock.fire()
+
o(oninit.callCount).equals(2)
+
done()
})
})
@@ -538,23 +583,28 @@ o.spec("route", function() {
route(root, "/a", {
"/a" : {
render: function() {
- return m("div")
+ return m("div", m("p"))
},
},
"/b" : {
render: function() {
- return m("div")
+ return m("div", m("a"))
},
},
})
var dom = root.firstChild
+ var child = dom.firstChild
+
o(root.firstChild.nodeName).equals("DIV")
route.set("/b")
callAsync(function() {
+ throttleMock.fire()
+
o(root.firstChild).equals(dom)
+ o(root.firstChild.firstChild).notEquals(child)
done()
})
@@ -588,6 +638,7 @@ o.spec("route", function() {
o(renderCount).equals(1)
redrawService.redraw()
+ throttleMock.fire()
o(matchCount).equals(1)
o(renderCount).equals(2)
@@ -623,6 +674,7 @@ o.spec("route", function() {
o(renderCount).equals(1)
redrawService.redraw()
+ throttleMock.fire()
o(matchCount).equals(1)
o(renderCount).equals(2)
@@ -818,10 +870,14 @@ o.spec("route", function() {
})
callAsync(function() {
+ throttleMock.fire()
+
route.set("/b")
callAsync(function() {
callAsync(function() {
callAsync(function() {
+ throttleMock.fire()
+
o(render.callCount).equals(0)
o(component.view.callCount).equals(2)
@@ -942,6 +998,7 @@ o.spec("route", function() {
o(onmatch.callCount).equals(1)
redrawService.redraw()
+ throttleMock.fire()
o(view.callCount).equals(2)
o(onmatch.callCount).equals(1)
@@ -1020,6 +1077,8 @@ o.spec("route", function() {
})
callAsync(function() {
+ throttleMock.fire()
+
o(onmatch.callCount).equals(1)
o(render.callCount).equals(1)
@@ -1027,6 +1086,8 @@ o.spec("route", function() {
callAsync(function() {
callAsync(function() {
+ throttleMock.fire()
+
o(onmatch.callCount).equals(2)
o(render.callCount).equals(2)
@@ -1077,9 +1138,15 @@ o.spec("route", function() {
route.set("/b")
callAsync(function() {
+ throttleMock.fire()
+
+ o(root.firstChild.nodeName).equals("B")
+
route.set("/a")
callAsync(function() {
+ throttleMock.fire()
+
o(root.firstChild.nodeName).equals("A")
done()
@@ -1144,7 +1211,9 @@ o.spec("route", function() {
route.set("/b")
+ // setting the route is asynchronous
callAsync(function() {
+ throttleMock.fire()
o(spy.callCount).equals(1)
done()
@@ -1184,9 +1253,7 @@ o.spec("route", function() {
})
})
- o("throttles", function(done, timeout) {
- timeout(200)
-
+ o("throttles", function() {
var i = 0
$window.location.href = prefix + "/"
route(root, "/", {
@@ -1200,12 +1267,11 @@ o.spec("route", function() {
redrawService.redraw()
var after = i
- setTimeout(function() {
- o(before).equals(1) // routes synchronously
- o(after).equals(2) // redraws synchronously
- o(i).equals(3) // throttles rest
- done()
- }, FRAME_BUDGET * 2)
+ throttleMock.fire()
+
+ o(before).equals(1) // routes synchronously
+ o(after).equals(1) // redraws asynchronously
+ o(i).equals(2)
})
o("m.route.param is available outside of route handlers", function(done) {
diff --git a/bundler/bundle.js b/bundler/bundle.js
index 54971214..af42e3ad 100644
--- a/bundler/bundle.js
+++ b/bundler/bundle.js
@@ -17,113 +17,109 @@ function parse(file) {
var error
function run(input, output) {
- try {
- var modules = {}
- var bindings = {}
- var declaration = /^\s*(?:var|let|const|function)[\t ]+([\w_$]+)/gm
- var include = /(?:((?:var|let|const|,|)[\t ]*)([\w_$\.\[\]"'`]+)(\s*=\s*))?require\(([^\)]+)\)(\s*[`\.\(\[])?/gm
- var uuid = 0
- var process = function(filepath, data) {
- data.replace(declaration, function(match, binding) {bindings[binding] = 0})
-
- return data.replace(include, function(match, def, variable, eq, dep, rest) {
- var filename = new Function("return " + dep).call(), pre = ""
-
- def = def || "", variable = variable || "", eq = eq || "", rest = rest || ""
- if (def[0] === ",") def = "\nvar ", pre = "\n"
- var dependency = resolve(filepath, filename)
- var localUUID = uuid // global uuid can update from nested `process` call, ensure same id is used on declaration and consumption
- var code = process(dependency, pre + (modules[dependency] == null ? exportCode(filename, dependency, def, variable, eq, rest, localUUID) : def + variable + eq + modules[dependency]))
- modules[dependency] = rest ? "_" + localUUID : variable
- uuid++
- return code + rest
- })
+ var modules = {}
+ var bindings = {}
+ var declaration = /^\s*(?:var|let|const|function)[\t ]+([\w_$]+)/gm
+ var include = /(?:((?:var|let|const|,|)[\t ]*)([\w_$\.\[\]"'`]+)(\s*=\s*))?require\(([^\)]+)\)(\s*[`\.\(\[])?/gm
+ var uuid = 0
+ var process = function(filepath, data) {
+ data.replace(declaration, function(match, binding) {bindings[binding] = 0})
+
+ return data.replace(include, function(match, def, variable, eq, dep, rest) {
+ var filename = new Function("return " + dep).call(), pre = ""
+
+ def = def || "", variable = variable || "", eq = eq || "", rest = rest || ""
+ if (def[0] === ",") def = "\nvar ", pre = "\n"
+ var dependency = resolve(filepath, filename)
+ var localUUID = uuid // global uuid can update from nested `process` call, ensure same id is used on declaration and consumption
+ var code = process(dependency, pre + (modules[dependency] == null ? exportCode(filename, dependency, def, variable, eq, rest, localUUID) : def + variable + eq + modules[dependency]))
+ modules[dependency] = rest ? "_" + localUUID : variable
+ uuid++
+ return code + rest
+ })
+ }
+
+ var resolve = function(filepath, filename) {
+ if (filename[0] !== ".") {
+ // resolve as npm dependency
+ var packagePath = "./node_modules/" + filename + "/package.json"
+ var meta = isFile(packagePath) ? parse(packagePath) : {}
+ var main = "./node_modules/" + filename + "/" + (meta.main || filename + ".js")
+ return path.resolve(isFile(main) ? main : "./node_modules/" + filename + "/index.js")
}
-
- var resolve = function(filepath, filename) {
- if (filename[0] !== ".") {
- // resolve as npm dependency
- var packagePath = "./node_modules/" + filename + "/package.json"
- var meta = isFile(packagePath) ? parse(packagePath) : {}
- var main = "./node_modules/" + filename + "/" + (meta.main || filename + ".js")
- return path.resolve(isFile(main) ? main : "./node_modules/" + filename + "/index.js")
- }
- else {
- // resolve as local dependency
- return path.resolve(path.dirname(filepath), filename + ".js")
- }
- }
-
- var exportCode = function(filename, filepath, def, variable, eq, rest, uuid) {
- var code = read(filepath)
- // if there's a syntax error, report w/ proper stack trace
- try {new Function(code)} catch (e) {
- proc.exec("node " + filepath, function(e) {
- if (e !== null && e.message !== error) {
- error = e.message
- console.log("\x1b[31m" + e.message + "\x1b[0m")
- }
- })
- }
-
- // disambiguate collisions
- var ignored = {}
- code.replace(include, function(match, def, variable, eq, dep) {
- var filename = new Function("return " + dep).call()
- var binding = modules[resolve(filepath, filename)]
- if (binding != null) ignored[binding] = true
- })
- if (code.match(new RegExp("module\\.exports\\s*=\\s*" + variable + "\s*$", "m"))) ignored[variable] = true
- for (var binding in bindings) {
- if (!ignored[binding]) {
- var before = code
- code = code.replace(new RegExp("(\\b)" + binding + "\\b", "g"), binding + bindings[binding])
- if (before !== code) bindings[binding]++
- }
- }
-
- // fix strings that got mangled by collision disambiguation
- var string = /(["'])((?:\\\1|.)*?)(\1)/g
- var candidates = Object.keys(bindings).map(function(binding) {return binding + (bindings[binding] - 1)}).join("|")
- code = code.replace(string, function(match, open, data, close) {
- var variables = new RegExp(Object.keys(bindings).map(function(binding) {return binding + (bindings[binding] - 1)}).join("|"), "g")
- var fixed = data.replace(variables, function(match) {
- return match.replace(/\d+$/, "")
- })
- return open + fixed + close
- })
-
- //fix props
- var props = new RegExp("(\\.\\s*)(" + candidates + ")|([\\{,]\\s*)(" + candidates + ")(\\s*:)", "gm")
- code = code.replace(props, function(match, dot, a, pre, b, post) {
- if (dot) return dot + a.replace(/\d+$/, "")
- else return pre + b.replace(/\d+$/, "") + post
- })
-
- return code
- .replace(/("|')use strict\1;?/gm, "") // remove extraneous "use strict"
- .replace(/module\.exports\s*=\s*/gm, rest ? "var _" + uuid + eq : def + (rest ? "_" : "") + variable + eq) // export
- + (rest ? "\n" + def + variable + eq + "_" + uuid : "") // if `rest` is truthy, it means the expression is fluent or higher-order (e.g. require(path).foo or require(path)(foo)
- }
-
- var versionTag = "bleeding-edge"
- var packageFile = __dirname + "/../package.json"
- var code = process(path.resolve(input), read(input))
- .replace(/^\s*((?:var|let|const|)[\t ]*)([\w_$\.]+)(\s*=\s*)(\2)(?=[\s]+(\w)|;|$)/gm, "") // remove assignments to self
- .replace(/;+(\r|\n|$)/g, ";$1") // remove redundant semicolons
- .replace(/(\r|\n)+/g, "\n").replace(/(\r|\n)$/, "") // remove multiline breaks
- .replace(versionTag, isFile(packageFile) ? parse(packageFile).version : versionTag) // set version
-
- code = ";(function() {\n" + code + "\n}());"
-
- if (!isFile(output) || code !== read(output)) {
- //try {new Function(code); console.log("build completed at " + new Date())} catch (e) {}
- error = null
- fs.writeFileSync(output, code, "utf8")
+ else {
+ // resolve as local dependency
+ return path.resolve(path.dirname(filepath), filename + ".js")
}
}
- catch (e) {
- console.error(e.message)
+
+ var exportCode = function(filename, filepath, def, variable, eq, rest, uuid) {
+ var code = read(filepath)
+ // if there's a syntax error, report w/ proper stack trace
+ try {new Function(code)} catch (e) {
+ proc.exec("node " + filepath, function(e) {
+ if (e !== null && e.message !== error) {
+ error = e.message
+ console.log("\x1b[31m" + e.message + "\x1b[0m")
+ }
+ })
+ }
+
+ // disambiguate collisions
+ var ignored = {}
+ code.replace(include, function(match, def, variable, eq, dep) {
+ var filename = new Function("return " + dep).call()
+ var binding = modules[resolve(filepath, filename)]
+ if (binding != null) ignored[binding] = true
+ })
+ if (code.match(new RegExp("module\\.exports\\s*=\\s*" + variable + "\s*$", "m"))) ignored[variable] = true
+ for (var binding in bindings) {
+ if (!ignored[binding]) {
+ var before = code
+ code = code.replace(new RegExp("(\\b)" + binding + "\\b", "g"), binding + bindings[binding])
+ if (before !== code) bindings[binding]++
+ }
+ }
+
+ // fix strings that got mangled by collision disambiguation
+ var string = /(["'])((?:\\\1|.)*?)(\1)/g
+ var candidates = Object.keys(bindings).map(function(binding) {return binding + (bindings[binding] - 1)}).join("|")
+ code = code.replace(string, function(match, open, data, close) {
+ var variables = new RegExp(Object.keys(bindings).map(function(binding) {return binding + (bindings[binding] - 1)}).join("|"), "g")
+ var fixed = data.replace(variables, function(match) {
+ return match.replace(/\d+$/, "")
+ })
+ return open + fixed + close
+ })
+
+ //fix props
+ var props = new RegExp("((?:[^:]\\/\\/.*)?\\.\\s*)(" + candidates + ")|([\\{,]\\s*)(" + candidates + ")(\\s*:)", "gm")
+ code = code.replace(props, function(match, dot, a, pre, b, post) {
+ if (dot && dot.indexOf("//") === 1) return match // Don't do anything because dot was matched in a comment
+ else if (dot) return dot + a.replace(/\d+$/, "")
+ else return pre + b.replace(/\d+$/, "") + post
+ })
+
+ return code
+ .replace(/("|')use strict\1;?/gm, "") // remove extraneous "use strict"
+ .replace(/module\.exports\s*=\s*/gm, rest ? "var _" + uuid + eq : def + (rest ? "_" : "") + variable + eq) // export
+ + (rest ? "\n" + def + variable + eq + "_" + uuid : "") // if `rest` is truthy, it means the expression is fluent or higher-order (e.g. require(path).foo or require(path)(foo)
+ }
+
+ var versionTag = "bleeding-edge"
+ var packageFile = __dirname + "/../package.json"
+ var code = process(path.resolve(input), read(input))
+ .replace(/^\s*((?:var|let|const|)[\t ]*)([\w_$\.]+)(\s*=\s*)(\2)(?=[\s]+(\w)|;|$)/gm, "") // remove assignments to self
+ .replace(/;+(\r|\n|$)/g, ";$1") // remove redundant semicolons
+ .replace(/(\r|\n)+/g, "\n").replace(/(\r|\n)$/, "") // remove multiline breaks
+ .replace(versionTag, isFile(packageFile) ? parse(packageFile).version : versionTag) // set version
+
+ code = ";(function() {\n" + code + "\n}());"
+
+ if (!isFile(output) || code !== read(output)) {
+ //try {new Function(code); console.log("build completed at " + new Date())} catch (e) {}
+ error = null
+ fs.writeFileSync(output, code, "utf8")
}
}
diff --git a/bundler/cli.js b/bundler/cli.js
index f702ce7e..93ad0adf 100644
--- a/bundler/cli.js
+++ b/bundler/cli.js
@@ -5,7 +5,7 @@ var fs = require("fs");
var bundle = require("./bundle")
var minify = require("./minify")
-var aliases = {o: "output", m: "minify", w: "watch", a: "aggressive"}
+var aliases = {o: "output", m: "minify", w: "watch", a: "aggressive", s: "save"}
var params = {}
var args = process.argv.slice(2), command = null
for (var i = 0; i < args.length; i++) {
@@ -27,8 +27,6 @@ function add(value) {
bundle(params.input, params.output, {watch: params.watch})
if (params.minify) {
minify(params.output, params.output, {watch: params.watch, advanced: params.aggressive}, function(stats) {
- var readme, kb;
-
function format(n) {
return n.toString().replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,")
}
@@ -36,14 +34,16 @@ if (params.minify) {
console.log("Original size: " + format(stats.originalGzipSize) + " bytes gzipped (" + format(stats.originalSize) + " bytes uncompressed)")
console.log("Compiled size: " + format(stats.compressedGzipSize) + " bytes gzipped (" + format(stats.compressedSize) + " bytes uncompressed)")
- readme = fs.readFileSync("./README.md", "utf8")
- kb = stats.compressedGzipSize / 1024
+ if (params.save) {
+ var readme = fs.readFileSync("./README.md", "utf8")
+ var kb = stats.compressedGzipSize / 1000
- fs.writeFileSync("./README.md",
- readme.replace(
- /()(.+?)()/,
- "$1" + (kb % 1 ? kb.toFixed(2) : kb) + " KB$3"
+ fs.writeFileSync("./README.md",
+ readme.replace(
+ /()(.+?)()/,
+ "$1" + (kb % 1 ? kb.toFixed(2) : kb) + " KB$3"
+ )
)
- )
+ }
})
}
diff --git a/bundler/tests/test-bundler.js b/bundler/tests/test-bundler.js
index 15e7f932..ff8bb32e 100644
--- a/bundler/tests/test-bundler.js
+++ b/bundler/tests/test-bundler.js
@@ -5,7 +5,7 @@ var bundle = require("../bundle")
var fs = require("fs")
-var ns = "bundler/tests/"
+var ns = "./"
function read(filepath) {
try {return fs.readFileSync(ns + filepath, "utf8")} catch (e) {/* ignore */}
}
diff --git a/docs/animation.md b/docs/animation.md
index 7e251580..95dd4c0c 100644
--- a/docs/animation.md
+++ b/docs/animation.md
@@ -17,7 +17,7 @@ Mithril does not provide any animation APIs per se, since these other options ar
### Animation on element creation
-Animating an element via CSS when the element created couldn't be simpler. Just add an animation to a CSS class normally:
+Animating an element via CSS when the element is created couldn't be simpler. Just add an animation to a CSS class normally:
```css
.fancy {animation:fade-in 0.5s;}
@@ -41,7 +41,7 @@ m.mount(document.body, FancyComponent)
### Animation on element removal
-The problem with animating before removing an element is that we must wait until the animation is complete before we can actually remove the element. Fortunately, Mithril offers a [`onbeforeremove`](lifecycle-methods.md#onbeforeremove) hook that allows us to defer the removal of an element.
+The problem with animating before removing an element is that we must wait until the animation is complete before we can actually remove the element. Fortunately, Mithril offers the [`onbeforeremove`](lifecycle-methods.md#onbeforeremove) hook that allows us to defer the removal of an element.
Let's create an `exit` animation that fades `opacity` from 1 to 0.
@@ -75,7 +75,7 @@ var FancyComponent = {
onbeforeremove: function(vnode) {
vnode.dom.classList.add("exit")
return new Promise(function(resolve) {
- setTimeout(resolve, 500)
+ vnode.dom.addEventListener("animationend", resolve)
})
},
view: function() {
@@ -86,7 +86,7 @@ var FancyComponent = {
`vnode.dom` points to the root DOM element of the component (`
`). We use the classList API here to add an `exit` class to `
`.
-Then we return a [Promise](promise.md) that resolves after half a second. When we return a promise from `onbeforeremove`, Mithril waits until the promise is resolved and only then it removes the element. In this case, it waits half a second, giving the exit animation the exact time it needs to complete.
+Then we return a [Promise](promise.md) that resolves when the `animationend` event fires. When we return a promise from `onbeforeremove`, Mithril waits until the promise is resolved and only then it removes the element. In this case, it waits for the exit animation to finish.
We can verify that both the enter and exit animations work by mounting the `Toggler` component:
diff --git a/docs/change-log.md b/docs/change-log.md
index e99fcea0..239dbed7 100644
--- a/docs/change-log.md
+++ b/docs/change-log.md
@@ -1,5 +1,6 @@
# Change log
+- [v2.0.0-rc](#v200rc)
- [v1.1.6](#v116)
- [v1.1.5](#v115)
- [v1.1.4](#v114)
@@ -10,6 +11,86 @@
- [v1.0.1](#v101)
- [Migrating from v0.2.x](#migrating-from-v02x)
- [Older docs](http://mithril.js.org/archive/v0.2.5/index.html)
+- [ospec change-log](../ospec/change-log.md)
+
+---
+
+### v2.0.0-rc
+
+#### Breaking changes
+
+- API: Component vnode `children` are not normalized into vnodes on ingestion; normalization only happens if and when they are ingested by the view ([#2155](https://github.com/MithrilJS/mithril.js/pull/2155/) (thanks to [@magikstm](https://github.com/magikstm) for related optimization [#2064](https://github.com/MithrilJS/mithril.js/pull/2064)))
+- API: `m.redraw()` is always asynchronous ([#1592](https://github.com/MithrilJS/mithril.js/pull/1592))
+- API: `m.mount()` will only render its own root when called, it will not trigger a `redraw()` ([#1592](https://github.com/MithrilJS/mithril.js/pull/1592))
+- API: Assigning to `vnode.state` (as in `vnode.state = ...`) is no longer supported. Instead, an error is thrown if `vnode.state` changes upon the invocation of a lifecycle hook.
+- API: `m.request` will no longer reject the Promise on server errors (eg. status >= 400) if the caller supplies an `extract` callback. This gives applications more control over handling server responses.
+- hyperscript: when attributes have a `null` or `undefined` value, they are treated as if they were absent. [#1773](https://github.com/MithrilJS/mithril.js/issues/1773) ([#2174](https://github.com/MithrilJS/mithril.js/pull/2174))
+- hyperscript: when an attribute is defined on both the first and second argument (as a CSS selector and an `attrs` field, respectively), the latter takes precedence, except for `class` attributes that are still added together. [#2172](https://github.com/MithrilJS/mithril.js/issues/2172) ([#2174](https://github.com/MithrilJS/mithril.js/pull/2174))
+- stream: when a stream conditionally returns HALT, dependant stream will also end ([#2200](https://github.com/MithrilJS/mithril.js/pull/2200))
+- render: remove some redundancy within the component initialization code ([#2213](https://github.com/MithrilJS/mithril.js/pull/2213))
+- render: Align custom elements to work like normal elements, minus all the HTML-specific magic. ([#2221](https://github.com/MithrilJS/mithril.js/pull/2221))
+- render: simplify component removal ([#2214](https://github.com/MithrilJS/mithril.js/pull/2214))
+
+#### News
+
+- API: Introduction of `m.redraw.sync()` ([#1592](https://github.com/MithrilJS/mithril.js/pull/1592))
+- API: Event handlers may also be objects with `handleEvent` methods ([#1949](https://github.com/MithrilJS/mithril.js/pull/1949), [#2222](https://github.com/MithrilJS/mithril.js/pull/2222)).
+- API: `m.route.link` accepts an optional `options` object ([#1930](https://github.com/MithrilJS/mithril.js/pull/1930))
+- API: `m.request` better error message on JSON parse error - ([#2195](https://github.com/MithrilJS/mithril.js/pull/2195), [@codeclown](https://github.com/codeclown))
+- API: `m.request` supports `timeout` as attr - ([#1966](https://github.com/MithrilJS/mithril.js/pull/1966))
+- API: `m.request` supports `responseType` as attr - ([#2193](https://github.com/MithrilJS/mithril.js/pull/2193))
+- Mocks: add limited support for the DOMParser API ([#2097](https://github.com/MithrilJS/mithril.js/pull/2097))
+- API: add support for raw SVG in `m.trust()` string ([#2097](https://github.com/MithrilJS/mithril.js/pull/2097))
+- render/core: remove the DOM nodes recycling pool ([#2122](https://github.com/MithrilJS/mithril.js/pull/2122))
+- render/core: revamp the core diff engine, and introduce a longest-increasing-subsequence-based logic to minimize DOM operations when re-ordering keyed nodes.
+
+#### Bug fixes
+
+- API: `m.route.set()` causes all mount points to be redrawn ([#1592](https://github.com/MithrilJS/mithril.js/pull/1592))
+- render/attrs: Using style objects in hyperscript calls will now properly diff style properties from one render to another as opposed to re-writing all element style properties every render.
+- render/attrs All vnodes attributes are properly removed when absent or set to `null` or `undefined` [#1804](https://github.com/MithrilJS/mithril.js/issues/1804) [#2082](https://github.com/MithrilJS/mithril.js/issues/2082) ([#1865](https://github.com/MithrilJS/mithril.js/pull/1865), [#2130](https://github.com/MithrilJS/mithril.js/pull/2130))
+- render/core: Render state correctly on select change event [#1916](https://github.com/MithrilJS/mithril.js/issues/1916) ([#1918](https://github.com/MithrilJS/mithril.js/pull/1918) [@robinchew](https://github.com/robinchew), [#2052](https://github.com/MithrilJS/mithril.js/pull/2052))
+- render/core: fix various updateNodes/removeNodes issues when the pool and fragments are involved [#1990](https://github.com/MithrilJS/mithril.js/issues/1990), [#1991](https://github.com/MithrilJS/mithril.js/issues/1991), [#2003](https://github.com/MithrilJS/mithril.js/issues/2003), [#2021](https://github.com/MithrilJS/mithril.js/pull/2021)
+- render/core: fix crashes when the keyed vnodes with the same `key` had different `tag` values [#2128](https://github.com/MithrilJS/mithril.js/issues/2128) [@JacksonJN](https://github.com/JacksonJN) ([#2130](https://github.com/MithrilJS/mithril.js/pull/2130))
+- render/core: fix cached nodes behavior in some keyed diff scenarios [#2132](https://github.com/MithrilJS/mithril.js/issues/2132) ([#2130](https://github.com/MithrilJS/mithril.js/pull/2130))
+- render/events: `addEventListener` and `removeEventListener` are always used to manage event subscriptions, preventing external interference.
+- render/events: Event listeners allocate less memory, swap at low cost, and are properly diffed now when rendered via `m.mount()`/`m.redraw()`.
+- render/events: `Object.prototype` properties can no longer interfere with event listener calls.
+- render/events: Event handlers, when set to literally `undefined` (or any non-function), are now correctly removed.
+- render/hooks: fixed an ommission that caused `oninit` to be called unnecessarily in some cases [#1992](https://github.com/MithrilJS/mithril.js/issues/1992)
+- docs: tweaks: ([#2104](https://github.com/MithrilJS/mithril.js/pull/2104) [@mikeyb](https://github.com/mikeyb), [#2205](https://github.com/MithrilJS/mithril.js/pull/2205), [@cavemansspa](https://github.com/cavemansspa))
+- render/core: avoid touching `Object.prototype.__proto__` setter with `key: "__proto__"` in certain situations ([#2251](https://github.com/MithrilJS/mithril.js/pull/2251))
+
+---
+
+### v1.1.7
+
+- Stream references no longer magically coerce to their underlying values ([#2150](https://github.com/MithrilJS/mithril.js/pull/2150), breaking change: `mithril-stream@2.0.0`)
+- Promise polyfill implementation separated from polyfilling logic.
+- `PromisePolyfill` is now available on the exported/global `m`.
+
+---
+
+### v1.1.6
+
+- core: render() function can no longer prevent from changing `document.activeElement` in lifecycle hooks ([#1988](https://github.com/MithrilJS/mithril.js/pull/1988), [@purplecode](https://github.com/purplecode))
+- core: don't call `onremove` on the children of components that return null from the view [#1921](https://github.com/MithrilJS/mithril.js/issues/1921) [@octavore](https://github.com/octavore) ([#1922](https://github.com/MithrilJS/mithril.js/pull/1922))
+- hypertext: correct handling of shared attributes object passed to `m()`. Will copy attributes when it's necessary [#1941](https://github.com/MithrilJS/mithril.js/issues/1941) [@s-ilya](https://github.com/s-ilya) ([#1942](https://github.com/MithrilJS/mithril.js/pull/1942))
+
+
+---
+
+### v1.1.5
+
+- API: If a user sets the Content-Type header within a request's options, that value will be the entire header value rather than being appended to the default value ([#1924](https://github.com/MithrilJS/mithril.js/pull/1924))
+
+---
+
+### v1.1.4
+
+#### Bug fixes:
+
+- Fix IE bug where active element is null causing render function to throw error ([#1943](https://github.com/MithrilJS/mithril.js/pull/1943), [@JacksonJN](https://github.com/JacksonJN))
---
@@ -75,10 +156,7 @@
- router: Don't overwrite the options object when redirecting from `onmatch with m.route.set()` [#1857](https://github.com/MithrilJS/mithril.js/issues/1857) ([#1889](https://github.com/MithrilJS/mithril.js/pull/1889))
- stream: Move the "use strict" directive inside the IIFE [#1831](https://github.com/MithrilJS/mithril.js/issues/1831) ([#1893](https://github.com/MithrilJS/mithril.js/pull/1893))
-#### Ospec improvements
-
-- Shell command: Ignore hidden directories and files ([#1855](https://github.com/MithrilJS/mithril.js/pull/1855) [@pdfernhout)](https://github.com/pdfernhout))
-- Library: Add the possibility to name new test suites ([#1529](https://github.com/MithrilJS/mithril.js/pull/1529))
+---
#### Docs / Repo maintenance
diff --git a/docs/examples.md b/docs/examples.md
index 749900cc..68e93dc3 100644
--- a/docs/examples.md
+++ b/docs/examples.md
@@ -3,6 +3,7 @@
Here are some examples of Mithril in action
- [Animation](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/animation/mosaic.html)
+- [Community Added Examples](https://how-to-mithril.js.org)
- [DBMonster](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/dbmonster/mithril/index.html)
- [Markdown Editor](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/editor/index.html)
- SVG: [Clock](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/svg/clock.html), [Ring](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/svg/ring.html), [Tiger](http://cdn.rawgit.com/MithrilJS/mithril.js/master/examples/svg/tiger.html)
diff --git a/docs/hyperscript.md b/docs/hyperscript.md
index 8ebe297b..d0ac2767 100644
--- a/docs/hyperscript.md
+++ b/docs/hyperscript.md
@@ -5,6 +5,7 @@
- [How it works](#how-it-works)
- [Flexibility](#flexibility)
- [CSS selectors](#css-selectors)
+- [Attributes passed as the second argument](attributes-passed-as-the-second-argument)
- [DOM attributes](#dom-attributes)
- [Style attribute](#style-attribute)
- [Events](#events)
@@ -144,7 +145,23 @@ m("a.link[href=/]", {
// Home
```
-If there are class names in both first and second arguments of `m()`, they are merged together as you would expect.
+### Attributes passed as the second argument
+
+You can pass attributes, properties, events and lifecycle hooks in the second, optional argument (see the next sections for details).
+
+```JS
+m("button", {
+ class: "my-button",
+ onclick: function() {/* ... */},
+ oncreate: function() {/* ... */}
+})
+```
+
+If the value of such an attribute is `null` or `undefined`, it is treated as if the attribute was absent.
+
+If there are class names in both first and second arguments of `m()`, they are merged together as you would expect. If the value of the class in the second argument is `null`or `undefined`, it is ignored.
+
+If another attribute is present in both the first and the second argument, the second one takes precedence even if it is is `null` or `undefined`.
---
@@ -161,6 +178,76 @@ m("input[readonly]")
m("input[readOnly]")
```
+This even includes custom elements. For example, you can use [A-Frame](https://aframe.io/docs/0.8.0/introduction/) within Mithril, no problem!
+
+```javascript
+m("a-scene", [
+ m("a-box", {
+ position: "-1 0.5 -3",
+ rotation: "0 45 0",
+ color: "#4CC3D9",
+ }),
+
+ m("a-sphere", {
+ position: "0 1.25 -5",
+ radius: "1.25",
+ color: "#EF2D5E",
+ }),
+
+ m("a-cylinder", {
+ position: "1 0.75 -3",
+ radius: "0.5",
+ height: "1.5",
+ color: "#FFC65D",
+ }),
+
+ m("a-plane", {
+ position: "0 0 -4",
+ rotation: "-90 0 0",
+ width: "4",
+ height: "4",
+ color: "#7BC8A4",
+ }),
+
+ m("a-sky", {
+ color: "#ECECEC",
+ }),
+])
+```
+
+And yes, this translates to both attributes and properties, and it works just like they would in the DOM. Using [Brick's `brick-deck`](http://brick.mozilla.io/docs/brick-deck) as an example, they have a `selected-index` attribute with a corresponding `selectedIndex` getter/setter property.
+
+```javascript
+m("brick-deck[selected-index=0]", [/* ... */]) // lowercase
+m("brick-deck[selectedIndex=0]", [/* ... */]) // uppercase
+// I know these look odd, but `brick-deck`'s `selectedIndex` property is a
+// string, not a number.
+m("brick-deck", {"selected-index": "0"}, [/* ... */])
+m("brick-deck", {"selectedIndex": "0"}, [/* ... */])
+```
+
+For custom elements, it doesn't auto-stringify properties, in case they are objects, numbers, or some other non-string value. So assuming you had some custom element `my-special-element` that has an `elem.whitelist` array getter/setter property, you could do this, and it'd work as you'd expect:
+
+```javascript
+m("my-special-element", {
+ whitelist: [
+ "https://example.com",
+ "http://neverssl.com",
+ "https://google.com",
+ ],
+})
+```
+
+If you have classes or IDs for those elements, the shorthands still work as you would expect. To pull another A-Frame example:
+
+```javascript
+// These two are equivalent
+m("a-entity#player")
+m("a-entity", {id: "player"})
+```
+
+Do note that all the properties with magic semantics, like lifecycle attributes, `onevent` handlers, `key`s, `class`, and `style`, those are still treated the same way they are for normal HTML elements.
+
---
### Style attribute
@@ -175,7 +262,7 @@ m("div[style=background:red]")
Using a string as a `style` would overwrite all inline styles in the element if it is redrawn, and not only CSS rules whose values have changed.
-Mithril does not attempt to add units to number values.
+Mithril does not attempt to add units to number values. It simply stringifies them.
---
diff --git a/docs/index.md b/docs/index.md
index 64018eff..97dd2b13 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -56,7 +56,7 @@ Let's create an HTML file to follow along:
```markup
-
+
-
+
+
+
diff --git a/performance/test-perf.js b/performance/test-perf.js
index 2ba9b622..020960c4 100644
--- a/performance/test-perf.js
+++ b/performance/test-perf.js
@@ -31,7 +31,7 @@ var browserMock = require("../test-utils/browserMock")
// Do this silly dance so browser testing works
var B = typeof Benchmark === "undefined" ? require("benchmark") : Benchmark
-var m, scratch;
+var scratch;
// set up browser env on before running tests
var doc = typeof document !== "undefined" ? document : null
@@ -43,15 +43,20 @@ if(!doc) {
doc = mock.document
}
-// Have to include mithril AFTER browser polyfill is set up
-m = require("../mithril") // eslint-disable-line global-require
+var m = require("../render/hyperscript")
+m.render = require("../render/render")(window).render
-scratch = doc.createElement("div");
-(doc.body || doc.documentElement).appendChild(scratch)
+function resetScratch() {
+ doc.documentElement.innerHTML = ""
+ scratch = doc.documentElement.firstChild
+}
+
+resetScratch()
// Initialize benchmark suite
var suite = new B.Suite("mithril perf")
+var xuite = {add: function(options) {console.log("skipping " + options.name)}} // eslint-disable-line no-unused-vars
suite.on("start", function() {
this.start = Date.now();
@@ -60,7 +65,7 @@ suite.on("start", function() {
suite.on("cycle", function(e) {
console.log(e.target.toString())
- scratch.innerHTML = ""
+ resetScratch()
})
suite.on("complete", function() {
@@ -70,7 +75,7 @@ suite.on("complete", function() {
suite.on("error", console.error.bind(console))
suite.add({
- name : "rerender without changes",
+ name : "rerender identical vnode",
onStart : function() {
this.vdom = m("div", {class: "foo bar", "data-foo": "bar", p: 2},
m("header",
@@ -119,6 +124,53 @@ suite.add({
}
})
+suite.add({
+ name : "rerender same tree",
+ fn : function() {
+ m.render(scratch, m("div", {class: "foo bar", "data-foo": "bar", p: 2},
+ m("header",
+ m("h1", {class: "asdf"}, "a ", "b", " c ", 0, " d"),
+ m("nav",
+ m("a", {href: "/foo"}, "Foo"),
+ m("a", {href: "/bar"}, "Bar")
+ )
+ ),
+ m("main",
+ m("form", {onSubmit: function onSubmit() {}},
+ m("input", {type: "checkbox", checked: true}),
+ m("input", {type: "checkbox", checked: false}),
+ m("fieldset",
+ m("label",
+ m("input", {type: "radio", checked: true})
+ ),
+ m("label",
+ m("input", {type: "radio"})
+ )
+ ),
+ m("button-bar",
+ m("button",
+ {style: "width:10px; height:10px; border:1px solid #FFF;"},
+ "Normal CSS"
+ ),
+ m("button",
+ {style: "top:0 ; right: 20"},
+ "Poor CSS"
+ ),
+ m("button",
+ {style: "invalid-prop:1;padding:1px;font:12px/1.1 arial,sans-serif;", icon: true},
+ "Poorer CSS"
+ ),
+ m("button",
+ {style: {margin: 0, padding: "10px", overflow: "visible"}},
+ "Object CSS"
+ )
+ )
+ )
+ )
+ ))
+ }
+})
+
suite.add({
name : "construct large VDOM tree",
@@ -181,12 +233,12 @@ suite.add({
suite.add({
name : "mutate styles/properties",
-
+ // minSamples: 100,
onStart : function () {
var counter = 0
var keyLooper = function (n) { return function (c) { return c % n ? (c + "px") : c } }
var get = function (obj, i) { return obj[i%obj.length] }
- var classes = ["foo", "foo bar", "", "baz-bat", null, "fooga"]
+ var classes = ["foo", "foo bar", "", "baz-bat", null, "fooga", null, null, undefined]
var styles = []
var multivalue = ["0 1px", "0 0 1px 0", "0", "1px", "20px 10px", "7em 5px", "1px 0 5em 2px"]
var stylekeys = [
@@ -212,21 +264,26 @@ suite.add({
this.count = 0
this.app = function (index) {
- return m("div",
- {
- class: get(classes, index),
- "data-index": index,
- title: index.toString(36)
- },
- m("input", {type: "checkbox", checked: index % 3 == 0}),
- m("input", {value: "test " + (Math.floor(index / 4)), disabled: index % 10 ? null : true}),
- m("div", {class: get(classes, index * 11)},
- m("p", {style: get(styles, index)}, "p1"),
- m("p", {style: get(styles, index + 1)}, "p2"),
- m("p", {style: get(styles, index * 2)}, "p3"),
- m("p", {style: get(styles, index * 3 + 1)}, "p4")
+ var last = index + 300
+ var vnodes = []
+ for (; index < last; index++) vnodes.push(
+ m("div.booga",
+ {
+ class: get(classes, index),
+ "data-index": index,
+ title: index.toString(36)
+ },
+ m("input.dooga", {type: "checkbox", checked: index % 3 == 0}),
+ m("input", {value: "test " + (Math.floor(index / 4)), disabled: index % 10 ? null : true}),
+ m("div", {class: get(classes, index * 11)},
+ m("p", {style: get(styles, index)}, "p1"),
+ m("p", {style: get(styles, index + 1)}, "p2"),
+ m("p", {style: get(styles, index * 2)}, "p3"),
+ m("p.zooga", {style: get(styles, index * 3 + 1), className: get(classes, index * 7)}, "p4")
+ )
)
)
+ return vnodes
}
},
@@ -311,16 +368,9 @@ var Root = {
}
}
-suite.add({
- name : "repeated trees (recycling)",
- fn : function () {
- m.render(scratch, [m(Root)])
- m.render(scratch, [])
- }
-})
suite.add({
- name : "repeated trees (no recycling)",
+ name : "repeated trees",
fn : function () {
m.render(scratch, [m(Root)])
m.render(scratch, [])
diff --git a/promise/polyfill.js b/promise/polyfill.js
new file mode 100644
index 00000000..68cd60e1
--- /dev/null
+++ b/promise/polyfill.js
@@ -0,0 +1,112 @@
+"use strict"
+/** @constructor */
+var PromisePolyfill = function(executor) {
+ if (!(this instanceof PromisePolyfill)) throw new Error("Promise must be called with `new`")
+ if (typeof executor !== "function") throw new TypeError("executor must be a function")
+
+ var self = this, resolvers = [], rejectors = [], resolveCurrent = handler(resolvers, true), rejectCurrent = handler(rejectors, false)
+ var instance = self._instance = {resolvers: resolvers, rejectors: rejectors}
+ var callAsync = typeof setImmediate === "function" ? setImmediate : setTimeout
+ function handler(list, shouldAbsorb) {
+ return function execute(value) {
+ var then
+ try {
+ if (shouldAbsorb && value != null && (typeof value === "object" || typeof value === "function") && typeof (then = value.then) === "function") {
+ if (value === self) throw new TypeError("Promise can't be resolved w/ itself")
+ executeOnce(then.bind(value))
+ }
+ else {
+ callAsync(function() {
+ if (!shouldAbsorb && list.length === 0) console.error("Possible unhandled promise rejection:", value)
+ for (var i = 0; i < list.length; i++) list[i](value)
+ resolvers.length = 0, rejectors.length = 0
+ instance.state = shouldAbsorb
+ instance.retry = function() {execute(value)}
+ })
+ }
+ }
+ catch (e) {
+ rejectCurrent(e)
+ }
+ }
+ }
+ function executeOnce(then) {
+ var runs = 0
+ function run(fn) {
+ return function(value) {
+ if (runs++ > 0) return
+ fn(value)
+ }
+ }
+ var onerror = run(rejectCurrent)
+ try {then(run(resolveCurrent), onerror)} catch (e) {onerror(e)}
+ }
+
+ executeOnce(executor)
+}
+PromisePolyfill.prototype.then = function(onFulfilled, onRejection) {
+ var self = this, instance = self._instance
+ function handle(callback, list, next, state) {
+ list.push(function(value) {
+ if (typeof callback !== "function") next(value)
+ else try {resolveNext(callback(value))} catch (e) {if (rejectNext) rejectNext(e)}
+ })
+ if (typeof instance.retry === "function" && state === instance.state) instance.retry()
+ }
+ var resolveNext, rejectNext
+ var promise = new PromisePolyfill(function(resolve, reject) {resolveNext = resolve, rejectNext = reject})
+ handle(onFulfilled, instance.resolvers, resolveNext, true), handle(onRejection, instance.rejectors, rejectNext, false)
+ return promise
+}
+PromisePolyfill.prototype.catch = function(onRejection) {
+ return this.then(null, onRejection)
+}
+PromisePolyfill.prototype.finally = function(callback) {
+ return this.then(
+ function(value) {
+ return PromisePolyfill.resolve(callback()).then(function() {
+ return value
+ })
+ },
+ function(reason) {
+ return PromisePolyfill.resolve(callback()).then(function() {
+ return PromisePolyfill.reject(reason);
+ })
+ }
+ )
+}
+PromisePolyfill.resolve = function(value) {
+ if (value instanceof PromisePolyfill) return value
+ return new PromisePolyfill(function(resolve) {resolve(value)})
+}
+PromisePolyfill.reject = function(value) {
+ return new PromisePolyfill(function(resolve, reject) {reject(value)})
+}
+PromisePolyfill.all = function(list) {
+ return new PromisePolyfill(function(resolve, reject) {
+ var total = list.length, count = 0, values = []
+ if (list.length === 0) resolve([])
+ else for (var i = 0; i < list.length; i++) {
+ (function(i) {
+ function consume(value) {
+ count++
+ values[i] = value
+ if (count === total) resolve(values)
+ }
+ if (list[i] != null && (typeof list[i] === "object" || typeof list[i] === "function") && typeof list[i].then === "function") {
+ list[i].then(consume, reject)
+ }
+ else consume(list[i])
+ })(i)
+ }
+ })
+}
+PromisePolyfill.race = function(list) {
+ return new PromisePolyfill(function(resolve, reject) {
+ for (var i = 0; i < list.length; i++) {
+ list[i].then(resolve, reject)
+ }
+ })
+}
+
+module.exports = PromisePolyfill
diff --git a/promise/promise.js b/promise/promise.js
index 53aefecc..7c6a2d11 100644
--- a/promise/promise.js
+++ b/promise/promise.js
@@ -1,105 +1,20 @@
"use strict"
-/** @constructor */
-var PromisePolyfill = function(executor) {
- if (!(this instanceof PromisePolyfill)) throw new Error("Promise must be called with `new`")
- if (typeof executor !== "function") throw new TypeError("executor must be a function")
- var self = this, resolvers = [], rejectors = [], resolveCurrent = handler(resolvers, true), rejectCurrent = handler(rejectors, false)
- var instance = self._instance = {resolvers: resolvers, rejectors: rejectors}
- var callAsync = typeof setImmediate === "function" ? setImmediate : setTimeout
- function handler(list, shouldAbsorb) {
- return function execute(value) {
- var then
- try {
- if (shouldAbsorb && value != null && (typeof value === "object" || typeof value === "function") && typeof (then = value.then) === "function") {
- if (value === self) throw new TypeError("Promise can't be resolved w/ itself")
- executeOnce(then.bind(value))
- }
- else {
- callAsync(function() {
- if (!shouldAbsorb && list.length === 0) console.error("Possible unhandled promise rejection:", value)
- for (var i = 0; i < list.length; i++) list[i](value)
- resolvers.length = 0, rejectors.length = 0
- instance.state = shouldAbsorb
- instance.retry = function() {execute(value)}
- })
- }
- }
- catch (e) {
- rejectCurrent(e)
- }
- }
- }
- function executeOnce(then) {
- var runs = 0
- function run(fn) {
- return function(value) {
- if (runs++ > 0) return
- fn(value)
- }
- }
- var onerror = run(rejectCurrent)
- try {then(run(resolveCurrent), onerror)} catch (e) {onerror(e)}
- }
-
- executeOnce(executor)
-}
-PromisePolyfill.prototype.then = function(onFulfilled, onRejection) {
- var self = this, instance = self._instance
- function handle(callback, list, next, state) {
- list.push(function(value) {
- if (typeof callback !== "function") next(value)
- else try {resolveNext(callback(value))} catch (e) {if (rejectNext) rejectNext(e)}
- })
- if (typeof instance.retry === "function" && state === instance.state) instance.retry()
- }
- var resolveNext, rejectNext
- var promise = new PromisePolyfill(function(resolve, reject) {resolveNext = resolve, rejectNext = reject})
- handle(onFulfilled, instance.resolvers, resolveNext, true), handle(onRejection, instance.rejectors, rejectNext, false)
- return promise
-}
-PromisePolyfill.prototype.catch = function(onRejection) {
- return this.then(null, onRejection)
-}
-PromisePolyfill.resolve = function(value) {
- if (value instanceof PromisePolyfill) return value
- return new PromisePolyfill(function(resolve) {resolve(value)})
-}
-PromisePolyfill.reject = function(value) {
- return new PromisePolyfill(function(resolve, reject) {reject(value)})
-}
-PromisePolyfill.all = function(list) {
- return new PromisePolyfill(function(resolve, reject) {
- var total = list.length, count = 0, values = []
- if (list.length === 0) resolve([])
- else for (var i = 0; i < list.length; i++) {
- (function(i) {
- function consume(value) {
- count++
- values[i] = value
- if (count === total) resolve(values)
- }
- if (list[i] != null && (typeof list[i] === "object" || typeof list[i] === "function") && typeof list[i].then === "function") {
- list[i].then(consume, reject)
- }
- else consume(list[i])
- })(i)
- }
- })
-}
-PromisePolyfill.race = function(list) {
- return new PromisePolyfill(function(resolve, reject) {
- for (var i = 0; i < list.length; i++) {
- list[i].then(resolve, reject)
- }
- })
-}
+var PromisePolyfill = require("./polyfill")
if (typeof window !== "undefined") {
- if (typeof window.Promise === "undefined") window.Promise = PromisePolyfill
+ if (typeof window.Promise === "undefined") {
+ window.Promise = PromisePolyfill
+ } else if (!window.Promise.prototype.finally) {
+ window.Promise.prototype.finally = PromisePolyfill.prototype.finally
+ }
module.exports = window.Promise
} else if (typeof global !== "undefined") {
- if (typeof global.Promise === "undefined") global.Promise = PromisePolyfill
+ if (typeof global.Promise === "undefined") {
+ global.Promise = PromisePolyfill
+ } else if (!global.Promise.prototype.finally) {
+ global.Promise.prototype.finally = PromisePolyfill.prototype.finally
+ }
module.exports = global.Promise
} else {
module.exports = PromisePolyfill
diff --git a/promise/tests/test-promise.js b/promise/tests/test-promise.js
index 24940838..c7e01260 100644
--- a/promise/tests/test-promise.js
+++ b/promise/tests/test-promise.js
@@ -2,7 +2,7 @@
var o = require("../../ospec/ospec")
var callAsync = require("../../test-utils/callAsync")
-var Promise = require("../../promise/promise")
+var Promise = require("../../promise/polyfill")
o.spec("promise", function() {
o.spec("constructor", function() {
@@ -15,6 +15,7 @@ o.spec("promise", function() {
o("constructor has correct methods", function() {
o(typeof Promise.prototype.then).equals("function")
o(typeof Promise.prototype.catch).equals("function")
+ o(typeof Promise.prototype.finally).equals("function")
o(typeof Promise.resolve).equals("function")
o(typeof Promise.reject).equals("function")
o(typeof Promise.race).equals("function")
@@ -53,6 +54,78 @@ o.spec("promise", function() {
o(value).equals(1)
}).then(done)
})
+ o("finally lets a fulfilled value pass though", function(done) {
+ var promise = Promise.resolve(1)
+ var spy = o.spy(function(){return 2})
+
+ promise.finally(spy).then(function(value){
+ o(value).equals(1)
+ o(spy.callCount).equals(1)
+ o(spy.args.length).equals(0)
+ o(spy.this).equals(undefined)
+ done()
+ })
+ })
+ o("finally lets a rejected reason pass though", function(done) {
+ var promise = Promise.reject(1)
+ var spy = o.spy(function(){return 2})
+
+ promise.finally(spy).catch(function(reason){
+ o(reason).equals(1)
+ o(spy.callCount).equals(1)
+ o(spy.args.length).equals(0)
+ o(spy.this).equals(undefined)
+ done()
+ })
+ })
+ o("finally overrrides a fulfilled value when it throws", function(done) {
+ var promise = Promise.resolve(1)
+ var spy = o.spy(function(){throw 2})
+
+ promise.finally(spy).catch(function(reason){
+ o(reason).equals(2)
+ o(spy.callCount).equals(1)
+ o(spy.args.length).equals(0)
+ o(spy.this).equals(undefined)
+ done()
+ })
+ })
+ o("finally overrrides a fulfilled value when it returns a rejected Promise", function(done) {
+ var promise = Promise.resolve(1)
+ var spy = o.spy(function(){return Promise.reject(2)})
+
+ promise.finally(spy).catch(function(reason){
+ o(reason).equals(2)
+ o(spy.callCount).equals(1)
+ o(spy.args.length).equals(0)
+ o(spy.this).equals(undefined)
+ done()
+ })
+ })
+ o("finally overrrides a rejected reason when it throws", function(done) {
+ var promise = Promise.reject(1)
+ var spy = o.spy(function(){throw 2})
+
+ promise.finally(spy).catch(function(reason){
+ o(reason).equals(2)
+ o(spy.callCount).equals(1)
+ o(spy.args.length).equals(0)
+ o(spy.this).equals(undefined)
+ done()
+ })
+ })
+ o("finally overrrides a rejected reason when it returns a rejected Promise", function(done) {
+ var promise = Promise.reject(1)
+ var spy = o.spy(function(){return Promise.reject(2)})
+
+ promise.finally(spy).catch(function(reason){
+ o(reason).equals(2)
+ o(spy.callCount).equals(1)
+ o(spy.args.length).equals(0)
+ o(spy.this).equals(undefined)
+ done()
+ })
+ })
})
o.spec("resolve", function() {
o("resolves once", function(done) {
diff --git a/render/hyperscript.js b/render/hyperscript.js
index 00c438c9..36eabbc5 100644
--- a/render/hyperscript.js
+++ b/render/hyperscript.js
@@ -31,7 +31,8 @@ function compileSelector(selector) {
function execSelector(state, attrs, children) {
var hasAttrs = false, childList, text
- var className = attrs.className || attrs.class
+ var classAttr = hasOwn.call(attrs, "class") ? "class" : "className"
+ var className = attrs[classAttr]
if (!isEmpty(state.attrs) && !isEmpty(attrs)) {
var newAttrs = {}
@@ -46,21 +47,20 @@ function execSelector(state, attrs, children) {
}
for (var key in state.attrs) {
- if (hasOwn.call(state.attrs, key)) {
+ if (hasOwn.call(state.attrs, key) && key !== "className" && !hasOwn.call(attrs, key)){
attrs[key] = state.attrs[key]
}
}
+ if (className != null || state.attrs.className != null) attrs.className =
+ className != null
+ ? state.attrs.className != null
+ ? state.attrs.className + " " + className
+ : className
+ : state.attrs.className != null
+ ? state.attrs.className
+ : null
- if (className !== undefined) {
- if (attrs.class !== undefined) {
- attrs.class = undefined
- attrs.className = className
- }
-
- if (state.attrs.className != null) {
- attrs.className = state.attrs.className + " " + className
- }
- }
+ if (classAttr === "class") attrs.class = null
for (var key in attrs) {
if (hasOwn.call(attrs, key) && key !== "key") {
@@ -75,20 +75,15 @@ function execSelector(state, attrs, children) {
childList = children
}
- return Vnode(state.tag, attrs.key, hasAttrs ? attrs : undefined, childList, text)
+ return Vnode(state.tag, attrs.key, hasAttrs ? attrs : null, childList, text)
}
function hyperscript(selector) {
- // Because sloppy mode sucks
- var attrs = arguments[1], start = 2, children
-
if (selector == null || typeof selector !== "string" && typeof selector !== "function" && typeof selector.view !== "function") {
throw Error("The selector must be either a string or a component.");
}
- if (typeof selector === "string") {
- var cached = selectorCache[selector] || compileSelector(selector)
- }
+ var attrs = arguments[1], start = 2, children
if (attrs == null) {
attrs = {}
@@ -105,12 +100,10 @@ function hyperscript(selector) {
while (start < arguments.length) children.push(arguments[start++])
}
- var normalized = Vnode.normalizeChildren(children)
-
if (typeof selector === "string") {
- return execSelector(cached, attrs, normalized)
+ return execSelector(selectorCache[selector] || compileSelector(selector), attrs, Vnode.normalizeChildren(children))
} else {
- return Vnode(selector, attrs.key, attrs, normalized)
+ return Vnode(selector, attrs.key, attrs, children)
}
}
diff --git a/render/render.js b/render/render.js
index c053517d..9d4f1cf4 100644
--- a/render/render.js
+++ b/render/render.js
@@ -4,7 +4,6 @@ var Vnode = require("../render/vnode")
module.exports = function($window) {
var $doc = $window.document
- var $emptyFragment = $doc.createDocumentFragment()
var nameSpace = {
svg: "http://www.w3.org/2000/svg",
@@ -18,6 +17,24 @@ module.exports = function($window) {
return vnode.attrs && vnode.attrs.xmlns || nameSpace[vnode.tag]
}
+ //sanity check to discourage people from doing `vnode.state = ...`
+ function checkState(vnode, original) {
+ if (vnode.state !== original) throw new Error("`vnode.state` must not be modified")
+ }
+
+ //Note: the hook is passed as the `this` argument to allow proxying the
+ //arguments without requiring a full array allocation to do so. It also
+ //takes advantage of the fact the current `vnode` is the first argument in
+ //all lifecycle methods.
+ function callHook(vnode) {
+ var original = vnode.state
+ try {
+ return this.apply(original, arguments)
+ } finally {
+ checkState(vnode, original)
+ }
+ }
+
//create
function createNodes(parent, vnodes, start, end, hooks, nextSibling, ns) {
for (var i = start; i < end; i++) {
@@ -33,25 +50,33 @@ module.exports = function($window) {
vnode.state = {}
if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks)
switch (tag) {
- case "#": return createText(parent, vnode, nextSibling)
- case "<": return createHTML(parent, vnode, nextSibling)
- case "[": return createFragment(parent, vnode, hooks, ns, nextSibling)
- default: return createElement(parent, vnode, hooks, ns, nextSibling)
+ case "#": createText(parent, vnode, nextSibling); break
+ case "<": createHTML(parent, vnode, ns, nextSibling); break
+ case "[": createFragment(parent, vnode, hooks, ns, nextSibling); break
+ default: createElement(parent, vnode, hooks, ns, nextSibling)
}
}
- else return createComponent(parent, vnode, hooks, ns, nextSibling)
+ else createComponent(parent, vnode, hooks, ns, nextSibling)
}
function createText(parent, vnode, nextSibling) {
vnode.dom = $doc.createTextNode(vnode.children)
insertNode(parent, vnode.dom, nextSibling)
- return vnode.dom
}
- function createHTML(parent, vnode, nextSibling) {
+ var possibleParents = {caption: "table", thead: "table", tbody: "table", tfoot: "table", tr: "tbody", th: "tr", td: "tr", colgroup: "table", col: "colgroup"}
+ function createHTML(parent, vnode, ns, nextSibling) {
var match = vnode.children.match(/^\s*?<(\w+)/im) || []
- var parent1 = {caption: "table", thead: "table", tbody: "table", tfoot: "table", tr: "tbody", th: "tr", td: "tr", colgroup: "table", col: "colgroup"}[match[1]] || "div"
- var temp = $doc.createElement(parent1)
-
- temp.innerHTML = vnode.children
+ // not using the proper parent makes the child element(s) vanish.
+ // var div = document.createElement("div")
+ // div.innerHTML = "
i
j
"
+ // console.log(div.innerHTML)
+ // --> "ij", no
in sight.
+ var temp = $doc.createElement(possibleParents[match[1]] || "div")
+ if (ns === "http://www.w3.org/2000/svg") {
+ temp.innerHTML = ""
+ temp = temp.firstChild
+ } else {
+ temp.innerHTML = vnode.children
+ }
vnode.dom = temp.firstChild
vnode.domSize = temp.childNodes.length
var fragment = $doc.createDocumentFragment()
@@ -60,7 +85,6 @@ module.exports = function($window) {
fragment.appendChild(child)
}
insertNode(parent, fragment, nextSibling)
- return fragment
}
function createFragment(parent, vnode, hooks, ns, nextSibling) {
var fragment = $doc.createDocumentFragment()
@@ -71,7 +95,6 @@ module.exports = function($window) {
vnode.dom = fragment.firstChild
vnode.domSize = fragment.childNodes.length
insertNode(parent, fragment, nextSibling)
- return fragment
}
function createElement(parent, vnode, hooks, ns, nextSibling) {
var tag = vnode.tag
@@ -91,7 +114,7 @@ module.exports = function($window) {
insertNode(parent, element, nextSibling)
- if (vnode.attrs != null && vnode.attrs.contenteditable != null) {
+ if (attrs != null && attrs.contenteditable != null) {
setContentEditable(vnode)
}
else {
@@ -102,166 +125,325 @@ module.exports = function($window) {
if (vnode.children != null) {
var children = vnode.children
createNodes(element, children, 0, children.length, hooks, null, ns)
- setLateAttrs(vnode)
+ if (vnode.tag === "select" && attrs != null) setLateSelectAttrs(vnode, attrs)
}
}
- return element
}
function initComponent(vnode, hooks) {
var sentinel
if (typeof vnode.tag.view === "function") {
vnode.state = Object.create(vnode.tag)
sentinel = vnode.state.view
- if (sentinel.$$reentrantLock$$ != null) return $emptyFragment
+ if (sentinel.$$reentrantLock$$ != null) return
sentinel.$$reentrantLock$$ = true
} else {
vnode.state = void 0
sentinel = vnode.tag
- if (sentinel.$$reentrantLock$$ != null) return $emptyFragment
+ if (sentinel.$$reentrantLock$$ != null) return
sentinel.$$reentrantLock$$ = true
vnode.state = (vnode.tag.prototype != null && typeof vnode.tag.prototype.view === "function") ? new vnode.tag(vnode) : vnode.tag(vnode)
}
- vnode._state = vnode.state
if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks)
- initLifecycle(vnode._state, vnode, hooks)
- vnode.instance = Vnode.normalize(vnode._state.view.call(vnode.state, vnode))
+ initLifecycle(vnode.state, vnode, hooks)
+ vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode))
if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument")
sentinel.$$reentrantLock$$ = null
}
function createComponent(parent, vnode, hooks, ns, nextSibling) {
initComponent(vnode, hooks)
if (vnode.instance != null) {
- var element = createNode(parent, vnode.instance, hooks, ns, nextSibling)
+ createNode(parent, vnode.instance, hooks, ns, nextSibling)
vnode.dom = vnode.instance.dom
vnode.domSize = vnode.dom != null ? vnode.instance.domSize : 0
- insertNode(parent, element, nextSibling)
- return element
}
else {
vnode.domSize = 0
- return $emptyFragment
}
}
//update
- function updateNodes(parent, old, vnodes, recycling, hooks, nextSibling, ns) {
+ /**
+ * @param {Element|Fragment} parent - the parent element
+ * @param {Vnode[] | null} old - the list of vnodes of the last `render()` call for
+ * this part of the tree
+ * @param {Vnode[] | null} vnodes - as above, but for the current `render()` call.
+ * @param {Function[]} hooks - an accumulator of post-render hooks (oncreate/onupdate)
+ * @param {Element | null} nextSibling - the next DOM node if we're dealing with a
+ * fragment that is not the last item in its
+ * parent
+ * @param {'svg' | 'math' | String | null} ns) - the current XML namespace, if any
+ * @returns void
+ */
+ // This function diffs and patches lists of vnodes, both keyed and unkeyed.
+ //
+ // We will:
+ //
+ // 1. describe its general structure
+ // 2. focus on the diff algorithm optimizations
+ // 3. discuss DOM node operations.
+
+ // ## Overview:
+ //
+ // The updateNodes() function:
+ // - deals with trivial cases
+ // - determines whether the lists are keyed or unkeyed based on the first non-null node
+ // of each list.
+ // - diffs them and patches the DOM if needed (that's the brunt of the code)
+ // - manages the leftovers: after diffing, are there:
+ // - old nodes left to remove?
+ // - new nodes to insert?
+ // deal with them!
+ //
+ // The lists are only iterated over once, with an exception for the nodes in `old` that
+ // are visited in the fourth part of the diff and in the `removeNodes` loop.
+
+ // ## Diffing
+ //
+ // Reading https://github.com/localvoid/ivi/blob/ddc09d06abaef45248e6133f7040d00d3c6be853/packages/ivi/src/vdom/implementation.ts#L617-L837
+ // may be good for context on longest increasing subsequence-based logic for moving nodes.
+ //
+ // In order to diff keyed lists, one has to
+ //
+ // 1) match nodes in both lists, per key, and update them accordingly
+ // 2) create the nodes present in the new list, but absent in the old one
+ // 3) remove the nodes present in the old list, but absent in the new one
+ // 4) figure out what nodes in 1) to move in order to minimize the DOM operations.
+ //
+ // To achieve 1) one can create a dictionary of keys => index (for the old list), then iterate
+ // over the new list and for each new vnode, find the corresponding vnode in the old list using
+ // the map.
+ // 2) is achieved in the same step: if a new node has no corresponding entry in the map, it is new
+ // and must be created.
+ // For the removals, we actually remove the nodes that have been updated from the old list.
+ // The nodes that remain in that list after 1) and 2) have been performed can be safely removed.
+ // The fourth step is a bit more complex and relies on the longest increasing subsequence (LIS)
+ // algorithm.
+ //
+ // the longest increasing subsequence is the list of nodes that can remain in place. Imagine going
+ // from `1,2,3,4,5` to `4,5,1,2,3` where the numbers are not necessarily the keys, but the indices
+ // corresponding to the keyed nodes in the old list (keyed nodes `e,d,c,b,a` => `b,a,e,d,c` would
+ // match the above lists, for example).
+ //
+ // In there are two increasing subsequences: `4,5` and `1,2,3`, the latter being the longest. We
+ // can update those nodes without moving them, and only call `insertNode` on `4` and `5`.
+ //
+ // @localvoid adapted the algo to also support node deletions and insertions (the `lis` is actually
+ // the longest increasing subsequence *of old nodes still present in the new list*).
+ //
+ // It is a general algorithm that is fireproof in all circumstances, but it requires the allocation
+ // and the construction of a `key => oldIndex` map, and three arrays (one with `newIndex => oldIndex`,
+ // the `LIS` and a temporary one to create the LIS).
+ //
+ // So we cheat where we can: if the tails of the lists are identical, they are guaranteed to be part of
+ // the LIS and can be updated without moving them.
+ //
+ // If two nodes are swapped, they are guaranteed not to be part of the LIS, and must be moved (with
+ // the exception of the last node if the list is fully reversed).
+ //
+ // ## Finding the next sibling.
+ //
+ // `updateNode()` and `createNode()` expect a nextSibling parameter to perform DOM operations.
+ // When the list is being traversed top-down, at any index, the DOM nodes up to the previous
+ // vnode reflect the content of the new list, whereas the rest of the DOM nodes reflect the old
+ // list. The next sibling must be looked for in the old list using `getNextSibling(... oldStart + 1 ...)`.
+ //
+ // In the other scenarios (swaps, upwards traversal, map-based diff),
+ // the new vnodes list is traversed upwards. The DOM nodes at the bottom of the list reflect the
+ // bottom part of the new vnodes list, and we can use the `v.dom` value of the previous node
+ // as the next sibling (cached in the `nextSibling` variable).
+
+
+ // ## DOM node moves
+ //
+ // In most scenarios `updateNode()` and `createNode()` perform the DOM operations. However,
+ // this is not the case if the node moved (second and fourth part of the diff algo). We move
+ // the old DOM nodes before updateNode runs because it enables us to use the cached `nextSibling`
+ // variable rather than fetching it using `getNextSibling()`.
+ //
+ // The fourth part of the diff currently inserts nodes unconditionally, leading to issues
+ // like #1791 and #1999. We need to be smarter about those situations where adjascent old
+ // nodes remain together in the new list in a way that isn't covered by parts one and
+ // three of the diff algo.
+
+ function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) {
if (old === vnodes || old == null && vnodes == null) return
- else if (old == null) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns)
- else if (vnodes == null) removeNodes(old, 0, old.length, vnodes)
+ else if (old == null || old.length === 0) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns)
+ else if (vnodes == null || vnodes.length === 0) removeNodes(old, 0, old.length)
else {
- if (old.length === vnodes.length) {
- var isUnkeyed = false
- for (var i = 0; i < vnodes.length; i++) {
- if (vnodes[i] != null && old[i] != null) {
- isUnkeyed = vnodes[i].key == null && old[i].key == null
+ var start = 0, oldStart = 0, isOldKeyed = null, isKeyed = null
+ for (; oldStart < old.length; oldStart++) {
+ if (old[oldStart] != null) {
+ isOldKeyed = old[oldStart].key != null
+ break
+ }
+ }
+ for (; start < vnodes.length; start++) {
+ if (vnodes[start] != null) {
+ isKeyed = vnodes[start].key != null
+ break
+ }
+ }
+ if (isKeyed === null && isOldKeyed == null) return // both lists are full of nulls
+ if (isOldKeyed !== isKeyed) {
+ removeNodes(old, oldStart, old.length)
+ createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns)
+ } else if (!isKeyed) {
+ // Don't index past the end of either list (causes deopts).
+ var commonLength = old.length < vnodes.length ? old.length : vnodes.length
+ // Rewind if necessary to the first non-null index on either side.
+ // We could alternatively either explicitly create or remove nodes when `start !== oldStart`
+ // but that would be optimizing for sparse lists which are more rare than dense ones.
+ start = start < oldStart ? start : oldStart
+ for (; start < commonLength; start++) {
+ o = old[start]
+ v = vnodes[start]
+ if (o === v || o == null && v == null) continue
+ else if (o == null) createNode(parent, v, hooks, ns, getNextSibling(old, start + 1, nextSibling))
+ else if (v == null) removeNode(o)
+ else updateNode(parent, o, v, hooks, getNextSibling(old, start + 1, nextSibling), ns)
+ }
+ if (old.length > commonLength) removeNodes(old, start, old.length)
+ if (vnodes.length > commonLength) createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns)
+ } else {
+ // keyed diff
+ var oldEnd = old.length - 1, end = vnodes.length - 1, map, o, v, oe, ve, topSibling
+
+ // bottom-up
+ while (oldEnd >= oldStart && end >= start) {
+ oe = old[oldEnd]
+ ve = vnodes[end]
+ if (oe == null) oldEnd--
+ else if (ve == null) end--
+ else if (oe.key === ve.key) {
+ if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns)
+ if (ve.dom != null) nextSibling = ve.dom
+ oldEnd--, end--
+ } else {
break
}
}
- if (isUnkeyed) {
- for (var i = 0; i < old.length; i++) {
- if (old[i] === vnodes[i]) continue
- else if (old[i] == null && vnodes[i] != null) createNode(parent, vnodes[i], hooks, ns, getNextSibling(old, i + 1, nextSibling))
- else if (vnodes[i] == null) removeNodes(old, i, i + 1, vnodes)
- else updateNode(parent, old[i], vnodes[i], hooks, getNextSibling(old, i + 1, nextSibling), recycling, ns)
- }
- return
- }
- }
- recycling = recycling || isRecyclable(old, vnodes)
- if (recycling) {
- var pool = old.pool
- 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 && !recycling) oldStart++, start++
- else if (o == null) oldStart++
- else if (v == null) start++
- else if (o.key === v.key) {
- var shouldRecycle = (pool != null && oldStart >= old.length - pool.length) || ((pool == null) && recycling)
- oldStart++, start++
- updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), shouldRecycle, ns)
- if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling)
- }
- else {
- var o = old[oldEnd]
- if (o === v && !recycling) oldEnd--, start++
- else if (o == null) oldEnd--
+ // top-down
+ while (oldEnd >= oldStart && end >= start) {
+ o = old[oldStart]
+ v = vnodes[start]
+ if (o == null) oldStart++
else if (v == null) start++
else if (o.key === v.key) {
- var shouldRecycle = (pool != null && oldEnd >= old.length - pool.length) || ((pool == null) && recycling)
- updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), shouldRecycle, ns)
- if (recycling || start < end) insertNode(parent, toFragment(o), getNextSibling(old, oldStart, nextSibling))
- oldEnd--, start++
+ oldStart++, start++
+ if (o !== v) updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), ns)
+ } else {
+ break
}
- else break
}
- }
- while (oldEnd >= oldStart && end >= start) {
- var o = old[oldEnd], v = vnodes[end]
- if (o === v && !recycling) oldEnd--, end--
- else if (o == null) oldEnd--
- else if (v == null) end--
- else if (o.key === v.key) {
- var shouldRecycle = (pool != null && oldEnd >= old.length - pool.length) || ((pool == null) && recycling)
- updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), shouldRecycle, ns)
- if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling)
- if (o.dom != null) nextSibling = o.dom
- oldEnd--, end--
+ // swaps and list reversals
+ while (oldEnd >= oldStart && end >= start) {
+ if (o == null) oldStart++
+ else if (v == null) start++
+ else if (oe == null) oldEnd--
+ else if (ve == null) end--
+ else if (start === end) break
+ else {
+ if (o.key !== ve.key || oe.key !== v.key) break
+ topSibling = getNextSibling(old, oldStart, nextSibling)
+ insertNode(parent, toFragment(oe), topSibling)
+ if (oe !== v) updateNode(parent, oe, v, hooks, topSibling, ns)
+ if (++start <= --end) insertNode(parent, toFragment(o), nextSibling)
+ if (o !== ve) updateNode(parent, o, ve, hooks, nextSibling, ns)
+ if (ve.dom != null) nextSibling = ve.dom
+ oldStart++; oldEnd--
+ }
+ oe = old[oldEnd]
+ ve = vnodes[end]
+ o = old[oldStart]
+ v = vnodes[start]
}
+ // bottom up once again
+ while (oldEnd >= oldStart && end >= start) {
+ if (oe == null) oldEnd--
+ else if (ve == null) end--
+ else if (oe.key === ve.key) {
+ if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns)
+ if (ve.dom != null) nextSibling = ve.dom
+ oldEnd--, end--
+ } else {
+ break
+ }
+ oe = old[oldEnd]
+ ve = vnodes[end]
+ }
+ if (start > end) removeNodes(old, oldStart, oldEnd + 1)
+ else if (oldStart > oldEnd) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns)
else {
- if (!map) map = getKeyMap(old, oldEnd)
- if (v != null) {
- var oldIndex = map[v.key]
- if (oldIndex != null) {
- var movable = old[oldIndex]
- var shouldRecycle = (pool != null && oldIndex >= old.length - pool.length) || ((pool == null) && recycling)
- updateNode(parent, movable, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns)
- insertNode(parent, toFragment(movable), nextSibling)
- old[oldIndex].skip = true
- if (movable.dom != null) nextSibling = movable.dom
- }
- else {
- var dom = createNode(parent, v, hooks, ns, nextSibling)
- nextSibling = dom
+ // inspired by ivi https://github.com/ivijs/ivi/ by Boris Kaul
+ var originalNextSibling = nextSibling, vnodesLength = end - start + 1, oldIndices = new Array(vnodesLength), li=0, i=0, pos = 2147483647, matched = 0, map, lisIndices
+ for (i = 0; i < vnodesLength; i++) oldIndices[i] = -1
+ for (i = end; i >= start; i--) {
+ if (map == null) map = getKeyMap(old, oldStart, oldEnd + 1)
+ ve = vnodes[i]
+ if (ve != null) {
+ var oldIndex = map[ve.key]
+ if (oldIndex != null) {
+ pos = (oldIndex < pos) ? oldIndex : -1 // becomes -1 if nodes were re-ordered
+ oldIndices[i-start] = oldIndex
+ oe = old[oldIndex]
+ old[oldIndex] = null
+ if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns)
+ if (ve.dom != null) nextSibling = ve.dom
+ matched++
+ }
+ }
+ }
+ nextSibling = originalNextSibling
+ if (matched !== oldEnd - oldStart + 1) removeNodes(old, oldStart, oldEnd + 1)
+ if (matched === 0) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns)
+ else {
+ if (pos === -1) {
+ // the indices of the indices of the items that are part of the
+ // longest increasing subsequence in the oldIndices list
+ lisIndices = makeLisIndices(oldIndices)
+ li = lisIndices.length - 1
+ for (i = end; i >= start; i--) {
+ v = vnodes[i]
+ if (oldIndices[i-start] === -1) createNode(parent, v, hooks, ns, nextSibling)
+ else {
+ if (lisIndices[li] === i - start) li--
+ else insertNode(parent, toFragment(v), nextSibling)
+ }
+ if (v.dom != null) nextSibling = vnodes[i].dom
+ }
+ } else {
+ for (i = end; i >= start; i--) {
+ v = vnodes[i]
+ if (oldIndices[i-start] === -1) createNode(parent, v, hooks, ns, nextSibling)
+ if (v.dom != null) nextSibling = vnodes[i].dom
+ }
}
}
- end--
}
- if (end < start) break
}
- createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns)
- removeNodes(old, oldStart, oldEnd + 1, vnodes)
}
}
- function updateNode(parent, old, vnode, hooks, nextSibling, recycling, ns) {
+ function updateNode(parent, old, vnode, hooks, nextSibling, ns) {
var oldTag = old.tag, tag = vnode.tag
if (oldTag === tag) {
vnode.state = old.state
- vnode._state = old._state
vnode.events = old.events
- if (!recycling && shouldNotUpdate(vnode, old)) return
+ if (shouldNotUpdate(vnode, old)) return
if (typeof oldTag === "string") {
if (vnode.attrs != null) {
- if (recycling) {
- vnode.state = {}
- initLifecycle(vnode.attrs, vnode, hooks)
- }
- else updateLifecycle(vnode.attrs, vnode, hooks)
+ updateLifecycle(vnode.attrs, vnode, hooks)
}
switch (oldTag) {
case "#": updateText(old, vnode); break
- case "<": updateHTML(parent, old, vnode, nextSibling); break
- case "[": updateFragment(parent, old, vnode, recycling, hooks, nextSibling, ns); break
- default: updateElement(old, vnode, recycling, hooks, ns)
+ case "<": updateHTML(parent, old, vnode, ns, nextSibling); break
+ case "[": updateFragment(parent, old, vnode, hooks, nextSibling, ns); break
+ default: updateElement(old, vnode, hooks, ns)
}
}
- else updateComponent(parent, old, vnode, hooks, nextSibling, recycling, ns)
+ else updateComponent(parent, old, vnode, hooks, nextSibling, ns)
}
else {
- removeNode(old, null)
+ removeNode(old)
createNode(parent, vnode, hooks, ns, nextSibling)
}
}
@@ -271,15 +453,15 @@ module.exports = function($window) {
}
vnode.dom = old.dom
}
- function updateHTML(parent, old, vnode, nextSibling) {
+ function updateHTML(parent, old, vnode, ns, nextSibling) {
if (old.children !== vnode.children) {
toFragment(old)
- createHTML(parent, vnode, nextSibling)
+ createHTML(parent, vnode, ns, nextSibling)
}
else vnode.dom = old.dom, vnode.domSize = old.domSize
}
- function updateFragment(parent, old, vnode, recycling, hooks, nextSibling, ns) {
- updateNodes(parent, old.children, vnode.children, recycling, hooks, nextSibling, ns)
+ function updateFragment(parent, old, vnode, hooks, nextSibling, ns) {
+ updateNodes(parent, old.children, vnode.children, hooks, nextSibling, ns)
var domSize = 0, children = vnode.children
vnode.dom = null
if (children != null) {
@@ -293,7 +475,7 @@ module.exports = function($window) {
if (domSize !== 1) vnode.domSize = domSize
}
}
- function updateElement(old, vnode, recycling, hooks, ns) {
+ function updateElement(old, vnode, hooks, ns) {
var element = vnode.dom = old.dom
ns = getNameSpace(vnode) || ns
@@ -314,26 +496,22 @@ module.exports = function($window) {
else {
if (old.text != null) old.children = [Vnode("#", undefined, undefined, old.text, undefined, old.dom.firstChild)]
if (vnode.text != null) vnode.children = [Vnode("#", undefined, undefined, vnode.text, undefined, undefined)]
- updateNodes(element, old.children, vnode.children, recycling, hooks, null, ns)
+ updateNodes(element, old.children, vnode.children, hooks, null, ns)
}
}
- function updateComponent(parent, old, vnode, hooks, nextSibling, recycling, ns) {
- if (recycling) {
- initComponent(vnode, hooks)
- } else {
- vnode.instance = Vnode.normalize(vnode._state.view.call(vnode.state, vnode))
- if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument")
- if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks)
- updateLifecycle(vnode._state, vnode, hooks)
- }
+ function updateComponent(parent, old, vnode, hooks, nextSibling, ns) {
+ vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode))
+ if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument")
+ if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks)
+ updateLifecycle(vnode.state, vnode, hooks)
if (vnode.instance != null) {
if (old.instance == null) createNode(parent, vnode.instance, hooks, ns, nextSibling)
- else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, recycling, ns)
+ else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, ns)
vnode.dom = vnode.instance.dom
vnode.domSize = vnode.instance.domSize
}
else if (old.instance != null) {
- removeNode(old.instance, null)
+ removeNode(old.instance)
vnode.dom = undefined
vnode.domSize = 0
}
@@ -342,28 +520,65 @@ module.exports = function($window) {
vnode.domSize = old.domSize
}
}
- function isRecyclable(old, vnodes) {
- if (old.pool != null && Math.abs(old.pool.length - vnodes.length) <= Math.abs(old.length - vnodes.length)) {
- var oldChildrenLength = old[0] && old[0].children && old[0].children.length || 0
- var poolChildrenLength = old.pool[0] && old.pool[0].children && old.pool[0].children.length || 0
- var vnodesChildrenLength = vnodes[0] && vnodes[0].children && vnodes[0].children.length || 0
- if (Math.abs(poolChildrenLength - vnodesChildrenLength) <= Math.abs(oldChildrenLength - vnodesChildrenLength)) {
- return true
- }
- }
- return false
- }
- function getKeyMap(vnodes, end) {
- var map = {}, i = 0
- for (var i = 0; i < end; i++) {
- var vnode = vnodes[i]
+ function getKeyMap(vnodes, start, end) {
+ var map = Object.create(null)
+ for (; start < end; start++) {
+ var vnode = vnodes[start]
if (vnode != null) {
var key = vnode.key
- if (key != null) map[key] = i
+ if (key != null) map[key] = start
}
}
return map
}
+ // Lifted from ivi https://github.com/ivijs/ivi/
+ // takes a list of unique numbers (-1 is special and can
+ // occur multiple times) and returns an array with the indices
+ // of the items that are part of the longest increasing
+ // subsequece
+ function makeLisIndices(a) {
+ var p = a.slice()
+ var result = []
+ result.push(0)
+ var u
+ var v
+ for (var i = 0, il = a.length; i < il; ++i) {
+ if (a[i] === -1) {
+ continue
+ }
+ var j = result[result.length - 1]
+ if (a[j] < a[i]) {
+ p[i] = j
+ result.push(i)
+ continue
+ }
+ u = 0
+ v = result.length - 1
+ while (u < v) {
+ var c = ((u + v) / 2) | 0 // eslint-disable-line no-bitwise
+ if (a[result[c]] < a[i]) {
+ u = c + 1
+ }
+ else {
+ v = c
+ }
+ }
+ if (a[i] < a[result[u]]) {
+ if (u > 0) {
+ p[i] = result[u - 1]
+ }
+ result[u] = i
+ }
+ }
+ u = result.length
+ v = result[u - 1]
+ while (u-- > 0) {
+ result[u] = v
+ v = p[v]
+ }
+ return result
+ }
+
function toFragment(vnode) {
var count = vnode.domSize
if (count != null || vnode.dom == null) {
@@ -385,7 +600,7 @@ module.exports = function($window) {
}
function insertNode(parent, dom, nextSibling) {
- if (nextSibling && nextSibling.parentNode) parent.insertBefore(dom, nextSibling)
+ if (nextSibling != null) parent.insertBefore(dom, nextSibling)
else parent.appendChild(dom)
}
@@ -399,26 +614,24 @@ module.exports = function($window) {
}
//remove
- function removeNodes(vnodes, start, end, context) {
+ function removeNodes(vnodes, start, end) {
for (var i = start; i < end; i++) {
var vnode = vnodes[i]
- if (vnode != null) {
- if (vnode.skip) vnode.skip = false
- else removeNode(vnode, context)
- }
+ if (vnode != null) removeNode(vnode)
}
}
- function removeNode(vnode, context) {
+ function removeNode(vnode) {
var expected = 1, called = 0
+ var original = vnode.state
if (vnode.attrs && typeof vnode.attrs.onbeforeremove === "function") {
- var result = vnode.attrs.onbeforeremove.call(vnode.state, vnode)
+ var result = callHook.call(vnode.attrs.onbeforeremove, vnode)
if (result != null && typeof result.then === "function") {
expected++
result.then(continuation, continuation)
}
}
- if (typeof vnode.tag !== "string" && typeof vnode._state.onbeforeremove === "function") {
- var result = vnode._state.onbeforeremove.call(vnode.state, vnode)
+ if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeremove === "function") {
+ var result = callHook.call(vnode.state.onbeforeremove, vnode)
if (result != null && typeof result.then === "function") {
expected++
result.then(continuation, continuation)
@@ -427,32 +640,21 @@ module.exports = function($window) {
continuation()
function continuation() {
if (++called === expected) {
+ checkState(vnode, original)
onremove(vnode)
if (vnode.dom) {
+ var parent = vnode.dom.parentNode
var count = vnode.domSize || 1
- if (count > 1) {
- var dom = vnode.dom
- while (--count) {
- removeNodeFromDOM(dom.nextSibling)
- }
- }
- removeNodeFromDOM(vnode.dom)
- if (context != null && vnode.domSize == null && !hasIntegrationMethods(vnode.attrs) && typeof vnode.tag === "string") { //TODO test custom elements
- if (!context.pool) context.pool = [vnode]
- else context.pool.push(vnode)
- }
+ while (--count) parent.removeChild(vnode.dom.nextSibling)
+ parent.removeChild(vnode.dom)
}
}
}
}
- function removeNodeFromDOM(node) {
- var parent = node.parentNode
- if (parent != null) parent.removeChild(node)
- }
function onremove(vnode) {
- if (vnode.attrs && typeof vnode.attrs.onremove === "function") vnode.attrs.onremove.call(vnode.state, vnode)
+ if (vnode.attrs && typeof vnode.attrs.onremove === "function") callHook.call(vnode.attrs.onremove, vnode)
if (typeof vnode.tag !== "string") {
- if (typeof vnode._state.onremove === "function") vnode._state.onremove.call(vnode.state, vnode)
+ if (typeof vnode.state.onremove === "function") callHook.call(vnode.state.onremove, vnode)
if (vnode.instance != null) onremove(vnode.instance)
} else {
var children = vnode.children
@@ -472,86 +674,109 @@ module.exports = function($window) {
}
}
function setAttr(vnode, key, old, value, ns) {
- var element = vnode.dom
- if (key === "key" || key === "is" || (old === value && !isFormAttribute(vnode, key)) && typeof value !== "object" || typeof value === "undefined" || isLifecycleMethod(key)) return
- var nsLastIndex = key.indexOf(":")
- if (nsLastIndex > -1 && key.substr(0, nsLastIndex) === "xlink") {
- element.setAttributeNS("http://www.w3.org/1999/xlink", key.slice(nsLastIndex + 1), value)
- }
- else if (key[0] === "o" && key[1] === "n" && typeof value === "function") updateEvent(vnode, key, value)
- else if (key === "style") updateStyle(element, old, value)
- else if (key in element && !isAttribute(key) && ns === undefined && !isCustomElement(vnode)) {
+ if (key === "key" || key === "is" || value == null || isLifecycleMethod(key) || (old === value && !isFormAttribute(vnode, key)) && typeof value !== "object") return
+ if (key[0] === "o" && key[1] === "n") return updateEvent(vnode, key, value)
+ if (key.slice(0, 6) === "xlink:") vnode.dom.setAttributeNS("http://www.w3.org/1999/xlink", key.slice(6), value)
+ else if (key === "style") updateStyle(vnode.dom, old, value)
+ else if (hasPropertyKey(vnode, key, ns)) {
if (key === "value") {
- var normalized = "" + value // eslint-disable-line no-implicit-coercion
+ // Only do the coercion if we're actually going to check the value.
+ /* eslint-disable no-implicit-coercion */
//setting input[value] to same value by typing on focused element moves cursor to end in Chrome
- if ((vnode.tag === "input" || vnode.tag === "textarea") && vnode.dom.value === normalized && vnode.dom === $doc.activeElement) return
+ if ((vnode.tag === "input" || vnode.tag === "textarea") && vnode.dom.value === "" + value && vnode.dom === $doc.activeElement) return
//setting select[value] to same value while having select open blinks select dropdown in Chrome
- if (vnode.tag === "select") {
- if (value === null) {
- if (vnode.dom.selectedIndex === -1 && vnode.dom === $doc.activeElement) return
- } else {
- if (old !== null && vnode.dom.value === normalized && vnode.dom === $doc.activeElement) return
- }
- }
+ if (vnode.tag === "select" && old !== null && vnode.dom.value === "" + value) return
//setting option[value] to same value while having select open blinks select dropdown in Chrome
- if (vnode.tag === "option" && old != null && vnode.dom.value === normalized) return
+ if (vnode.tag === "option" && old !== null && vnode.dom.value === "" + value) return
+ /* eslint-enable no-implicit-coercion */
}
// If you assign an input type that is not supported by IE 11 with an assignment expression, an error will occur.
- if (vnode.tag === "input" && key === "type") {
- element.setAttribute(key, value)
- return
- }
- element[key] = value
- }
- else {
+ if (vnode.tag === "input" && key === "type") vnode.dom.setAttribute(key, value)
+ else vnode.dom[key] = value
+ } else {
if (typeof value === "boolean") {
- if (value) element.setAttribute(key, "")
- else element.removeAttribute(key)
+ if (value) vnode.dom.setAttribute(key, "")
+ else vnode.dom.removeAttribute(key)
}
- else element.setAttribute(key === "className" ? "class" : key, value)
+ else vnode.dom.setAttribute(key === "className" ? "class" : key, value)
}
}
- function setLateAttrs(vnode) {
- var attrs = vnode.attrs
- if (vnode.tag === "select" && attrs != null) {
- if ("value" in attrs) setAttr(vnode, "value", null, attrs.value, undefined)
- if ("selectedIndex" in attrs) setAttr(vnode, "selectedIndex", null, attrs.selectedIndex, undefined)
+ function removeAttr(vnode, key, old, ns) {
+ if (key === "key" || key === "is" || old == null || isLifecycleMethod(key)) return
+ if (key[0] === "o" && key[1] === "n" && !isLifecycleMethod(key)) updateEvent(vnode, key, undefined)
+ else if (key === "style") updateStyle(vnode.dom, old, null)
+ else if (
+ hasPropertyKey(vnode, key, ns)
+ && key !== "className"
+ && !(vnode.tag === "option" && key === "value")
+ && !(vnode.tag === "input" && key === "type")
+ ) {
+ vnode.dom[key] = null
+ } else {
+ var nsLastIndex = key.indexOf(":")
+ if (nsLastIndex !== -1) key = key.slice(nsLastIndex + 1)
+ if (old !== false) vnode.dom.removeAttribute(key === "className" ? "class" : key)
}
}
+ function setLateSelectAttrs(vnode, attrs) {
+ if ("value" in attrs) {
+ if(attrs.value === null) {
+ if (vnode.dom.selectedIndex !== -1) vnode.dom.value = null
+ } else {
+ var normalized = "" + attrs.value // eslint-disable-line no-implicit-coercion
+ if (vnode.dom.value !== normalized || vnode.dom.selectedIndex === -1) {
+ vnode.dom.value = normalized
+ }
+ }
+ }
+ if ("selectedIndex" in attrs) setAttr(vnode, "selectedIndex", null, attrs.selectedIndex, undefined)
+ }
function updateAttrs(vnode, old, attrs, ns) {
if (attrs != null) {
for (var key in attrs) {
setAttr(vnode, key, old && old[key], attrs[key], ns)
}
}
+ var val
if (old != null) {
for (var key in old) {
- if (attrs == null || !(key in attrs)) {
- if (key === "className") key = "class"
- if (key[0] === "o" && key[1] === "n" && !isLifecycleMethod(key)) updateEvent(vnode, key, undefined)
- else if (key !== "key") vnode.dom.removeAttribute(key)
+ if (((val = old[key]) != null) && (attrs == null || attrs[key] == null)) {
+ removeAttr(vnode, key, val, ns)
}
}
}
}
function isFormAttribute(vnode, attr) {
- return attr === "value" || attr === "checked" || attr === "selectedIndex" || attr === "selected" && vnode.dom === $doc.activeElement
+ return attr === "value" || attr === "checked" || attr === "selectedIndex" || attr === "selected" && vnode.dom === $doc.activeElement || vnode.tag === "option" && vnode.dom.parentNode === $doc.activeElement
}
function isLifecycleMethod(attr) {
return attr === "oninit" || attr === "oncreate" || attr === "onupdate" || attr === "onremove" || attr === "onbeforeremove" || attr === "onbeforeupdate"
}
- function isAttribute(attr) {
- return attr === "href" || attr === "list" || attr === "form" || attr === "width" || attr === "height"// || attr === "type"
- }
- function isCustomElement(vnode){
- return vnode.attrs.is || vnode.tag.indexOf("-") > -1
- }
- function hasIntegrationMethods(source) {
- return source != null && (source.oncreate || source.onupdate || source.onbeforeremove || source.onremove)
+ function hasPropertyKey(vnode, key, ns) {
+ // Filter out namespaced keys
+ return ns === undefined && (
+ // If it's a custom element, just keep it.
+ vnode.tag.indexOf("-") > -1 || vnode.attrs != null && vnode.attrs.is ||
+ // If it's a normal element, let's try to avoid a few browser bugs.
+ key !== "href" && key !== "list" && key !== "form" && key !== "width" && key !== "height"// && key !== "type"
+ // Defer the property check until *after* we check everything.
+ ) && key in vnode.dom
}
//style
function updateStyle(element, old, style) {
+ if (old != null && style != null && typeof old === "object" && typeof style === "object" && style !== old) {
+ // Both old & new are (different) objects.
+ // Update style properties that have changed
+ for (var key in style) {
+ if (style[key] !== old[key]) element.style[key] = style[key]
+ }
+ // Remove style properties that no longer exist
+ for (var key in old) {
+ if (!(key in style)) element.style[key] = ""
+ }
+ return
+ }
if (old === style) element.style.cssText = "", old = null
if (style == null) element.style.cssText = ""
else if (typeof style === "string") element.style.cssText = style
@@ -560,47 +785,68 @@ module.exports = function($window) {
for (var key in style) {
element.style[key] = style[key]
}
- if (old != null && typeof old !== "string") {
- for (var key in old) {
- if (!(key in style)) element.style[key] = ""
- }
- }
+ }
+ }
+
+ // Here's an explanation of how this works:
+ // 1. The event names are always (by design) prefixed by `on`.
+ // 2. The EventListener interface accepts either a function or an object
+ // with a `handleEvent` method.
+ // 3. The object does not inherit from `Object.prototype`, to avoid
+ // any potential interference with that (e.g. setters).
+ // 4. The event name is remapped to the handler before calling it.
+ // 5. In function-based event handlers, `ev.target === this`. We replicate
+ // that below.
+ // 6. In function-based event handlers, `return false` prevents the default
+ // action and stops event propagation. We replicate that below.
+ function EventDict() {}
+ EventDict.prototype = Object.create(null)
+ EventDict.prototype.handleEvent = function (ev) {
+ var handler = this["on" + ev.type]
+ var result
+ if (typeof handler === "function") result = handler.call(ev.target, ev)
+ else if (typeof handler.handleEvent === "function") handler.handleEvent(ev)
+ if (typeof onevent === "function") onevent.call(ev.target, ev)
+ if (result === false) {
+ ev.preventDefault()
+ ev.stopPropagation()
}
}
//event
function updateEvent(vnode, key, value) {
- var element = vnode.dom
- var callback = typeof onevent !== "function" ? value : function(e) {
- var result = value.call(element, e)
- onevent.call(element, e)
- return result
- }
- if (key in element) element[key] = typeof value === "function" ? callback : null
- else {
- var eventName = key.slice(2)
- if (vnode.events === undefined) vnode.events = {}
- if (vnode.events[key] === callback) return
- if (vnode.events[key] != null) element.removeEventListener(eventName, vnode.events[key], false)
- if (typeof value === "function") {
- vnode.events[key] = callback
- element.addEventListener(eventName, vnode.events[key], false)
+ if (vnode.events != null) {
+ if (vnode.events[key] === value) return
+ if (value != null && (typeof value === "function" || typeof value === "object")) {
+ if (vnode.events[key] == null) vnode.dom.addEventListener(key.slice(2), vnode.events, false)
+ vnode.events[key] = value
+ } else {
+ if (vnode.events[key] != null) vnode.dom.removeEventListener(key.slice(2), vnode.events, false)
+ vnode.events[key] = undefined
}
+ } else if (value != null && (typeof value === "function" || typeof value === "object")) {
+ vnode.events = new EventDict()
+ vnode.dom.addEventListener(key.slice(2), vnode.events, false)
+ vnode.events[key] = value
}
}
//lifecycle
function initLifecycle(source, vnode, hooks) {
- if (typeof source.oninit === "function") source.oninit.call(vnode.state, vnode)
- if (typeof source.oncreate === "function") hooks.push(source.oncreate.bind(vnode.state, vnode))
+ if (typeof source.oninit === "function") callHook.call(source.oninit, vnode)
+ if (typeof source.oncreate === "function") hooks.push(callHook.bind(source.oncreate, vnode))
}
function updateLifecycle(source, vnode, hooks) {
- if (typeof source.onupdate === "function") hooks.push(source.onupdate.bind(vnode.state, vnode))
+ if (typeof source.onupdate === "function") hooks.push(callHook.bind(source.onupdate, vnode))
}
function shouldNotUpdate(vnode, old) {
var forceVnodeUpdate, forceComponentUpdate
- if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") forceVnodeUpdate = vnode.attrs.onbeforeupdate.call(vnode.state, vnode, old)
- if (typeof vnode.tag !== "string" && typeof vnode._state.onbeforeupdate === "function") forceComponentUpdate = vnode._state.onbeforeupdate.call(vnode.state, vnode, old)
+ if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") {
+ forceVnodeUpdate = callHook.call(vnode.attrs.onbeforeupdate, vnode, old)
+ }
+ if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeupdate === "function") {
+ forceComponentUpdate = callHook.call(vnode.state.onbeforeupdate, vnode, old)
+ }
if (!(forceVnodeUpdate === undefined && forceComponentUpdate === undefined) && !forceVnodeUpdate && !forceComponentUpdate) {
vnode.dom = old.dom
vnode.domSize = old.domSize
@@ -620,10 +866,10 @@ module.exports = function($window) {
if (dom.vnodes == null) dom.textContent = ""
if (!Array.isArray(vnodes)) vnodes = [vnodes]
- updateNodes(dom, dom.vnodes, Vnode.normalizeChildren(vnodes), false, hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace)
+ updateNodes(dom, dom.vnodes, Vnode.normalizeChildren(vnodes), hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace)
dom.vnodes = vnodes
// document.activeElement can return null in IE https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement
- if (active != null && $doc.activeElement !== active) active.focus()
+ if (active != null && $doc.activeElement !== active && typeof active.focus === "function") active.focus()
for (var i = 0; i < hooks.length; i++) hooks[i]()
}
diff --git a/render/tests/index.html b/render/tests/index.html
index eda51921..5c9fc0b7 100644
--- a/render/tests/index.html
+++ b/render/tests/index.html
@@ -21,6 +21,7 @@
+
diff --git a/render/tests/test-attributes.js b/render/tests/test-attributes.js
index fcc7f150..9b6f2ba3 100644
--- a/render/tests/test-attributes.js
+++ b/render/tests/test-attributes.js
@@ -44,47 +44,92 @@ o.spec("attributes", function() {
o(b.dom.hasAttribute("id")).equals(true)
o(b.dom.getAttribute("id")).equals("test")
+ // #1804
render(root, [c]);
- // #1804
- // TODO: uncomment
- // o(c.dom.hasAttribute("id")).equals(false)
+ o(c.dom.hasAttribute("id")).equals(false)
})
})
o.spec("customElements", function(){
- o("when vnode is customElement, custom setAttribute called", function(){
-
- var normal = [
- {tag: "input", attrs: {value: "hello"}},
- {tag: "input", attrs: {value: "hello"}},
- {tag: "input", attrs: {value: "hello"}}
- ]
-
- var custom = [
- {tag: "custom-element", attrs: {custom: "x"}},
- {tag: "input", attrs: {is: "something-special", custom: "x"}},
- {tag: "custom-element", attrs: {is: "something-special", custom: "x"}}
- ]
-
- var view = normal.concat(custom)
-
+ o("when vnode is customElement without property, custom setAttribute called", function(){
var f = $window.document.createElement
- var spy
+ var spies = []
$window.document.createElement = function(tag, is){
var el = f(tag, is)
- if(!spy){
- spy = o.spy(el.setAttribute)
- }
+ var spy = o.spy(el.setAttribute)
el.setAttribute = spy
-
+ spies.push(spy)
+ spy.elem = el
return el
}
- render(root, view)
+ render(root, [
+ {tag: "input", attrs: {value: "hello"}},
+ {tag: "input", attrs: {value: "hello"}},
+ {tag: "input", attrs: {value: "hello"}},
+ {tag: "custom-element", attrs: {custom: "x"}},
+ {tag: "input", attrs: {is: "something-special", custom: "x"}},
+ {tag: "custom-element", attrs: {is: "something-special", custom: "x"}}
+ ])
- o(spy.callCount).equals(custom.length)
+ o(spies[0].callCount).equals(0)
+ o(spies[1].callCount).equals(0)
+ o(spies[2].callCount).equals(0)
+ o(spies[3].calls).deepEquals([{this: spies[3].elem, args: ["custom", "x"]}])
+ o(spies[4].calls).deepEquals([{this: spies[4].elem, args: ["custom", "x"]}])
+ o(spies[5].calls).deepEquals([{this: spies[5].elem, args: ["custom", "x"]}])
+ })
+
+ o("when vnode is customElement with property, custom setAttribute not called", function(){
+ var f = $window.document.createElement
+ var spies = []
+ var getters = []
+ var setters = []
+
+ $window.document.createElement = function(tag, is){
+ var el = f(tag, is)
+ var spy = o.spy(el.setAttribute)
+ el.setAttribute = spy
+ spies.push(spy)
+ spy.elem = el
+ if (tag === "custom-element" || is && is.is === "something-special") {
+ var custom = "foo"
+ var getter, setter
+ Object.defineProperty(el, "custom", {
+ configurable: true,
+ enumerable: true,
+ get: getter = o.spy(function () { return custom }),
+ set: setter = o.spy(function (value) { custom = value })
+ })
+ getters.push(getter)
+ setters.push(setter)
+ }
+ return el
+ }
+
+ render(root, [
+ {tag: "input", attrs: {value: "hello"}},
+ {tag: "input", attrs: {value: "hello"}},
+ {tag: "input", attrs: {value: "hello"}},
+ {tag: "custom-element", attrs: {custom: "x"}},
+ {tag: "input", attrs: {is: "something-special", custom: "x"}},
+ {tag: "custom-element", attrs: {is: "something-special", custom: "x"}}
+ ])
+
+ o(spies[0].callCount).equals(0)
+ o(spies[1].callCount).equals(0)
+ o(spies[2].callCount).equals(0)
+ o(spies[3].callCount).equals(0)
+ o(spies[4].callCount).equals(0)
+ o(spies[5].callCount).equals(0)
+ o(getters[0].callCount).equals(0)
+ o(getters[1].callCount).equals(0)
+ o(getters[2].callCount).equals(0)
+ o(setters[0].calls).deepEquals([{this: spies[3].elem, args: ["x"]}])
+ o(setters[1].calls).deepEquals([{this: spies[4].elem, args: ["x"]}])
+ o(setters[2].calls).deepEquals([{this: spies[5].elem, args: ["x"]}])
})
})
@@ -147,7 +192,7 @@ o.spec("attributes", function() {
o("a lack of attribute removes `value`", function() {
var a = {tag: "input", attrs: {}}
var b = {tag: "input", attrs: {value: "test"}}
- // var c = {tag: "input", attrs: {}}
+ var c = {tag: "input", attrs: {}}
render(root, [a])
@@ -158,10 +203,9 @@ o.spec("attributes", function() {
o(a.dom.value).equals("test")
// https://github.com/MithrilJS/mithril.js/issues/1804#issuecomment-304521235
- // TODO: Uncomment
- // render(root, [c])
+ render(root, [c])
- // o(a.dom.value).equals("")
+ o(a.dom.value).equals("")
})
o("can be set as number", function() {
var a = {tag: "input", attrs: {value: 1}}
@@ -276,17 +320,16 @@ o.spec("attributes", function() {
o.spec("textarea.value", function() {
o("can be removed by not passing a value", function() {
var a = {tag: "textarea", attrs: {value:"x"}}
- // var b = {tag: "textarea", attrs: {}}
+ var b = {tag: "textarea", attrs: {}}
render(root, [a])
o(a.dom.value).equals("x")
// https://github.com/MithrilJS/mithril.js/issues/1804#issuecomment-304521235
- // TODO: Uncomment
- // render(root, [b])
+ render(root, [b])
- // o(b.dom.value).equals("")
+ o(b.dom.value).equals("")
})
o("isn't set when equivalent to the previous value and focused", function() {
var $window = domMock({spy: o.spy})
@@ -352,7 +395,7 @@ o.spec("attributes", function() {
o(canvas.dom.width).equals(100)
})
})
- o.spec("svg class", function() {
+ o.spec("svg", function() {
o("when className is specified then it should be added as a class", function() {
var a = {tag: "svg", attrs: {className: "test"}}
@@ -360,6 +403,26 @@ o.spec("attributes", function() {
o(a.dom.attributes["class"].value).equals("test")
})
+ /* eslint-disable no-script-url */
+ o("handles xlink:href", function() {
+ var vnode = {tag: "svg", ns: "http://www.w3.org/2000/svg", children: [
+ {tag: "a", ns: "http://www.w3.org/2000/svg", attrs: {"xlink:href": "javascript:;"}}
+ ]}
+ render(root, [vnode])
+
+ o(vnode.dom.nodeName).equals("svg")
+ o(vnode.dom.firstChild.attributes["href"].value).equals("javascript:;")
+ o(vnode.dom.firstChild.attributes["href"].namespaceURI).equals("http://www.w3.org/1999/xlink")
+
+ vnode = {tag: "svg", ns: "http://www.w3.org/2000/svg", children: [
+ {tag: "a", ns: "http://www.w3.org/2000/svg", attrs: {}}
+ ]}
+ render(root, [vnode])
+
+ o(vnode.dom.nodeName).equals("svg")
+ o("href" in vnode.dom.firstChild.attributes).equals(false)
+ })
+ /* eslint-enable no-script-url */
})
o.spec("option.value", function() {
o("can be set as text", function() {
@@ -376,7 +439,7 @@ o.spec("attributes", function() {
o(a.dom.value).equals("1")
})
- o("null becomes the empty string", function() {
+ o("null removes the attribute", function() {
var a = {tag: "option", attrs: {value: null}}
var b = {tag: "option", attrs: {value: "test"}}
var c = {tag: "option", attrs: {value: null}}
@@ -384,7 +447,7 @@ o.spec("attributes", function() {
render(root, [a]);
o(a.dom.value).equals("")
- o(a.dom.getAttribute("value")).equals("")
+ o(a.dom.hasAttribute("value")).equals(false)
render(root, [b]);
@@ -394,7 +457,7 @@ o.spec("attributes", function() {
render(root, [c]);
o(c.dom.value).equals("")
- o(c.dom.getAttribute("value")).equals("")
+ o(c.dom.hasAttribute("value")).equals(false)
})
o("'' and 0 are different values", function() {
var a = {tag: "option", attrs: {value: 0}, children:[{tag:"#", children:""}]}
@@ -462,6 +525,19 @@ o.spec("attributes", function() {
{tag:"option", attrs: {value: ""}}
]}
}
+ /* FIXME
+ This incomplete test is meant for testing #1916.
+ However it cannot be completed until #1978 is addressed
+ which is a lack a working select.selected / option.selected
+ attribute. Ask isiahmeadows.
+
+ o("render select options", function() {
+ var select = {tag: "select", selectedIndex: 0, children: [
+ {tag:"option", attrs: {value: "1", selected: ""}}
+ ]}
+ render(root, select)
+ })
+ */
o("can be set as text", function() {
var a = makeSelect()
var b = makeSelect("2")
diff --git a/render/tests/test-component.js b/render/tests/test-component.js
index c391ddd2..dd27ef10 100644
--- a/render/tests/test-component.js
+++ b/render/tests/test-component.js
@@ -764,97 +764,6 @@ o.spec("component", function() {
o(attrs[hook].callCount).equals(methods[hook].callCount)(hook)
})
})
- o("lifecycle timing megatest (for a single component with the state overwritten)", function() {
- var methods = {
- view: o.spy(function(vnode) {
- o(vnode.state).equals(1)
- return ""
- })
- }
- var attrs = {}
- var hooks = [
- "oninit", "oncreate", "onbeforeupdate",
- "onupdate", "onbeforeremove", "onremove"
- ]
- hooks.forEach(function(hook) {
- // the `attrs` hooks are called before the component ones
- attrs[hook] = o.spy(function(vnode) {
- o(vnode.state).equals(1)
- o(attrs[hook].callCount).equals(methods[hook].callCount + 1)
- })
- methods[hook] = o.spy(function(vnode) {
- o(vnode.state).equals(1)
- o(attrs[hook].callCount).equals(methods[hook].callCount)
- })
- })
-
- var attrsOninit = attrs.oninit
- var methodsOninit = methods.oninit
- attrs.oninit = o.spy(function(vnode){
- vnode.state = 1
- return attrsOninit.call(this, vnode)
- })
- methods.oninit = o.spy(function(vnode){
- vnode.state = 1
- return methodsOninit.call(this, vnode)
- })
-
- var component = createComponent(methods)
-
- o(methods.view.callCount).equals(0)
- o(methods.oninit.callCount).equals(0)
- o(methods.oncreate.callCount).equals(0)
- o(methods.onbeforeupdate.callCount).equals(0)
- o(methods.onupdate.callCount).equals(0)
- o(methods.onbeforeremove.callCount).equals(0)
- o(methods.onremove.callCount).equals(0)
-
- hooks.forEach(function(hook) {
- o(attrs[hook].callCount).equals(methods[hook].callCount)(hook)
- })
-
- render(root, [{tag: component, attrs: attrs}])
-
- o(methods.view.callCount).equals(1)
- o(methods.oninit.callCount).equals(1)
- o(methods.oncreate.callCount).equals(1)
- o(methods.onbeforeupdate.callCount).equals(0)
- o(methods.onupdate.callCount).equals(0)
- o(methods.onbeforeremove.callCount).equals(0)
- o(methods.onremove.callCount).equals(0)
-
- hooks.forEach(function(hook) {
- o(attrs[hook].callCount).equals(methods[hook].callCount)(hook)
- })
-
- render(root, [{tag: component, attrs: attrs}])
-
- o(methods.view.callCount).equals(2)
- o(methods.oninit.callCount).equals(1)
- o(methods.oncreate.callCount).equals(1)
- o(methods.onbeforeupdate.callCount).equals(1)
- o(methods.onupdate.callCount).equals(1)
- o(methods.onbeforeremove.callCount).equals(0)
- o(methods.onremove.callCount).equals(0)
-
- hooks.forEach(function(hook) {
- o(attrs[hook].callCount).equals(methods[hook].callCount)(hook)
- })
-
- render(root, [])
-
- o(methods.view.callCount).equals(2)
- o(methods.oninit.callCount).equals(1)
- o(methods.oncreate.callCount).equals(1)
- o(methods.onbeforeupdate.callCount).equals(1)
- o(methods.onupdate.callCount).equals(1)
- o(methods.onbeforeremove.callCount).equals(1)
- o(methods.onremove.callCount).equals(1)
-
- hooks.forEach(function(hook) {
- o(attrs[hook].callCount).equals(methods[hook].callCount)(hook)
- })
- })
o("hook state and arguments validation", function(){
var methods = {
view: o.spy(function(vnode) {
@@ -899,7 +808,7 @@ o.spec("component", function() {
o(methods[hook].args.length).equals(attrs[hook].args.length)(hook)
})
})
- o("recycled components get a fresh state", function() {
+ o("no recycling occurs (was: recycled components get a fresh state)", function() {
var step = 0
var firstState
var view = o.spy(function(vnode) {
@@ -918,7 +827,7 @@ o.spec("component", function() {
step = 1
render(root, [{tag: "div", children: [{tag: component, key: 1}]}])
- o(child).equals(root.firstChild.firstChild)
+ o(child).notEquals(root.firstChild.firstChild) // this used to be a recycling pool test
o(view.callCount).equals(2)
})
})
diff --git a/render/tests/test-createElement.js b/render/tests/test-createElement.js
index 6e3dcdd7..ea4d01c8 100644
--- a/render/tests/test-createElement.js
+++ b/render/tests/test-createElement.js
@@ -1,4 +1,3 @@
-/* eslint-disable no-script-url */
"use strict"
var o = require("../../ospec/ospec")
@@ -54,6 +53,7 @@ o.spec("createElement", function() {
o(vnode.dom.childNodes[0].nodeName).equals("A")
o(vnode.dom.childNodes[1].nodeName).equals("B")
})
+ /* eslint-disable no-script-url */
o("creates svg", function() {
var vnode = {tag: "svg", ns: "http://www.w3.org/2000/svg", children: [
{tag: "a", ns: "http://www.w3.org/2000/svg", attrs: {"xlink:href": "javascript:;"}},
@@ -71,6 +71,7 @@ o.spec("createElement", function() {
o(vnode.dom.childNodes[1].firstChild.nodeName).equals("body")
o(vnode.dom.childNodes[1].firstChild.namespaceURI).equals("http://www.w3.org/1999/xhtml")
})
+ /* eslint-enable no-script-url */
o("sets attributes correctly for svg", function() {
var vnode = {tag: "svg", ns: "http://www.w3.org/2000/svg", attrs: {viewBox: "0 0 100 100"}}
render(root, [vnode])
diff --git a/render/tests/test-createHTML.js b/render/tests/test-createHTML.js
index bb0d508c..a337213b 100644
--- a/render/tests/test-createHTML.js
+++ b/render/tests/test-createHTML.js
@@ -31,7 +31,7 @@ o.spec("createHTML", function() {
o(vnode.dom).equals(null)
o(vnode.domSize).equals(0)
})
- o("handles multiple children", function() {
+ o("handles multiple children in HTML", function() {
var vnode = {tag: "<", children: ""}
render(root, [vnode])
@@ -51,4 +51,34 @@ o.spec("createHTML", function() {
o(vnode.dom.nodeName).equals(tag.toUpperCase())
})
})
+ o("creates SVG", function() {
+ var vnode = {tag: "<", children: ""}
+ render(root, [{tag:"svg", children: [vnode]}])
+
+ o(vnode.dom.nodeName).equals("g")
+ o(vnode.dom.namespaceURI).equals("http://www.w3.org/2000/svg")
+ })
+ o("creates text SVG", function() {
+ var vnode = {tag: "<", children: "a"}
+ render(root, [{tag:"svg", children: [vnode]}])
+
+ o(vnode.dom.nodeValue).equals("a")
+ })
+ o("handles empty SVG", function() {
+ var vnode = {tag: "<", children: ""}
+ render(root, [{tag:"svg", children: [vnode]}])
+
+ o(vnode.dom).equals(null)
+ o(vnode.domSize).equals(0)
+ })
+ o("handles multiple children in SVG", function() {
+ var vnode = {tag: "<", children: ""}
+ render(root, [{tag:"svg", children: [vnode]}])
+
+ o(vnode.domSize).equals(2)
+ o(vnode.dom.nodeName).equals("g")
+ o(vnode.dom.namespaceURI).equals("http://www.w3.org/2000/svg")
+ o(vnode.dom.nextSibling.nodeName).equals("text")
+ o(vnode.dom.nextSibling.namespaceURI).equals("http://www.w3.org/2000/svg")
+ })
})
diff --git a/render/tests/test-event.js b/render/tests/test-event.js
index 1f7eec2a..02246e18 100644
--- a/render/tests/test-event.js
+++ b/render/tests/test-event.js
@@ -32,8 +32,75 @@ o.spec("event", function() {
o(onevent.this).equals(div.dom)
o(onevent.args[0].type).equals("click")
o(onevent.args[0].target).equals(div.dom)
+ o(e.$defaultPrevented).equals(false)
+ o(e.$propagationStopped).equals(false)
})
-
+
+ o("handles onclick returning false", function() {
+ var spy = o.spy(function () { return false })
+ var div = {tag: "div", attrs: {onclick: spy}}
+ var e = $window.document.createEvent("MouseEvents")
+ e.initEvent("click", true, true)
+
+ render(root, [div])
+ div.dom.dispatchEvent(e)
+
+ o(spy.callCount).equals(1)
+ o(spy.this).equals(div.dom)
+ o(spy.args[0].type).equals("click")
+ o(spy.args[0].target).equals(div.dom)
+ o(onevent.callCount).equals(1)
+ o(onevent.this).equals(div.dom)
+ o(onevent.args[0].type).equals("click")
+ o(onevent.args[0].target).equals(div.dom)
+ o(e.$defaultPrevented).equals(true)
+ o(e.$propagationStopped).equals(true)
+ })
+
+ o("handles click EventListener object", function() {
+ var spy = o.spy()
+ var listener = {handleEvent: spy}
+ var div = {tag: "div", attrs: {onclick: listener}}
+ var e = $window.document.createEvent("MouseEvents")
+ e.initEvent("click", true, true)
+
+ render(root, [div])
+ div.dom.dispatchEvent(e)
+
+ o(spy.callCount).equals(1)
+ o(spy.this).equals(listener)
+ o(spy.args[0].type).equals("click")
+ o(spy.args[0].target).equals(div.dom)
+ o(onevent.callCount).equals(1)
+ o(onevent.this).equals(div.dom)
+ o(onevent.args[0].type).equals("click")
+ o(onevent.args[0].target).equals(div.dom)
+ o(e.$defaultPrevented).equals(false)
+ o(e.$propagationStopped).equals(false)
+ })
+
+ o("handles click EventListener object returning false", function() {
+ var spy = o.spy(function () { return false })
+ var listener = {handleEvent: spy}
+ var div = {tag: "div", attrs: {onclick: listener}}
+ var e = $window.document.createEvent("MouseEvents")
+ e.initEvent("click", true, true)
+
+ render(root, [div])
+ div.dom.dispatchEvent(e)
+
+ o(spy.callCount).equals(1)
+ o(spy.this).equals(listener)
+ o(spy.args[0].type).equals("click")
+ o(spy.args[0].target).equals(div.dom)
+ o(onevent.callCount).equals(1)
+ o(onevent.this).equals(div.dom)
+ o(onevent.args[0].type).equals("click")
+ o(onevent.args[0].target).equals(div.dom)
+ o(e.$defaultPrevented).equals(false)
+ o(e.$propagationStopped).equals(false)
+ })
+
o("removes event", function() {
var spy = o.spy()
var vnode = {tag: "a", attrs: {onclick: spy}}
@@ -45,7 +112,130 @@ o.spec("event", function() {
var e = $window.document.createEvent("MouseEvents")
e.initEvent("click", true, true)
vnode.dom.dispatchEvent(e)
-
+
+ o(spy.callCount).equals(0)
+ })
+
+ o("removes event when null", function() {
+ var spy = o.spy()
+ var vnode = {tag: "a", attrs: {onclick: spy}}
+ var updated = {tag: "a", attrs: {onclick: null}}
+
+ render(root, [vnode])
+ render(root, [updated])
+
+ var e = $window.document.createEvent("MouseEvents")
+ e.initEvent("click", true, true)
+ vnode.dom.dispatchEvent(e)
+
+ o(spy.callCount).equals(0)
+ })
+
+ o("removes event when undefined", function() {
+ var spy = o.spy()
+ var vnode = {tag: "a", attrs: {onclick: spy}}
+ var updated = {tag: "a", attrs: {onclick: undefined}}
+
+ render(root, [vnode])
+ render(root, [updated])
+
+ var e = $window.document.createEvent("MouseEvents")
+ e.initEvent("click", true, true)
+ vnode.dom.dispatchEvent(e)
+
+ o(spy.callCount).equals(0)
+ })
+
+ o("removes event added via addEventListener when null", function() {
+ var spy = o.spy()
+ var vnode = {tag: "a", attrs: {ontouchstart: spy}}
+ var updated = {tag: "a", attrs: {ontouchstart: null}}
+
+ render(root, [vnode])
+ render(root, [updated])
+
+ var e = $window.document.createEvent("TouchEvents")
+ e.initEvent("touchstart", true, true)
+ vnode.dom.dispatchEvent(e)
+
+ o(spy.callCount).equals(0)
+ })
+
+ o("removes event added via addEventListener", function() {
+ var spy = o.spy()
+ var vnode = {tag: "a", attrs: {ontouchstart: spy}}
+ var updated = {tag: "a", attrs: {}}
+
+ render(root, [vnode])
+ render(root, [updated])
+
+ var e = $window.document.createEvent("TouchEvents")
+ e.initEvent("touchstart", true, true)
+ vnode.dom.dispatchEvent(e)
+
+ o(spy.callCount).equals(0)
+ })
+
+ o("removes event added via addEventListener when undefined", function() {
+ var spy = o.spy()
+ var vnode = {tag: "a", attrs: {ontouchstart: spy}}
+ var updated = {tag: "a", attrs: {ontouchstart: undefined}}
+
+ render(root, [vnode])
+ render(root, [updated])
+
+ var e = $window.document.createEvent("TouchEvents")
+ e.initEvent("touchstart", true, true)
+ vnode.dom.dispatchEvent(e)
+
+ o(spy.callCount).equals(0)
+ })
+
+ o("removes EventListener object", function() {
+ var spy = o.spy()
+ var listener = {handleEvent: spy}
+ var vnode = {tag: "a", attrs: {onclick: listener}}
+ var updated = {tag: "a", attrs: {}}
+
+ render(root, [vnode])
+ render(root, [updated])
+
+ var e = $window.document.createEvent("MouseEvents")
+ e.initEvent("click", true, true)
+ vnode.dom.dispatchEvent(e)
+
+ o(spy.callCount).equals(0)
+ })
+
+ o("removes EventListener object when null", function() {
+ var spy = o.spy()
+ var listener = {handleEvent: spy}
+ var vnode = {tag: "a", attrs: {onclick: listener}}
+ var updated = {tag: "a", attrs: {onclick: null}}
+
+ render(root, [vnode])
+ render(root, [updated])
+
+ var e = $window.document.createEvent("MouseEvents")
+ e.initEvent("click", true, true)
+ vnode.dom.dispatchEvent(e)
+
+ o(spy.callCount).equals(0)
+ })
+
+ o("removes EventListener object when undefined", function() {
+ var spy = o.spy()
+ var listener = {handleEvent: spy}
+ var vnode = {tag: "a", attrs: {onclick: listener}}
+ var updated = {tag: "a", attrs: {onclick: undefined}}
+
+ render(root, [vnode])
+ render(root, [updated])
+
+ var e = $window.document.createEvent("MouseEvents")
+ e.initEvent("click", true, true)
+ vnode.dom.dispatchEvent(e)
+
o(spy.callCount).equals(0)
})
@@ -72,6 +262,30 @@ o.spec("event", function() {
o(div.dom.attributes["id"].value).equals("b")
})
+ o("fires click EventListener object only once after redraw", function() {
+ var spy = o.spy()
+ var listener = {handleEvent: spy}
+ var div = {tag: "div", attrs: {id: "a", onclick: listener}}
+ var updated = {tag: "div", attrs: {id: "b", onclick: listener}}
+ var e = $window.document.createEvent("MouseEvents")
+ e.initEvent("click", true, true)
+
+ render(root, [div])
+ render(root, [updated])
+ div.dom.dispatchEvent(e)
+
+ o(spy.callCount).equals(1)
+ o(spy.this).equals(listener)
+ o(spy.args[0].type).equals("click")
+ o(spy.args[0].target).equals(div.dom)
+ o(onevent.callCount).equals(1)
+ o(onevent.this).equals(div.dom)
+ o(onevent.args[0].type).equals("click")
+ o(onevent.args[0].target).equals(div.dom)
+ o(div.dom).equals(updated.dom)
+ o(div.dom.attributes["id"].value).equals("b")
+ })
+
o("handles ontransitionend", function() {
var spy = o.spy()
var div = {tag: "div", attrs: {ontransitionend: spy}}
@@ -90,4 +304,24 @@ o.spec("event", function() {
o(onevent.args[0].type).equals("transitionend")
o(onevent.args[0].target).equals(div.dom)
})
+
+ o("handles transitionend EventListener object", function() {
+ var spy = o.spy()
+ var listener = {handleEvent: spy}
+ var div = {tag: "div", attrs: {ontransitionend: listener}}
+ var e = $window.document.createEvent("HTMLEvents")
+ e.initEvent("transitionend", true, true)
+
+ render(root, [div])
+ div.dom.dispatchEvent(e)
+
+ o(spy.callCount).equals(1)
+ o(spy.this).equals(listener)
+ o(spy.args[0].type).equals("transitionend")
+ o(spy.args[0].target).equals(div.dom)
+ o(onevent.callCount).equals(1)
+ o(onevent.this).equals(div.dom)
+ o(onevent.args[0].type).equals("transitionend")
+ o(onevent.args[0].target).equals(div.dom)
+ })
})
diff --git a/render/tests/test-hyperscript.js b/render/tests/test-hyperscript.js
index f7c5f889..bb4a0f62 100644
--- a/render/tests/test-hyperscript.js
+++ b/render/tests/test-hyperscript.js
@@ -16,52 +16,51 @@ o.spec("hyperscript", function() {
o(vnode.tag).equals("a")
})
- o("v1.0.1 bug-for-bug regression suite", function(){
+ o("class and className normalization", function(){
o(m("a", {
class: null
}).attrs).deepEquals({
- class: undefined,
- className: null
+ class: null
})
o(m("a", {
class: undefined
}).attrs).deepEquals({
- class: undefined,
+ class: null
})
o(m("a", {
class: false
}).attrs).deepEquals({
- class: undefined,
+ class: null,
className: false
})
o(m("a", {
class: true
}).attrs).deepEquals({
- class: undefined,
+ class: null,
className: true
})
o(m("a.x", {
class: null
}).attrs).deepEquals({
- class: undefined,
- className: "x null"
+ class: null,
+ className: "x"
})
o(m("a.x", {
class: undefined
}).attrs).deepEquals({
- class: undefined,
+ class: null,
className: "x"
})
o(m("a.x", {
class: false
}).attrs).deepEquals({
- class: undefined,
+ class: null,
className: "x false"
})
o(m("a.x", {
class: true
}).attrs).deepEquals({
- class: undefined,
+ class: null,
className: "x true"
})
o(m("a", {
@@ -97,7 +96,7 @@ o.spec("hyperscript", function() {
o(m("a.x", {
className: false
}).attrs).deepEquals({
- className: "x"
+ className: "x false"
})
o(m("a.x", {
className: true
@@ -272,7 +271,7 @@ o.spec("hyperscript", function() {
var vnode = m("div", {key:"a"})
o(vnode.tag).equals("div")
- o(vnode.attrs).equals(undefined)
+ o(vnode.attrs).equals(null)
o(vnode.key).equals("a")
})
o("handles many attrs", function() {
@@ -303,6 +302,63 @@ o.spec("hyperscript", function() {
o(vnode.attrs.className).equals("a b")
})
})
+ o.spec("custom element attrs", function() {
+ o("handles string attr", function() {
+ var vnode = m("custom-element", {a: "b"})
+
+ o(vnode.tag).equals("custom-element")
+ o(vnode.attrs.a).equals("b")
+ })
+ o("handles falsy string attr", function() {
+ var vnode = m("custom-element", {a: ""})
+
+ o(vnode.tag).equals("custom-element")
+ o(vnode.attrs.a).equals("")
+ })
+ o("handles number attr", function() {
+ var vnode = m("custom-element", {a: 1})
+
+ o(vnode.tag).equals("custom-element")
+ o(vnode.attrs.a).equals(1)
+ })
+ o("handles falsy number attr", function() {
+ var vnode = m("custom-element", {a: 0})
+
+ o(vnode.tag).equals("custom-element")
+ o(vnode.attrs.a).equals(0)
+ })
+ o("handles boolean attr", function() {
+ var vnode = m("custom-element", {a: true})
+
+ o(vnode.tag).equals("custom-element")
+ o(vnode.attrs.a).equals(true)
+ })
+ o("handles falsy boolean attr", function() {
+ var vnode = m("custom-element", {a: false})
+
+ o(vnode.tag).equals("custom-element")
+ o(vnode.attrs.a).equals(false)
+ })
+ o("handles only key in attrs", function() {
+ var vnode = m("custom-element", {key:"a"})
+
+ o(vnode.tag).equals("custom-element")
+ o(vnode.attrs).equals(null)
+ o(vnode.key).equals("a")
+ })
+ o("handles many attrs", function() {
+ var vnode = m("custom-element", {a: "b", c: "d"})
+
+ o(vnode.tag).equals("custom-element")
+ o(vnode.attrs.a).equals("b")
+ o(vnode.attrs.c).equals("d")
+ })
+ o("handles className attrs property", function() {
+ var vnode = m("custom-element", {className: "a"})
+
+ o(vnode.attrs.className).equals("a")
+ })
+ })
o.spec("children", function() {
o("handles string single child", function() {
var vnode = m("div", {}, ["a"])
@@ -490,20 +546,20 @@ o.spec("hyperscript", function() {
o("handles children without attr", function() {
var vnode = m("div", [m("i"), m("s")])
- o(vnode.attrs).equals(undefined)
+ o(vnode.attrs).equals(null)
o(vnode.children[0].tag).equals("i")
o(vnode.children[1].tag).equals("s")
})
o("handles child without attr unwrapped", function() {
var vnode = m("div", m("i"))
- o(vnode.attrs).equals(undefined)
+ o(vnode.attrs).equals(null)
o(vnode.children[0].tag).equals("i")
})
o("handles children without attr unwrapped", function() {
var vnode = m("div", m("i"), m("s"))
- o(vnode.attrs).equals(undefined)
+ o(vnode.attrs).equals(null)
o(vnode.children[0].tag).equals("i")
o(vnode.children[1].tag).equals("s")
})
@@ -524,6 +580,15 @@ o.spec("hyperscript", function() {
m(".a", attrs)
o(attrs).deepEquals({a: "b"})
})
+ o("non-nullish attr takes precedence over selector", function() {
+ o(m("[a=b]", {a: "c"}).attrs).deepEquals({a: "c"})
+ })
+ o("null attr takes precedence over selector", function() {
+ o(m("[a=b]", {a: null}).attrs).deepEquals({a: null})
+ })
+ o("undefined attr takes precedence over selector", function() {
+ o(m("[a=b]", {a: undefined}).attrs).deepEquals({a: undefined})
+ })
o("handles fragment children without attr unwrapped", function() {
var vnode = m("div", [m("i")], [m("s")])
@@ -551,19 +616,29 @@ o.spec("hyperscript", function() {
o.spec("components", function() {
o("works with POJOs", function() {
var component = {
- view: function() {
- return m("div")
- }
+ view: function() {}
}
var vnode = m(component, {id: "a"}, "b")
o(vnode.tag).equals(component)
o(vnode.attrs.id).equals("a")
o(vnode.children.length).equals(1)
- o(vnode.children[0].tag).equals("#")
- o(vnode.children[0].children).equals("b")
+ o(vnode.children[0]).equals("b")
})
- o("works with functions", function() {
+ o("works with constructibles", function() {
+ var component = o.spy()
+ component.prototype.view = function() {}
+
+ var vnode = m(component, {id: "a"}, "b")
+
+ o(component.callCount).equals(0)
+
+ o(vnode.tag).equals(component)
+ o(vnode.attrs.id).equals("a")
+ o(vnode.children.length).equals(1)
+ o(vnode.children[0]).equals("b")
+ })
+ o("works with closures", function () {
var component = o.spy()
var vnode = m(component, {id: "a"}, "b")
@@ -573,8 +648,7 @@ o.spec("hyperscript", function() {
o(vnode.tag).equals(component)
o(vnode.attrs.id).equals("a")
o(vnode.children.length).equals(1)
- o(vnode.children[0].tag).equals("#")
- o(vnode.children[0].children).equals("b")
+ o(vnode.children[0]).equals("b")
})
})
})
diff --git a/render/tests/test-input.js b/render/tests/test-input.js
index 1f9a24d7..c443db1a 100644
--- a/render/tests/test-input.js
+++ b/render/tests/test-input.js
@@ -59,6 +59,16 @@ o.spec("form inputs", function() {
o(updated.dom.value).equals("aaa")
})
+ o("clear element value if vdom value is set to undefined (aka removed)", function() {
+ var input = {tag: "input", attrs: {value: "aaa", oninput: function() {}}}
+ var updated = {tag: "input", attrs: {value: undefined, oninput: function() {}}}
+
+ render(root, [input])
+ render(root, [updated])
+
+ o(updated.dom.value).equals("")
+ })
+
o("syncs input checked attribute if DOM value differs from vdom value", function() {
var input = {tag: "input", attrs: {type: "checkbox", checked: true, onclick: function() {}}}
var updated = {tag: "input", attrs: {type: "checkbox", checked: true, onclick: function() {}}}
diff --git a/render/tests/test-normalizeComponentChildren.js b/render/tests/test-normalizeComponentChildren.js
new file mode 100644
index 00000000..86f75ffe
--- /dev/null
+++ b/render/tests/test-normalizeComponentChildren.js
@@ -0,0 +1,34 @@
+"use strict"
+
+var o = require("../../ospec/ospec")
+var m = require("../../render/hyperscript")
+var domMock = require("../../test-utils/domMock")
+var vdom = require("../../render/render")
+
+o.spec("component children", function () {
+ var $window = domMock()
+ var root = $window.document.createElement("div")
+ var render = vdom($window).render
+
+ o.spec("component children", function () {
+ var component = {
+ view: function (vnode) {
+ return vnode.children
+ }
+ }
+
+ var vnode = m(component, "a")
+
+ render(root, vnode)
+
+ o("are not normalized on ingestion", function () {
+ o(vnode.children[0]).equals("a")
+ })
+
+ o("are normalized upon view interpolation", function () {
+ o(vnode.instance.children.length).equals(1)
+ o(vnode.instance.children[0].tag).equals("#")
+ o(vnode.instance.children[0].children).equals("a")
+ })
+ })
+})
diff --git a/render/tests/test-onbeforeremove.js b/render/tests/test-onbeforeremove.js
index 0e83d4a0..834f7510 100644
--- a/render/tests/test-onbeforeremove.js
+++ b/render/tests/test-onbeforeremove.js
@@ -36,10 +36,7 @@ o.spec("onbeforeremove", function() {
o(update.callCount).equals(0)
})
o("calls onbeforeremove when removing element", function(done) {
- var vnode = {tag: "div", attrs: {
- oninit: function() {vnode.state = {}},
- onbeforeremove: remove
- }}
+ var vnode = {tag: "div", attrs: {onbeforeremove: remove}}
render(root, [vnode])
render(root, [])
@@ -47,6 +44,7 @@ o.spec("onbeforeremove", function() {
function remove(node) {
o(node).equals(vnode)
o(this).equals(vnode.state)
+ o(this != null && typeof this === "object").equals(true)
o(root.childNodes.length).equals(1)
o(root.firstChild).equals(vnode.dom)
diff --git a/render/tests/test-onbeforeupdate.js b/render/tests/test-onbeforeupdate.js
index d8b2f2d6..d0b1ecab 100644
--- a/render/tests/test-onbeforeupdate.js
+++ b/render/tests/test-onbeforeupdate.js
@@ -129,7 +129,7 @@ o.spec("onbeforeupdate", function() {
render(root, temp)
render(root, updated)
- o(vnodes[0].dom).equals(updated[0].dom)
+ o(vnodes[0].dom).notEquals(updated[0].dom) // this used to be a recycling pool test
o(updated[0].dom.nodeName).equals("DIV")
o(onbeforeupdate.callCount).equals(0)
})
diff --git a/render/tests/test-oninit.js b/render/tests/test-oninit.js
index 4d94cae4..f6ffb873 100644
--- a/render/tests/test-oninit.js
+++ b/render/tests/test-oninit.js
@@ -199,4 +199,27 @@ o.spec("oninit", function() {
o(vnode.dom.oninit).equals(undefined)
o(vnode.dom.attributes["oninit"]).equals(undefined)
})
+
+ o("No spurious oninit calls in mapped keyed diff when the pool is involved (#1992)", function () {
+ var oninit1 = o.spy()
+ var oninit2 = o.spy()
+ var oninit3 = o.spy()
+
+ render(root, [
+ {tag: "p", key: 1, attrs: {oninit: oninit1}},
+ {tag: "p", key: 2, attrs: {oninit: oninit2}},
+ {tag: "p", key: 3, attrs: {oninit: oninit3}},
+ ])
+ render(root, [
+ {tag: "p", key: 1, attrs: {oninit: oninit1}},
+ {tag: "p", key: 3, attrs: {oninit: oninit3}},
+ ])
+ render(root, [
+ {tag: "p", key: 3, attrs: {oninit: oninit3}},
+ ])
+
+ o(oninit1.callCount).equals(1)
+ o(oninit2.callCount).equals(1)
+ o(oninit3.callCount).equals(1)
+ })
})
diff --git a/render/tests/test-onremove.js b/render/tests/test-onremove.js
index f509e704..6cc3fd5c 100644
--- a/render/tests/test-onremove.js
+++ b/render/tests/test-onremove.js
@@ -91,7 +91,7 @@ o.spec("onremove", function() {
o(vnode.dom.attributes["onremove"]).equals(undefined)
o(vnode.events).equals(undefined)
})
- o("calls onremove on recycle", function() {
+ o("calls onremove on keyed nodes", function() {
var remove = o.spy()
var vnodes = [{tag: "div", key: 1}]
var temp = [{tag: "div", key: 2, attrs: {onremove: remove}}]
@@ -101,6 +101,7 @@ o.spec("onremove", function() {
render(root, temp)
render(root, updated)
+ o(vnodes[0].dom).notEquals(updated[0].dom) // this used to be a recycling pool test
o(remove.callCount).equals(1)
})
o("does not recycle when there's an onremove", function() {
@@ -132,7 +133,7 @@ o.spec("onremove", function() {
})
render(root, {tag: comp})
render(root, null)
-
+
o(spy.callCount).equals(1)
})
o("calls onremove on nested component child", function() {
@@ -148,7 +149,7 @@ o.spec("onremove", function() {
})
render(root, {tag: comp})
render(root, null)
-
+
o(spy.callCount).equals(1)
})
o("doesn't call onremove on children when the corresponding view returns null (after removing the parent)", function() {
@@ -191,6 +192,28 @@ o.spec("onremove", function() {
o(spy.callCount).equals(0)
o(threw).equals(false)
})
+ o("onremove doesn't fire on nodes that go from pool to pool (#1990)", function() {
+ var onremove = o.spy();
+
+ render(root, [m("div", m("div")), m("div", m("div", {onremove: onremove}))]);
+ render(root, [m("div", m("div"))]);
+ render(root, []);
+
+ o(onremove.callCount).equals(1)
+ })
+ o("doesn't fire when removing the children of a node that's brought back from the pool (#1991 part 2)", function() {
+ var onremove = o.spy()
+ var vnode = {tag: "div", key: 1, children: [{tag: "div", attrs: {onremove: onremove}}]}
+ var temp = {tag: "div", key: 2}
+ var updated = {tag: "div", key: 1, children: [{tag: "p"}]}
+
+ render(root, [vnode])
+ render(root, [temp])
+ render(root, [updated])
+
+ o(vnode.dom).notEquals(updated.dom) // this used to be a recycling pool test
+ o(onremove.callCount).equals(1)
+ })
})
})
-})
\ No newline at end of file
+})
diff --git a/render/tests/test-render-hyperscript-integration.js b/render/tests/test-render-hyperscript-integration.js
new file mode 100644
index 00000000..73d96a8f
--- /dev/null
+++ b/render/tests/test-render-hyperscript-integration.js
@@ -0,0 +1,614 @@
+"use strict"
+
+var o = require("../../ospec/ospec")
+var m = require("../../render/hyperscript")
+var domMock = require("../../test-utils/domMock")
+var vdom = require("../../render/render")
+
+o.spec("render/hyperscript integration", function() {
+ var $window, root, render
+ o.beforeEach(function() {
+ $window = domMock()
+ root = $window.document.createElement("div")
+ render = vdom($window).render
+ })
+ o.spec("setting class", function() {
+ o("selector only", function() {
+ render(root, m(".foo"))
+
+ o(root.firstChild.className).equals("foo")
+ })
+ o("class only", function() {
+ render(root, m("div", {class: "foo"}))
+
+ o(root.firstChild.className).equals("foo")
+ })
+ o("className only", function() {
+ render(root, m("div", {className: "foo"}))
+
+ o(root.firstChild.className).equals("foo")
+ })
+ o("selector and class", function() {
+ render(root, m(".bar", {class: "foo"}))
+
+ o(root.firstChild.className.split(" ").sort()).deepEquals(["bar", "foo"])
+ })
+ o("selector and className", function() {
+ render(root, m(".bar", {className: "foo"}))
+
+ o(root.firstChild.className.split(" ").sort()).deepEquals(["bar", "foo"])
+ })
+ o("selector and a null class", function() {
+ render(root, m(".foo", {class: null}))
+
+ o(root.firstChild.className).equals("foo")
+ })
+ o("selector and a null className", function() {
+ render(root, m(".foo", {className: null}))
+
+ o(root.firstChild.className).equals("foo")
+ })
+ o("selector and an undefined class", function() {
+ render(root, m(".foo", {class: undefined}))
+
+ o(root.firstChild.className).equals("foo")
+ })
+ o("selector and an undefined className", function() {
+ render(root, m(".foo", {className: undefined}))
+
+ o(root.firstChild.className).equals("foo")
+ })
+ })
+ o.spec("updating class", function() {
+ o.spec("from selector only", function() {
+ o("to selector only", function() {
+ render(root, m(".foo1"))
+ render(root, m(".foo2"))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to class only", function() {
+ render(root, m(".foo1"))
+ render(root, m("div", {class: "foo2"}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to className only", function() {
+ render(root, m(".foo1"))
+ render(root, m("div", {className: "foo2"}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and class", function() {
+ render(root, m(".foo1"))
+ render(root, m(".bar2", {class: "foo2"}))
+
+ o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
+ })
+ o("to selector and className", function() {
+ render(root, m(".foo1"))
+ render(root, m(".bar2", {className: "foo2"}))
+
+ o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
+ })
+ o("to selector and a null class", function() {
+ render(root, m(".foo1"))
+ render(root, m(".foo2", {class: null}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and a null className", function() {
+ render(root, m(".foo1"))
+ render(root, m(".foo2", {className: null}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and an undefined class", function() {
+ render(root, m(".foo1"))
+ render(root, m(".foo2", {class: undefined}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and an undefined className", function() {
+ render(root, m(".foo1"))
+ render(root, m(".foo2", {className: undefined}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ })
+ o.spec("from class only", function() {
+ o("to selector only", function() {
+ render(root, m("div", {class: "foo2"}))
+ render(root, m(".foo2"))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to class only", function() {
+ render(root, m("div", {class: "foo2"}))
+ render(root, m("div", {class: "foo2"}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to className only", function() {
+ render(root, m("div", {class: "foo2"}))
+ render(root, m("div", {className: "foo2"}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and class", function() {
+ render(root, m("div", {class: "foo2"}))
+ render(root, m(".bar2", {class: "foo2"}))
+
+ o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
+ })
+ o("to selector and className", function() {
+ render(root, m(".bar2", {className: "foo2"}))
+
+ o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
+ })
+ o("to selector and a null class", function() {
+ render(root, m("div", {class: "foo2"}))
+ render(root, m(".foo2", {class: null}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and a null className", function() {
+ render(root, m("div", {class: "foo2"}))
+ render(root, m(".foo2", {className: null}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and an undefined class", function() {
+ render(root, m("div", {class: "foo2"}))
+ render(root, m(".foo2", {class: undefined}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and an undefined className", function() {
+ render(root, m("div", {class: "foo2"}))
+ render(root, m(".foo2", {className: undefined}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ })
+ o.spec("from ", function() {
+ o("to selector only", function() {
+ render(root, m(".foo2"))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to class only", function() {
+ render(root, m("div", {class: "foo2"}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to className only", function() {
+ render(root, m("div", {className: "foo2"}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and class", function() {
+ render(root, m(".bar2", {class: "foo2"}))
+
+ o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
+ })
+ o("to selector and className", function() {
+ render(root, m(".bar2", {className: "foo2"}))
+
+ o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
+ })
+ o("to selector and a null class", function() {
+ render(root, m(".foo2", {class: null}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and a null className", function() {
+ render(root, m(".foo2", {className: null}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and an undefined class", function() {
+ render(root, m(".foo2", {class: undefined}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and an undefined className", function() {
+ render(root, m(".foo2", {className: undefined}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ })
+ o.spec("from className only", function() {
+ o("to selector only", function() {
+ render(root, m("div", {className: "foo1"}))
+ render(root, m(".foo2"))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to class only", function() {
+ render(root, m("div", {className: "foo1"}))
+ render(root, m("div", {class: "foo2"}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to className only", function() {
+ render(root, m("div", {className: "foo1"}))
+ render(root, m("div", {className: "foo2"}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and class", function() {
+ render(root, m("div", {className: "foo1"}))
+ render(root, m(".bar2", {class: "foo2"}))
+
+ o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
+ })
+ o("to selector and className", function() {
+ render(root, m("div", {className: "foo1"}))
+ render(root, m(".bar2", {className: "foo2"}))
+
+ o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
+ })
+ o("to selector and a null class", function() {
+ render(root, m("div", {className: "foo1"}))
+ render(root, m(".foo2", {class: null}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and a null className", function() {
+ render(root, m("div", {className: "foo1"}))
+ render(root, m(".foo2", {className: null}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and an undefined class", function() {
+ render(root, m("div", {className: "foo1"}))
+ render(root, m(".foo2", {class: undefined}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and an undefined className", function() {
+ render(root, m("div", {className: "foo1"}))
+ render(root, m(".foo2", {className: undefined}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ })
+ o.spec("from selector and class", function() {
+ o("to selector only", function() {
+ render(root, m(".bar1", {class: "foo1"}))
+ render(root, m(".foo2"))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to class only", function() {
+ render(root, m(".bar1", {class: "foo1"}))
+ render(root, m("div", {class: "foo2"}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to className only", function() {
+ render(root, m(".bar1", {class: "foo1"}))
+ render(root, m("div", {className: "foo2"}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and class", function() {
+ render(root, m(".bar1", {class: "foo1"}))
+ render(root, m(".bar2", {class: "foo2"}))
+
+ o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
+ })
+ o("to selector and className", function() {
+ render(root, m(".bar1", {class: "foo1"}))
+ render(root, m(".bar2", {className: "foo2"}))
+
+ o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
+ })
+ o("to selector and a null class", function() {
+ render(root, m(".bar1", {class: "foo1"}))
+ render(root, m(".foo2", {class: null}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and a null className", function() {
+ render(root, m(".bar1", {class: "foo1"}))
+ render(root, m(".foo2", {className: null}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and an undefined class", function() {
+ render(root, m(".bar1", {class: "foo1"}))
+ render(root, m(".foo2", {class: undefined}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and an undefined className", function() {
+ render(root, m(".bar1", {class: "foo1"}))
+ render(root, m(".foo2", {className: undefined}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ })
+ o.spec("from selector and className", function() {
+ o("to selector only", function() {
+ render(root, m(".bar1", {className: "foo1"}))
+ render(root, m(".foo2"))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to class only", function() {
+ render(root, m(".bar1", {className: "foo1"}))
+ render(root, m("div", {class: "foo2"}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to className only", function() {
+ render(root, m(".bar1", {className: "foo1"}))
+ render(root, m("div", {className: "foo2"}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and class", function() {
+ render(root, m(".bar1", {className: "foo1"}))
+ render(root, m(".bar2", {class: "foo2"}))
+
+ o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
+ })
+ o("to selector and className", function() {
+ render(root, m(".bar1", {className: "foo1"}))
+ render(root, m(".bar2", {className: "foo2"}))
+
+ o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
+ })
+ o("to selector and a null class", function() {
+ render(root, m(".bar1", {className: "foo1"}))
+ render(root, m(".foo2", {class: null}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and a null className", function() {
+ render(root, m(".bar1", {className: "foo1"}))
+ render(root, m(".foo2", {className: null}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and an undefined class", function() {
+ render(root, m(".bar1", {className: "foo1"}))
+ render(root, m(".foo2", {class: undefined}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and an undefined className", function() {
+ render(root, m(".bar1", {className: "foo1"}))
+ render(root, m(".foo2", {className: undefined}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ })
+ o.spec("from and a null class", function() {
+ o("to selector only", function() {
+ render(root, m(".foo1", {class: null}))
+ render(root, m(".foo2"))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to class only", function() {
+ render(root, m(".foo1", {class: null}))
+ render(root, m("div", {class: "foo2"}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to className only", function() {
+ render(root, m(".foo1", {class: null}))
+ render(root, m("div", {className: "foo2"}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and class", function() {
+ render(root, m(".foo1", {class: null}))
+ render(root, m(".bar2", {class: "foo2"}))
+
+ o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
+ })
+ o("to selector and className", function() {
+ render(root, m(".foo1", {class: null}))
+ render(root, m(".bar2", {className: "foo2"}))
+
+ o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
+ })
+ o("to selector and a null class", function() {
+ render(root, m(".foo1", {class: null}))
+ render(root, m(".foo2", {class: null}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and a null className", function() {
+ render(root, m(".foo1", {class: null}))
+ render(root, m(".foo2", {className: null}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and an undefined class", function() {
+ render(root, m(".foo1", {class: null}))
+ render(root, m(".foo2", {class: undefined}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and an undefined className", function() {
+ render(root, m(".foo1", {class: null}))
+ render(root, m(".foo2", {className: undefined}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ })
+ o.spec("from selector and a null className", function() {
+ o("to selector only", function() {
+ render(root, m(".foo1", {className: null}))
+ render(root, m(".foo2"))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to class only", function() {
+ render(root, m(".foo1", {className: null}))
+ render(root, m("div", {class: "foo2"}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to className only", function() {
+ render(root, m(".foo1", {className: null}))
+ render(root, m("div", {className: "foo2"}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and class", function() {
+ render(root, m(".foo1", {className: null}))
+ render(root, m(".bar2", {class: "foo2"}))
+
+ o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
+ })
+ o("to selector and className", function() {
+ render(root, m(".foo1", {className: null}))
+ render(root, m(".bar2", {className: "foo2"}))
+
+ o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
+ })
+ o("to selector and a null class", function() {
+ render(root, m(".foo1", {className: null}))
+ render(root, m(".foo2", {class: null}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and a null className", function() {
+ render(root, m(".foo1", {className: null}))
+ render(root, m(".foo2", {className: null}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and an undefined class", function() {
+ render(root, m(".foo1", {className: null}))
+ render(root, m(".foo2", {class: undefined}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and an undefined className", function() {
+ render(root, m(".foo1", {className: null}))
+ render(root, m(".foo2", {className: undefined}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ })
+ o.spec("from selector and an undefined class", function() {
+ o("to selector only", function() {
+ render(root, m(".foo1", {class: undefined}))
+ render(root, m(".foo2"))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to class only", function() {
+ render(root, m(".foo1", {class: undefined}))
+ render(root, m("div", {class: "foo2"}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to className only", function() {
+ render(root, m(".foo1", {class: undefined}))
+ render(root, m("div", {className: "foo2"}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and class", function() {
+ render(root, m(".foo1", {class: undefined}))
+ render(root, m(".bar2", {class: "foo2"}))
+
+ o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
+ })
+ o("to selector and className", function() {
+ render(root, m(".foo1", {class: undefined}))
+ render(root, m(".bar2", {className: "foo2"}))
+
+ o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
+ })
+ o("to selector and a null class", function() {
+ render(root, m(".foo1", {class: undefined}))
+ render(root, m(".foo2", {class: null}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and a null className", function() {
+ render(root, m(".foo1", {class: undefined}))
+ render(root, m(".foo2", {className: null}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and an undefined class", function() {
+ render(root, m(".foo1", {class: undefined}))
+ render(root, m(".foo2", {class: undefined}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and an undefined className", function() {
+ render(root, m(".foo1", {class: undefined}))
+ render(root, m(".foo2", {className: undefined}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ })
+ o.spec("from selector and an undefined className", function() {
+ o("to selector only", function() {
+ render(root, m(".foo1", {className: undefined}))
+ render(root, m(".foo2"))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to class only", function() {
+ render(root, m(".foo1", {className: undefined}))
+ render(root, m("div", {class: "foo2"}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to className only", function() {
+ render(root, m(".foo1", {className: undefined}))
+ render(root, m("div", {className: "foo2"}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and class", function() {
+ render(root, m(".foo1", {className: undefined}))
+ render(root, m(".bar2", {class: "foo2"}))
+
+ o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
+ })
+ o("to selector and className", function() {
+ render(root, m(".foo1", {className: undefined}))
+ render(root, m(".bar2", {className: "foo2"}))
+
+ o(root.firstChild.className.split(" ").sort()).deepEquals(["bar2", "foo2"])
+ })
+ o("to selector and a null class", function() {
+ render(root, m(".foo1", {className: undefined}))
+ render(root, m(".foo2", {class: null}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and a null className", function() {
+ render(root, m(".foo1", {className: undefined}))
+ render(root, m(".foo2", {className: null}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and an undefined class", function() {
+ render(root, m(".foo1", {className: undefined}))
+ render(root, m(".foo2", {class: undefined}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ o("to selector and an undefined className", function() {
+ render(root, m(".foo1", {className: undefined}))
+ render(root, m(".foo2", {className: undefined}))
+
+ o(root.firstChild.className).equals("foo2")
+ })
+ })
+ })
+})
diff --git a/render/tests/test-updateElement.js b/render/tests/test-updateElement.js
index 313fe1a9..8a190a4c 100644
--- a/render/tests/test-updateElement.js
+++ b/render/tests/test-updateElement.js
@@ -192,6 +192,85 @@ o.spec("updateElement", function() {
o(updated.dom.style.backgroundColor).equals("")
o(updated.dom.style.color).equals("gold")
})
+ o("does not re-render element styles for equivalent style objects", function() {
+ var style = {color: "gold"}
+ var vnode = {tag: "a", attrs: {style: style}}
+
+ render(root, [vnode])
+
+ root.firstChild.style.color = "red"
+ style = {color: "gold"}
+ var updated = {tag: "a", attrs: {style: style}}
+ render(root, [updated])
+
+ o(updated.dom.style.color).equals("red")
+ })
+ o("setting style to `null` removes all styles", function() {
+ var vnode = {"tag": "p", attrs: {style: "background-color: red"}}
+ var updated = {"tag": "p", attrs: {style: null}}
+
+ render(root, [vnode])
+
+ o("style" in vnode.dom.attributes).equals(true)
+ o(vnode.dom.attributes.style.value).equals("background-color: red;")
+
+ render(root, [updated])
+
+ //browsers disagree here
+ try {
+
+ o(updated.dom.attributes.style.value).equals("")
+
+ } catch (e) {
+
+ o("style" in updated.dom.attributes).equals(false)
+
+ }
+ })
+ o("setting style to `undefined` removes all styles", function() {
+ var vnode = {"tag": "p", attrs: {style: "background-color: red"}}
+ var updated = {"tag": "p", attrs: {style: undefined}}
+
+ render(root, [vnode])
+
+ o("style" in vnode.dom.attributes).equals(true)
+ o(vnode.dom.attributes.style.value).equals("background-color: red;")
+
+ render(root, [updated])
+
+ //browsers disagree here
+ try {
+
+ o(updated.dom.attributes.style.value).equals("")
+
+ } catch (e) {
+
+ o("style" in updated.dom.attributes).equals(false)
+
+ }
+ })
+ o("not setting style removes all styles", function() {
+ var vnode = {"tag": "p", attrs: {style: "background-color: red"}}
+ var updated = {"tag": "p", attrs: {}}
+
+ render(root, [vnode])
+
+ o("style" in vnode.dom.attributes).equals(true)
+ o(vnode.dom.attributes.style.value).equals("background-color: red;")
+
+ render(root, [updated])
+
+ //browsers disagree here
+ try {
+
+ o(updated.dom.attributes.style.value).equals("")
+
+ } catch (e) {
+
+ o("style" in updated.dom.attributes).equals(false)
+
+ }
+ })
o("replaces el", function() {
var vnode = {tag: "a"}
var updated = {tag: "b"}
@@ -224,7 +303,7 @@ o.spec("updateElement", function() {
o(updated.dom.firstChild.namespaceURI).equals("http://www.w3.org/2000/svg")
})
- o("restores correctly when recycling", function() {
+ o("doesn't restore since we're not recycling", function() {
var vnode = {tag: "div", key: 1}
var updated = {tag: "div", key: 2}
@@ -237,9 +316,9 @@ o.spec("updateElement", function() {
var c = vnode.dom
o(root.childNodes.length).equals(1)
- o(a).equals(c)
+ o(a).notEquals(c) // this used to be a recycling pool test
})
- o("restores correctly when recycling via map", function() {
+ o("doesn't restore since we're not recycling (via map)", function() {
var a = {tag: "div", key: 1}
var b = {tag: "div", key: 2}
var c = {tag: "div", key: 3}
@@ -256,6 +335,6 @@ o.spec("updateElement", function() {
var y = root.childNodes[1]
o(root.childNodes.length).equals(3)
- o(x).equals(y)
+ o(x).notEquals(y) // this used to be a recycling pool test
})
})
diff --git a/render/tests/test-updateNodes.js b/render/tests/test-updateNodes.js
index fa5231ea..3fffaaf3 100644
--- a/render/tests/test-updateNodes.js
+++ b/render/tests/test-updateNodes.js
@@ -134,17 +134,17 @@ o.spec("updateNodes", function() {
o("reverses els w/ odd count", function() {
var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}, {tag: "i", key: 3}]
var updated = [{tag: "i", key: 3}, {tag: "b", key: 2}, {tag: "a", key: 1}]
-
+ var expectedTags = updated.map(function(vn) {return vn.tag})
render(root, vnodes)
render(root, updated)
+ var tagNames = [].map.call(root.childNodes, function(n) {return n.nodeName.toLowerCase()})
+
o(root.childNodes.length).equals(3)
o(updated[0].dom.nodeName).equals("I")
- o(updated[0].dom).equals(root.childNodes[0])
o(updated[1].dom.nodeName).equals("B")
- o(updated[1].dom).equals(root.childNodes[1])
o(updated[2].dom.nodeName).equals("A")
- o(updated[2].dom).equals(root.childNodes[2])
+ o(tagNames).deepEquals(expectedTags)
})
o("creates el at start", function() {
var vnodes = [{tag: "a", key: 1}]
@@ -264,6 +264,21 @@ o.spec("updateNodes", function() {
o(updated[2].dom.nodeName).equals("S")
o(updated[2].dom).equals(root.childNodes[2])
})
+ o("creates, deletes, reverses els at same time with '__proto__' key", function() {
+ var vnodes = [{tag: "a", key: "__proto__"}, {tag: "i", key: 3}, {tag: "b", key: 2}]
+ var updated = [{tag: "b", key: 2}, {tag: "a", key: "__proto__"}, {tag: "s", key: 4}]
+
+ render(root, vnodes)
+ render(root, updated)
+
+ o(root.childNodes.length).equals(3)
+ o(updated[0].dom.nodeName).equals("B")
+ o(updated[0].dom).equals(root.childNodes[0])
+ o(updated[1].dom.nodeName).equals("A")
+ o(updated[1].dom).equals(root.childNodes[1])
+ o(updated[2].dom.nodeName).equals("S")
+ o(updated[2].dom).equals(root.childNodes[2])
+ })
o("adds to empty array followed by el", function() {
var vnodes = [{tag: "[", key: 1, children: []}, {tag: "b", key: 2}]
var updated = [{tag: "[", key: 1, children: [{tag: "a"}]}, {tag: "b", key: 2}]
@@ -614,6 +629,38 @@ o.spec("updateNodes", function() {
o(updated[0].dom.nodeName).equals("I")
o(updated[0].dom).equals(root.childNodes[0])
})
+ o("cached keyed nodes move when the list is reversed", function(){
+ var a = {tag: "a", key: "a"}
+ var b = {tag: "b", key: "b"}
+ var c = {tag: "c", key: "c"}
+ var d = {tag: "d", key: "d"}
+
+ render(root, [a, b, c, d])
+ render(root, [d, c, b, a])
+
+ o(root.childNodes.length).equals(4)
+ o(root.childNodes[0].nodeName).equals("D")
+ o(root.childNodes[1].nodeName).equals("C")
+ o(root.childNodes[2].nodeName).equals("B")
+ o(root.childNodes[3].nodeName).equals("A")
+ })
+ o("cached keyed nodes move when diffed via the map", function() {
+ var onupdate = o.spy()
+ var a = {tag: "a", key: "a", attrs: {onupdate: onupdate}}
+ var b = {tag: "b", key: "b", attrs: {onupdate: onupdate}}
+ var c = {tag: "c", key: "c", attrs: {onupdate: onupdate}}
+ var d = {tag: "d", key: "d", attrs: {onupdate: onupdate}}
+
+ render(root, [a, b, c, d])
+ render(root, [b, d, a, c])
+
+ o(root.childNodes.length).equals(4)
+ o(root.childNodes[0].nodeName).equals("B")
+ o(root.childNodes[1].nodeName).equals("D")
+ o(root.childNodes[2].nodeName).equals("A")
+ o(root.childNodes[3].nodeName).equals("C")
+ o(onupdate.callCount).equals(0)
+ })
o("removes then create different bigger", function() {
var vnodes = [{tag: "a", key: 1}, {tag: "b", key: 2}]
var temp = []
@@ -776,7 +823,7 @@ o.spec("updateNodes", function() {
o(root.childNodes[0].childNodes[1].childNodes.length).equals(1)
o(root.childNodes[1].childNodes.length).equals(0)
})
- o("recycles", function() {
+ o("doesn't recycle", function() {
var vnodes = [{tag: "div", key: 1}]
var temp = []
var updated = [{tag: "div", key: 1}]
@@ -785,10 +832,10 @@ o.spec("updateNodes", function() {
render(root, temp)
render(root, updated)
- o(vnodes[0].dom).equals(updated[0].dom)
+ o(vnodes[0].dom).notEquals(updated[0].dom) // this used to be a recycling pool test
o(updated[0].dom.nodeName).equals("DIV")
})
- o("recycles when not keyed", function() {
+ o("doesn't recycle when not keyed", function() {
var vnodes = [{tag: "div"}]
var temp = []
var updated = [{tag: "div"}]
@@ -798,19 +845,22 @@ o.spec("updateNodes", function() {
render(root, updated)
o(root.childNodes.length).equals(1)
- o(vnodes[0].dom).equals(updated[0].dom)
+ o(vnodes[0].dom).notEquals(updated[0].dom) // this used to be a recycling pool test
o(updated[0].dom.nodeName).equals("DIV")
})
- o("recycles deep", function() {
+ o("doesn't recycle deep", function() {
var vnodes = [{tag: "div", children: [{tag: "a", key: 1}]}]
var temp = [{tag: "div"}]
var updated = [{tag: "div", children: [{tag: "a", key: 1}]}]
render(root, vnodes)
+
+ var oldChild = vnodes[0].dom.firstChild
+
render(root, temp)
render(root, updated)
- o(vnodes[0].dom.firstChild).equals(updated[0].dom.firstChild)
+ o(oldChild).notEquals(updated[0].dom.firstChild) // this used to be a recycling pool test
o(updated[0].dom.firstChild.nodeName).equals("A")
})
o("mixed unkeyed tags are not broken by recycle", function() {
@@ -839,6 +889,19 @@ o.spec("updateNodes", function() {
o(root.childNodes[0].nodeName).equals("A")
o(root.childNodes[1].nodeName).equals("B")
})
+ o("onremove doesn't fire from nodes in the pool (#1990)", function () {
+ var onremove = o.spy()
+ render(root, [
+ {tag: "div", children: [{tag: "div", attrs: {onremove: onremove}}]},
+ {tag: "div", children: [{tag: "div", attrs: {onremove: onremove}}]}
+ ])
+ render(root, [
+ {tag: "div", children: [{tag: "div", attrs: {onremove: onremove}}]}
+ ])
+ render(root,[])
+
+ o(onremove.callCount).equals(2)
+ })
o("cached, non-keyed nodes skip diff", function () {
var onupdate = o.spy();
var cached = {tag:"a", attrs:{onupdate: onupdate}}
@@ -857,6 +920,72 @@ o.spec("updateNodes", function() {
o(onupdate.callCount).equals(0)
})
+ o("keyed cached elements are re-initialized when brought back from the pool (#2003)", function () {
+ var onupdate = o.spy()
+ var oncreate = o.spy()
+ var cached = {
+ tag: "B", key: 1, children: [
+ {tag: "A", attrs: {oncreate: oncreate, onupdate: onupdate}, text: "A"}
+ ]
+ }
+ render(root, [{tag: "div", children: [cached]}])
+ render(root, [])
+ render(root, [{tag: "div", children: [cached]}])
+
+ o(oncreate.callCount).equals(2)
+ o(onupdate.callCount).equals(0)
+ })
+
+ o("unkeyed cached elements are re-initialized when brought back from the pool (#2003)", function () {
+ var onupdate = o.spy()
+ var oncreate = o.spy()
+ var cached = {
+ tag: "B", children: [
+ {tag: "A", attrs: {oncreate: oncreate, onupdate: onupdate}, text: "A"}
+ ]
+ }
+ render(root, [{tag: "div", children: [cached]}])
+ render(root, [])
+ render(root, [{tag: "div", children: [cached]}])
+
+ o(oncreate.callCount).equals(2)
+ o(onupdate.callCount).equals(0)
+ })
+
+ o("keyed cached elements are re-initialized when brought back from nested pools (#2003)", function () {
+ var onupdate = o.spy()
+ var oncreate = o.spy()
+ var cached = {
+ tag: "B", key: 1, children: [
+ {tag: "A", attrs: {oncreate: oncreate, onupdate: onupdate}, text: "A"}
+ ]
+ }
+ render(root, [{tag: "div", children: [cached]}])
+ render(root, [{tag: "div", children: []}])
+ render(root, [])
+ render(root, [{tag: "div", children: [cached]}])
+
+ o(oncreate.callCount).equals(2)
+ o(onupdate.callCount).equals(0)
+ })
+
+ o("unkeyed cached elements are re-initialized when brought back from nested pools (#2003)", function () {
+ var onupdate = o.spy()
+ var oncreate = o.spy()
+ var cached = {
+ tag: "B", children: [
+ {tag: "A", attrs: {oncreate: oncreate, onupdate: onupdate}, text: "A"}
+ ]
+ }
+ render(root, [{tag: "div", children: [cached]}])
+ render(root, [{tag: "div", children: []}])
+ render(root, [])
+ render(root, [{tag: "div", children: [cached]}])
+
+ o(oncreate.callCount).equals(2)
+ o(onupdate.callCount).equals(0)
+ })
+
o("null stays in place", function() {
var create = o.spy()
var update = o.spy()
@@ -904,6 +1033,231 @@ o.spec("updateNodes", function() {
o(vnode.dom).notEquals(updated.dom)
})
+ o("don't add back elements from fragments that are restored from the pool #1991", function() {
+ render(root, [
+ {tag: "[", children: []},
+ {tag: "[", children: []}
+ ])
+ render(root, [
+ {tag: "[", children: []},
+ {tag: "[", children: [{tag: "div"}]}
+ ])
+ render(root, [
+ {tag: "[", children: [null]}
+ ])
+ render(root, [
+ {tag: "[", children: []},
+ {tag: "[", children: []}
+ ])
+
+ o(root.childNodes.length).equals(0)
+ })
+ o("don't add back elements from fragments that are being removed #1991", function() {
+ render(root, [
+ {tag: "[", children: []},
+ {tag: "p"},
+ ])
+ render(root, [
+ {tag: "[", children: [{tag: "div", text: 5}]}
+ ])
+ render(root, [
+ {tag: "[", children: []},
+ {tag: "[", children: []}
+ ])
+
+ o(root.childNodes.length).equals(0)
+ })
+ o("handles null values in unkeyed lists of different length (#2003)", function() {
+ var oncreate = o.spy();
+ var onremove = o.spy();
+ var onupdate = o.spy();
+ function attrs() {
+ return {oncreate: oncreate, onremove: onremove, onupdate: onupdate}
+ }
+
+ render(root, [{tag: "div", attrs: attrs()}, null]);
+ render(root, [null, {tag: "div", attrs: attrs()}, null]);
+
+ o(oncreate.callCount).equals(2)
+ o(onremove.callCount).equals(1)
+ o(onupdate.callCount).equals(0)
+ })
+ o("supports changing the element of a keyed element in a list when traversed bottom-up", function() {
+ try {
+ render(root, [{tag: "a", key: 2}])
+ render(root, [{tag: "b", key: 1}, {tag: "b", key: 2}])
+
+ o(root.childNodes.length).equals(2)
+ o(root.childNodes[0].nodeName).equals("B")
+ o(root.childNodes[1].nodeName).equals("B")
+ } catch (e) {
+ o(e).equals(null)
+ }
+ })
+ o("supports changing the element of a keyed element in a list when looking up nodes using the map", function() {
+ try {
+ render(root, [{tag: "x", key: 1}, {tag: "y", key: 2}, {tag: "z", key: 3}])
+ render(root, [{tag: "b", key: 2}, {tag: "c", key: 1}, {tag: "d", key: 4}, {tag: "e", key: 3}])
+
+ o(root.childNodes.length).equals(4)
+ o(root.childNodes[0].nodeName).equals("B")
+ o(root.childNodes[1].nodeName).equals("C")
+ o(root.childNodes[2].nodeName).equals("D")
+ o(root.childNodes[3].nodeName).equals("E")
+ } catch (e) {
+ o(e).equals(null)
+ }
+ })
+ o("don't fetch the nextSibling from the pool", function() {
+ render(root, [{tag: "[", children: [{tag: "div", key: 1}, {tag: "div", key: 2}]}, {tag: "p"}])
+ render(root, [{tag: "[", children: []}, {tag: "p"}])
+ render(root, [{tag: "[", children: [{tag: "div", key: 2}, {tag: "div", key: 1}]}, {tag: "p"}])
+
+ o([].map.call(root.childNodes, function(el) {return el.nodeName})).deepEquals(["DIV", "DIV", "P"])
+ })
+ o("minimizes DOM operations when scrambling a keyed lists", function() {
+ var vnodes = [{tag: "a", key: "a"}, {tag: "b", key: "b"}, {tag: "c", key: "c"}, {tag: "d", key: "d"}]
+ var updated = [{tag: "b", key: "b"}, {tag: "a", key: "a"}, {tag: "d", key: "d"}, {tag: "c", key: "c"}]
+ var expectedTagNames = updated.map(function(vn) {return vn.tag})
+
+ render(root, vnodes)
+
+ root.appendChild = o.spy(root.appendChild)
+ root.insertBefore = o.spy(root.insertBefore)
+
+ render(root, updated)
+
+ var tagNames = [].map.call(root.childNodes, function(n) {return n.nodeName.toLowerCase()})
+
+ o(root.appendChild.callCount + root.insertBefore.callCount).equals(2)
+ o(tagNames).deepEquals(expectedTagNames)
+ })
+ o("minimizes DOM operations when reversing a keyed lists with an odd number of items", function() {
+ var vnodes = [{tag: "a", key: "a"}, {tag: "b", key: "b"}, {tag: "c", key: "c"}, {tag: "d", key: "d"}]
+ var updated = [{tag: "d", key: "d"}, {tag: "c", key: "c"}, {tag: "b", key: "b"}, {tag: "a", key: "a"}]
+ var expectedTagNames = updated.map(function(vn) {return vn.tag})
+
+ render(root, vnodes)
+
+ root.appendChild = o.spy(root.appendChild)
+ root.insertBefore = o.spy(root.insertBefore)
+
+ render(root, updated)
+
+ var tagNames = [].map.call(root.childNodes, function(n) {return n.nodeName.toLowerCase()})
+
+ o(root.appendChild.callCount + root.insertBefore.callCount).equals(3)
+ o(tagNames).deepEquals(expectedTagNames)
+ })
+ o("minimizes DOM operations when reversing a keyed lists with an even number of items", function() {
+ var vnodes = [{tag: "a", key: "a"}, {tag: "b", key: "b"}, {tag: "c", key: "c"}]
+ var updated = [{tag: "c", key: "c"}, {tag: "b", key: "b"}, {tag: "a", key: "a"}]
+ var expectedTagNames = updated.map(function(vn) {return vn.tag})
+
+ render(root, vnodes)
+
+ root.appendChild = o.spy(root.appendChild)
+ root.insertBefore = o.spy(root.insertBefore)
+
+ render(root, updated)
+
+ var tagNames = [].map.call(root.childNodes, function(n) {return n.nodeName.toLowerCase()})
+
+ o(root.appendChild.callCount + root.insertBefore.callCount).equals(2)
+ o(tagNames).deepEquals(expectedTagNames)
+ })
+ o("minimizes DOM operations when scrambling a keyed lists with prefixes and suffixes", function() {
+ var vnodes = [{tag: "i", key: "i"}, {tag: "a", key: "a"}, {tag: "b", key: "b"}, {tag: "c", key: "c"}, {tag: "d", key: "d"}, {tag: "j", key: "j"}]
+ var updated = [{tag: "i", key: "i"}, {tag: "b", key: "b"}, {tag: "a", key: "a"}, {tag: "d", key: "d"}, {tag: "c", key: "c"}, {tag: "j", key: "j"}]
+ var expectedTagNames = updated.map(function(vn) {return vn.tag})
+
+ render(root, vnodes)
+
+ root.appendChild = o.spy(root.appendChild)
+ root.insertBefore = o.spy(root.insertBefore)
+
+ render(root, updated)
+
+ var tagNames = [].map.call(root.childNodes, function(n) {return n.nodeName.toLowerCase()})
+
+ o(root.appendChild.callCount + root.insertBefore.callCount).equals(2)
+ o(tagNames).deepEquals(expectedTagNames)
+ })
+ o("minimizes DOM operations when reversing a keyed lists with an odd number of items with prefixes and suffixes", function() {
+ var vnodes = [{tag: "i", key: "i"}, {tag: "a", key: "a"}, {tag: "b", key: "b"}, {tag: "c", key: "c"}, {tag: "d", key: "d"}, {tag: "j", key: "j"}]
+ var updated = [{tag: "i", key: "i"}, {tag: "d", key: "d"}, {tag: "c", key: "c"}, {tag: "b", key: "b"}, {tag: "a", key: "a"}, {tag: "j", key: "j"}]
+ var expectedTagNames = updated.map(function(vn) {return vn.tag})
+
+ render(root, vnodes)
+
+ root.appendChild = o.spy(root.appendChild)
+ root.insertBefore = o.spy(root.insertBefore)
+
+ render(root, updated)
+
+ var tagNames = [].map.call(root.childNodes, function(n) {return n.nodeName.toLowerCase()})
+
+ o(root.appendChild.callCount + root.insertBefore.callCount).equals(3)
+ o(tagNames).deepEquals(expectedTagNames)
+ })
+ o("minimizes DOM operations when reversing a keyed lists with an even number of items with prefixes and suffixes", function() {
+ var vnodes = [{tag: "i", key: "i"}, {tag: "a", key: "a"}, {tag: "b", key: "b"}, {tag: "c", key: "c"}, {tag: "j", key: "j"}]
+ var updated = [{tag: "i", key: "i"}, {tag: "c", key: "c"}, {tag: "b", key: "b"}, {tag: "a", key: "a"}, {tag: "j", key: "j"}]
+ var expectedTagNames = updated.map(function(vn) {return vn.tag})
+
+ render(root, vnodes)
+
+ root.appendChild = o.spy(root.appendChild)
+ root.insertBefore = o.spy(root.insertBefore)
+
+ render(root, updated)
+
+ var tagNames = [].map.call(root.childNodes, function(n) {return n.nodeName.toLowerCase()})
+
+ o(root.appendChild.callCount + root.insertBefore.callCount).equals(2)
+ o(tagNames).deepEquals(expectedTagNames)
+ })
+ o("scrambling sample 1", function() {
+ function vnodify(str) {
+ return str.split(",").map(function(k) {return {tag: k, key: k}})
+ }
+ var vnodes = vnodify("k0,k1,k2,k3,k4,k5,k6,k7,k8,k9")
+ var updated = vnodify("k4,k1,k2,k9,k0,k3,k6,k5,k8,k7")
+ var expectedTagNames = updated.map(function(vn) {return vn.tag})
+
+ render(root, vnodes)
+
+ root.appendChild = o.spy(root.appendChild)
+ root.insertBefore = o.spy(root.insertBefore)
+
+ render(root, updated)
+
+ var tagNames = [].map.call(root.childNodes, function(n) {return n.nodeName.toLowerCase()})
+
+ o(root.appendChild.callCount + root.insertBefore.callCount).equals(5)
+ o(tagNames).deepEquals(expectedTagNames)
+ })
+ o("scrambling sample 2", function() {
+ function vnodify(str) {
+ return str.split(",").map(function(k) {return {tag: k, key: k}})
+ }
+ var vnodes = vnodify("k0,k1,k2,k3,k4,k5,k6,k7,k8,k9")
+ var updated = vnodify("b,d,k1,k0,k2,k3,k4,a,c,k5,k6,k7,k8,k9")
+ var expectedTagNames = updated.map(function(vn) {return vn.tag})
+
+ render(root, vnodes)
+
+ root.appendChild = o.spy(root.appendChild)
+ root.insertBefore = o.spy(root.insertBefore)
+
+ render(root, updated)
+
+ var tagNames = [].map.call(root.childNodes, function(n) {return n.nodeName.toLowerCase()})
+
+ o(root.appendChild.callCount + root.insertBefore.callCount).equals(5)
+ o(tagNames).deepEquals(expectedTagNames)
+ })
+
components.forEach(function(cmp){
o.spec(cmp.kind, function(){
var createComponent = cmp.create
@@ -940,6 +1294,19 @@ o.spec("updateNodes", function() {
o(root.childNodes[0].nodeName).equals("A")
o(root.childNodes[1].nodeName).equals("S")
})
+ o("removing a component that returns a fragment doesn't throw (regression test for incidental bug introduced while debugging some Flems)", function() {
+ var component = createComponent({
+ view: function() {return {tag: "[", children:[{tag: "a"}, {tag: "b"}]}}
+ })
+ try {
+ render(root, [{tag: component}])
+ render(root, [])
+
+ o(root.childNodes.length).equals(0)
+ } catch (e) {
+ o(e).equals(null)
+ }
+ })
})
})
})
diff --git a/render/tests/test-updateNodesFuzzer.js b/render/tests/test-updateNodesFuzzer.js
new file mode 100644
index 00000000..8580c139
--- /dev/null
+++ b/render/tests/test-updateNodesFuzzer.js
@@ -0,0 +1,157 @@
+"use strict"
+
+var o = require("../../ospec/ospec")
+var domMock = require("../../test-utils/domMock")
+var vdom = require("../../render/render")
+
+// pilfered and adapted from https://github.com/domvm/domvm/blob/7aaec609e4c625b9acf9a22d035d6252a5ca654f/test/src/flat-list-keyed-fuzz.js
+o.spec("updateNodes keyed list Fuzzer", function() {
+ var i = 0, $window, root, render
+ o.beforeEach(function() {
+ $window = domMock()
+ root = $window.document.createElement("div")
+ render = vdom($window).render
+ })
+
+
+ void [
+ {delMax: 0, movMax: 50, insMax: 9},
+ {delMax: 3, movMax: 5, insMax: 5},
+ {delMax: 7, movMax: 15, insMax: 0},
+ {delMax: 5, movMax: 100, insMax: 3},
+ {delMax: 5, movMax: 0, insMax: 3},
+ ].forEach(function(c) {
+ var tests = 250
+
+ while (tests--) {
+ var test = fuzzTest(c.delMax, c.movMax, c.insMax)
+ o(i++ + ": " + test.list.join() + " -> " + test.updated.join(), function() {
+ render(root, test.list.map(function(x){return {tag: x, key: x}}))
+ addSpies(root)
+ render(root, test.updated.map(function(x){return {tag: x, key: x}}))
+
+ if (root.appendChild.callCount + root.insertBefore.callCount !== test.expected.creations + test.expected.moves) console.log(test, {aC: root.appendChild.callCount, iB: root.insertBefore.callCount}, [].map.call(root.childNodes, function(n){return n.nodeName.toLowerCase()}))
+
+ o(root.appendChild.callCount + root.insertBefore.callCount).equals(test.expected.creations + test.expected.moves)("moves")
+ o(root.removeChild.callCount).equals(test.expected.deletions)("deletions")
+ o([].map.call(root.childNodes, function(n){return n.nodeName.toLowerCase()})).deepEquals(test.updated)
+ })
+ }
+ })
+})
+
+// https://en.wikipedia.org/wiki/Longest_increasing_subsequence
+// impl borrowed from https://github.com/ivijs/ivi
+function longestIncreasingSubsequence(a) {
+ var p = a.slice()
+ var result = []
+ result.push(0)
+ var u
+ var v
+
+ for (var i = 0, il = a.length; i < il; ++i) {
+ var j = result[result.length - 1]
+ if (a[j] < a[i]) {
+ p[i] = j
+ result.push(i)
+ continue
+ }
+
+ u = 0
+ v = result.length - 1
+
+ while (u < v) {
+ var c = ((u + v) / 2) | 0 // eslint-disable-line no-bitwise
+ if (a[result[c]] < a[i]) {
+ u = c + 1
+ } else {
+ v = c
+ }
+ }
+
+ if (a[i] < a[result[u]]) {
+ if (u > 0) {
+ p[i] = result[u - 1]
+ }
+ result[u] = i
+ }
+ }
+
+ u = result.length
+ v = result[u - 1]
+
+ while (u-- > 0) {
+ result[u] = v
+ v = p[v]
+ }
+
+ return result
+}
+
+function rand(min, max) {
+ return Math.floor(Math.random() * (max - min + 1)) + min
+}
+
+function ins(arr, qty) {
+ var p = ["a","b","c","d","e","f","g","h","i"]
+
+ while (qty-- > 0)
+ arr.splice(rand(0, arr.length - 1), 0, p.shift())
+}
+
+function del(arr, qty) {
+ while (qty-- > 0)
+ arr.splice(rand(0, arr.length - 1), 1)
+}
+
+function mov(arr, qty) {
+ while (qty-- > 0) {
+ var from = rand(0, arr.length - 1)
+ var to = rand(0, arr.length - 1)
+
+ arr.splice(to, 0, arr.splice(from, 1)[0])
+ }
+}
+
+function fuzzTest(delMax, movMax, insMax) {
+ var list = ["k0","k1","k2","k3","k4","k5","k6","k7","k8","k9"]
+ var copy = list.slice()
+
+ var delCount = rand(0, delMax),
+ movCount = rand(0, movMax),
+ insCount = rand(0, insMax)
+
+ del(copy, delCount)
+ mov(copy, movCount)
+
+ var expected = {
+ creations: insCount,
+ deletions: delCount,
+ moves: 0
+ }
+
+ if (movCount > 0) {
+ var newPos = copy.map(function(v) {
+ return list.indexOf(v)
+ }).filter(function(i) {
+ return i != -1
+ })
+ var lis = longestIncreasingSubsequence(newPos)
+ expected.moves = copy.length - lis.length
+ }
+
+ ins(copy, insCount)
+
+ return {
+ expected: expected,
+ list: list,
+ updated: copy
+ }
+}
+
+function addSpies(node) {
+ node.appendChild = o.spy(node.appendChild)
+ node.insertBefore = o.spy(node.insertBefore)
+ node.removeChild = o.spy(node.removeChild)
+}
+
diff --git a/render/vnode.js b/render/vnode.js
index ce137703..98acdd41 100644
--- a/render/vnode.js
+++ b/render/vnode.js
@@ -1,16 +1,17 @@
"use strict"
function Vnode(tag, key, attrs, children, text, dom) {
- return {tag: tag, key: key, attrs: attrs, children: children, text: text, dom: dom, domSize: undefined, state: undefined, _state: undefined, events: undefined, instance: undefined, skip: false}
+ return {tag: tag, key: key, attrs: attrs, children: children, text: text, dom: dom, domSize: undefined, state: undefined, events: undefined, instance: undefined}
}
Vnode.normalize = function(node) {
if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined)
if (node != null && typeof node !== "object") return Vnode("#", undefined, undefined, node === false ? "" : node, undefined, undefined)
return node
}
-Vnode.normalizeChildren = function normalizeChildren(children) {
- for (var i = 0; i < children.length; i++) {
- children[i] = Vnode.normalize(children[i])
+Vnode.normalizeChildren = function normalizeChildren(input) {
+ var children = []
+ for (var i = 0; i < input.length; i++) {
+ children[i] = Vnode.normalize(input[i])
}
return children
}
diff --git a/request/request.js b/request/request.js
index 84f5e4ce..d40f8d06 100644
--- a/request/request.js
+++ b/request/request.js
@@ -75,6 +75,10 @@ module.exports = function($window, Promise) {
}
if (args.withCredentials) xhr.withCredentials = args.withCredentials
+ if (args.timeout) xhr.timeout = args.timeout
+
+ if (args.responseType) xhr.responseType = args.responseType
+
for (var key in args.headers) if ({}.hasOwnProperty.call(args.headers, key)) {
xhr.setRequestHeader(key, args.headers[key])
}
@@ -88,12 +92,13 @@ module.exports = function($window, Promise) {
if (xhr.readyState === 4) {
try {
var response = (args.extract !== extract) ? args.extract(xhr, args) : args.deserialize(args.extract(xhr, args))
- if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304 || FILE_PROTOCOL_REGEX.test(args.url)) {
+ if (args.extract !== extract || (xhr.status >= 200 && xhr.status < 300) || xhr.status === 304 || FILE_PROTOCOL_REGEX.test(args.url)) {
resolve(cast(args.type, response))
}
else {
var error = new Error(xhr.responseText)
- for (var key in response) error[key] = response[key]
+ error.code = xhr.status
+ error.response = response
reject(error)
}
}
@@ -159,7 +164,7 @@ module.exports = function($window, Promise) {
function deserialize(data) {
try {return data !== "" ? JSON.parse(data) : null}
- catch (e) {throw new Error(data)}
+ catch (e) {throw new Error("Invalid JSON: " + data)}
}
function extract(xhr) {return xhr.responseText}
diff --git a/request/tests/test-request.js b/request/tests/test-request.js
index 7f965498..e69a93d7 100644
--- a/request/tests/test-request.js
+++ b/request/tests/test-request.js
@@ -230,7 +230,7 @@ o.spec("xhr", function() {
}
})
xhr({method: "GET", url: "/item", deserialize: deserialize}).then(function(data) {
- o(data).equals('{"test":123}')
+ o(data).equals("{\"test\":123}")
}).then(done)
})
o("deserialize parameter works in POST", function(done) {
@@ -244,7 +244,7 @@ o.spec("xhr", function() {
}
})
xhr({method: "POST", url: "/item", deserialize: deserialize}).then(function(data) {
- o(data).equals('{"test":123}')
+ o(data).equals("{\"test\":123}")
}).then(done)
})
o("extract parameter works in GET", function(done) {
@@ -258,7 +258,7 @@ o.spec("xhr", function() {
}
})
xhr({method: "GET", url: "/item", extract: extract}).then(function(data) {
- o(data).equals('{"test":123}')
+ o(data).equals("{\"test\":123}")
}).then(done)
})
o("extract parameter works in POST", function(done) {
@@ -272,7 +272,7 @@ o.spec("xhr", function() {
}
})
xhr({method: "POST", url: "/item", extract: extract}).then(function(data) {
- o(data).equals('{"test":123}')
+ o(data).equals("{\"test\":123}")
}).then(done)
})
o("ignores deserialize if extract is defined", function(done) {
@@ -435,6 +435,34 @@ o.spec("xhr", function() {
done()
})
})
+ o("set timeout to xhr instance", function() {
+ mock.$defineRoutes({
+ "GET /item": function() {
+ return {status: 200, responseText: ""}
+ }
+ })
+ return xhr({
+ method: "GET", url: "/item",
+ timeout: 42,
+ config: function(xhr) {
+ o(xhr.timeout).equals(42)
+ }
+ })
+ })
+ o("set responseType to xhr instance", function() {
+ mock.$defineRoutes({
+ "GET /item": function() {
+ return {status: 200, responseText: ""}
+ }
+ })
+ return xhr({
+ method: "GET", url: "/item",
+ responseType: "blob",
+ config: function(xhr) {
+ o(xhr.responseType).equals("blob")
+ }
+ })
+ })
/*o("data maintains after interpolate", function() {
mock.$defineRoutes({
"PUT /items/:x": function() {
@@ -458,9 +486,10 @@ o.spec("xhr", function() {
xhr({method: "GET", url: "/item"}).catch(function(e) {
o(e instanceof Error).equals(true)
o(e.message).equals(JSON.stringify({error: "error"}))
+ o(e.code).equals(500)
}).then(done)
})
- o("extends Error with JSON response", function(done) {
+ o("adds response to Error", function(done) {
mock.$defineRoutes({
"GET /item": function() {
return {status: 500, responseText: JSON.stringify({message: "error", stack: "error on line 1"})}
@@ -468,8 +497,8 @@ o.spec("xhr", function() {
})
xhr({method: "GET", url: "/item"}).catch(function(e) {
o(e instanceof Error).equals(true)
- o(e.message).equals("error")
- o(e.stack).equals("error on line 1")
+ o(e.response.message).equals("error")
+ o(e.response.stack).equals("error on line 1")
}).then(done)
})
o("rejects on non-JSON server error", function(done) {
@@ -479,7 +508,7 @@ o.spec("xhr", function() {
}
})
xhr({method: "GET", url: "/item"}).catch(function(e) {
- o(e.message).equals("error")
+ o(e.message).equals("Invalid JSON: error")
}).then(done)
})
o("triggers all branched catches upon rejection", function(done) {
@@ -518,5 +547,35 @@ o.spec("xhr", function() {
o(e instanceof Error).equals(true)
}).then(done)
})
+ o("does not reject on status error code when extract provided", function(done) {
+ mock.$defineRoutes({
+ "GET /item": function() {
+ return {status: 500, responseText: JSON.stringify({message: "error"})}
+ }
+ })
+ xhr({
+ method: "GET", url: "/item",
+ extract: function(xhr) {return JSON.parse(xhr.responseText)}
+ }).then(function(data) {
+ o(data.message).equals("error")
+ done()
+ })
+ })
+ o("rejects on error in extract", function(done) {
+ mock.$defineRoutes({
+ "GET /item": function() {
+ return {status: 200, responseText: JSON.stringify({a: 1})}
+ }
+ })
+ xhr({
+ method: "GET", url: "/item",
+ extract: function() {throw new Error("error")}
+ }).catch(function(e) {
+ o(e instanceof Error).equals(true)
+ o(e.message).equals("error")
+ }).then(function() {
+ done()
+ })
+ })
})
})
diff --git a/stream/change-log.md b/stream/change-log.md
new file mode 100644
index 00000000..45ccd0da
--- /dev/null
+++ b/stream/change-log.md
@@ -0,0 +1,7 @@
+# Change log for stream
+
+## 2.0.0
+- stream: Removed `valueOf` & `toString` methods ([#2150](https://github.com/MithrilJS/mithril.js/pull/2150)
+
+## 1.1.0
+- stream: Move the "use strict" directive inside the IIFE [#1831](https://github.com/MithrilJS/mithril.js/issues/1831) ([#1893](https://github.com/MithrilJS/mithril.js/pull/1893))
diff --git a/stream/stream.js b/stream/stream.js
index fd2dcffb..9bf1fed5 100644
--- a/stream/stream.js
+++ b/stream/stream.js
@@ -19,7 +19,7 @@ function initStream(stream) {
stream.constructor = createStream
stream._state = {id: guid++, value: undefined, state: 0, derive: undefined, recover: undefined, deps: {}, parents: [], endStream: undefined, unregister: undefined}
stream.map = stream["fantasy-land/map"] = map, stream["fantasy-land/ap"] = ap, stream["fantasy-land/of"] = createStream
- stream.valueOf = valueOf, stream.toJSON = toJSON, stream.toString = valueOf
+ stream.toJSON = toJSON
Object.defineProperties(stream, {
end: {get: function() {
@@ -53,7 +53,7 @@ function updateDependency(stream, mustSync) {
var state = stream._state, parents = state.parents
if (parents.length > 0 && parents.every(active) && (mustSync || parents.some(changed))) {
var value = stream._state.derive()
- if (value === HALT) return false
+ if (value === HALT) return unregisterStream(stream)
updateState(stream, value)
}
}
@@ -101,7 +101,6 @@ function unregisterStream(stream) {
function map(fn) {return combine(function(stream) {return fn(stream())}, [this])}
function ap(stream) {return combine(function(s1, s2) {return s1()(s2())}, [stream, this])}
-function valueOf() {return this._state.value}
function toJSON() {return this._state.value != null && typeof this._state.value.toJSON === "function" ? this._state.value.toJSON() : this._state.value}
function valid(stream) {return stream._state }
@@ -117,7 +116,9 @@ function merge(streams) {
function scan(reducer, seed, stream) {
var newStream = combine(function (s) {
- return seed = reducer(seed, s._state.value)
+ var next = reducer(seed, s._state.value)
+ if (next !== HALT) return seed = next
+ return HALT
}, [stream])
if (newStream._state.state === 0) newStream(seed)
diff --git a/stream/tests/index.html b/stream/tests/index.html
index 86cb8947..8db5634b 100644
--- a/stream/tests/index.html
+++ b/stream/tests/index.html
@@ -8,11 +8,8 @@
-
-
-
diff --git a/stream/tests/test-scan.js b/stream/tests/test-scan.js
index db5d885f..2f2c2bb2 100644
--- a/stream/tests/test-scan.js
+++ b/stream/tests/test-scan.js
@@ -30,4 +30,36 @@ o.spec("scan", function() {
o(result[2]).equals(undefined)
o(result[3]).deepEquals({a: 1})
})
+
+ o("reducer can return HALT to prevent child updates", function() {
+ var count = 0
+ var action = stream()
+ var store = stream.scan(function (arr, value) {
+ switch (typeof value) {
+ case "number":
+ return arr.concat(value)
+ default:
+ return stream.HALT
+ }
+ }, [], action)
+ var child = store.map(function (p) {
+ count++
+ return p
+ })
+ var result
+
+ action(7)
+ action("11")
+ action(undefined)
+ action({a: 1})
+
+ result = child()
+
+ // check we got the expect result
+ o(result[0]).equals(7)
+
+ // check child received minimum # of updates
+ o(count).equals(2)
+ })
+
})
diff --git a/stream/tests/test-stream.js b/stream/tests/test-stream.js
index 09b01209..69ca811d 100644
--- a/stream/tests/test-stream.js
+++ b/stream/tests/test-stream.js
@@ -164,6 +164,27 @@ o.spec("stream", function() {
o(b()).equals(undefined)
o(count).equals(0)
})
+ o("combine can conditionaly halt", function() {
+ var count = 0
+ var halt = false
+ var a = Stream(1)
+ var b = Stream.combine(function(a) {
+ if (halt) {
+ return Stream.HALT
+ }
+ return a()
+ }, [a])["fantasy-land/map"](function(a) {
+ count++
+ return a
+ })
+ o(b()).equals(1)
+ o(count).equals(1)
+ halt = true
+ count = 0
+ a(2)
+ o(b()).equals(1)
+ o(count).equals(0)
+ })
o("combine will throw with a helpful error if given non-stream values", function () {
var spy = o.spy()
var a = Stream(1)
@@ -276,31 +297,6 @@ o.spec("stream", function() {
o(spy.callCount).equals(1)
})
})
- o.spec("valueOf", function() {
- o("works", function() {
- o(Stream(1).valueOf()).equals(1)
- o(Stream("a").valueOf()).equals("a")
- o(Stream(true).valueOf()).equals(true)
- o(Stream(null).valueOf()).equals(null)
- o(Stream(undefined).valueOf()).equals(undefined)
- o(Stream({a: 1}).valueOf()).deepEquals({a: 1})
- o(Stream([1, 2, 3]).valueOf()).deepEquals([1, 2, 3])
- o(Stream().valueOf()).equals(undefined)
- })
- o("allows implicit value access in mathematical operations", function() {
- o(Stream(1) + Stream(1)).equals(2)
- })
- })
- o.spec("toString", function() {
- o("aliases valueOf", function() {
- var stream = Stream(1)
-
- o(stream.toString).equals(stream.valueOf)
- })
- o("allows implicit value access in string operations", function() {
- o(Stream("a") + Stream("b")).equals("ab")
- })
- })
o.spec("toJSON", function() {
o("works", function() {
o(Stream(1).toJSON()).equals(1)
diff --git a/test-utils/domMock.js b/test-utils/domMock.js
index 060ba4f7..ce494683 100644
--- a/test-utils/domMock.js
+++ b/test-utils/domMock.js
@@ -2,7 +2,7 @@
/*
Known limitations:
-
+- the innerHTML setter and the DOMParser only support a small subset of the true HTML/XML syntax.
- `option.selected` can't be set/read when the option doesn't have a `select` parent
- `element.attributes` is just a map of attribute names => Attr objects stubs
- ...
@@ -18,6 +18,9 @@ module.exports = function(options) {
options = options || {}
var spy = options.spy || function(f){return f}
var spymap = []
+
+ var hasOwn = ({}.hasOwnProperty)
+
function registerSpies(element, spies) {
if(options.spy) {
var i = spymap.indexOf(element)
@@ -37,6 +40,30 @@ module.exports = function(options) {
function isModernEvent(type) {
return type === "transitionstart" || type === "transitionend" || type === "animationstart" || type === "animationend"
}
+ function dispatchEvent(e) {
+ var stopped = false
+ e.stopImmediatePropagation = function() {
+ e.stopPropagation()
+ stopped = true
+ }
+ e.currentTarget = this
+ if (this._events[e.type] != null) {
+ for (var i = 0; i < this._events[e.type].handlers.length; i++) {
+ var useCapture = this._events[e.type].options[i].capture
+ if (useCapture && e.eventPhase < 3 || !useCapture && e.eventPhase > 1) {
+ var handler = this._events[e.type].handlers[i]
+ if (typeof handler === "function") try {handler.call(this, e)} catch(e) {setTimeout(function(){throw e})}
+ else try {handler.handleEvent(e)} catch(e) {setTimeout(function(){throw e})}
+ if (stopped) return
+ }
+ }
+ }
+ // this is inaccurate. Normally the event fires in definition order, including legacy events
+ // this would require getters/setters for each of them though and we haven't gotten around to
+ // adding them since it would be at a high perf cost or would entail some heavy refactoring of
+ // the mocks (prototypes instead of closures).
+ if (e.eventPhase > 1 && typeof this["on" + e.type] === "function" && !isModernEvent(e.type)) try {this["on" + e.type](e)} catch(e) {setTimeout(function(){throw e})}
+ }
function appendChild(child) {
var ancestor = this
while (ancestor !== child && ancestor !== null) ancestor = ancestor.parentNode
@@ -47,7 +74,7 @@ module.exports = function(options) {
var index = this.childNodes.indexOf(child)
if (index > -1) this.childNodes.splice(index, 1)
if (child.nodeType === 11) {
- while (child.firstChild != null) this.appendChild(child.firstChild)
+ while (child.firstChild != null) appendChild.call(this, child.firstChild)
child.childNodes = []
}
else {
@@ -75,8 +102,9 @@ module.exports = function(options) {
var index = this.childNodes.indexOf(child)
if (reference !== null && refIndex < 0) throw new TypeError("Invalid argument")
if (index > -1) this.childNodes.splice(index, 1)
- if (reference === null) this.appendChild(child)
+ if (reference === null) appendChild.call(this, child)
else {
+ if (index !== -1 && refIndex > index) refIndex--
if (child.nodeType === 11) {
this.childNodes.splice.apply(this.childNodes, [refIndex, 0].concat(child.childNodes))
while (child.firstChild) {
@@ -102,9 +130,8 @@ module.exports = function(options) {
// this is the correct kind of conversion, passing a Symbol throws in browsers too.
var nodeValue = "" + value
/*eslint-enable no-implicit-coercion*/
-
this.attributes[name] = {
- namespaceURI: null,
+ namespaceURI: hasOwn.call(this.attributes, name) ? this.attributes[name].namespaceURI : null,
get value() {return nodeValue},
set value(value) {
/*eslint-disable no-implicit-coercion*/
@@ -159,9 +186,43 @@ module.exports = function(options) {
res.unshift(declList)
return res
}
-
+ function parseMarkup(value, root, voidElements, xmlns) {
+ var depth = 0, stack = [root]
+ value.replace(/<([a-z0-9\-]+?)((?:\s+?[^=]+?=(?:"[^"]*?"|'[^']*?'|[^\s>]*))*?)(\s*\/)?>|<\/([a-z0-9\-]+?)>|([^<]+)/g, function(match, startTag, attrs, selfClosed, endTag, text) {
+ if (startTag) {
+ var element = xmlns == null ? $window.document.createElement(startTag) : $window.document.createElementNS(xmlns, startTag)
+ attrs.replace(/\s+?([^=]+?)=(?:"([^"]*?)"|'([^']*?)'|([^\s>]*))/g, function(match, key, doubleQuoted, singleQuoted, unquoted) {
+ var keyParts = key.split(":")
+ var name = keyParts.pop()
+ var ns = keyParts[0]
+ var value = doubleQuoted || singleQuoted || unquoted || ""
+ if (ns != null) element.setAttributeNS(ns, name, value)
+ else element.setAttribute(name, value)
+ })
+ stack[depth].appendChild(element)
+ if (!selfClosed && voidElements.indexOf(startTag.toLowerCase()) < 0) stack[++depth] = element
+ }
+ else if (endTag) {
+ depth--
+ }
+ else if (text) {
+ stack[depth].appendChild($window.document.createTextNode(text)) // FIXME handle html entities
+ }
+ })
+ }
+ function DOMParser() {}
+ DOMParser.prototype.parseFromString = function(src, mime) {
+ if (mime !== "image/svg+xml") throw new Error("The DOMParser mock only supports the \"image/svg+xml\" MIME type")
+ var match = src.match(/^