Skip to content

Commit a7db9f6

Browse files
committed
Add component key to allow targeting separate components of the same name on a page.
1 parent 8210f75 commit a7db9f6

File tree

11 files changed

+98
-35
lines changed

11 files changed

+98
-35
lines changed

django_unicorn/components.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ def __init__(
8787
using=None,
8888
component_name=None,
8989
component_id=None,
90+
component_key="",
9091
frontend_context_variables={},
9192
init_js=False,
9293
**kwargs,
@@ -103,6 +104,7 @@ def __init__(
103104

104105
self.component_id = component_id
105106
self.component_name = component_name
107+
self.component_key = component_key
106108
self.frontend_context_variables = frontend_context_variables
107109
self.init_js = init_js
108110

@@ -121,13 +123,15 @@ def render(self):
121123
root_element = UnicornTemplateResponse._get_root_element(soup)
122124
root_element["unicorn:id"] = self.component_id
123125
root_element["unicorn:name"] = self.component_name
126+
root_element["unicorn:key"] = self.component_key
124127
root_element["unicorn:checksum"] = checksum
125128

126129
if self.init_js:
127130
script_tag = soup.new_tag("script")
128131
init = {
129132
"id": self.component_id,
130133
"name": self.component_name,
134+
"key": self.component_key,
131135
"data": orjson.loads(self.frontend_context_variables),
132136
}
133137
init = orjson.dumps(init).decode("utf-8")
@@ -166,6 +170,7 @@ def _desoupify(soup):
166170
class UnicornView(TemplateView):
167171
response_class = UnicornTemplateResponse
168172
component_name: str = ""
173+
component_key: str = ""
169174
request = None
170175

171176
# Caches to reduce the amount of time introspecting the class
@@ -292,6 +297,7 @@ def render(self, init_js=False) -> str:
292297
context=self.get_context_data(),
293298
component_name=self.component_name,
294299
component_id=self.component_id,
300+
component_key=self.component_key,
295301
frontend_context_variables=frontend_context_variables,
296302
init_js=init_js,
297303
)
@@ -561,6 +567,7 @@ def _is_public(self, name: str) -> bool:
561567
# Component methods
562568
"component_id",
563569
"component_name",
570+
"component_key",
564571
"reset",
565572
"mount",
566573
"hydrate",
@@ -591,6 +598,7 @@ def _is_public(self, name: str) -> bool:
591598
def create(
592599
component_id: str,
593600
component_name: str,
601+
component_key: str = "",
594602
use_cache=True,
595603
request: HttpRequest = None,
596604
kwargs: Dict[str, Any] = {},
@@ -602,6 +610,8 @@ def create(
602610
param component_id: Id of the component. Required.
603611
param component_name: Name of the component. Used to locate the correct `UnicornView`
604612
component class and template if necessary. Required.
613+
param component_key: Key of the component to allow multiple components of the same name
614+
to be differentiated. Optional.
605615
param use_cache: Get component from cache or force construction of component. Defaults to `True`.
606616
param kwargs: Keyword arguments for the component passed in from the template. Defaults to `{}`.
607617
@@ -613,13 +623,11 @@ def create(
613623
assert component_name, "Component name is required"
614624

615625
if use_cache:
616-
cache_key = f"{component_name}-{component_id}"
617-
618-
if cache_key in constructed_views_cache:
619-
component = constructed_views_cache[cache_key]
626+
if component_id in constructed_views_cache:
627+
component = constructed_views_cache[component_id]
620628
component.setup(request)
621629
component._validate_called = False
622-
logger.debug(f"Retrieve {cache_key} from constructed views cache")
630+
logger.debug(f"Retrieve {component_id} from constructed views cache")
623631
return component
624632

625633
locations = []
@@ -672,6 +680,7 @@ def _get_component_class(
672680
component = component_class(
673681
component_id=component_id,
674682
component_name=component_name,
683+
component_key=component_key,
675684
request=request,
676685
**kwargs,
677686
)
@@ -680,8 +689,7 @@ def _get_component_class(
680689
component._validate_called = False
681690

682691
# Put the component in a "cache" to skip looking for the component on the next request
683-
cache_key = f"{component_name}-{component_id}"
684-
constructed_views_cache[cache_key] = component
692+
constructed_views_cache[component_id] = component
685693

686694
return component
687695
except ModuleNotFoundError as e:

django_unicorn/static/js/component.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export class Component {
1515
constructor(args) {
1616
this.id = args.id;
1717
this.name = args.name;
18+
this.key = args.key;
1819
this.messageUrl = args.messageUrl;
1920
this.csrfTokenHeaderName = args.csrfTokenHeaderName;
2021

django_unicorn/static/js/unicorn.js

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Component } from "./component.js";
2-
import { isEmpty } from "./utils.js";
2+
import { hasValue, isEmpty } from "./utils.js";
33

44
let messageUrl = "";
55
const csrfTokenHeaderName = "X-CSRFToken";
@@ -28,26 +28,59 @@ export function componentInit(args) {
2828
}
2929

3030
/**
31-
* Call an action on the specified component.
31+
* Gets the component with the specified name or key.
32+
* Component keys are searched first, then names.
33+
*
34+
* @param {String} componentNameOrKey The name or key of the component to search for.
3235
*/
33-
export function call(componentName, methodName) {
36+
export function getComponent(componentNameOrKey) {
3437
let component;
3538

3639
Object.keys(components).forEach((id) => {
3740
if (isEmpty(component)) {
3841
const _component = components[id];
3942

40-
if (_component.name === componentName) {
43+
if (_component.key === componentNameOrKey) {
4144
component = _component;
4245
}
4346
}
4447
});
4548

49+
if (isEmpty(component)) {
50+
Object.keys(components).forEach((id) => {
51+
if (isEmpty(component)) {
52+
const _component = components[id];
53+
54+
if (_component.name === componentNameOrKey) {
55+
component = _component;
56+
}
57+
}
58+
});
59+
}
60+
4661
if (!component) {
47-
throw Error("No component found for: ", componentName);
62+
throw Error(`No component found for: ${componentNameOrKey}`);
4863
}
4964

65+
return component;
66+
}
67+
68+
/**
69+
* Call an action on the specified component.
70+
*/
71+
export function call(componentNameOrKey, methodName) {
72+
const component = getComponent(componentNameOrKey);
73+
5074
component.callMethod(methodName, (err) => {
5175
console.error(err);
5276
});
5377
}
78+
79+
/**
80+
* Gets the last return value.
81+
*/
82+
export function getReturnValue(componentNameOrKey) {
83+
const component = getComponent(componentNameOrKey);
84+
85+
return component.return.value;
86+
}

django_unicorn/templatetags/unicorn.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Dict
2+
13
from django import template
24
from django.conf import settings
35

@@ -52,9 +54,13 @@ def unicorn(parser, token):
5254

5355

5456
class UnicornNode(template.Node):
55-
def __init__(self, component_name, kwargs):
57+
def __init__(self, component_name: str, kwargs: Dict):
5658
self.component_name = component_name
5759
self.kwargs = kwargs
60+
self.component_key = ""
61+
62+
if "key" in kwargs:
63+
self.component_key = kwargs.pop("key")
5864

5965
def render(self, context):
6066
request = None
@@ -79,6 +85,7 @@ def render(self, context):
7985
view = UnicornView.create(
8086
component_id=component_id,
8187
component_name=self.component_name,
88+
component_key=self.component_key,
8289
kwargs=resolved_kwargs,
8390
request=request,
8491
)

example/www/templates/www/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ <h2>Index</h2>
77

88
<button onclick="Unicorn.call('hello_world', 'set_name');">Method call from outside of the component</button>
99

10+
<button onclick="alert(Unicorn.getReturnValue('hello_world'));">Last return value from component</button>
11+
1012
{% unicorn 'unicorn.components.hello_world.HelloWorldView' %}
1113

1214
{% endblock content %}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55

66
<h2>Text inputs</h2>
77

8-
{% unicorn 'text-inputs' %}
8+
<button onclick="Unicorn.call('zxcv', 'set_name');">Method call from outside of the component</button>
9+
10+
{% unicorn 'text-inputs' key="asdf" %}
11+
12+
{% unicorn 'text-inputs' key="zxcv" %}
913

1014
{% endblock content %}

tests/views/message/test_call_method.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import orjson
2+
import shortuuid
23

34
from django_unicorn.utils import generate_checksum
45

@@ -9,7 +10,7 @@ def test_message_call_method(client):
910
"actionQueue": [{"payload": {"name": "test_method"}, "type": "callMethod",}],
1011
"data": data,
1112
"checksum": generate_checksum(orjson.dumps(data)),
12-
"id": "FDHcbzGf",
13+
"id": shortuuid.uuid()[:8],
1314
}
1415

1516
response = client.post(
@@ -29,7 +30,7 @@ def test_message_call_method_redirect(client):
2930
"actionQueue": [{"payload": {"name": "test_redirect"}, "type": "callMethod",}],
3031
"data": data,
3132
"checksum": generate_checksum(orjson.dumps(data)),
32-
"id": "FDHcbzGf",
33+
"id": shortuuid.uuid()[:8],
3334
}
3435

3536
response = client.post(
@@ -54,7 +55,7 @@ def test_message_call_method_refresh_redirect(client):
5455
],
5556
"data": data,
5657
"checksum": generate_checksum(orjson.dumps(data)),
57-
"id": "FDHcbzGf",
58+
"id": shortuuid.uuid()[:8],
5859
}
5960

6061
response = client.post(
@@ -80,7 +81,7 @@ def test_message_call_method_hash_update(client):
8081
],
8182
"data": data,
8283
"checksum": generate_checksum(orjson.dumps(data)),
83-
"id": "FDHcbzGf",
84+
"id": shortuuid.uuid()[:8],
8485
}
8586

8687
response = client.post(
@@ -103,7 +104,7 @@ def test_message_call_method_return_value(client):
103104
],
104105
"data": data,
105106
"checksum": generate_checksum(orjson.dumps(data)),
106-
"id": "FDHcbzGf",
107+
"id": shortuuid.uuid()[:8],
107108
}
108109

109110
response = client.post(
@@ -115,7 +116,10 @@ def test_message_call_method_return_value(client):
115116
body = orjson.loads(response.content)
116117

117118
assert "return" in body
118-
assert body["return"] == "booya"
119+
return_data = body["return"]
120+
assert return_data.get("method") == "test_return_value"
121+
assert return_data.get("params") == []
122+
assert return_data.get("value") == "booya"
119123

120124

121125
def test_message_call_method_setter(client):
@@ -124,7 +128,7 @@ def test_message_call_method_setter(client):
124128
"actionQueue": [{"payload": {"name": "method_count=2"}, "type": "callMethod",}],
125129
"data": data,
126130
"checksum": generate_checksum(orjson.dumps(data)),
127-
"id": "FDHcbzGf",
131+
"id": shortuuid.uuid()[:8],
128132
}
129133

130134
response = client.post(
@@ -146,7 +150,7 @@ def test_message_call_method_nested_setter(client):
146150
],
147151
"data": data,
148152
"checksum": generate_checksum(orjson.dumps(data)),
149-
"id": "FDHcbzGf",
153+
"id": shortuuid.uuid()[:8],
150154
}
151155

152156
response = client.post(
@@ -168,7 +172,7 @@ def test_message_call_method_toggle(client):
168172
],
169173
"data": data,
170174
"checksum": generate_checksum(orjson.dumps(data)),
171-
"id": "FDHcbzGf",
175+
"id": shortuuid.uuid()[:8],
172176
}
173177

174178
response = client.post(
@@ -190,7 +194,7 @@ def test_message_call_method_nested_toggle(client):
190194
],
191195
"data": data,
192196
"checksum": generate_checksum(orjson.dumps(data)),
193-
"id": "FDHcbzGf",
197+
"id": shortuuid.uuid()[:8],
194198
}
195199

196200
response = client.post(
@@ -212,7 +216,7 @@ def test_message_call_method_params(client):
212216
],
213217
"data": data,
214218
"checksum": generate_checksum(orjson.dumps(data)),
215-
"id": "FDHcbzGf",
219+
"id": shortuuid.uuid()[:8],
216220
}
217221

218222
response = client.post(
@@ -234,7 +238,7 @@ def test_message_call_method_no_validation(client):
234238
],
235239
"data": data,
236240
"checksum": generate_checksum(orjson.dumps(data)),
237-
"id": "FDHcbzGf",
241+
"id": shortuuid.uuid()[:8],
238242
}
239243

240244
response = client.post(
@@ -256,7 +260,7 @@ def test_message_call_method_validation(client):
256260
],
257261
"data": data,
258262
"checksum": generate_checksum(orjson.dumps(data)),
259-
"id": "FDHcbzGf",
263+
"id": shortuuid.uuid()[:8],
260264
}
261265

262266
response = client.post(

tests/views/message/test_db_input.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import orjson
22
import pytest
3+
import shortuuid
34

45
from django_unicorn.utils import generate_checksum
56
from example.coffee.models import Flavor
@@ -25,7 +26,7 @@ def test_message_db_input_update(client):
2526
],
2627
"data": data,
2728
"checksum": generate_checksum(orjson.dumps(data)),
28-
"id": "FDHcbzGf",
29+
"id": shortuuid.uuid()[:8],
2930
}
3031

3132
response = client.post(
@@ -72,7 +73,7 @@ def test_message_db_input_create(client):
7273
],
7374
"data": data,
7475
"checksum": generate_checksum(orjson.dumps(data)),
75-
"id": "FDHcbzGf",
76+
"id": shortuuid.uuid()[:8],
7677
}
7778

7879
assert Flavor.objects.all().count() == 0

0 commit comments

Comments
 (0)