Skip to content

Commit

Permalink
Rewrite morphers
Browse files Browse the repository at this point in the history
- 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()
  • Loading branch information
imankulov authored and adamghill committed Sep 18, 2023
1 parent 98da4f3 commit 01e4f40
Show file tree
Hide file tree
Showing 11 changed files with 163 additions and 132 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module.exports = {
"import/no-unresolved": 0,
"linebreak-style": 0,
"comma-dangle": 0,
"import/extensions": ["error", "always", { ignorePackages: true }],
"import/prefer-default-export": 0,
"no-unused-expressions": ["error", { allowTernary: true }],
"no-underscore-dangle": 0,
Expand Down
16 changes: 16 additions & 0 deletions django_unicorn/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,22 @@ def get_cache_alias():
return get_setting("CACHE_ALIAS", "default")


def get_morpher():
return get_setting("MORPHER", "morphdom")


def get_morpher_options():
options = get_setting("MORPHER_OPTIONS", {})

# Legacy "RELOAD_SCRIPT_ELEMENTS" setting that needs to go to
# MORPHER_OPTIONS["RELOAD_SCRIPT_ELEMENTS"].
reload_script_elements = get_setting("RELOAD_SCRIPT_ELEMENTS", False)
if reload_script_elements:
options["RELOAD_SCRIPT_ELEMENTS"] = True

return options


def get_script_location():
"""
Valid choices: "append", "after". Default is "after".
Expand Down
3 changes: 1 addition & 2 deletions django_unicorn/static/unicorn/js/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,14 @@ export class Component {
this.messageUrl = args.messageUrl;
this.csrfTokenHeaderName = args.csrfTokenHeaderName;
this.csrfTokenCookieName = args.csrfTokenCookieName;
this.reloadScriptElements = args.reloadScriptElements;
this.hash = args.hash;
this.data = args.data || {};
this.syncUrl = `${this.messageUrl}/${this.name}`;

this.document = args.document || document;
this.walker = args.walker || walk;
this.window = args.window || window;
this.morpherName = args.morpherName || "morphdom";
this.morpher = args.morpher;

this.root = undefined;
this.modelEls = [];
Expand Down
10 changes: 3 additions & 7 deletions django_unicorn/static/unicorn/js/messageSender.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { $, getCsrfToken, hasValue, isFunction } from "./utils.js";
import { getMorphFn } from "./morpher.js";

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

if (parent.dom) {
getMorphFn(component.morpherName)(
component.morpher.morph(
parentComponent.root,
parent.dom,
component.reloadScriptElements,
);
}

Expand Down Expand Up @@ -216,10 +214,9 @@ export function send(component, callback) {
}

if (targetDom) {
getMorphFn(component.morpherName)(
component.morpher.morph(
targetDom,
partial.dom,
component.reloadScriptElements,
);
}
}
Expand All @@ -229,10 +226,9 @@ export function send(component, callback) {
component.refreshChecksum();
}
} else {
getMorphFn(component.morpherName)(
component.morpher.morph(
component.root,
rerenderedComponent,
component.reloadScriptElements,
);
}

Expand Down
62 changes: 0 additions & 62 deletions django_unicorn/static/unicorn/js/morphdom/2.6.1/options.js

This file was deleted.

37 changes: 10 additions & 27 deletions django_unicorn/static/unicorn/js/morpher.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,13 @@
import morphdom from "./morphdom/2.6.1/morphdom.js";
import {getMorphdomOptions} from "./morphdom/2.6.1/options.js";


export function getMorphFn(morpherName) {
if (morpherName === "morphdom") {
return morphdomMorph;
}
if (morpherName === "alpine") {
if (typeof Alpine === "undefined" || !Alpine.morph) {
throw Error(`
Alpine morpher requires Alpine to be loaded. Add Alpine and Alpine Morph to your page. E.g., add the following to your base.html:
<script defer src="https://unpkg.com/@alpinejs/[email protected]/dist/cdn.min.js"></script>
<script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script>
`);
}
return alpineMorph;
import { MorphdomMorpher} from "./morphers/morphdom.js";
import { AlpineMorpher } from "./morphers/alpine.js";

export function getMorpher(morpherName, morpherOptions) {
const MorpherClass = {
morphdom: MorphdomMorpher,
alpine: AlpineMorpher,
}[morpherName];
if (MorpherClass) {
return new MorpherClass(morpherOptions);
}
throw Error(`No morpher found for: ${morpherName}`);
}


function morphdomMorph(el, newHtml, reloadScriptElements) {
morphdom(el, newHtml, getMorphdomOptions(reloadScriptElements));
}

function alpineMorph(el, newHtml) {
return Alpine.morph(el, newHtml);
}
33 changes: 33 additions & 0 deletions django_unicorn/static/unicorn/js/morphers/alpine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export class AlpineMorpher {
constructor(options) {
// Check if window has Alpine and Alpine Morph
if (!window.Alpine || !window.Alpine.morph) {
throw Error(`
Alpine morpher requires Alpine to be loaded. Add Alpine and Alpine Morph to your page.
See https://www.django-unicorn.com/docs/custom-morphers/#alpine for more information.
`);
}
this.options = options;
}

morph(dom, htmlElement) {
return window.Alpine.morph(dom, htmlElement, this.getOptions());
}

getOptions() {
return {
key(el) {
if (el.attributes) {
const key =
el.getAttribute("unicorn:key") ||
el.getAttribute("u:key") ||
el.id;
if (key) {
return key;
}
}
return el.id
}
}
}
}
76 changes: 76 additions & 0 deletions django_unicorn/static/unicorn/js/morphers/morphdom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import morphdom from "../morphdom/2.6.1/morphdom.js";


export class MorphdomMorpher {
constructor(options) {
this.options = options;
}

morph(dom, htmlElement) {
return morphdom(dom, htmlElement, this.getOptions());
}

getOptions() {
const reloadScriptElements = this.options.RELOAD_SCRIPT_ELEMENTS || false;
return {
childrenOnly: false,
// eslint-disable-next-line consistent-return
getNodeKey(node) {
// A node's unique identifier. Used to rearrange elements rather than
// creating and destroying an element that already exists.
if (node.attributes) {
const key =
node.getAttribute("unicorn:key") ||
node.getAttribute("u:key") ||
node.id;

if (key) {
return key;
}
}
},
// eslint-disable-next-line consistent-return
onBeforeElUpdated(fromEl, toEl) {
// Because morphdom also supports vDom nodes, it uses isSameNode to detect
// sameness. When dealing with DOM nodes, we want isEqualNode, otherwise
// isSameNode will ALWAYS return false.
if (fromEl.isEqualNode(toEl)) {
return false;
}

if (reloadScriptElements) {
if (fromEl.nodeName === "SCRIPT" && toEl.nodeName === "SCRIPT") {
// https://github.com/patrick-steele-idem/morphdom/issues/178#issuecomment-652562769
const script = document.createElement("script");
// copy over the attributes
[...toEl.attributes].forEach((attr) => {
script.setAttribute(attr.nodeName, attr.nodeValue);
});

script.innerHTML = toEl.innerHTML;
fromEl.replaceWith(script);

return false;
}
}

return true;
},
onNodeAdded(node) {
if (reloadScriptElements) {
if (node.nodeName === "SCRIPT") {
// https://github.com/patrick-steele-idem/morphdom/issues/178#issuecomment-652562769
const script = document.createElement("script");
// copy over the attributes
[...node.attributes].forEach((attr) => {
script.setAttribute(attr.nodeName, attr.nodeValue);
});

script.innerHTML = node.innerHTML;
node.replaceWith(script);
}
}
},
}
}
}
21 changes: 8 additions & 13 deletions django_unicorn/static/unicorn/js/unicorn.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@ import { isEmpty, hasValue } from "./utils.js";
import { components, lifecycleEvents } from "./store.js";

let messageUrl = "";
let reloadScriptElements = false;
let csrfTokenHeaderName = "X-CSRFToken";
let csrfTokenCookieName = "csrftoken";
let morpherName = "morphdom";
let morpher;

/**
* Initializes the Unicorn object.
*
* @typedef
*/
export function init(_messageUrl, _csrfTokenHeaderName, _csrfTokenCookieName, _reloadScriptElements, _morpherName) {
export function init(_messageUrl, _csrfTokenHeaderName, _csrfTokenCookieName, _morpher) {
window.morpher = _morpher;
messageUrl = _messageUrl;
reloadScriptElements = _reloadScriptElements || false;
morpher = _morpher;

if (hasValue(_csrfTokenHeaderName)) {
csrfTokenHeaderName = _csrfTokenHeaderName;
Expand All @@ -22,17 +24,11 @@ export function init(_messageUrl, _csrfTokenHeaderName, _csrfTokenCookieName, _r
if (hasValue(_csrfTokenCookieName)) {
csrfTokenCookieName = _csrfTokenCookieName;
}

if (hasValue(_morpherName)) {
morpherName = _morpherName;
}

return {
messageUrl,
csrfTokenHeaderName,
csrfTokenCookieName,
reloadScriptElements,
morpherName,
morpher,
};
}

Expand All @@ -43,8 +39,7 @@ export function componentInit(args) {
args.messageUrl = messageUrl;
args.csrfTokenHeaderName = csrfTokenHeaderName;
args.csrfTokenCookieName = csrfTokenCookieName;
args.morpherName = morpherName;
args.reloadScriptElements = reloadScriptElements;
args.morpher = morpher;

const component = new Component(args);
components[component.id] = component;
Expand Down
Loading

0 comments on commit 01e4f40

Please sign in to comment.