From e6e0d138c2ecdd2500a1e9630d5ffbe442867e0d Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Sat, 15 Feb 2025 09:28:26 -0600 Subject: [PATCH 1/6] proof-of-concept for a plugin system. --- README.md | 20 ++++ src/idiomorph.js | 65 +++++++---- test/index.html | 1 + test/plugins.js | 277 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 343 insertions(+), 20 deletions(-) create mode 100644 test/plugins.js diff --git a/README.md b/README.md index a1280de..b872391 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,26 @@ Idiomorph.morph(document.documentElement, newPageSource, {head:{style: 'morph'}} The `head` object also offers callbacks for configuring head merging specifics. +### Plugins + +Idiomorph supports a plugin system that allows you to extend the functionality of the library, by registering an object of callbacks: + +```js +Idiomorph.registerPlugin({ + name: 'logger', + onBeforeNodeAdded: function(node) { + console.log('Node added:', node); + }, + onBeforeNodeRemoved: function(node) { + console.log('Node removed:', node); + }, +}); + +Idiomorph.plugins // { logger: { ...} }; +``` + +These callbacks will be called in addition to any other callbacks that are registered in `Idiomorph.morph`. Multiple plugins can be registered. + ### Setting Defaults All the behaviors specified above can be set to a different default by mutating the `Idiomorph.defaults` object, including diff --git a/src/idiomorph.js b/src/idiomorph.js index f27e2e1..43841e3 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -144,6 +144,11 @@ var Idiomorph = (function () { restoreFocus: true, }; + let plugins = {}; + function addPlugin(plugin) { + plugins[plugin.name] = plugin; + } + /** * Core idiomorph function for morphing one DOM tree to another * @@ -348,6 +353,26 @@ var Idiomorph = (function () { } } + function withNodeCallbacks(ctx, name, node, fn) { + const allPlugins = [...Object.values(plugins), ctx.callbacks]; + + const shouldAbort = allPlugins.some((plugin) => { + const beforeFn = plugin[`beforeNode${name}`]; + return beforeFn && beforeFn(node) === false; + }); + + if (shouldAbort) return; + + const resultNode = fn(); + + allPlugins.reverse().forEach((plugin) => { + const afterFn = plugin[`afterNode${name}`]; + afterFn && afterFn(resultNode); + }); + + return resultNode; + } + /** * This performs the action of inserting a new node while handling situations where the node contains * elements with persistent ids and possible state info we can still preserve by moving in and then morphing @@ -359,23 +384,22 @@ var Idiomorph = (function () { * @returns {Node|null} */ function createNode(oldParent, newChild, insertionPoint, ctx) { - if (ctx.callbacks.beforeNodeAdded(newChild) === false) return null; - if (ctx.idMap.has(newChild)) { - // node has children with ids with possible state so create a dummy elt of same type and apply full morph algorithm - const newEmptyChild = document.createElement( - /** @type {Element} */ (newChild).tagName, - ); - oldParent.insertBefore(newEmptyChild, insertionPoint); - morphNode(newEmptyChild, newChild, ctx); - ctx.callbacks.afterNodeAdded(newEmptyChild); - return newEmptyChild; - } else { - // optimisation: no id state to preserve so we can just insert a clone of the newChild and its descendants - const newClonedChild = document.importNode(newChild, true); // importNode to not mutate newParent - oldParent.insertBefore(newClonedChild, insertionPoint); - ctx.callbacks.afterNodeAdded(newClonedChild); - return newClonedChild; - } + return withNodeCallbacks(ctx, "Added", newChild, () => { + if (ctx.idMap.has(newChild)) { + // node has children with ids with possible state so create a dummy elt of same type and apply full morph algorithm + const newEmptyChild = document.createElement( + /** @type {Element} */ (newChild).tagName, + ); + oldParent.insertBefore(newEmptyChild, insertionPoint); + morphNode(newEmptyChild, newChild, ctx); + return newEmptyChild; + } else { + // optimisation: no id state to preserve so we can just insert a clone of the newChild and its descendants + const newClonedChild = document.importNode(newChild, true); // importNode to not mutate newParent + oldParent.insertBefore(newClonedChild, insertionPoint); + return newClonedChild; + } + }); } //============================================================================= @@ -505,9 +529,9 @@ var Idiomorph = (function () { moveBefore(ctx.pantry, node, null); } else { // remove for realsies - if (ctx.callbacks.beforeNodeRemoved(node) === false) return; - node.parentNode?.removeChild(node); - ctx.callbacks.afterNodeRemoved(node); + withNodeCallbacks(ctx, "Removed", node, () => { + return node.parentNode?.removeChild(node); + }); } } @@ -1300,5 +1324,6 @@ var Idiomorph = (function () { return { morph, defaults, + addPlugin, }; })(); diff --git a/test/index.html b/test/index.html index f835d73..3f5db9b 100644 --- a/test/index.html +++ b/test/index.html @@ -45,6 +45,7 @@

Mocha Test Suite

+ diff --git a/test/plugins.js b/test/plugins.js new file mode 100644 index 0000000..ddfa70c --- /dev/null +++ b/test/plugins.js @@ -0,0 +1,277 @@ +describe("Plugin system", function () { + setup(); + + it("can add plugins", function () { + let calls = []; + + const plugin = { + name: "foo", + beforeNodeAdded: function (node) { + calls.push(["beforeNodeAdded", node.outerHTML]); + }, + afterNodeAdded: function (node) { + calls.push(["afterNodeAdded", node.outerHTML]); + }, + beforeNodeRemoved: function (node) { + calls.push(["beforeNodeRemoved", node.outerHTML]); + }, + afterNodeRemoved: function (node) { + calls.push(["afterNodeRemoved", node.outerHTML]); + }, + }; + Idiomorph.addPlugin(plugin); + + Idiomorph.morph(make("
"), "
"); + + calls.should.eql([ + ["beforeNodeAdded", "
"], + ["afterNodeAdded", "
"], + ["beforeNodeRemoved", "
"], + ["afterNodeRemoved", "
"], + ]); + }); + + it("can add multiple plugins", function () { + let calls = []; + + const plugin1 = { + name: "foo", + beforeNodeAdded: function (node) { + calls.push(["beforeNodeAdded1", node.outerHTML]); + }, + afterNodeAdded: function (node) { + calls.push(["afterNodeAdded1", node.outerHTML]); + }, + beforeNodeRemoved: function (node) { + calls.push(["beforeNodeRemoved1", node.outerHTML]); + }, + afterNodeRemoved: function (node) { + calls.push(["afterNodeRemoved1", node.outerHTML]); + }, + }; + Idiomorph.addPlugin(plugin1); + + const plugin2 = { + name: "bar", + beforeNodeAdded: function (node) { + calls.push(["beforeNodeAdded2", node.outerHTML]); + }, + afterNodeAdded: function (node) { + calls.push(["afterNodeAdded2", node.outerHTML]); + }, + beforeNodeRemoved: function (node) { + calls.push(["beforeNodeRemoved2", node.outerHTML]); + }, + afterNodeRemoved: function (node) { + calls.push(["afterNodeRemoved2", node.outerHTML]); + }, + }; + Idiomorph.addPlugin(plugin2); + + Idiomorph.morph(make("
"), "
"); + + calls.should.eql([ + ["beforeNodeAdded1", "
"], + ["beforeNodeAdded2", "
"], + ["afterNodeAdded2", "
"], + ["afterNodeAdded1", "
"], + ["beforeNodeRemoved1", "
"], + ["beforeNodeRemoved2", "
"], + ["afterNodeRemoved2", "
"], + ["afterNodeRemoved1", "
"], + ]); + }); + + it("can add multiple plugins alongside callbacks", function () { + let calls = []; + + const plugin1 = { + name: "foo", + beforeNodeAdded: function (node) { + calls.push(["beforeNodeAdded1", node.outerHTML]); + }, + afterNodeAdded: function (node) { + calls.push(["afterNodeAdded1", node.outerHTML]); + }, + beforeNodeRemoved: function (node) { + calls.push(["beforeNodeRemoved1", node.outerHTML]); + }, + afterNodeRemoved: function (node) { + calls.push(["afterNodeRemoved1", node.outerHTML]); + }, + }; + Idiomorph.addPlugin(plugin1); + + const plugin2 = { + name: "bar", + beforeNodeAdded: function (node) { + calls.push(["beforeNodeAdded2", node.outerHTML]); + }, + afterNodeAdded: function (node) { + calls.push(["afterNodeAdded2", node.outerHTML]); + }, + beforeNodeRemoved: function (node) { + calls.push(["beforeNodeRemoved2", node.outerHTML]); + }, + afterNodeRemoved: function (node) { + calls.push(["afterNodeRemoved2", node.outerHTML]); + }, + }; + Idiomorph.addPlugin(plugin2); + + Idiomorph.morph(make("
"), "
", { + callbacks: { + beforeNodeAdded: function (node) { + calls.push(["beforeNodeAddedCallback", node.outerHTML]); + }, + afterNodeAdded: function (node) { + calls.push(["afterNodeAddedCallback", node.outerHTML]); + }, + beforeNodeRemoved: function (node) { + calls.push(["beforeNodeRemovedCallback", node.outerHTML]); + }, + afterNodeRemoved: function (node) { + calls.push(["afterNodeRemovedCallback", node.outerHTML]); + }, + }, + }); + + calls.should.eql([ + ["beforeNodeAdded1", "
"], + ["beforeNodeAdded2", "
"], + ["beforeNodeAddedCallback", "
"], + ["afterNodeAddedCallback", "
"], + ["afterNodeAdded2", "
"], + ["afterNodeAdded1", "
"], + ["beforeNodeRemoved1", "
"], + ["beforeNodeRemoved2", "
"], + ["beforeNodeRemovedCallback", "
"], + ["afterNodeRemovedCallback", "
"], + ["afterNodeRemoved2", "
"], + ["afterNodeRemoved1", "
"], + ]); + }); + + it("the first beforeNodeAdded => false halts the entire operation", function () { + let calls = []; + + const plugin1 = { + name: "foo", + beforeNodeAdded: function (node) { + calls.push(["beforeNodeAdded1", node.outerHTML]); + return false; + }, + afterNodeAdded: function (node) { + calls.push(["afterNodeAdded1", node.outerHTML]); + }, + }; + Idiomorph.addPlugin(plugin1); + + const plugin2 = { + name: "bar", + beforeNodeAdded: function (node) { + calls.push(["beforeNodeAdded2", node.outerHTML]); + }, + afterNodeAdded: function (node) { + calls.push(["afterNodeAdded2", node.outerHTML]); + }, + }; + Idiomorph.addPlugin(plugin2); + + Idiomorph.morph(make("

"), "


", { + callbacks: { + beforeNodeAdded: function (node) { + calls.push(["beforeNodeAddedCallback", node.outerHTML]); + }, + afterNodeAdded: function (node) { + calls.push(["afterNodeAddedCallback", node.outerHTML]); + }, + }, + }); + + calls.should.eql([ + ["beforeNodeAdded1", "
"] + ]); + }); + + it("the first beforeNodeRemoved => false halts the entire operation", function () { + let calls = []; + + const plugin1 = { + name: "foo", + beforeNodeRemoved: function (node) { + calls.push(["beforeNodeRemoved1", node.outerHTML]); + return false + }, + afterNodeRemoved: function (node) { + calls.push(["afterNodeRemoved1", node.outerHTML]); + }, + }; + Idiomorph.addPlugin(plugin1); + + const plugin2 = { + name: "bar", + beforeNodeRemoved: function (node) { + calls.push(["beforeNodeRemoved2", node.outerHTML]); + }, + afterNodeRemoved: function (node) { + calls.push(["afterNodeRemoved2", node.outerHTML]); + }, + }; + Idiomorph.addPlugin(plugin2); + + Idiomorph.morph(make("
"), "
", { + callbacks: { + beforeNodeRemoved: function (node) { + calls.push(["beforeNodeRemovedCallback", node.outerHTML]); + }, + afterNodeRemoved: function (node) { + calls.push(["afterNodeRemovedCallback", node.outerHTML]); + }, + }, + }); + + calls.should.eql([ + ["beforeNodeRemoved1", "
"] + ]); + }); + + it("plugin callbacks are not all required to exist", function () { + let calls = []; + + const plugin1 = { + name: "foo", + beforeNodeAdded: function (node) { + calls.push(["beforeNodeAdded1", node.outerHTML]); + }, + afterNodeAdded: function (node) { + calls.push(["afterNodeAdded1", node.outerHTML]); + }, + }; + Idiomorph.addPlugin(plugin1); + + const plugin2 = { + name: "bar", + }; + Idiomorph.addPlugin(plugin2); + + Idiomorph.morph(make("
"), "
", { + callbacks: { + beforeNodeAdded: function (node) { + calls.push(["beforeNodeAddedCallback", node.outerHTML]); + }, + afterNodeAdded: function (node) { + calls.push(["afterNodeAddedCallback", node.outerHTML]); + }, + }, + }); + + calls.should.eql([ + ["beforeNodeAdded1", "
"], + ["beforeNodeAddedCallback", "
"], + ["afterNodeAddedCallback", "
"], + ["afterNodeAdded1", "
"], + ]); + }); +}); + From 6a9e55263d1ee87d71ce6d4f53b32bf7b0cf5d67 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Sun, 16 Feb 2025 10:56:34 -0600 Subject: [PATCH 2/6] get the rest of the node callbacks working with the plugin system. --- src/idiomorph.js | 103 ++++++++------- test/plugins.js | 325 ++++++++++++++++++++++------------------------- 2 files changed, 204 insertions(+), 224 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 43841e3..58ccb17 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -149,6 +149,28 @@ var Idiomorph = (function () { plugins[plugin.name] = plugin; } + function withNodeCallbacks(ctx, name, oldNode, newNode, fn) { + const allPlugins = [...Object.values(plugins), ctx.callbacks]; + + const shouldAbort = allPlugins.some((plugin) => { + const beforeFn = plugin[`beforeNode${name}`]; + return beforeFn && beforeFn(oldNode, newNode) === false; + }); + + if (shouldAbort) { + return name === "Added" ? null : oldNode; + } + + const resultNode = fn(); + + allPlugins.reverse().forEach((plugin) => { + const afterFn = plugin[`afterNode${name}`]; + afterFn && afterFn(resultNode, newNode); + }); + + return resultNode; + } + /** * Core idiomorph function for morphing one DOM tree to another * @@ -353,26 +375,6 @@ var Idiomorph = (function () { } } - function withNodeCallbacks(ctx, name, node, fn) { - const allPlugins = [...Object.values(plugins), ctx.callbacks]; - - const shouldAbort = allPlugins.some((plugin) => { - const beforeFn = plugin[`beforeNode${name}`]; - return beforeFn && beforeFn(node) === false; - }); - - if (shouldAbort) return; - - const resultNode = fn(); - - allPlugins.reverse().forEach((plugin) => { - const afterFn = plugin[`afterNode${name}`]; - afterFn && afterFn(resultNode); - }); - - return resultNode; - } - /** * This performs the action of inserting a new node while handling situations where the node contains * elements with persistent ids and possible state info we can still preserve by moving in and then morphing @@ -384,7 +386,7 @@ var Idiomorph = (function () { * @returns {Node|null} */ function createNode(oldParent, newChild, insertionPoint, ctx) { - return withNodeCallbacks(ctx, "Added", newChild, () => { + return withNodeCallbacks(ctx, "Added", newChild, undefined, () => { if (ctx.idMap.has(newChild)) { // node has children with ids with possible state so create a dummy elt of same type and apply full morph algorithm const newEmptyChild = document.createElement( @@ -529,7 +531,7 @@ var Idiomorph = (function () { moveBefore(ctx.pantry, node, null); } else { // remove for realsies - withNodeCallbacks(ctx, "Removed", node, () => { + withNodeCallbacks(ctx, "Removed", node, undefined, () => { return node.parentNode?.removeChild(node); }); } @@ -642,31 +644,28 @@ var Idiomorph = (function () { return null; } - if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) { - return oldNode; - } - - if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) { - // ignore the head element - } else if ( - oldNode instanceof HTMLHeadElement && - ctx.head.style !== "morph" - ) { - // ok to cast: if newContent wasn't also a , it would've got caught in the `!isSoftMatch` branch above - handleHeadElement( - oldNode, - /** @type {HTMLHeadElement} */ (newContent), - ctx, - ); - } else { - morphAttributes(oldNode, newContent, ctx); - if (!ignoreValueOfActiveElement(oldNode, ctx)) { - // @ts-ignore newContent can be a node here because .firstChild will be null - morphChildren(ctx, oldNode, newContent); + return withNodeCallbacks(ctx, "Morphed", oldNode, newContent, () => { + if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) { + // ignore the head element + } else if ( + oldNode instanceof HTMLHeadElement && + ctx.head.style !== "morph" + ) { + // ok to cast: if newContent wasn't also a , it would've got caught in the `!isSoftMatch` branch above + handleHeadElement( + oldNode, + /** @type {HTMLHeadElement} */ (newContent), + ctx, + ); + } else { + morphAttributes(oldNode, newContent, ctx); + if (!ignoreValueOfActiveElement(oldNode, ctx)) { + // @ts-ignore newContent can be a node here because .firstChild will be null + morphChildren(ctx, oldNode, newContent); + } } - } - ctx.callbacks.afterNodeMorphed(oldNode, newContent); - return oldNode; + return oldNode; + }); } /** @@ -957,7 +956,7 @@ var Idiomorph = (function () { document.createRange().createContextualFragment(newNode.outerHTML) .firstChild ); - if (ctx.callbacks.beforeNodeAdded(newElt) !== false) { + withNodeCallbacks(ctx, "Added", newElt, undefined, () => { if ( ("href" in newElt && newElt.href) || ("src" in newElt && newElt.src) @@ -972,18 +971,18 @@ var Idiomorph = (function () { promises.push(promise); } oldHead.appendChild(newElt); - ctx.callbacks.afterNodeAdded(newElt); added.push(newElt); - } + return newElt; + }); } // remove all removed elements, after we have appended the new elements to avoid // additional network requests for things like style sheets for (const removedElement of removed) { - if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) { + withNodeCallbacks(ctx, "Removed", removedElement, undefined, () => { oldHead.removeChild(removedElement); - ctx.callbacks.afterNodeRemoved(removedElement); - } + return removedElement; + }); } ctx.head.afterHeadMorphed(oldHead, { diff --git a/test/plugins.js b/test/plugins.js index ddfa70c..e7e91f0 100644 --- a/test/plugins.js +++ b/test/plugins.js @@ -1,277 +1,258 @@ +const toString = (node) => node.outerHTML || node.textContent; + +function buildLoggingPlugin(name, log) { + return { + name, + beforeNodeAdded: function (node) { + log.push([`${name}:beforeNodeAdded`, toString(node)]); + }, + afterNodeAdded: function (node) { + log.push([`${name}:afterNodeAdded`, toString(node)]); + }, + beforeNodeRemoved: function (node) { + log.push([`${name}:beforeNodeRemoved`, toString(node)]); + }, + afterNodeRemoved: function (node) { + log.push([`${name}:afterNodeRemoved`, toString(node)]); + }, + beforeNodeMorphed: function (oldNode, newNode) { + log.push([ + `${name}:beforeNodeMorphed`, + toString(oldNode), + toString(newNode), + ]); + }, + afterNodeMorphed: function (oldNode, newNode) { + log.push([ + `${name}:afterNodeMorphed`, + toString(oldNode), + toString(newNode), + ]); + }, + }; +} + describe("Plugin system", function () { setup(); it("can add plugins", function () { - let calls = []; - - const plugin = { - name: "foo", - beforeNodeAdded: function (node) { - calls.push(["beforeNodeAdded", node.outerHTML]); - }, - afterNodeAdded: function (node) { - calls.push(["afterNodeAdded", node.outerHTML]); - }, - beforeNodeRemoved: function (node) { - calls.push(["beforeNodeRemoved", node.outerHTML]); - }, - afterNodeRemoved: function (node) { - calls.push(["afterNodeRemoved", node.outerHTML]); - }, - }; + let log = []; + const plugin = buildLoggingPlugin("foo", log); Idiomorph.addPlugin(plugin); - Idiomorph.morph(make("
"), "
"); + Idiomorph.morph(make(`


A

`), `
B`, { + morphStyle: "innerHTML", + }); - calls.should.eql([ - ["beforeNodeAdded", "
"], - ["afterNodeAdded", "
"], - ["beforeNodeRemoved", "
"], - ["afterNodeRemoved", "
"], + log.should.eql([ + ["foo:beforeNodeAdded", "
"], + ["foo:afterNodeAdded", "
"], + ["foo:beforeNodeRemoved", "
"], + ["foo:afterNodeRemoved", "
"], + ["foo:beforeNodeMorphed", "A", "B"], + ["foo:beforeNodeMorphed", "A", "B"], + ["foo:afterNodeMorphed", "B", "B"], + ["foo:afterNodeMorphed", "B", "B"], ]); }); it("can add multiple plugins", function () { - let calls = []; + let log = []; + const fooPlugin = buildLoggingPlugin("foo", log); + const barPlugin = buildLoggingPlugin("bar", log); + Idiomorph.addPlugin(fooPlugin); + Idiomorph.addPlugin(barPlugin); - const plugin1 = { - name: "foo", - beforeNodeAdded: function (node) { - calls.push(["beforeNodeAdded1", node.outerHTML]); - }, - afterNodeAdded: function (node) { - calls.push(["afterNodeAdded1", node.outerHTML]); - }, - beforeNodeRemoved: function (node) { - calls.push(["beforeNodeRemoved1", node.outerHTML]); - }, - afterNodeRemoved: function (node) { - calls.push(["afterNodeRemoved1", node.outerHTML]); - }, - }; - Idiomorph.addPlugin(plugin1); - - const plugin2 = { - name: "bar", - beforeNodeAdded: function (node) { - calls.push(["beforeNodeAdded2", node.outerHTML]); - }, - afterNodeAdded: function (node) { - calls.push(["afterNodeAdded2", node.outerHTML]); - }, - beforeNodeRemoved: function (node) { - calls.push(["beforeNodeRemoved2", node.outerHTML]); - }, - afterNodeRemoved: function (node) { - calls.push(["afterNodeRemoved2", node.outerHTML]); - }, - }; - Idiomorph.addPlugin(plugin2); - - Idiomorph.morph(make("
"), "
"); + Idiomorph.morph(make(`


A

`), `
B`, { + morphStyle: "innerHTML", + }); - calls.should.eql([ - ["beforeNodeAdded1", "
"], - ["beforeNodeAdded2", "
"], - ["afterNodeAdded2", "
"], - ["afterNodeAdded1", "
"], - ["beforeNodeRemoved1", "
"], - ["beforeNodeRemoved2", "
"], - ["afterNodeRemoved2", "
"], - ["afterNodeRemoved1", "
"], + log.should.eql([ + ["foo:beforeNodeAdded", "
"], + ["bar:beforeNodeAdded", "
"], + ["bar:afterNodeAdded", "
"], + ["foo:afterNodeAdded", "
"], + ["foo:beforeNodeRemoved", "
"], + ["bar:beforeNodeRemoved", "
"], + ["bar:afterNodeRemoved", "
"], + ["foo:afterNodeRemoved", "
"], + ["foo:beforeNodeMorphed", "A", "B"], + ["bar:beforeNodeMorphed", "A", "B"], + ["foo:beforeNodeMorphed", "A", "B"], + ["bar:beforeNodeMorphed", "A", "B"], + ["bar:afterNodeMorphed", "B", "B"], + ["foo:afterNodeMorphed", "B", "B"], + ["bar:afterNodeMorphed", "B", "B"], + ["foo:afterNodeMorphed", "B", "B"], ]); }); it("can add multiple plugins alongside callbacks", function () { - let calls = []; + let log = []; + const fooPlugin = buildLoggingPlugin("foo", log); + const barPlugin = buildLoggingPlugin("bar", log); + Idiomorph.addPlugin(fooPlugin); + Idiomorph.addPlugin(barPlugin); - const plugin1 = { - name: "foo", - beforeNodeAdded: function (node) { - calls.push(["beforeNodeAdded1", node.outerHTML]); - }, - afterNodeAdded: function (node) { - calls.push(["afterNodeAdded1", node.outerHTML]); - }, - beforeNodeRemoved: function (node) { - calls.push(["beforeNodeRemoved1", node.outerHTML]); - }, - afterNodeRemoved: function (node) { - calls.push(["afterNodeRemoved1", node.outerHTML]); - }, - }; - Idiomorph.addPlugin(plugin1); - - const plugin2 = { - name: "bar", - beforeNodeAdded: function (node) { - calls.push(["beforeNodeAdded2", node.outerHTML]); - }, - afterNodeAdded: function (node) { - calls.push(["afterNodeAdded2", node.outerHTML]); - }, - beforeNodeRemoved: function (node) { - calls.push(["beforeNodeRemoved2", node.outerHTML]); - }, - afterNodeRemoved: function (node) { - calls.push(["afterNodeRemoved2", node.outerHTML]); - }, - }; - Idiomorph.addPlugin(plugin2); - - Idiomorph.morph(make("
"), "
", { + Idiomorph.morph(make(`


A

`), `
B`, { + morphStyle: "innerHTML", callbacks: { beforeNodeAdded: function (node) { - calls.push(["beforeNodeAddedCallback", node.outerHTML]); + log.push([`beforeNodeAdded`, toString(node)]); }, afterNodeAdded: function (node) { - calls.push(["afterNodeAddedCallback", node.outerHTML]); + log.push([`afterNodeAdded`, toString(node)]); }, beforeNodeRemoved: function (node) { - calls.push(["beforeNodeRemovedCallback", node.outerHTML]); + log.push([`beforeNodeRemoved`, toString(node)]); }, afterNodeRemoved: function (node) { - calls.push(["afterNodeRemovedCallback", node.outerHTML]); + log.push([`afterNodeRemoved`, toString(node)]); + }, + beforeNodeMorphed: function (oldNode, newNode) { + log.push([`beforeNodeMorphed`, toString(oldNode), toString(newNode)]); + }, + afterNodeMorphed: function (oldNode, newNode) { + log.push([`afterNodeMorphed`, toString(oldNode), toString(newNode)]); }, }, }); - calls.should.eql([ - ["beforeNodeAdded1", "
"], - ["beforeNodeAdded2", "
"], - ["beforeNodeAddedCallback", "
"], - ["afterNodeAddedCallback", "
"], - ["afterNodeAdded2", "
"], - ["afterNodeAdded1", "
"], - ["beforeNodeRemoved1", "
"], - ["beforeNodeRemoved2", "
"], - ["beforeNodeRemovedCallback", "
"], - ["afterNodeRemovedCallback", "
"], - ["afterNodeRemoved2", "
"], - ["afterNodeRemoved1", "
"], + log.should.eql([ + ["foo:beforeNodeAdded", "
"], + ["bar:beforeNodeAdded", "
"], + ["beforeNodeAdded", "
"], + ["afterNodeAdded", "
"], + ["bar:afterNodeAdded", "
"], + ["foo:afterNodeAdded", "
"], + ["foo:beforeNodeRemoved", "
"], + ["bar:beforeNodeRemoved", "
"], + ["beforeNodeRemoved", "
"], + ["afterNodeRemoved", "
"], + ["bar:afterNodeRemoved", "
"], + ["foo:afterNodeRemoved", "
"], + ["foo:beforeNodeMorphed", "A", "B"], + ["bar:beforeNodeMorphed", "A", "B"], + ["beforeNodeMorphed", "A", "B"], + ["foo:beforeNodeMorphed", "A", "B"], + ["bar:beforeNodeMorphed", "A", "B"], + ["beforeNodeMorphed", "A", "B"], + ["afterNodeMorphed", "B", "B"], + ["bar:afterNodeMorphed", "B", "B"], + ["foo:afterNodeMorphed", "B", "B"], + ["afterNodeMorphed", "B", "B"], + ["bar:afterNodeMorphed", "B", "B"], + ["foo:afterNodeMorphed", "B", "B"], ]); }); it("the first beforeNodeAdded => false halts the entire operation", function () { - let calls = []; + let log = []; - const plugin1 = { + Idiomorph.addPlugin({ name: "foo", beforeNodeAdded: function (node) { - calls.push(["beforeNodeAdded1", node.outerHTML]); + log.push(["foo:beforeNodeAdded", node.outerHTML]); return false; }, afterNodeAdded: function (node) { - calls.push(["afterNodeAdded1", node.outerHTML]); + log.push(["foo:afterNodeAdded", node.outerHTML]); }, - }; - Idiomorph.addPlugin(plugin1); + }); - const plugin2 = { + Idiomorph.addPlugin({ name: "bar", beforeNodeAdded: function (node) { - calls.push(["beforeNodeAdded2", node.outerHTML]); + log.push(["bar:beforeNodeAdded", node.outerHTML]); }, afterNodeAdded: function (node) { - calls.push(["afterNodeAdded2", node.outerHTML]); + log.push(["bar:afterNodeAdded", node.outerHTML]); }, - }; - Idiomorph.addPlugin(plugin2); + }); Idiomorph.morph(make("

"), "


", { callbacks: { beforeNodeAdded: function (node) { - calls.push(["beforeNodeAddedCallback", node.outerHTML]); + log.push(["beforeNodeAddedCallback", node.outerHTML]); }, afterNodeAdded: function (node) { - calls.push(["afterNodeAddedCallback", node.outerHTML]); + log.push(["afterNodeAddedCallback", node.outerHTML]); }, }, }); - calls.should.eql([ - ["beforeNodeAdded1", "
"] - ]); + log.should.eql([["foo:beforeNodeAdded", "
"]]); }); it("the first beforeNodeRemoved => false halts the entire operation", function () { - let calls = []; + let log = []; - const plugin1 = { + Idiomorph.addPlugin({ name: "foo", beforeNodeRemoved: function (node) { - calls.push(["beforeNodeRemoved1", node.outerHTML]); - return false + log.push(["foo:beforeNodeRemoved", node.outerHTML]); + return false; }, afterNodeRemoved: function (node) { - calls.push(["afterNodeRemoved1", node.outerHTML]); + log.push(["foo:afterNodeRemoved", node.outerHTML]); }, - }; - Idiomorph.addPlugin(plugin1); + }); - const plugin2 = { + Idiomorph.addPlugin({ name: "bar", beforeNodeRemoved: function (node) { - calls.push(["beforeNodeRemoved2", node.outerHTML]); + log.push(["bar:beforeNodeRemoved2", node.outerHTML]); }, afterNodeRemoved: function (node) { - calls.push(["afterNodeRemoved2", node.outerHTML]); + log.push(["bar:afterNodeRemoved2", node.outerHTML]); }, - }; - Idiomorph.addPlugin(plugin2); + }); Idiomorph.morph(make("
"), "
", { callbacks: { beforeNodeRemoved: function (node) { - calls.push(["beforeNodeRemovedCallback", node.outerHTML]); + log.push(["beforeNodeRemoved", node.outerHTML]); }, afterNodeRemoved: function (node) { - calls.push(["afterNodeRemovedCallback", node.outerHTML]); + log.push(["afterNodeRemoved", node.outerHTML]); }, }, }); - calls.should.eql([ - ["beforeNodeRemoved1", "
"] - ]); + log.should.eql([["foo:beforeNodeRemoved", "
"]]); }); it("plugin callbacks are not all required to exist", function () { - let calls = []; + let log = []; - const plugin1 = { + Idiomorph.addPlugin({ name: "foo", beforeNodeAdded: function (node) { - calls.push(["beforeNodeAdded1", node.outerHTML]); + log.push(["foo:beforeNodeAdded", node.outerHTML]); }, afterNodeAdded: function (node) { - calls.push(["afterNodeAdded1", node.outerHTML]); + log.push(["foo:afterNodeAdded", node.outerHTML]); }, - }; - Idiomorph.addPlugin(plugin1); + }); - const plugin2 = { - name: "bar", - }; - Idiomorph.addPlugin(plugin2); + Idiomorph.addPlugin({ name: "bar" }); Idiomorph.morph(make("
"), "
", { callbacks: { beforeNodeAdded: function (node) { - calls.push(["beforeNodeAddedCallback", node.outerHTML]); + log.push(["beforeNodeAdded", node.outerHTML]); }, afterNodeAdded: function (node) { - calls.push(["afterNodeAddedCallback", node.outerHTML]); + log.push(["afterNodeAdded", node.outerHTML]); }, }, }); - calls.should.eql([ - ["beforeNodeAdded1", "
"], - ["beforeNodeAddedCallback", "
"], - ["afterNodeAddedCallback", "
"], - ["afterNodeAdded1", "
"], + log.should.eql([ + ["foo:beforeNodeAdded", "
"], + ["beforeNodeAdded", "
"], + ["afterNodeAdded", "
"], + ["foo:afterNodeAdded", "
"], ]); }); }); - From 2dca99c8d17239fad9a3c3230cd49d97a234a37e Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Sun, 16 Feb 2025 11:34:43 -0600 Subject: [PATCH 3/6] add beforeAttributeUpdated callback to plugin system. --- src/idiomorph.js | 13 +- test/plugins.js | 331 +++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 302 insertions(+), 42 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 58ccb17..fcd660c 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -171,6 +171,17 @@ var Idiomorph = (function () { return resultNode; } + function beforeAttributeUpdatedCallbacks(ctx, attr, element, updateType) { + const allPlugins = [...Object.values(plugins), ctx.callbacks]; + + const shouldAbort = allPlugins.some((plugin) => { + const beforeFn = plugin[`beforeAttributeUpdated`]; + return beforeFn && beforeFn(attr, element, updateType) === false; + }); + + if (shouldAbort) return false; + } + /** * Core idiomorph function for morphing one DOM tree to another * @@ -839,7 +850,7 @@ var Idiomorph = (function () { return true; } return ( - ctx.callbacks.beforeAttributeUpdated(attr, element, updateType) === + beforeAttributeUpdatedCallbacks(ctx, attr, element, updateType) === false ); } diff --git a/test/plugins.js b/test/plugins.js index e7e91f0..8547206 100644 --- a/test/plugins.js +++ b/test/plugins.js @@ -29,6 +29,14 @@ function buildLoggingPlugin(name, log) { toString(newNode), ]); }, + beforeAttributeUpdated: function (attribute, node, updateType) { + log.push([ + `${name}:beforeAttributeUpdated`, + toString(node), + attribute, + updateType, + ]); + }, }; } @@ -40,19 +48,49 @@ describe("Plugin system", function () { const plugin = buildLoggingPlugin("foo", log); Idiomorph.addPlugin(plugin); - Idiomorph.morph(make(`


A

`), `
B`, { - morphStyle: "innerHTML", - }); + Idiomorph.morph( + make(`


A

`), + `
B`, + { + morphStyle: "innerHTML", + }, + ); log.should.eql([ ["foo:beforeNodeAdded", "
"], ["foo:afterNodeAdded", "
"], ["foo:beforeNodeRemoved", "
"], ["foo:afterNodeRemoved", "
"], - ["foo:beforeNodeMorphed", "A", "B"], + [ + "foo:beforeNodeMorphed", + `A`, + `B`, + ], + [ + "foo:beforeAttributeUpdated", + `A`, + "name", + "update", + ], + [ + "foo:beforeAttributeUpdated", + `A`, + "new", + "update", + ], + [ + "foo:beforeAttributeUpdated", + `A`, + "old", + "remove", + ], ["foo:beforeNodeMorphed", "A", "B"], ["foo:afterNodeMorphed", "B", "B"], - ["foo:afterNodeMorphed", "B", "B"], + [ + "foo:afterNodeMorphed", + `B`, + `B`, + ], ]); }); @@ -63,9 +101,13 @@ describe("Plugin system", function () { Idiomorph.addPlugin(fooPlugin); Idiomorph.addPlugin(barPlugin); - Idiomorph.morph(make(`


A

`), `
B`, { - morphStyle: "innerHTML", - }); + Idiomorph.morph( + make(`


A

`), + `
B`, + { + morphStyle: "innerHTML", + }, + ); log.should.eql([ ["foo:beforeNodeAdded", "
"], @@ -76,14 +118,66 @@ describe("Plugin system", function () { ["bar:beforeNodeRemoved", "
"], ["bar:afterNodeRemoved", "
"], ["foo:afterNodeRemoved", "
"], - ["foo:beforeNodeMorphed", "A", "B"], - ["bar:beforeNodeMorphed", "A", "B"], + [ + "foo:beforeNodeMorphed", + `A`, + `B`, + ], + [ + "bar:beforeNodeMorphed", + `A`, + `B`, + ], + [ + "foo:beforeAttributeUpdated", + `A`, + "name", + "update", + ], + [ + "bar:beforeAttributeUpdated", + `A`, + "name", + "update", + ], + [ + "foo:beforeAttributeUpdated", + `A`, + "new", + "update", + ], + [ + "bar:beforeAttributeUpdated", + `A`, + "new", + "update", + ], + [ + "foo:beforeAttributeUpdated", + `A`, + "old", + "remove", + ], + [ + "bar:beforeAttributeUpdated", + `A`, + "old", + "remove", + ], ["foo:beforeNodeMorphed", "A", "B"], ["bar:beforeNodeMorphed", "A", "B"], ["bar:afterNodeMorphed", "B", "B"], ["foo:afterNodeMorphed", "B", "B"], - ["bar:afterNodeMorphed", "B", "B"], - ["foo:afterNodeMorphed", "B", "B"], + [ + "bar:afterNodeMorphed", + `B`, + `B`, + ], + [ + "foo:afterNodeMorphed", + `B`, + `B`, + ], ]); }); @@ -94,29 +188,49 @@ describe("Plugin system", function () { Idiomorph.addPlugin(fooPlugin); Idiomorph.addPlugin(barPlugin); - Idiomorph.morph(make(`


A

`), `
B`, { - morphStyle: "innerHTML", - callbacks: { - beforeNodeAdded: function (node) { - log.push([`beforeNodeAdded`, toString(node)]); - }, - afterNodeAdded: function (node) { - log.push([`afterNodeAdded`, toString(node)]); - }, - beforeNodeRemoved: function (node) { - log.push([`beforeNodeRemoved`, toString(node)]); - }, - afterNodeRemoved: function (node) { - log.push([`afterNodeRemoved`, toString(node)]); - }, - beforeNodeMorphed: function (oldNode, newNode) { - log.push([`beforeNodeMorphed`, toString(oldNode), toString(newNode)]); - }, - afterNodeMorphed: function (oldNode, newNode) { - log.push([`afterNodeMorphed`, toString(oldNode), toString(newNode)]); + Idiomorph.morph( + make(`


A

`), + `
B`, + { + morphStyle: "innerHTML", + callbacks: { + beforeNodeAdded: function (node) { + log.push([`beforeNodeAdded`, toString(node)]); + }, + afterNodeAdded: function (node) { + log.push([`afterNodeAdded`, toString(node)]); + }, + beforeNodeRemoved: function (node) { + log.push([`beforeNodeRemoved`, toString(node)]); + }, + afterNodeRemoved: function (node) { + log.push([`afterNodeRemoved`, toString(node)]); + }, + beforeNodeMorphed: function (oldNode, newNode) { + log.push([ + `beforeNodeMorphed`, + toString(oldNode), + toString(newNode), + ]); + }, + afterNodeMorphed: function (oldNode, newNode) { + log.push([ + `afterNodeMorphed`, + toString(oldNode), + toString(newNode), + ]); + }, + beforeAttributeUpdated: function (attribute, node, updateType) { + log.push([ + `beforeAttributeUpdated`, + toString(node), + attribute, + updateType, + ]); + }, }, }, - }); + ); log.should.eql([ ["foo:beforeNodeAdded", "
"], @@ -131,18 +245,86 @@ describe("Plugin system", function () { ["afterNodeRemoved", "
"], ["bar:afterNodeRemoved", "
"], ["foo:afterNodeRemoved", "
"], - ["foo:beforeNodeMorphed", "A", "B"], - ["bar:beforeNodeMorphed", "A", "B"], - ["beforeNodeMorphed", "A", "B"], + [ + "foo:beforeNodeMorphed", + `A`, + `B`, + ], + [ + "bar:beforeNodeMorphed", + `A`, + `B`, + ], + [ + "beforeNodeMorphed", + `A`, + `B`, + ], + [ + "foo:beforeAttributeUpdated", + `A`, + "name", + "update", + ], + [ + "bar:beforeAttributeUpdated", + `A`, + "name", + "update", + ], + ["beforeAttributeUpdated", `A`, "name", "update"], + [ + "foo:beforeAttributeUpdated", + `A`, + "new", + "update", + ], + [ + "bar:beforeAttributeUpdated", + `A`, + "new", + "update", + ], + ["beforeAttributeUpdated", `A`, "new", "update"], + [ + "foo:beforeAttributeUpdated", + `A`, + "old", + "remove", + ], + [ + "bar:beforeAttributeUpdated", + `A`, + "old", + "remove", + ], + [ + "beforeAttributeUpdated", + `A`, + "old", + "remove", + ], ["foo:beforeNodeMorphed", "A", "B"], ["bar:beforeNodeMorphed", "A", "B"], ["beforeNodeMorphed", "A", "B"], ["afterNodeMorphed", "B", "B"], ["bar:afterNodeMorphed", "B", "B"], ["foo:afterNodeMorphed", "B", "B"], - ["afterNodeMorphed", "B", "B"], - ["bar:afterNodeMorphed", "B", "B"], - ["foo:afterNodeMorphed", "B", "B"], + [ + "afterNodeMorphed", + `B`, + `B`, + ], + [ + "bar:afterNodeMorphed", + `B`, + `B`, + ], + [ + "foo:afterNodeMorphed", + `B`, + `B`, + ], ]); }); @@ -184,6 +366,44 @@ describe("Plugin system", function () { log.should.eql([["foo:beforeNodeAdded", "
"]]); }); + it("the first beforeNodeMorphed => false halts the entire operation", function () { + let log = []; + + Idiomorph.addPlugin({ + name: "foo", + beforeNodeMorphed: function (node) { + log.push(["foo:beforeNodeMorphed", node.outerHTML]); + return false; + }, + afterNodeMorphed: function (node) { + log.push(["foo:afterNodeMorphed", node.outerHTML]); + }, + }); + + Idiomorph.addPlugin({ + name: "bar", + beforeNodeMorphed: function (node) { + log.push(["bar:beforeNodeMorphed", node.outerHTML]); + }, + afterNodeMorphed: function (node) { + log.push(["bar:afterNodeMorphed", node.outerHTML]); + }, + }); + + Idiomorph.morph(make(`
`), `
`, { + callbacks: { + beforeNodeMorphed: function (node) { + log.push(["beforeNodeMorphed", node.outerHTML]); + }, + afterNodeMorphed: function (node) { + log.push(["afterNodeMorphed", node.outerHTML]); + }, + }, + }); + + log.should.eql([["foo:beforeNodeMorphed", `
`]]); + }); + it("the first beforeNodeRemoved => false halts the entire operation", function () { let log = []; @@ -201,10 +421,10 @@ describe("Plugin system", function () { Idiomorph.addPlugin({ name: "bar", beforeNodeRemoved: function (node) { - log.push(["bar:beforeNodeRemoved2", node.outerHTML]); + log.push(["bar:beforeNodeRemoved", node.outerHTML]); }, afterNodeRemoved: function (node) { - log.push(["bar:afterNodeRemoved2", node.outerHTML]); + log.push(["bar:afterNodeRemoved", node.outerHTML]); }, }); @@ -222,6 +442,35 @@ describe("Plugin system", function () { log.should.eql([["foo:beforeNodeRemoved", "
"]]); }); + it("the first beforeAttributeUpdated => false halts the entire operation", function () { + let log = []; + + Idiomorph.addPlugin({ + name: "foo", + beforeAttributeUpdated: function (attr, node, updateType) { + log.push(["foo:beforeAttributeUpdated", node.outerHTML]); + return false; + }, + }); + + Idiomorph.addPlugin({ + name: "bar", + beforeAttributeUpdated: function (attr, node, updateType) { + log.push(["bar:beforeAttributeUpdated", node.outerHTML]); + }, + }); + + Idiomorph.morph(make(`
`), `
`, { + callbacks: { + beforeAttributeUpdated: function (attr, node, updateType) { + log.push(["beforeAttributeUpdated", node.outerHTML]); + }, + }, + }); + + log.should.eql([["foo:beforeAttributeUpdated", `
`]]); + }); + it("plugin callbacks are not all required to exist", function () { let log = []; From 6a0350e4fc5f8e3dee1106aa6bdb4be85638a248 Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Sun, 16 Feb 2025 11:48:04 -0600 Subject: [PATCH 4/6] refactor callbacks to reduce array allocations. --- src/idiomorph.js | 70 +++++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index fcd660c..732ba9f 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -149,39 +149,6 @@ var Idiomorph = (function () { plugins[plugin.name] = plugin; } - function withNodeCallbacks(ctx, name, oldNode, newNode, fn) { - const allPlugins = [...Object.values(plugins), ctx.callbacks]; - - const shouldAbort = allPlugins.some((plugin) => { - const beforeFn = plugin[`beforeNode${name}`]; - return beforeFn && beforeFn(oldNode, newNode) === false; - }); - - if (shouldAbort) { - return name === "Added" ? null : oldNode; - } - - const resultNode = fn(); - - allPlugins.reverse().forEach((plugin) => { - const afterFn = plugin[`afterNode${name}`]; - afterFn && afterFn(resultNode, newNode); - }); - - return resultNode; - } - - function beforeAttributeUpdatedCallbacks(ctx, attr, element, updateType) { - const allPlugins = [...Object.values(plugins), ctx.callbacks]; - - const shouldAbort = allPlugins.some((plugin) => { - const beforeFn = plugin[`beforeAttributeUpdated`]; - return beforeFn && beforeFn(attr, element, updateType) === false; - }); - - if (shouldAbort) return false; - } - /** * Core idiomorph function for morphing one DOM tree to another * @@ -193,7 +160,7 @@ var Idiomorph = (function () { function morph(oldNode, newContent, config = {}) { oldNode = normalizeElement(oldNode); const newNode = normalizeParent(newContent); - const ctx = createMorphContext(oldNode, newNode, config); + const ctx = createMorphContext(oldNode, newNode, config, plugins); const morphedNodes = saveAndRestoreFocus(ctx, () => { return withHeadBlocking( @@ -871,6 +838,36 @@ var Idiomorph = (function () { return morphNode; })(); + //============================================================================= + // Callback Functions + //============================================================================= + function withNodeCallbacks(ctx, name, oldNode, newNode, fn) { + const shouldPrevent = ctx.callbacks.some((plugin) => { + return plugin[`beforeNode${name}`]?.(oldNode, newNode) === false; + }); + + if (shouldPrevent) { + return name === "Added" ? null : oldNode; + } + + const resultNode = fn(); + + // iterate backwards without a new array or index allocation + ctx.callbacks.reduceRight((_, plugin) => { + plugin[`afterNode${name}`]?.(resultNode, newNode); + }, null); + + return resultNode; + } + + function beforeAttributeUpdatedCallbacks(ctx, attr, element, updateType) { + const shouldPrevent = ctx.callbacks.some((plugin) => { + return plugin[`beforeAttributeUpdated`]?.(attr, element, updateType) === false; + }); + + if (shouldPrevent) return false; + } + //============================================================================= // Head Management Functions //============================================================================= @@ -1015,7 +1012,7 @@ var Idiomorph = (function () { * @param {Config} config * @returns {MorphContext} */ - function createMorphContext(oldNode, newContent, config) { + function createMorphContext(oldNode, newContent, config, plugins) { const { persistentIds, idMap } = createIdMaps(oldNode, newContent); const mergedConfig = mergeDefaults(config); @@ -1023,6 +1020,7 @@ var Idiomorph = (function () { if (!["innerHTML", "outerHTML"].includes(morphStyle)) { throw `Do not understand how to morph style ${morphStyle}`; } + const callbacks = [...Object.values(plugins), mergedConfig.callbacks]; return { target: oldNode, @@ -1035,7 +1033,7 @@ var Idiomorph = (function () { idMap: idMap, persistentIds: persistentIds, pantry: createPantry(), - callbacks: mergedConfig.callbacks, + callbacks: callbacks, head: mergedConfig.head, }; } From 0956efddd6a2279001468e300fa4957ca8ddba1b Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Sun, 16 Feb 2025 12:22:54 -0600 Subject: [PATCH 5/6] satisfy tsc. --- src/idiomorph.js | 83 +++++++++++++++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 30 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index 732ba9f..931d034 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -11,7 +11,7 @@ */ /** - * @typedef {object} ConfigCallbacks + * @typedef {object} Callbacks * * @property {function(Node): boolean} [beforeNodeAdded] * @property {function(Node): void} [afterNodeAdded] @@ -22,6 +22,10 @@ * @property {function(string, Element, "update" | "remove"): boolean} [beforeAttributeUpdated] */ +/** + * @typedef {Callbacks & { name: string }} IdiomorphPlugin + */ + /** * @typedef {object} Config * @@ -29,7 +33,7 @@ * @property {boolean} [ignoreActive] * @property {boolean} [ignoreActiveValue] * @property {boolean} [restoreFocus] - * @property {ConfigCallbacks} [callbacks] + * @property {Callbacks} [callbacks] * @property {ConfigHead} [head] */ @@ -51,18 +55,6 @@ * @property {(function(Element, {added: Node[], kept: Element[], removed: Element[]}): void) | NoOp} afterHeadMorphed */ -/** - * @typedef {object} ConfigCallbacksInternal - * - * @property {(function(Node): boolean) | NoOp} beforeNodeAdded - * @property {(function(Node): void) | NoOp} afterNodeAdded - * @property {(function(Node, Node): boolean) | NoOp} beforeNodeMorphed - * @property {(function(Node, Node): void) | NoOp} afterNodeMorphed - * @property {(function(Node): boolean) | NoOp} beforeNodeRemoved - * @property {(function(Node): void) | NoOp} afterNodeRemoved - * @property {(function(string, Element, "update" | "remove"): boolean) | NoOp} beforeAttributeUpdated - */ - /** * @typedef {object} ConfigInternal * @@ -70,7 +62,7 @@ * @property {boolean} [ignoreActive] * @property {boolean} [ignoreActiveValue] * @property {boolean} [restoreFocus] - * @property {ConfigCallbacksInternal} callbacks + * @property {Callbacks} callbacks * @property {ConfigHeadInternal} head */ @@ -109,7 +101,7 @@ var Idiomorph = (function () { * @property {ConfigInternal['restoreFocus']} restoreFocus * @property {Map>} idMap * @property {Set} persistentIds - * @property {ConfigInternal['callbacks']} callbacks + * @property {Array} callbacks * @property {ConfigInternal['head']} head * @property {HTMLDivElement} pantry */ @@ -118,7 +110,6 @@ var Idiomorph = (function () { // AND NOW IT BEGINS... //============================================================================= - const noOp = () => {}; /** * Default configuration values, updatable by users now * @type {ConfigInternal} @@ -126,27 +117,36 @@ var Idiomorph = (function () { const defaults = { morphStyle: "outerHTML", callbacks: { - beforeNodeAdded: noOp, - afterNodeAdded: noOp, - beforeNodeMorphed: noOp, - afterNodeMorphed: noOp, - beforeNodeRemoved: noOp, - afterNodeRemoved: noOp, - beforeAttributeUpdated: noOp, + beforeNodeAdded: () => true, + afterNodeAdded: () => {}, + beforeNodeMorphed: () => true, + afterNodeMorphed: () => {}, + beforeNodeRemoved: () => true, + afterNodeRemoved: () => {}, + beforeAttributeUpdated: () => true, }, head: { style: "merge", shouldPreserve: (elt) => elt.getAttribute("im-preserve") === "true", shouldReAppend: (elt) => elt.getAttribute("im-re-append") === "true", - shouldRemove: noOp, - afterHeadMorphed: noOp, + shouldRemove: () => {}, + afterHeadMorphed: () => {}, }, restoreFocus: true, }; - let plugins = {}; + /** @type {Map} */ + let plugins = new Map(); + /** + * Add a plugin to the morphing system + * @param {IdiomorphPlugin} plugin + * @returns {void} + */ function addPlugin(plugin) { - plugins[plugin.name] = plugin; + const name = plugin.name; + // @ts-ignore we can delete this property + delete plugin.name; + plugins.set(name, plugin); } /** @@ -841,8 +841,17 @@ var Idiomorph = (function () { //============================================================================= // Callback Functions //============================================================================= + /** + * @param {MorphContext} ctx + * @param {string} name + * @param {Node | null} oldNode + * @param {Node | null | undefined} newNode + * @param {function} fn + * @returns {Node | null} + */ function withNodeCallbacks(ctx, name, oldNode, newNode, fn) { const shouldPrevent = ctx.callbacks.some((plugin) => { + // @ts-ignore - we know this is a function return plugin[`beforeNode${name}`]?.(oldNode, newNode) === false; }); @@ -853,16 +862,27 @@ var Idiomorph = (function () { const resultNode = fn(); // iterate backwards without a new array or index allocation + // @ts-ignore ctx.callbacks.reduceRight((_, plugin) => { + // @ts-ignore - we know this is a function plugin[`afterNode${name}`]?.(resultNode, newNode); }, null); return resultNode; } + /** + * @param {MorphContext} ctx + * @param {string} attr + * @param {Element} element + * @param {"update" | "remove"} updateType + * @returns {boolean | undefined} + */ function beforeAttributeUpdatedCallbacks(ctx, attr, element, updateType) { const shouldPrevent = ctx.callbacks.some((plugin) => { - return plugin[`beforeAttributeUpdated`]?.(attr, element, updateType) === false; + return ( + plugin[`beforeAttributeUpdated`]?.(attr, element, updateType) === false + ); }); if (shouldPrevent) return false; @@ -909,6 +929,7 @@ var Idiomorph = (function () { * @returns {Promise[]} */ function handleHeadElement(oldHead, newHead, ctx) { + /** @type {Node[]} */ let added = []; let removed = []; let preserved = []; @@ -957,6 +978,7 @@ var Idiomorph = (function () { // nodes to append to the head tag nodesToAppend.push(...srcToNewHeadNodes.values()); + /** @type {Promise[]} */ let promises = []; for (const newNode of nodesToAppend) { // TODO: This could theoretically be null, based on type @@ -1010,6 +1032,7 @@ var Idiomorph = (function () { * @param {Element} oldNode * @param {Element} newContent * @param {Config} config + * @param {Map} plugins * @returns {MorphContext} */ function createMorphContext(oldNode, newContent, config, plugins) { @@ -1020,7 +1043,7 @@ var Idiomorph = (function () { if (!["innerHTML", "outerHTML"].includes(morphStyle)) { throw `Do not understand how to morph style ${morphStyle}`; } - const callbacks = [...Object.values(plugins), mergedConfig.callbacks]; + const callbacks = [...plugins.values(), mergedConfig.callbacks]; return { target: oldNode, From c8129757541bbcd343fc77c1bfedbebaf5fdb2de Mon Sep 17 00:00:00 2001 From: Micah Geisel Date: Sun, 16 Feb 2025 12:47:40 -0600 Subject: [PATCH 6/6] correct docs. --- README.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index b872391..2fcd42c 100644 --- a/README.md +++ b/README.md @@ -162,23 +162,21 @@ The `head` object also offers callbacks for configuring head merging specifics. ### Plugins -Idiomorph supports a plugin system that allows you to extend the functionality of the library, by registering an object of callbacks: +Idiomorph supports a plugin system that allows you to extend the functionality of the library, by adding an object of callbacks: ```js -Idiomorph.registerPlugin({ +Idiomorph.addPlugin({ name: 'logger', - onBeforeNodeAdded: function(node) { + beforeNodeAdded: function(node) { console.log('Node added:', node); }, - onBeforeNodeRemoved: function(node) { + beforeNodeRemoved: function(node) { console.log('Node removed:', node); }, }); - -Idiomorph.plugins // { logger: { ...} }; ``` -These callbacks will be called in addition to any other callbacks that are registered in `Idiomorph.morph`. Multiple plugins can be registered. +These callbacks will be called in addition to any other callbacks that are registered in `Idiomorph.morph`. Multiple plugins can be added. ### Setting Defaults