diff --git a/README.md b/README.md index e50cd82c..ea3cdee2 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Mithril's `config` method is now replaced by several lifecycle methods to improv ## Robustness -There are over 1800 assertions in the test suite, and tests cover even difficult-to-test things like `location.href`, `element.innerHTML` and `XMLHttpRequest` usage. +There are over 2100 assertions in the test suite, and tests cover even difficult-to-test things like `location.href`, `element.innerHTML` and `XMLHttpRequest` usage. ## Modularity diff --git a/render/node.js b/render/node.js index 6f1b51ef..160cd639 100644 --- a/render/node.js +++ b/render/node.js @@ -1,5 +1,5 @@ function Node(tag, key, attrs, children, text, dom) { - return {tag: tag, key: key, attrs: attrs, children: children, text: text, dom: dom, domSize: undefined, state: {}} + return {tag: tag, key: key, attrs: attrs, children: children, text: text, dom: dom, domSize: undefined, state: {}, events: undefined} } Node.normalize = function(node) { if (node instanceof Array) return Node("[", undefined, undefined, Node.normalizeChildren(node), undefined, undefined) diff --git a/render/render.js b/render/render.js index b2984c83..379a821a 100644 --- a/render/render.js +++ b/render/render.js @@ -161,6 +161,7 @@ module.exports = function($window, onevent) { var oldTag = old.tag, tag = vnode.tag if (oldTag === tag) { vnode.state = old.state + vnode.events = old.events if (shouldUpdate(vnode, old)) return if (vnode.attrs != null) { updateLifecycle(vnode.attrs, vnode, hooks, recycling) @@ -345,11 +346,17 @@ module.exports = function($window, onevent) { element.setAttributeNS("http://www.w3.org/1999/xlink", key.slice(nsLastIndex + 1), value) } else if (key[0] === "o" && key[1] === "n" && typeof value === "function") { - element[key] = function(e) { + var eventName = key.slice(2) + if (vnode.events === undefined) vnode.events = {} + if (vnode.events[key] != null) { + element.removeEventListener(eventName, vnode.events[key], false) + } + vnode.events[key] = function(e) { var result = value.call(element, e) if (typeof onevent === "function") onevent.call(element, e) return result } + element.addEventListener(eventName, vnode.events[key], false) } else if (key === "style") updateStyle(element, old, value) else if (key in element && !isAttribute(key) && vnode.ns === undefined) element[key] = value diff --git a/render/tests/test-event.js b/render/tests/test-event.js index e11c36b8..46e3f45b 100644 --- a/render/tests/test-event.js +++ b/render/tests/test-event.js @@ -31,4 +31,46 @@ o.spec("event", function() { o(onevent.args[0].type).equals("click") o(onevent.args[0].target).equals(div.dom) }) + + o("fires onclick only once after redraw", function() { + var spy = o.spy() + var div = {tag: "div", attrs: {id: "a", onclick: spy}} + var updated = {tag: "div", attrs: {id: "b", onclick: spy}} + 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(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(div.dom).equals(updated.dom) + o(div.dom.attributes["id"].nodeValue).equals("b") + }) + + o("handles ontransitionend", function() { + var spy = o.spy() + var div = {tag: "div", attrs: {ontransitionend: spy}} + var e = $window.document.createEvent("AnimationEvent") + e.initEvent("transitionend", 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("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) + }) }) \ No newline at end of file diff --git a/test-utils/domMock.js b/test-utils/domMock.js index f85b40bb..82fa9e28 100644 --- a/test-utils/domMock.js +++ b/test-utils/domMock.js @@ -1,6 +1,9 @@ "use strict" module.exports = function() { + function isModernEvent(type) { + return type === "transitionstart" || type === "transitionend" || type === "animationstart" || type === "animationend" + } function appendChild(child) { var ancestor = this while (ancestor !== child && ancestor !== null) ancestor = ancestor.parentNode @@ -77,6 +80,7 @@ module.exports = function() { document: { createElement: function(tag, is) { var style = {} + var events = {} var element = { nodeType: 1, nodeName: tag.toUpperCase(), @@ -148,9 +152,24 @@ module.exports = function() { } }, focus: function() {activeElement = this}, + addEventListener: function(type, callback, useCapture) { + if (events[type] == null) events[type] = [callback] + else events[type].push(callback) + }, + removeEventListener: function(type, callback, useCapture) { + if (events[type] != null) { + var index = events[type].indexOf(callback) + if (index > -1) events[type].splice(index, 1) + } + }, dispatchEvent: function(e) { e.target = this - if (typeof this["on" + e.type] === "function") this["on" + e.type](e) + if (events[e.type] != null) { + for (var i = 0; i < events[e.type].length; i++) { + events[e.type][i].call(this, e) + } + } + if (typeof this["on" + e.type] === "function" && !isModernEvent(e.type)) this["on" + e.type](e) }, } if (element.nodeName === "A") { diff --git a/test-utils/tests/test-domMock.js b/test-utils/tests/test-domMock.js index 0019b5ea..a075fe92 100644 --- a/test-utils/tests/test-domMock.js +++ b/test-utils/tests/test-domMock.js @@ -484,31 +484,72 @@ o.spec("domMock", function() { }) }) o.spec("events", function() { - var spy, div, e - o.beforeEach(function() { - spy = o.spy() - div = $document.createElement("div") - e = $document.createEvent("MouseEvents") - e.initEvent("click", true, true) + o.spec("click", function() { + var spy, div, e + o.beforeEach(function() { + spy = o.spy() + div = $document.createElement("div") + e = $document.createEvent("MouseEvents") + e.initEvent("click", true, true) + + $document.body.appendChild(div) + }) + o.afterEach(function() { + $document.body.removeChild(div) + }) - $document.body.appendChild(div) + o("addEventListener works", function() { + div.addEventListener("click", spy, false) + div.dispatchEvent(e) + + o(spy.callCount).equals(1) + o(spy.this).equals(div) + o(spy.args[0].type).equals("click") + o(spy.args[0].target).equals(div) + }) + o("removeEventListener works", function(done) { + div.addEventListener("click", spy, false) + div.removeEventListener("click", spy, false) + div.dispatchEvent(e) + + o(spy.callCount).equals(0) + done() + }) + o("click fires onclick", function() { + div.onclick = spy + div.dispatchEvent(e) + + o(spy.callCount).equals(1) + o(spy.this).equals(div) + o(spy.args[0].type).equals("click") + o(spy.args[0].target).equals(div) + }) + o("click without onclick doesn't throw", function(done) { + div.dispatchEvent(e) + done() + }) }) - o.afterEach(function() { - $document.body.removeChild(div) - }) - - o("click fires onclick", function() { - div.onclick = spy - div.dispatchEvent(e) + o.spec("transitionend", function() { + var spy, div, e + o.beforeEach(function() { + spy = o.spy() + div = $document.createElement("div") + e = $document.createEvent("AnimationEvent") + e.initEvent("transitionend", true, true) + + $document.body.appendChild(div) + }) + o.afterEach(function() { + $document.body.removeChild(div) + }) - o(spy.callCount).equals(1) - o(spy.this).equals(div) - o(spy.args[0].type).equals("click") - o(spy.args[0].target).equals(div) - }) - o("click without onclick doesn't throw", function(done) { - div.dispatchEvent(e) - done() + o("ontransitionend does not fire", function(done) { + div.ontransitionend = spy + div.dispatchEvent(e) + + o(spy.callCount).equals(0) + done() + }) }) }) o.spec("attributes", function() {