Skip to content

Commit 01e4f40

Browse files
imankulovadamghill
authored andcommitted
Rewrite morphers
- Define AlpineMorpher and MorphdomMorpher classes with morph() methods. - Instantiate one of the morphers in the {% unicorn_scripts %} templatetag and pass it to Unicorn.init() - Update the message sender to use component.morpher.morph()
1 parent 98da4f3 commit 01e4f40

File tree

11 files changed

+163
-132
lines changed

11 files changed

+163
-132
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ module.exports = {
1414
"import/no-unresolved": 0,
1515
"linebreak-style": 0,
1616
"comma-dangle": 0,
17+
"import/extensions": ["error", "always", { ignorePackages: true }],
1718
"import/prefer-default-export": 0,
1819
"no-unused-expressions": ["error", { allowTernary: true }],
1920
"no-underscore-dangle": 0,

django_unicorn/settings.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,22 @@ def get_cache_alias():
3535
return get_setting("CACHE_ALIAS", "default")
3636

3737

38+
def get_morpher():
39+
return get_setting("MORPHER", "morphdom")
40+
41+
42+
def get_morpher_options():
43+
options = get_setting("MORPHER_OPTIONS", {})
44+
45+
# Legacy "RELOAD_SCRIPT_ELEMENTS" setting that needs to go to
46+
# MORPHER_OPTIONS["RELOAD_SCRIPT_ELEMENTS"].
47+
reload_script_elements = get_setting("RELOAD_SCRIPT_ELEMENTS", False)
48+
if reload_script_elements:
49+
options["RELOAD_SCRIPT_ELEMENTS"] = True
50+
51+
return options
52+
53+
3854
def get_script_location():
3955
"""
4056
Valid choices: "append", "after". Default is "after".

django_unicorn/static/unicorn/js/component.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,14 @@ export class Component {
2626
this.messageUrl = args.messageUrl;
2727
this.csrfTokenHeaderName = args.csrfTokenHeaderName;
2828
this.csrfTokenCookieName = args.csrfTokenCookieName;
29-
this.reloadScriptElements = args.reloadScriptElements;
3029
this.hash = args.hash;
3130
this.data = args.data || {};
3231
this.syncUrl = `${this.messageUrl}/${this.name}`;
3332

3433
this.document = args.document || document;
3534
this.walker = args.walker || walk;
3635
this.window = args.window || window;
37-
this.morpherName = args.morpherName || "morphdom";
36+
this.morpher = args.morpher;
3837

3938
this.root = undefined;
4039
this.modelEls = [];

django_unicorn/static/unicorn/js/messageSender.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { $, getCsrfToken, hasValue, isFunction } from "./utils.js";
2-
import { getMorphFn } from "./morpher.js";
32

43
/**
54
* Calls the message endpoint and merges the results into the document.
@@ -171,10 +170,9 @@ export function send(component, callback) {
171170
}
172171

173172
if (parent.dom) {
174-
getMorphFn(component.morpherName)(
173+
component.morpher.morph(
175174
parentComponent.root,
176175
parent.dom,
177-
component.reloadScriptElements,
178176
);
179177
}
180178

@@ -216,10 +214,9 @@ export function send(component, callback) {
216214
}
217215

218216
if (targetDom) {
219-
getMorphFn(component.morpherName)(
217+
component.morpher.morph(
220218
targetDom,
221219
partial.dom,
222-
component.reloadScriptElements,
223220
);
224221
}
225222
}
@@ -229,10 +226,9 @@ export function send(component, callback) {
229226
component.refreshChecksum();
230227
}
231228
} else {
232-
getMorphFn(component.morpherName)(
229+
component.morpher.morph(
233230
component.root,
234231
rerenderedComponent,
235-
component.reloadScriptElements,
236232
);
237233
}
238234

django_unicorn/static/unicorn/js/morphdom/2.6.1/options.js

Lines changed: 0 additions & 62 deletions
This file was deleted.
Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,13 @@
1-
import morphdom from "./morphdom/2.6.1/morphdom.js";
2-
import {getMorphdomOptions} from "./morphdom/2.6.1/options.js";
3-
4-
5-
export function getMorphFn(morpherName) {
6-
if (morpherName === "morphdom") {
7-
return morphdomMorph;
8-
}
9-
if (morpherName === "alpine") {
10-
if (typeof Alpine === "undefined" || !Alpine.morph) {
11-
throw Error(`
12-
Alpine morpher requires Alpine to be loaded. Add Alpine and Alpine Morph to your page. E.g., add the following to your base.html:
13-
14-
<script defer src="https://unpkg.com/@alpinejs/[email protected]/dist/cdn.min.js"></script>
15-
<script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script>
16-
`);
17-
}
18-
return alpineMorph;
1+
import { MorphdomMorpher} from "./morphers/morphdom.js";
2+
import { AlpineMorpher } from "./morphers/alpine.js";
3+
4+
export function getMorpher(morpherName, morpherOptions) {
5+
const MorpherClass = {
6+
morphdom: MorphdomMorpher,
7+
alpine: AlpineMorpher,
8+
}[morpherName];
9+
if (MorpherClass) {
10+
return new MorpherClass(morpherOptions);
1911
}
2012
throw Error(`No morpher found for: ${morpherName}`);
2113
}
22-
23-
24-
function morphdomMorph(el, newHtml, reloadScriptElements) {
25-
morphdom(el, newHtml, getMorphdomOptions(reloadScriptElements));
26-
}
27-
28-
function alpineMorph(el, newHtml) {
29-
return Alpine.morph(el, newHtml);
30-
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
export class AlpineMorpher {
2+
constructor(options) {
3+
// Check if window has Alpine and Alpine Morph
4+
if (!window.Alpine || !window.Alpine.morph) {
5+
throw Error(`
6+
Alpine morpher requires Alpine to be loaded. Add Alpine and Alpine Morph to your page.
7+
See https://www.django-unicorn.com/docs/custom-morphers/#alpine for more information.
8+
`);
9+
}
10+
this.options = options;
11+
}
12+
13+
morph(dom, htmlElement) {
14+
return window.Alpine.morph(dom, htmlElement, this.getOptions());
15+
}
16+
17+
getOptions() {
18+
return {
19+
key(el) {
20+
if (el.attributes) {
21+
const key =
22+
el.getAttribute("unicorn:key") ||
23+
el.getAttribute("u:key") ||
24+
el.id;
25+
if (key) {
26+
return key;
27+
}
28+
}
29+
return el.id
30+
}
31+
}
32+
}
33+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import morphdom from "../morphdom/2.6.1/morphdom.js";
2+
3+
4+
export class MorphdomMorpher {
5+
constructor(options) {
6+
this.options = options;
7+
}
8+
9+
morph(dom, htmlElement) {
10+
return morphdom(dom, htmlElement, this.getOptions());
11+
}
12+
13+
getOptions() {
14+
const reloadScriptElements = this.options.RELOAD_SCRIPT_ELEMENTS || false;
15+
return {
16+
childrenOnly: false,
17+
// eslint-disable-next-line consistent-return
18+
getNodeKey(node) {
19+
// A node's unique identifier. Used to rearrange elements rather than
20+
// creating and destroying an element that already exists.
21+
if (node.attributes) {
22+
const key =
23+
node.getAttribute("unicorn:key") ||
24+
node.getAttribute("u:key") ||
25+
node.id;
26+
27+
if (key) {
28+
return key;
29+
}
30+
}
31+
},
32+
// eslint-disable-next-line consistent-return
33+
onBeforeElUpdated(fromEl, toEl) {
34+
// Because morphdom also supports vDom nodes, it uses isSameNode to detect
35+
// sameness. When dealing with DOM nodes, we want isEqualNode, otherwise
36+
// isSameNode will ALWAYS return false.
37+
if (fromEl.isEqualNode(toEl)) {
38+
return false;
39+
}
40+
41+
if (reloadScriptElements) {
42+
if (fromEl.nodeName === "SCRIPT" && toEl.nodeName === "SCRIPT") {
43+
// https://github.com/patrick-steele-idem/morphdom/issues/178#issuecomment-652562769
44+
const script = document.createElement("script");
45+
// copy over the attributes
46+
[...toEl.attributes].forEach((attr) => {
47+
script.setAttribute(attr.nodeName, attr.nodeValue);
48+
});
49+
50+
script.innerHTML = toEl.innerHTML;
51+
fromEl.replaceWith(script);
52+
53+
return false;
54+
}
55+
}
56+
57+
return true;
58+
},
59+
onNodeAdded(node) {
60+
if (reloadScriptElements) {
61+
if (node.nodeName === "SCRIPT") {
62+
// https://github.com/patrick-steele-idem/morphdom/issues/178#issuecomment-652562769
63+
const script = document.createElement("script");
64+
// copy over the attributes
65+
[...node.attributes].forEach((attr) => {
66+
script.setAttribute(attr.nodeName, attr.nodeValue);
67+
});
68+
69+
script.innerHTML = node.innerHTML;
70+
node.replaceWith(script);
71+
}
72+
}
73+
},
74+
}
75+
}
76+
}

django_unicorn/static/unicorn/js/unicorn.js

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,19 @@ import { isEmpty, hasValue } from "./utils.js";
33
import { components, lifecycleEvents } from "./store.js";
44

55
let messageUrl = "";
6-
let reloadScriptElements = false;
76
let csrfTokenHeaderName = "X-CSRFToken";
87
let csrfTokenCookieName = "csrftoken";
9-
let morpherName = "morphdom";
8+
let morpher;
109

1110
/**
1211
* Initializes the Unicorn object.
12+
*
13+
* @typedef
1314
*/
14-
export function init(_messageUrl, _csrfTokenHeaderName, _csrfTokenCookieName, _reloadScriptElements, _morpherName) {
15+
export function init(_messageUrl, _csrfTokenHeaderName, _csrfTokenCookieName, _morpher) {
16+
window.morpher = _morpher;
1517
messageUrl = _messageUrl;
16-
reloadScriptElements = _reloadScriptElements || false;
18+
morpher = _morpher;
1719

1820
if (hasValue(_csrfTokenHeaderName)) {
1921
csrfTokenHeaderName = _csrfTokenHeaderName;
@@ -22,17 +24,11 @@ export function init(_messageUrl, _csrfTokenHeaderName, _csrfTokenCookieName, _r
2224
if (hasValue(_csrfTokenCookieName)) {
2325
csrfTokenCookieName = _csrfTokenCookieName;
2426
}
25-
26-
if (hasValue(_morpherName)) {
27-
morpherName = _morpherName;
28-
}
29-
3027
return {
3128
messageUrl,
3229
csrfTokenHeaderName,
3330
csrfTokenCookieName,
34-
reloadScriptElements,
35-
morpherName,
31+
morpher,
3632
};
3733
}
3834

@@ -43,8 +39,7 @@ export function componentInit(args) {
4339
args.messageUrl = messageUrl;
4440
args.csrfTokenHeaderName = csrfTokenHeaderName;
4541
args.csrfTokenCookieName = csrfTokenCookieName;
46-
args.morpherName = morpherName;
47-
args.reloadScriptElements = reloadScriptElements;
42+
args.morpher = morpher;
4843

4944
const component = new Component(args);
5045
components[component.id] = component;

0 commit comments

Comments
 (0)