diff --git a/README.md b/README.md index a1280de..2fcd42c 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,24 @@ 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 adding an object of callbacks: + +```js +Idiomorph.addPlugin({ + name: 'logger', + beforeNodeAdded: function(node) { + console.log('Node added:', node); + }, + beforeNodeRemoved: function(node) { + console.log('Node removed:', node); + }, +}); +``` + +These callbacks will be called in addition to any other callbacks that are registered in `Idiomorph.morph`. Multiple plugins can be added. + ### 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..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,24 +117,38 @@ 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, }; + /** @type {Map} */ + let plugins = new Map(); + /** + * Add a plugin to the morphing system + * @param {IdiomorphPlugin} plugin + * @returns {void} + */ + function addPlugin(plugin) { + const name = plugin.name; + // @ts-ignore we can delete this property + delete plugin.name; + plugins.set(name, plugin); + } + /** * Core idiomorph function for morphing one DOM tree to another * @@ -155,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( @@ -359,23 +364,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, 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( + /** @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 +509,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, undefined, () => { + return node.parentNode?.removeChild(node); + }); } } @@ -618,31 +622,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; + }); } /** @@ -816,7 +817,7 @@ var Idiomorph = (function () { return true; } return ( - ctx.callbacks.beforeAttributeUpdated(attr, element, updateType) === + beforeAttributeUpdatedCallbacks(ctx, attr, element, updateType) === false ); } @@ -837,6 +838,56 @@ var Idiomorph = (function () { return morphNode; })(); + //============================================================================= + // 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; + }); + + if (shouldPrevent) { + return name === "Added" ? null : oldNode; + } + + 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 + ); + }); + + if (shouldPrevent) return false; + } + //============================================================================= // Head Management Functions //============================================================================= @@ -878,6 +929,7 @@ var Idiomorph = (function () { * @returns {Promise[]} */ function handleHeadElement(oldHead, newHead, ctx) { + /** @type {Node[]} */ let added = []; let removed = []; let preserved = []; @@ -926,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 @@ -933,7 +986,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) @@ -948,18 +1001,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, { @@ -979,9 +1032,10 @@ var Idiomorph = (function () { * @param {Element} oldNode * @param {Element} newContent * @param {Config} config + * @param {Map} plugins * @returns {MorphContext} */ - function createMorphContext(oldNode, newContent, config) { + function createMorphContext(oldNode, newContent, config, plugins) { const { persistentIds, idMap } = createIdMaps(oldNode, newContent); const mergedConfig = mergeDefaults(config); @@ -989,6 +1043,7 @@ var Idiomorph = (function () { if (!["innerHTML", "outerHTML"].includes(morphStyle)) { throw `Do not understand how to morph style ${morphStyle}`; } + const callbacks = [...plugins.values(), mergedConfig.callbacks]; return { target: oldNode, @@ -1001,7 +1056,7 @@ var Idiomorph = (function () { idMap: idMap, persistentIds: persistentIds, pantry: createPantry(), - callbacks: mergedConfig.callbacks, + callbacks: callbacks, head: mergedConfig.head, }; } @@ -1300,5 +1355,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..8547206 --- /dev/null +++ b/test/plugins.js @@ -0,0 +1,507 @@ +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), + ]); + }, + beforeAttributeUpdated: function (attribute, node, updateType) { + log.push([ + `${name}:beforeAttributeUpdated`, + toString(node), + attribute, + updateType, + ]); + }, + }; +} + +describe("Plugin system", function () { + setup(); + + it("can add plugins", function () { + let log = []; + const plugin = buildLoggingPlugin("foo", log); + Idiomorph.addPlugin(plugin); + + Idiomorph.morph( + make(`


A

