Skip to content

Commit 22bbca3

Browse files
committed
Add ability to call JavaScript functions from the component view.
1 parent f8cf29b commit 22bbca3

File tree

11 files changed

+187
-58
lines changed

11 files changed

+187
-58
lines changed

django_unicorn/components/unicorn_template_response.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ def render(self):
7878
"name": self.component.component_name,
7979
"key": self.component.component_key,
8080
"data": orjson.loads(frontend_context_variables),
81+
"calls": self.component.calls,
8182
}
8283
init = orjson.dumps(init).decode("utf-8")
8384
init_script = f"Unicorn.componentInit({init});"

django_unicorn/components/unicorn_view.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ def construct_component(
109109
**kwargs,
110110
)
111111

112+
component.calls = []
112113
component.children = []
113114
component._children_set = False
114115

@@ -135,6 +136,9 @@ class UnicornView(TemplateView):
135136
# Dictionary with key: attribute name; value: pickled attribute value
136137
_resettable_attributes_cache: Dict[str, Any] = {}
137138

139+
# JavaScript method calls
140+
calls = []
141+
138142
def __init__(self, **kwargs):
139143
super().__init__(**kwargs)
140144

@@ -211,15 +215,39 @@ def reset(self):
211215
)
212216
pass
213217

218+
def call(self, function_name):
219+
"""
220+
Add a JavaScript method name and arguments to be called after the component is rendered.
221+
"""
222+
self.calls.append({"fn": function_name})
223+
214224
def mount(self):
215225
"""
216-
Hook that gets called when a component is first created.
226+
Hook that gets called when the component is first created.
217227
"""
218228
pass
219229

220230
def hydrate(self):
221231
"""
222-
Hook that gets called when a component's data is hydrated.
232+
Hook that gets called when the component's data is hydrated.
233+
"""
234+
pass
235+
236+
def complete(self):
237+
"""
238+
Hook that gets called when the component's methods are all called.
239+
"""
240+
pass
241+
242+
def rendered(self, html):
243+
"""
244+
Hook that gets called after the component has been rendered.
245+
"""
246+
pass
247+
248+
def parent_rendered(self, html):
249+
"""
250+
Hook that gets called after the component's parent has been rendered.
223251
"""
224252
pass
225253

@@ -561,13 +589,18 @@ def _is_public(self, name: str) -> bool:
561589
"update",
562590
"calling",
563591
"called",
592+
"complete",
593+
"rendered",
594+
"parent_rendered",
564595
"validate",
565596
"is_valid",
566597
"get_frontend_context_variables",
567598
"errors",
568599
"updated",
569600
"parent",
570601
"children",
602+
"call",
603+
"calls",
571604
)
572605
excludes = []
573606

@@ -628,6 +661,7 @@ def _get_component_class(
628661
component = constructed_views_cache[component_id]
629662
component.setup(request)
630663
component._validate_called = False
664+
component.calls = []
631665
logger.debug(f"Retrieve {component_id} from constructed views cache")
632666

633667
return component

django_unicorn/static/js/component.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
import { components } from "./store.js";
99
import { send } from "./messageSender.js";
1010
import morphdom from "./morphdom/2.6.1/morphdom.js";
11-
import { $, contains, hasValue, isEmpty, isFunction, walk } from "./utils.js";
11+
import { $, hasValue, isEmpty, isFunction, walk } from "./utils.js";
1212

1313
/**
1414
* Encapsulate component.
@@ -20,7 +20,7 @@ export class Component {
2020
this.key = args.key;
2121
this.messageUrl = args.messageUrl;
2222
this.csrfTokenHeaderName = args.csrfTokenHeaderName;
23-
this.data = args.data;
23+
this.data = args.data || {};
2424
this.syncUrl = `${this.messageUrl}/${this.name}`;
2525

2626
this.document = args.document || document;
@@ -49,6 +49,8 @@ export class Component {
4949
this.init();
5050
this.refreshEventListeners();
5151
this.initPolling();
52+
53+
this.callCalls(args.calls);
5254
}
5355

5456
/**
@@ -113,6 +115,20 @@ export class Component {
113115
return parentComponent;
114116
}
115117

118+
/**
119+
* Call JavaScript functions on the `window`.
120+
* @param {Array} calls A list of objects that specify the methods to call.
121+
*
122+
* `calls`: [{"fn": "someFunctionName"},]
123+
*/
124+
callCalls(calls) {
125+
calls = calls || [];
126+
127+
calls.forEach((call) => {
128+
this.window[call.fn]();
129+
});
130+
}
131+
116132
/**
117133
* Sets event listeners on unicorn elements.
118134
*/

django_unicorn/static/js/messageSender.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,9 @@ export function send(component, callback) {
223223
});
224224
});
225225

226+
// Call any JavaScript functions from the response
227+
component.callCalls(responseJson.calls);
228+
226229
const triggeringElements = component.lastTriggeringElements;
227230
component.lastTriggeringElements = [];
228231

django_unicorn/views.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,7 @@ def _process_component_request(
501501
"id": component_request.id,
502502
"data": updated_data,
503503
"errors": component.errors,
504+
"calls": component.calls,
504505
"checksum": generate_checksum(orjson.dumps(component_request.data)),
505506
}
506507

example/unicorn/components/js.py

Lines changed: 10 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,55 +2,19 @@
22

33

