Skip to content

Commit 86374c5

Browse files
proof-of-concept for a plugin system.
1 parent 7b4f427 commit 86374c5

4 files changed

Lines changed: 271 additions & 15 deletions

File tree

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,26 @@ Idiomorph.morph(document.documentElement, newPageSource, {head:{style: 'morph'}}
160160

161161
The `head` object also offers callbacks for configuring head merging specifics.
162162

163+
### Plugins
164+
165+
Idiomorph supports a plugin system that allows you to extend the functionality of the library, by registering an object of callbacks:
166+
167+
```js
168+
Idiomorph.registerPlugin({
169+
name: 'logger',
170+
onBeforeNodeAdded: function(node) {
171+
console.log('Node added:', node);
172+
},
173+
onBeforeNodeRemoved: function(node) {
174+
console.log('Node removed:', node);
175+
},
176+
});
177+
178+
Idiomorph.plugins // { logger: { ...} };
179+
```
180+
181+
These callbacks will be called in addition to any other callbacks that are registered in `Idiomorph.morph`. Multiple plugins can be registered.
182+
163183
### Setting Defaults
164184

165185
All the behaviors specified above can be set to a different default by mutating the `Idiomorph.defaults` object, including

src/idiomorph.js

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@ var Idiomorph = (function () {
144144
restoreFocus: true,
145145
};
146146

147+
let plugins = {};
148+
function addPlugin(plugin) {
149+
plugins[plugin.name] = plugin;
150+
}
151+
147152
/**
148153
* Core idiomorph function for morphing one DOM tree to another
149154
*
@@ -348,6 +353,26 @@ var Idiomorph = (function () {
348353
}
349354
}
350355

356+
function withNodeCallbacks(ctx, name, node, fn) {
357+
const allPlugins = [...Object.values(plugins), ctx.callbacks];
358+
359+
const shouldAbort = allPlugins.some((plugin) => {
360+
const beforeFn = plugin[`beforeNode${name}`];
361+
return beforeFn && beforeFn(node) === false;
362+
});
363+
364+
if (shouldAbort) return;
365+
366+
const resultNode = fn();
367+
368+
allPlugins.reverse().forEach((plugin) => {
369+
const afterFn = plugin[`afterNode${name}`];
370+
afterFn && afterFn(resultNode);
371+
});
372+
373+
return resultNode;
374+
}
375+
351376
/**
352377
* This performs the action of inserting a new node while handling situations where the node contains
353378
* elements with persistent ids and possible state info we can still preserve by moving in and then morphing
@@ -359,21 +384,20 @@ var Idiomorph = (function () {
359384
* @returns {Node|null}
360385
*/
361386
function createNode(oldParent, newChild, insertionPoint, ctx) {
362-
if (ctx.callbacks.beforeNodeAdded(newChild) === false) return null;
363-
if (ctx.idMap.has(newChild) && newChild instanceof Element) {
364-
// node has children with ids with possible state so create a dummy elt of same type and apply full morph algorithm
365-
const newEmptyChild = document.createElement(newChild.tagName);
366-
oldParent.insertBefore(newEmptyChild, insertionPoint);
367-
morphNode(newEmptyChild, newChild, ctx);
368-
ctx.callbacks.afterNodeAdded(newEmptyChild);
369-
return newEmptyChild;
370-
} else {
371-
// optimisation: no id state to preserve so we can just insert a clone of the newChild and its descendants
372-
const newClonedChild = document.importNode(newChild, true); // importNode to not mutate newParent
373-
oldParent.insertBefore(newClonedChild, insertionPoint);
374-
ctx.callbacks.afterNodeAdded(newClonedChild);
375-
return newClonedChild;
376-
}
387+
return withNodeCallbacks(ctx, "Added", newChild, () => {
388+
if (ctx.idMap.has(newChild) && newChild instanceof Element) {
389+
// node has children with ids with possible state so create a dummy elt of same type and apply full morph algorithm
390+
const newEmptyChild = document.createElement(newChild.tagName);
391+
oldParent.insertBefore(newEmptyChild, insertionPoint);
392+
morphNode(newEmptyChild, newChild, ctx);
393+
return newEmptyChild;
394+
} else {
395+
// optimisation: no id state to preserve so we can just insert a clone of the newChild and its descendants
396+
const newClonedChild = document.importNode(newChild, true); // importNode to not mutate newParent
397+
oldParent.insertBefore(newClonedChild, insertionPoint);
398+
return newClonedChild;
399+
}
400+
});
377401
}
378402

379403
//=============================================================================
@@ -1277,5 +1301,7 @@ var Idiomorph = (function () {
12771301
return {
12781302
morph,
12791303
defaults,
1304+
addPlugin,
1305+
plugins,
12801306
};
12811307
})();

test/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ <h2>Mocha Test Suite</h2>
4545
<script src="hooks.js"></script>
4646
<script src="htmx-integration.js"></script>
4747
<script src="ops.js"></script>
48+
<script src="plugins.js"></script>
4849
<script src="preserve-focus.js"></script>
4950
<script src="restore-focus.js"></script>
5051
<script src="retain-hidden-state.js"></script>

test/plugins.js

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
describe("Plugin system", function () {
2+
setup();
3+
4+
afterEach(() => {
5+
const obj = Idiomorph.plugins;
6+
for (const key in obj) {
7+
if (obj.hasOwnProperty(key)) {
8+
delete obj[key];
9+
}
10+
}
11+
});
12+
13+
it("can add plugins", function () {
14+
let calls = [];
15+
16+
const plugin = {
17+
name: "foo",
18+
beforeNodeAdded: function (node) {
19+
calls.push(["beforeNodeAdded", node.outerHTML]);
20+
},
21+
afterNodeAdded: function (node) {
22+
calls.push(["afterNodeAdded", node.outerHTML]);
23+
},
24+
};
25+
Idiomorph.addPlugin(plugin);
26+
Idiomorph.plugins.foo.should.equal(plugin);
27+
28+
Idiomorph.morph(make("<p>"), "<p><hr>");
29+
30+
calls.should.eql([
31+
["beforeNodeAdded", "<hr>"],
32+
["afterNodeAdded", "<hr>"],
33+
]);
34+
});
35+
36+
it("can add multiple plugins", function () {
37+
let calls = [];
38+
39+
const plugin1 = {
40+
name: "foo",
41+
beforeNodeAdded: function (node) {
42+
calls.push(["beforeNodeAdded1", node.outerHTML]);
43+
},
44+
afterNodeAdded: function (node) {
45+
calls.push(["afterNodeAdded1", node.outerHTML]);
46+
},
47+
};
48+
Idiomorph.addPlugin(plugin1);
49+
50+
const plugin2 = {
51+
name: "bar",
52+
beforeNodeAdded: function (node) {
53+
calls.push(["beforeNodeAdded2", node.outerHTML]);
54+
},
55+
afterNodeAdded: function (node) {
56+
calls.push(["afterNodeAdded2", node.outerHTML]);
57+
},
58+
};
59+
Idiomorph.addPlugin(plugin2);
60+
61+
Idiomorph.plugins.foo.should.equal(plugin1);
62+
Idiomorph.plugins.bar.should.equal(plugin2);
63+
64+
Idiomorph.morph(make("<p>"), "<p><hr>");
65+
66+
calls.should.eql([
67+
["beforeNodeAdded1", "<hr>"],
68+
["beforeNodeAdded2", "<hr>"],
69+
["afterNodeAdded2", "<hr>"],
70+
["afterNodeAdded1", "<hr>"],
71+
]);
72+
});
73+
74+
it("can add multiple plugins alongside callbacks", function () {
75+
let calls = [];
76+
77+
const plugin1 = {
78+
name: "foo",
79+
beforeNodeAdded: function (node) {
80+
calls.push(["beforeNodeAdded1", node.outerHTML]);
81+
},
82+
afterNodeAdded: function (node) {
83+
calls.push(["afterNodeAdded1", node.outerHTML]);
84+
},
85+
};
86+
Idiomorph.addPlugin(plugin1);
87+
88+
const plugin2 = {
89+
name: "bar",
90+
beforeNodeAdded: function (node) {
91+
calls.push(["beforeNodeAdded2", node.outerHTML]);
92+
},
93+
afterNodeAdded: function (node) {
94+
calls.push(["afterNodeAdded2", node.outerHTML]);
95+
},
96+
};
97+
Idiomorph.addPlugin(plugin2);
98+
99+
Idiomorph.plugins.foo.should.equal(plugin1);
100+
Idiomorph.plugins.bar.should.equal(plugin2);
101+
102+
Idiomorph.morph(make("<p>"), "<p><hr>", {
103+
callbacks: {
104+
beforeNodeAdded: function (node) {
105+
calls.push(["beforeNodeAddedCallback", node.outerHTML]);
106+
},
107+
afterNodeAdded: function (node) {
108+
calls.push(["afterNodeAddedCallback", node.outerHTML]);
109+
},
110+
},
111+
});
112+
113+
calls.should.eql([
114+
["beforeNodeAdded1", "<hr>"],
115+
["beforeNodeAdded2", "<hr>"],
116+
["beforeNodeAddedCallback", "<hr>"],
117+
["afterNodeAddedCallback", "<hr>"],
118+
["afterNodeAdded2", "<hr>"],
119+
["afterNodeAdded1", "<hr>"],
120+
]);
121+
});
122+
123+
it("the first beforeNodeAdded => false halts the entire operation", function () {
124+
let calls = [];
125+
126+
const plugin1 = {
127+
name: "foo",
128+
beforeNodeAdded: function (node) {
129+
calls.push(["beforeNodeAdded1", node.outerHTML]);
130+
return false;
131+
},
132+
afterNodeAdded: function (node) {
133+
calls.push(["afterNodeAdded1", node.outerHTML]);
134+
},
135+
};
136+
Idiomorph.addPlugin(plugin1);
137+
138+
const plugin2 = {
139+
name: "bar",
140+
beforeNodeAdded: function (node) {
141+
calls.push(["beforeNodeAdded2", node.outerHTML]);
142+
},
143+
afterNodeAdded: function (node) {
144+
calls.push(["afterNodeAdded2", node.outerHTML]);
145+
},
146+
};
147+
Idiomorph.addPlugin(plugin2);
148+
149+
Idiomorph.plugins.foo.should.equal(plugin1);
150+
Idiomorph.plugins.bar.should.equal(plugin2);
151+
152+
Idiomorph.morph(make("<p>"), "<p><hr>", {
153+
callbacks: {
154+
beforeNodeAdded: function (node) {
155+
calls.push(["beforeNodeAddedCallback", node.outerHTML]);
156+
},
157+
afterNodeAdded: function (node) {
158+
calls.push(["afterNodeAddedCallback", node.outerHTML]);
159+
},
160+
},
161+
});
162+
163+
calls.should.eql([
164+
["beforeNodeAdded1", "<hr>"]
165+
]);
166+
});
167+
168+
it("plugin callbacks are not all required to exist", function () {
169+
let calls = [];
170+
171+
const plugin1 = {
172+
name: "foo",
173+
beforeNodeAdded: function (node) {
174+
calls.push(["beforeNodeAdded1", node.outerHTML]);
175+
},
176+
afterNodeAdded: function (node) {
177+
calls.push(["afterNodeAdded1", node.outerHTML]);
178+
},
179+
};
180+
Idiomorph.addPlugin(plugin1);
181+
182+
const plugin2 = {
183+
name: "bar",
184+
};
185+
Idiomorph.addPlugin(plugin2);
186+
187+
Idiomorph.plugins.foo.should.equal(plugin1);
188+
Idiomorph.plugins.bar.should.equal(plugin2);
189+
190+
Idiomorph.morph(make("<p>"), "<p><hr>", {
191+
callbacks: {
192+
beforeNodeAdded: function (node) {
193+
calls.push(["beforeNodeAddedCallback", node.outerHTML]);
194+
},
195+
afterNodeAdded: function (node) {
196+
calls.push(["afterNodeAddedCallback", node.outerHTML]);
197+
},
198+
},
199+
});
200+
201+
calls.should.eql([
202+
["beforeNodeAdded1", "<hr>"],
203+
["beforeNodeAddedCallback", "<hr>"],
204+
["afterNodeAddedCallback", "<hr>"],
205+
["afterNodeAdded1", "<hr>"],
206+
]);
207+
});
208+
});
209+

0 commit comments

Comments
 (0)