`), + `
B`, + { + morphStyle: "innerHTML", + }, + ); + + log.should.eql([ + ["foo:beforeNodeAdded", "
"], + ["foo:afterNodeAdded", "
"], + ["foo:beforeNodeRemoved", "
"], + ["foo:afterNodeRemoved", "
"], + [ + "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`, + ], + ]); + }); + + it("can add multiple plugins", function () { + let log = []; + const fooPlugin = buildLoggingPlugin("foo", log); + const barPlugin = buildLoggingPlugin("bar", log); + Idiomorph.addPlugin(fooPlugin); + Idiomorph.addPlugin(barPlugin); + + Idiomorph.morph( + make(`


A

`), + `
B`, + { + morphStyle: "innerHTML", + }, + ); + + 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: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`, + ], + ]); + }); + + it("can add multiple plugins alongside callbacks", function () { + let log = []; + const fooPlugin = buildLoggingPlugin("foo", log); + const barPlugin = buildLoggingPlugin("bar", log); + 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), + ]); + }, + beforeAttributeUpdated: function (attribute, node, updateType) { + log.push([ + `beforeAttributeUpdated`, + toString(node), + attribute, + updateType, + ]); + }, + }, + }, + ); + + 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: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`, + ], + ]); + }); + + it("the first beforeNodeAdded => false halts the entire operation", function () { + let log = []; + + Idiomorph.addPlugin({ + name: "foo", + beforeNodeAdded: function (node) { + log.push(["foo:beforeNodeAdded", node.outerHTML]); + return false; + }, + afterNodeAdded: function (node) { + log.push(["foo:afterNodeAdded", node.outerHTML]); + }, + }); + + Idiomorph.addPlugin({ + name: "bar", + beforeNodeAdded: function (node) { + log.push(["bar:beforeNodeAdded", node.outerHTML]); + }, + afterNodeAdded: function (node) { + log.push(["bar:afterNodeAdded", node.outerHTML]); + }, + }); + + Idiomorph.morph(make("

"), "


", { + callbacks: { + beforeNodeAdded: function (node) { + log.push(["beforeNodeAddedCallback", node.outerHTML]); + }, + afterNodeAdded: function (node) { + log.push(["afterNodeAddedCallback", node.outerHTML]); + }, + }, + }); + + 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 = []; + + Idiomorph.addPlugin({ + name: "foo", + beforeNodeRemoved: function (node) { + log.push(["foo:beforeNodeRemoved", node.outerHTML]); + return false; + }, + afterNodeRemoved: function (node) { + log.push(["foo:afterNodeRemoved", node.outerHTML]); + }, + }); + + Idiomorph.addPlugin({ + name: "bar", + beforeNodeRemoved: function (node) { + log.push(["bar:beforeNodeRemoved", node.outerHTML]); + }, + afterNodeRemoved: function (node) { + log.push(["bar:afterNodeRemoved", node.outerHTML]); + }, + }); + + Idiomorph.morph(make("
"), "
", { + callbacks: { + beforeNodeRemoved: function (node) { + log.push(["beforeNodeRemoved", node.outerHTML]); + }, + afterNodeRemoved: function (node) { + log.push(["afterNodeRemoved", node.outerHTML]); + }, + }, + }); + + 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 = []; + + Idiomorph.addPlugin({ + name: "foo", + beforeNodeAdded: function (node) { + log.push(["foo:beforeNodeAdded", node.outerHTML]); + }, + afterNodeAdded: function (node) { + log.push(["foo:afterNodeAdded", node.outerHTML]); + }, + }); + + Idiomorph.addPlugin({ name: "bar" }); + + Idiomorph.morph(make("
"), "
", { + callbacks: { + beforeNodeAdded: function (node) { + log.push(["beforeNodeAdded", node.outerHTML]); + }, + afterNodeAdded: function (node) { + log.push(["afterNodeAdded", node.outerHTML]); + }, + }, + }); + + log.should.eql([ + ["foo:beforeNodeAdded", "
"], + ["beforeNodeAdded", "
"], + ["afterNodeAdded", "
"], + ["foo:afterNodeAdded", "
"], + ]); + }); +});