From 53672e62d7d7bea87d484c09c0d0600d660d2fc5 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Fri, 13 Jun 2014 23:33:02 -0400 Subject: [PATCH] implement keys --- mithril.js | 57 ++++++++++++++++++- tests/e2e/tests.js | 125 +++++++++++++++++++++++++++++++++++++++++ tests/mithril-tests.js | 41 ++++++++++++++ tests/mock.js | 3 +- 4 files changed, 223 insertions(+), 3 deletions(-) diff --git a/mithril.js b/mithril.js index a8e827b5..0f5715fa 100644 --- a/mithril.js +++ b/mithril.js @@ -52,6 +52,55 @@ Mithril = m = new function app(window) { if (dataType == "[object Array]") { var nodes = [], intact = cached.length === data.length, subArrayCount = 0 + var DELETION = 1, INSERTION = 2 , MOVE = 3 + var existing = {}, shouldMaintainIdentities = false + for (var i = 0; i < cached.length; i++) { + if (cached[i] && cached[i].attrs && cached[i].attrs.key !== undefined) { + shouldMaintainIdentities = true + existing[cached[i].attrs.key] = {action: DELETION, index: i} + } + } + if (shouldMaintainIdentities) { + for (var i = 0; i < data.length; i++) { + if (data[i] && data[i].attrs && data[i].attrs.key !== undefined) { + var key = data[i].attrs.key + if (!existing[key]) existing[key] = {action: INSERTION, index: i} + else existing[key] = {action: MOVE, index: i, from: existing[key].index, element: parentElement.childNodes[existing[key].index]} + } + } + var actions = Object.keys(existing).map(function(key) {return existing[key]}) + var changes = actions + .sort(function(a, b) {return a.action - b.action || b.index - a.index}) + var newCached = new Array(cached.length) + var children = [] + for (var i = 0, child; child = parentElement.childNodes[i]; i++) children.push(child) + + for (var i = 0, change; change = changes[i]; i++) { + if (change.action == DELETION) { + clear(cached[change.index].nodes) + children.splice(change.index, 1) + newCached.splice(change.index, 1) + } + if (change.action == INSERTION) { + var dummy = window.document.createElement("x") + dummy.key = data[change.index].attrs.key.toString() + parentElement.insertBefore(dummy, parentElement.childNodes[change.index]) + children.splice(change.index, 0, dummy) + newCached.splice(change.index, 0, {attrs: {key: data[change.index].attrs.key}, nodes: [dummy]}) + } + + if (change.action == MOVE) { + if (parentElement.childNodes[change.index] !== change.element) { + parentElement.insertBefore(change.element, parentElement.childNodes[change.index]) + } + newCached[change.index] = cached[change.from] + } + } + cached = newCached + cached.nodes = [] + for (var i = 0, child; child = parentElement.childNodes[i]; i++) cached.nodes.push(child) + } + for (var i = 0, cacheCount = 0; i < data.length; i++) { var item = build(parentElement, null, cached, index, data[i], cached[cacheCount], shouldReattach, index + subArrayCount || subArrayCount, editable, namespace, configs) if (item === undefined) continue @@ -72,6 +121,7 @@ Mithril = m = new function app(window) { if (data.length < cached.length) cached.length = data.length cached.nodes = nodes } + } else if (dataType == "[object Object]") { if (data.tag != cached.tag || Object.keys(data.attrs).join() != Object.keys(cached.attrs).join() || data.attrs.id != cached.attrs.id) clear(cached.nodes) @@ -471,7 +521,7 @@ Mithril = m = new function app(window) { function identity(value) {return value} function ajax(options) { - var xhr = window.XDomainRequest ? new window.XDomainRequest : new window.XMLHttpRequest + var xhr = new window.XMLHttpRequest xhr.open(options.method, options.url, true, options.user, options.password) xhr.onreadystatechange = function() { if (xhr.readyState === 4) { @@ -479,7 +529,10 @@ Mithril = m = new function app(window) { else options.onerror({type: "error", target: xhr}) } } - if (typeof options.config == "function") options.config(xhr, options) + if (typeof options.config == "function") { + var maybeXhr = options.config(xhr, options) + if (maybeXhr !== undefined) xhr = maybeXhr + } xhr.send(options.data) return xhr } diff --git a/tests/e2e/tests.js b/tests/e2e/tests.js index 0b4cf22c..1a1e4241 100644 --- a/tests/e2e/tests.js +++ b/tests/e2e/tests.js @@ -89,3 +89,128 @@ test('config handler context', function() { }}) m.render(dummyEl, view); }) + +test('node identity remove firstChild', function() { + expect(2); + var view1 = m('div', {}, [ + m('div', {key:1}, 'E1'), + m('div', {key:2}, 'E2'), + ]); + m.render(dummyEl, view1); + + var node2 = dummyEl.firstChild.lastChild; + equal(node2.innerHTML, 'E2') + + var view2 = m('div', {}, [ + m('div', {key:2}, 'E2'), + ]); + m.render(dummyEl, view2); + + equal(dummyEl.firstChild.firstChild, node2); + +}) + +test('node identity change order', function() { + expect(2); + var view1 = m('div', {}, [ + m('div', {key:1}, 'E1'), + m('div', {key:2}, 'E2'), + m('div', {key:3}, 'E3'), + ]); + m.render(dummyEl, view1); + + var e2 = dummyEl.firstChild.firstChild.nextSibling; + equal(e2.innerHTML, 'E2') + + var view2 = m('div', {}, [ + m('div', {key:2}, 'E2'), + m('div', {key:1}, 'E1'), + m('div', {key:3}, 'E3'), + ]); + m.render(dummyEl, view2); + + equal(dummyEl.firstChild.firstChild, e2); +}) + +test('node identity remove in the middle', function() { + expect(2); + var view1 = m('div', {}, [ + m('div', {key:1}, 'E1'), + m('div', {key:2}, 'E2'), + m('div', {key:3}, 'E3'), + ]); + m.render(dummyEl, view1); + + var e3 = dummyEl.firstChild.lastChild; + equal(e3.innerHTML, 'E3') + + var view2 = m('div', {}, [ + m('div', {key:1}, 'E1'), + m('div', {key:3}, 'E3'), + ]); + m.render(dummyEl, view2); + + equal(dummyEl.firstChild.firstChild.nextSibling, e3); + +}) + +test('node identity remove last', function() { + expect(4); + var view1 = m('div', {}, [ + m('div', {key:1}, 'E1'), + m('div', {key:2}, 'E2'), + m('div', {key:3}, 'E3'), + ]); + m.render(dummyEl, view1); + + var e1 = dummyEl.firstChild.firstChild; + equal(e1.innerHTML, 'E1') + var e2 = dummyEl.firstChild.firstChild.nextSibling; + equal(e2.innerHTML, 'E2') + + var view2 = m('div', {}, [ + m('div', {key:1}, 'E1'), + m('div', {key:2}, 'E2'), + ]); + m.render(dummyEl, view2); + + equal(dummyEl.firstChild.firstChild, e1); + equal(dummyEl.firstChild.firstChild.nextSibling, e2); + +}) + +test('node identity shuffle and remove', function() { + expect(8); + var view1 = m('div', {}, [ + m('div', {key:1}, 'E1'), + m('div', {key:2}, 'E2'), + m('div', {key:3}, 'E3'), + m('div', {key:4}, 'E4'), + m('div', {key:5}, 'E5'), + ]); + m.render(dummyEl, view1); + + var e1 = dummyEl.firstChild.firstChild; + equal(e1.innerHTML, 'E1') + var e2 = e1.nextSibling; + equal(e2.innerHTML, 'E2') + var e3 = e2.nextSibling; + equal(e3.innerHTML, 'E3') + var e4 = e3.nextSibling; + equal(e4.innerHTML, 'E4') + var e5 = e4.nextSibling; + equal(e5.innerHTML, 'E5') + + var view2 = m('div', {}, [ + m('div', {key:4}, 'E4'), + m('div', {key:10}, 'E10'), + m('div', {key:1}, 'E1'), + m('div', {key:2}, 'E2'), + ]); + m.render(dummyEl, view2); + + equal(dummyEl.firstChild.firstChild, e4, 'e4 is first element'); + equal(dummyEl.firstChild.firstChild.nextSibling.nextSibling, e1, 'e1 is third element'); + equal(dummyEl.firstChild.firstChild.nextSibling.nextSibling.nextSibling, e2, 'e2 is fourth element'); + +}) \ No newline at end of file diff --git a/tests/mithril-tests.js b/tests/mithril-tests.js index 2c58e475..cb363b17 100644 --- a/tests/mithril-tests.js +++ b/tests/mithril-tests.js @@ -507,6 +507,47 @@ function testMithril(mock) { m.render(root, m("div", ["asdf", "asdf2", "asdf3"])); return true }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/98 + //insert at beginning + var root = mock.document.createElement("div") + m.render(root, [m("a", {key: 1}), m("a", {key: 2}), m("a", {key: 3})]) + var firstBefore = root.childNodes[0] + m.render(root, [m("a", {key: 4}), m("a", {key: 1}), m("a", {key: 2}), m("a", {key: 3})]) + var firstAfter = root.childNodes[1] + return firstBefore == firstAfter && root.childNodes[0].key == 4 && root.childNodes.length == 4 + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/98 + var root = mock.document.createElement("div") + m.render(root, [m("a", {key: 1}), m("a", {key: 2}), m("a", {key: 3})]) + var firstBefore = root.childNodes[0] + m.render(root, [m("a", {key: 4}), m("a", {key: 1}), m("a", {key: 2})]) + var firstAfter = root.childNodes[1] + return firstBefore == firstAfter && root.childNodes[0].key == 4 && root.childNodes.length == 3 + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/98 + var root = mock.document.createElement("div") + m.render(root, [m("a", {key: 1}), m("a", {key: 2}), m("a", {key: 3})]) + var firstBefore = root.childNodes[1] + m.render(root, [m("a", {key: 2}), m("a", {key: 3}), m("a", {key: 4})]) + var firstAfter = root.childNodes[0] + return firstBefore == firstAfter && root.childNodes[0].key === "2" && root.childNodes.length === 3 + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/98 + var root = mock.document.createElement("div") + m.render(root, [m("a", {key: 1}), m("a", {key: 2}), m("a", {key: 3}), m("a", {key: 4}), m("a", {key: 5})]) + var firstBefore = root.childNodes[0] + var secondBefore = root.childNodes[1] + var fourthBefore = root.childNodes[3] + m.render(root, [m("a", {key: 4}), m("a", {key: 10}), m("a", {key: 1}), m("a", {key: 2})]) + var firstAfter = root.childNodes[2] + var secondAfter = root.childNodes[3] + var fourthAfter = root.childNodes[0] + return firstBefore === firstAfter && secondBefore === secondAfter && fourthBefore === fourthAfter && root.childNodes[1].key == "10" && root.childNodes.length === 4 + }) //end m.render //m.redraw diff --git a/tests/mock.js b/tests/mock.js index 88534df0..c8e6a15c 100644 --- a/tests/mock.js +++ b/tests/mock.js @@ -17,7 +17,8 @@ mock.window = new function() { if (referenceIndex < 0) this.childNodes.push(node) else { var index = this.childNodes.indexOf(node) - this.childNodes.splice(referenceIndex, index < 0 ? 0 : 1, node) + if (index > -1) this.childNodes.splice(index, 1) + this.childNodes.splice(referenceIndex, 0, node) } }, insertAdjacentHTML: function(position, html) {