44
class JsView(UnicornView):
5+
def mount(self):
6+
# self.call("callAlert")
7+
pass
8+
9+
def call_javascript(self):
10+
self.call("callAlert")
11+
12+
def choose_state():
13+
pass
14+
515
states = (
616
"Alabama",
717
"Alaska",
8-
"Arizona",
9-
"Arkansas",
10-
"California",
11-
"Colorado",
12-
"Connecticut",
13-
"Delaware",
14-
"Florida",
15-
"Georgia",
16-
"Hawaii",
17-
"Idaho",
18-
"Illinois",
19-
"Indiana",
20-
"Iowa",
21-
"Kansas",
22-
"Kentucky",
23-
"Louisiana",
24-
"Maine",
25-
"Maryland",
26-
"Massachusetts",
27-
"Michigan",
28-
"Minnesota",
29-
"Mississippi",
30-
"Missouri",
31-
"Montana",
32-
"Nebraska",
33-
"Nevada",
34-
"New Hampshire",
35-
"New Jersey",
36-
"New Mexico",
37-
"New York",
38-
"North Carolina",
39-
"North Dakota",
40-
"Ohio",
41-
"Oklahoma",
42-
"Oregon",
43-
"Pennsylvania",
44-
"Rhode Island",
45-
"South Carolina",
46-
"South Dakota",
47-
"Tennessee",
48-
"Texas",
49-
"Utah",
50-
"Vermont",
51-
"Virginia",
52-
"Washington",
53-
"West Virginia",
5418
"Wisconsin",
5519
"Wyoming",
5620
)
Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
<div>
2-
{% for state in states %}
3-
{{ state }}
4-
{% endfor %}
2+
<script>
3+
function callAlert() {
4+
alert("hello");
5+
}
6+
</script>
7+
8+
<h2>
9+
<button unicorn:click="call_javascript">Component view calls JavaScript</button>
10+
</h2>
11+
12+
<div>
13+
<h2>
14+
<code>javascript_exclude</code>
15+
</h2>
16+
{% for state in states %}
17+
{{ state }}
18+
{% endfor %}
19+
</div>
520
</div>

tests/components/test_get_locations.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,8 @@ def test_get_locations_apps_setting_set(settings):
108108
def test_get_locations_apps_setting_invalid(settings):
109109
settings.UNICORN["APPS"] = "project"
110110

111-
expected = [
112-
("HelloWorldView", "project.components.hello_world"),
113-
]
114-
115111
with pytest.raises(AssertionError) as e:
116-
actual = get_locations("hello-world")
112+
get_locations("hello-world")
117113

118114
assert e.type == AssertionError
119115
settings.UNICORN["APPS"] = ("unicorn",)

tests/templatetags/test_unicorn_render.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ def __init__(self, *args, **kwargs):
2929
self.model_id = kwargs.get("model_id")
3030

3131

32+
class FakeComponentCalls(UnicornView):
33+
template_name = "templates/test_component_parent.html"
34+
35+
def mount(self):
36+
self.call("testCall")
37+
38+
3239
def test_unicorn_render_kwarg():
3340
token = Token(
3441
TokenType.TEXT,
@@ -219,3 +226,33 @@ def test_unicorn_render_parent_component_one_script_tag(settings):
219226

220227
assert "<script" in html
221228
assert len(re.findall("<script", html)) == 1
229+
230+
231+
def test_unicorn_render_calls(settings):
232+
settings.DEBUG = True
233+
token = Token(
234+
TokenType.TEXT,
235+
"unicorn 'tests.templatetags.test_unicorn_render.FakeComponentCalls'",
236+
)
237+
unicorn_node = unicorn(None, token)
238+
context = {}
239+
html = unicorn_node.render(context)
240+
241+
assert "<script" in html
242+
assert len(re.findall("<script", html)) == 1
243+
assert '"calls":[{"fn":"testCall"}]});' in html
244+
245+
246+
def test_unicorn_render_calls_no_mount_call(settings):
247+
settings.DEBUG = True
248+
token = Token(
249+
TokenType.TEXT,
250+
"unicorn 'tests.templatetags.test_unicorn_render.FakeComponentParent'",
251+
)
252+
unicorn_node = unicorn(None, token)
253+
context = {}
254+
html = unicorn_node.render(context)
255+
256+
assert "<script" in html
257+
assert len(re.findall("<script", html)) == 1
258+
assert '"calls":[]});' in html

tests/views/message/test_calls.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from django_unicorn.components import UnicornView
2+
from tests.views.message.utils import post_and_get_response
3+
4+
5+
class FakeCallsComponent(UnicornView):
6+
template_name = "templates/test_component.html"
7+
8+
def test_call(self):
9+
self.call("testCall")
10+
11+
def test_call2(self):
12+
self.call("testCall2")
13+
14+
15+
FAKE_CALLS_COMPONENT_URL = "/message/tests.views.message.test_calls.FakeCallsComponent"
16+
17+
18+
def test_message_calls(client):
19+
action_queue = [
20+
{"payload": {"name": "test_call"}, "type": "callMethod", "target": None,}
21+
]
22+
23+
response = post_and_get_response(
24+
client, url=FAKE_CALLS_COMPONENT_URL, action_queue=action_queue
25+
)
26+
27+
body = response.json()
28+
assert body.get("calls") == [{"fn": "testCall"}]
29+
30+
31+
def test_message_multiple_calls(client):
32+
action_queue = [
33+
{"payload": {"name": "test_call"}, "type": "callMethod", "target": None,},
34+
{"payload": {"name": "test_call2"}, "type": "callMethod", "target": None,},
35+
]
36+
response = post_and_get_response(
37+
client, url=FAKE_CALLS_COMPONENT_URL, action_queue=action_queue
38+
)
39+
40+
body = response.json()
41+
assert body.get("calls") == [{"fn": "testCall"}, {"fn": "testCall2"}]

0 commit comments

Comments
 (0)