From dfd95f551ff008bb8f89f5fe2f35b476290451e7 Mon Sep 17 00:00:00 2001 From: mcjazzyfunky Date: Tue, 30 Mar 2021 13:26:45 +0200 Subject: [PATCH] Added support for 'ref' properties (#184) --- index.js | 22 ++++++- tests/refs.test.js | 140 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 tests/refs.test.js diff --git a/index.js b/index.js index cf8a006..ea49c88 100644 --- a/index.js +++ b/index.js @@ -20,6 +20,11 @@ var patchProperty = (node, key, oldValue, newValue, isSvg) => { } else if (!oldValue) { node.addEventListener(key, listener) } + } else if (key === "ref") { + if (newValue !== oldValue) { + oldValue && passToRef(oldValue, null) + newValue && passToRef(newValue, node) + } } else if (!isSvg && key !== "list" && key !== "form" && key in node) { node[key] = newValue == null ? "" : newValue } else if (newValue == null || newValue === false) { @@ -60,13 +65,14 @@ var patchNode = (parent, node, oldVNode, newVNode, isSvg) => { ) { if (oldVNode.tag !== newVNode.tag) node.nodeValue = newVNode.tag } else if (oldVNode == null || oldVNode.tag !== newVNode.tag) { + oldVNode && dismissVNode(oldVNode) + node = parent.insertBefore( createNode((newVNode = vdomify(newVNode)), isSvg), node ) - if (oldVNode != null) { - parent.removeChild(oldVNode.node) - } + + oldVNode && parent.removeChild(oldVNode.node) } else { var tmpVKid, oldVKid, @@ -136,6 +142,7 @@ var patchNode = (parent, node, oldVNode, newVNode, isSvg) => { } } else if (newHead > newTail) { while (oldHead <= oldTail) { + dismissVNode(oldVKids[oldHead]) node.removeChild(oldVKids[oldHead++].node) } } else { @@ -154,6 +161,7 @@ var patchNode = (parent, node, oldVNode, newVNode, isSvg) => { (newKey != null && newKey === getKey(oldVKids[oldHead + 1])) ) { if (oldKey == null) { + dismissVNode(oldVKid) node.removeChild(oldVKid.node) } oldHead++ @@ -203,12 +211,14 @@ var patchNode = (parent, node, oldVNode, newVNode, isSvg) => { while (oldHead <= oldTail) { if (getKey((oldVKid = oldVKids[oldHead++])) == null) { + dismissVNode(oldVKid) node.removeChild(oldVKid.node) } } for (var i in keyed) { if (newKeyed[i] == null) { + dismissVNode(keyed[i]) node.removeChild(keyed[i].node) } } @@ -221,6 +231,12 @@ var patchNode = (parent, node, oldVNode, newVNode, isSvg) => { var vdomify = (newVNode) => newVNode !== true && newVNode !== false && newVNode ? newVNode : text("") +var passToRef = (ref, value) => + ref && (typeof ref === 'function' ? ref(value) : (ref.current = value)) + +var dismissVNode = (vnode) => + vnode && vnode.props && vnode.props.ref && passToRef(vnode.props.ref, null) + var recycleNode = (node) => node.nodeType === TEXT_NODE ? text(node.nodeValue, node) diff --git a/tests/refs.test.js b/tests/refs.test.js new file mode 100644 index 0000000..83c15b6 --- /dev/null +++ b/tests/refs.test.js @@ -0,0 +1,140 @@ +import { t as tw, equal } from "twist" +import { JSDOM } from "jsdom" +import { h, patch } from "../index.js" + +// we enhance twist's original function `t` +// with support for generator functions as source +// for tests as alternative to arrays +function t(name, tests) { + return tw(name, Array.isArray(tests) ? tests : Array.from(tests())) +} + +// === ref helpers ==================================================== + +function createRefObject() { + let current = null + let previous = null + + return { + get current() { + return current + }, + + set current(value) { + previous = current + current = value + }, + + get previous() { + return previous + } + } +} + +function createRefCallback() { + let current = null + let previous = null + + const ret = (value) => { + previous = current + current = value + } + + Object.defineProperties(ret, { + current: { get: () => current }, + previous: { get: () => previous } + }) + + return ret +} + +// === tests ========================================================= + +export default [ + t("ref support for superfine", [ + t("test ref helpers (only needed for testing)", [ + t("testing createRefObject", function* () { + const refObject = createRefObject() + yield equal(refObject.current, null) + yield equal(refObject.previous, null) + + refObject.current = 11 + yield equal(refObject.current, 11) + yield equal(refObject.previous, null) + + refObject.current = 22 + yield equal(refObject.current, 22) + yield equal(refObject.previous, 11) + + refObject.current = 33 + yield equal(refObject.current, 33) + yield equal(refObject.previous, 22) + }), + t("testing createRefCallback", function* () { + const refCallback = createRefCallback() + yield equal(refCallback.current, null) + yield equal(refCallback.previous, null) + + refCallback(111) + yield equal(refCallback.current, 111) + yield equal(refCallback.previous, null) + + refCallback(222) + yield equal(refCallback.current, 222) + yield equal(refCallback.previous, 111) + + refCallback(333) + yield equal(refCallback.current, 333) + yield equal(refCallback.previous, 222) + }), + ]), + t("test behavior of refs", [ + t("ref objects and ref callback should work properly", function* () { + const container = new JSDOM('
') + .window.document.getElementById('root') + + const render = (vnode) => patch(container.firstChild, vnode) + const refObject = createRefObject() + const refCallback = createRefCallback() + + container.appendChild(document.createElement("span")) + + render(h("div", { ref: refObject })) + yield equal(refObject.current?.tagName, "DIV") + yield equal(refObject.previous, null) + yield equal(refCallback.current, null) + yield equal(refCallback.previous, null) + + render(h("input", { ref: refCallback })) + yield equal(refObject.current, null) + yield equal(refObject.previous?.tagName ,"DIV") + yield equal(refCallback.current?.tagName ,"INPUT") + yield equal(refCallback.previous, null) + + render(h("input", { ref: refCallback })) + yield equal(refObject.current, null) + yield equal(refObject.previous?.tagName, "DIV") + yield equal(refCallback.current?.tagName, "INPUT") + yield equal(refCallback.previous, null) + + render(h("button", { ref: refObject })) + yield equal(refObject.current?.tagName, "BUTTON") + yield equal(refObject.previous, null) + yield equal(refCallback.current, null) + yield equal(refCallback.previous?.tagName, "INPUT") + + render(h("button", { ref: refCallback })) + yield equal(refObject.current, null) + yield equal(refObject.previous?.tagName, "BUTTON") + yield equal(refCallback.current?.tagName ,"BUTTON") + yield equal(refCallback.previous, null) + + render(h("input", { ref: refCallback })) + yield equal(refObject.current, null) + yield equal(refObject.previous?.tagName, "BUTTON") + yield equal(refCallback.current?.tagName, "INPUT") + yield equal(refCallback.previous, null) + }) + ]) + ]) +]