Skip to content

Commit a1c7973

Browse files
committed
Target DOM changes from an action to only update a part of the DOM with partial attribute.
1 parent 6a122ee commit a1c7973

File tree

13 files changed

+453
-20
lines changed

13 files changed

+453
-20
lines changed

django_unicorn/static/js/attribute.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export class Attribute {
1414
this.isPoll = false;
1515
this.isLoading = false;
1616
this.isTarget = false;
17+
this.isPartial = false;
1718
this.isDirty = false;
1819
this.isKey = false;
1920
this.isPK = false;
@@ -46,6 +47,8 @@ export class Attribute {
4647
this.isLoading = true;
4748
} else if (contains(this.name, ":target")) {
4849
this.isTarget = true;
50+
} else if (contains(this.name, ":partial")) {
51+
this.isPartial = true;
4952
} else if (contains(this.name, ":dirty")) {
5053
this.isDirty = true;
5154
} else if (this.name === "unicorn:key" || this.name === "u:key") {

django_unicorn/static/js/component.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -216,10 +216,11 @@ export class Component {
216216
/**
217217
* Calls the method for a particular component.
218218
*/
219-
callMethod(methodName, errCallback) {
219+
callMethod(methodName, partial, errCallback) {
220220
const action = {
221221
type: "callMethod",
222222
payload: { name: methodName },
223+
partial,
223224
};
224225
this.actionQueue.push(action);
225226

@@ -276,7 +277,7 @@ export class Component {
276277
);
277278

278279
// Call the method once before the timer starts
279-
this.callMethod(this.poll.method, this.handlePollError);
280+
this.callMethod(this.poll.method, null, this.handlePollError);
280281
this.startPolling();
281282
}
282283
}
@@ -293,15 +294,15 @@ export class Component {
293294
this.poll.disableData = this.poll.disableData.slice(1);
294295

295296
if (this.data[this.poll.disableData]) {
296-
this.callMethod(this.poll.method, this.handlePollError);
297+
this.callMethod(this.poll.method, null, this.handlePollError);
297298
}
298299

299300
this.poll.disableData = `!${this.poll.disableData}`;
300301
} else if (!this.data[this.poll.disableData]) {
301-
this.callMethod(this.poll.method, this.handlePollError);
302+
this.callMethod(this.poll.method, null, this.handlePollError);
302303
}
303304
} else {
304-
this.callMethod(this.poll.method, this.handlePollError);
305+
this.callMethod(this.poll.method, null, this.handlePollError);
305306
}
306307
}
307308
}, this.poll.timing);

django_unicorn/static/js/element.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export class Element {
3232
this.db = {};
3333
this.field = {};
3434
this.target = null;
35+
this.partial = {};
3536
this.key = null;
3637
this.events = [];
3738
this.errors = [];
@@ -97,6 +98,14 @@ export class Element {
9798
}
9899
} else if (attribute.isTarget) {
99100
this.target = attribute.value;
101+
} else if (attribute.isPartial) {
102+
if (attribute.modifiers.id) {
103+
this.partial.id = attribute.value;
104+
} else if (attribute.modifiers.key) {
105+
this.partial.key = attribute.value;
106+
} else {
107+
this.partial.target = attribute.value;
108+
}
100109
} else if (attribute.eventType) {
101110
const action = {};
102111
action.name = attribute.value;

django_unicorn/static/js/eventListeners.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ export function addActionEventListener(component, eventType) {
102102
let targetElement = new Element(event.target);
103103

104104
// Make sure that the target element is a unicorn element.
105+
// Handles events fired from an element inside a unicorn element
106+
// e.g. <button u:click="click"><span>Click!</span></button>
105107
if (targetElement && !targetElement.isUnicorn) {
106108
targetElement = targetElement.getUnicornParent();
107109
}
@@ -138,6 +140,7 @@ export function addActionEventListener(component, eventType) {
138140
}
139141
});
140142

143+
// Add the value of any child element of the target that is a lazy db to the action queue
141144
const dbElsInTargetScope = component.dbEls.filter((e) =>
142145
e.el.isSameNode(childEl)
143146
);
@@ -238,11 +241,11 @@ export function addActionEventListener(component, eventType) {
238241
if (action.key) {
239242
if (action.key === toKebabCase(event.key)) {
240243
handleLoading(component, targetElement);
241-
component.callMethod(action.name);
244+
component.callMethod(action.name, targetElement.partial);
242245
}
243246
} else {
244247
handleLoading(component, targetElement);
245-
component.callMethod(action.name);
248+
component.callMethod(action.name, targetElement.partial);
246249
}
247250
}
248251
});

django_unicorn/static/js/messageSender.js

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getCsrfToken, hasValue, isFunction } from "./utils.js";
1+
import { $, getCsrfToken, hasValue, isFunction } from "./utils.js";
22
import { MORPHDOM_OPTIONS } from "./morphdom/2.6.1/options.js";
33

44
/**
@@ -104,7 +104,9 @@ export function send(component, callback) {
104104
component.return = responseJson.return || {};
105105

106106
const parent = responseJson.parent || {};
107-
const rerenderedComponent = responseJson.dom;
107+
const rerenderedComponent = responseJson.dom || {};
108+
const partials = responseJson.partials || [];
109+
const { checksum } = responseJson;
108110

109111
// Handle poll
110112
const poll = responseJson.poll || {};
@@ -146,7 +148,33 @@ export function send(component, callback) {
146148
}
147149
}
148150

149-
component.morphdom(component.root, rerenderedComponent, MORPHDOM_OPTIONS);
151+
if (partials.length > 0) {
152+
for (let i = 0; i < partials.length; i++) {
153+
const partial = partials[i];
154+
let targetDom = null;
155+
156+
if (partial.key) {
157+
targetDom = $(`[unicorn\\:key="${partial.key}"]`, component.root);
158+
} else if (partial.id) {
159+
targetDom = $(`#${partial.id}`, component.root);
160+
}
161+
162+
if (targetDom) {
163+
component.morphdom(targetDom, partial.dom, MORPHDOM_OPTIONS);
164+
}
165+
}
166+
167+
if (checksum) {
168+
component.root.setAttribute("unicorn:checksum", checksum);
169+
component.refreshChecksum();
170+
}
171+
} else {
172+
component.morphdom(
173+
component.root,
174+
rerenderedComponent,
175+
MORPHDOM_OPTIONS
176+
);
177+
}
150178

151179
// Re-init to refresh the root and checksum based on the new data
152180
component.init();

django_unicorn/static/js/unicorn.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export function getComponent(componentNameOrKey) {
7070
export function call(componentNameOrKey, methodName) {
7171
const component = getComponent(componentNameOrKey);
7272

73-
component.callMethod(methodName, (err) => {
73+
component.callMethod(methodName, null, (err) => {
7474
console.error(err);
7575
});
7676
}

django_unicorn/views.py

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from .errors import UnicornViewError
1717
from .message import ComponentRequest, Return
1818
from .serializer import loads
19+
from .utils import generate_checksum
1920

2021

2122
logger = logging.getLogger(__name__)
@@ -227,10 +228,12 @@ def message(request: HttpRequest, component_name: str = None) -> JsonResponse:
227228

228229
is_reset_called = False
229230
return_data = None
231+
partials = []
230232

231233
for action in component_request.action_queue:
232234
action_type = action.get("type")
233235
payload = action.get("payload", {})
236+
partials.append(action.get("partial"))
234237

235238
if action_type == "syncInput":
236239
property_name = payload.get("name")
@@ -290,6 +293,8 @@ def message(request: HttpRequest, component_name: str = None) -> JsonResponse:
290293
instance.save()
291294
pk = instance.pk
292295
elif action_type == "callMethod":
296+
print("payload", payload)
297+
293298
call_method_name = payload.get("name", "")
294299
assert call_method_name, "Missing 'name' key for callMethod"
295300

@@ -366,14 +371,61 @@ def message(request: HttpRequest, component_name: str = None) -> JsonResponse:
366371
component.validate(model_names=model_names_to_validate)
367372

368373
rendered_component = component.render()
374+
partial_doms = []
375+
376+
if partials and all(partials):
377+
soup = BeautifulSoup(rendered_component, features="html.parser")
378+
379+
for partial in partials:
380+
partial_found = False
381+
only_id = False
382+
only_key = False
383+
384+
target = partial.get("target")
385+
386+
if not target:
387+
target = partial.get("key")
388+
389+
if target:
390+
only_key = True
391+
392+
if not target:
393+
target = partial.get("id")
394+
395+
if target:
396+
only_id = True
397+
398+
assert target, "Partial target is required"
399+
400+
if not only_id:
401+
for element in soup.find_all():
402+
if (
403+
"unicorn:key" in element.attrs
404+
and element.attrs["unicorn:key"] == target
405+
):
406+
partial_doms.append({"key": target, "dom": str(element)})
407+
partial_found = True
408+
break
409+
410+
if not partial_found and not only_key:
411+
for element in soup.find_all():
412+
if "id" in element.attrs and element.attrs["id"] == target:
413+
partial_doms.append({"id": target, "dom": str(element)})
414+
partial_found = True
415+
break
369416

370417
res = {
371418
"id": component_request.id,
372-
"dom": rendered_component,
373419
"data": component_request.data,
374420
"errors": component.errors,
421+
"checksum": generate_checksum(orjson.dumps(component_request.data)),
375422
}
376423

424+
if partial_doms:
425+
res.update({"partials": partial_doms})
426+
else:
427+
res.update({"dom": rendered_component})
428+
377429
if return_data:
378430
res.update(
379431
{"return": return_data.get_data(),}
@@ -396,9 +448,9 @@ def message(request: HttpRequest, component_name: str = None) -> JsonResponse:
396448
parent_component.get_frontend_context_variables()
397449
)
398450

399-
dom = parent_component.render()
451+
parent_dom = parent_component.render()
400452

401-
soup = BeautifulSoup(dom, features="html.parser")
453+
soup = BeautifulSoup(parent_dom, features="html.parser")
402454
checksum = None
403455

404456
# TODO: This doesn't create the same checksum for some reason
@@ -413,7 +465,7 @@ def message(request: HttpRequest, component_name: str = None) -> JsonResponse:
413465
{
414466
"parent": {
415467
"id": parent_component.component_id,
416-
"dom": dom,
468+
"dom": parent_dom,
417469
"checksum": checksum,
418470
"data": loads(parent_frontend_context_variables),
419471
"errors": parent_component.errors,

example/unicorn/templates/unicorn/html-inputs.html

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
<div>
2-
<div>
2+
<div id="boolean-toggles-id">
33
<h2>Checkbox</h2>
44
<input type="checkbox" unicorn:model="is_checked" id="check">
55
{% if is_checked %}Yes! 🦄{% else %}Nope 🙁{% endif %}
66
</div>
77

8-
<div>
8+
<div unicorn:key="boolean-toggles-key">
99
<h2>Boolean Toggles</h2>
10-
{% if another_check %}YAYAYA{% else %}Noooooooo{% endif %}<br />
11-
<button unicorn:click="$toggle('is_checked', 'another_check')" id="toggle-check">Toggle booleans</button>
10+
{% if another_check %}Checked ✅{% else %}Not checked ❎{% endif %}
11+
<br />
12+
<button unicorn:click="$toggle('is_checked', 'another_check')">Toggle booleans (normal)</button><br />
13+
<button unicorn:click="$toggle('is_checked', 'another_check')" u:partial.key="boolean-toggles-key">Toggle booleans (partial.key)</button>
14+
<button unicorn:click="$toggle('is_checked', 'another_check')" u:partial.id="boolean-toggles-id">Toggle booleans (partial.id)</button><br />
15+
<button unicorn:click="$toggle('is_checked', 'another_check')" u:partial="boolean-toggles-id">Toggle booleans (partial boolean-toggles-id)</button>
16+
<button unicorn:click="$toggle('is_checked', 'another_check')" u:partial="boolean-toggles-key">Toggle booleans (partial boolean-toggles-key)</button>
1217
</div>
1318

1419
<div>

example/www/templates/www/html-inputs.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{% extends "www/base.html" %}
2-
{% load static unicorn %}
2+
{% load unicorn %}
33

44
{% block content %}
55

0 commit comments

Comments
 (0)