Skip to content

Commit a7237ca

Browse files
committed
Add resolved method that will only get called once even when there are
multiple actions.
1 parent f981dcc commit a7237ca

File tree

7 files changed

+105
-14
lines changed

7 files changed

+105
-14
lines changed

django_unicorn/components/unicorn_view.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,12 @@ def updated(self, name, value):
333333
"""
334334
pass
335335

336+
def resolved(self, name, value):
337+
"""
338+
Hook that gets called when a component's data is resolved.
339+
"""
340+
pass
341+
336342
def calling(self, name, args):
337343
"""
338344
Hook that gets called when a component's method is about to get called.
@@ -611,6 +617,7 @@ def _set_property(
611617
*,
612618
call_updating_method: bool = False,
613619
call_updated_method: bool = False,
620+
call_resolved_method: bool = False,
614621
) -> None:
615622
# Get the correct value type by using the form if it is available
616623
data = self._attributes()
@@ -642,6 +649,12 @@ def _set_property(
642649

643650
if hasattr(self, updated_function_name):
644651
getattr(self, updated_function_name)(value)
652+
653+
if call_resolved_method:
654+
resolved_function_name = f"resolved_{name}"
655+
656+
if hasattr(self, resolved_function_name):
657+
getattr(self, resolved_function_name)(value)
645658
except AttributeError:
646659
raise
647660

@@ -749,6 +762,7 @@ def _is_public(self, name: str) -> bool:
749762
"get_frontend_context_variables",
750763
"errors",
751764
"updated",
765+
"resolved",
752766
"parent",
753767
"children",
754768
"call",

django_unicorn/views/action_parsers/sync_input.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,17 @@
88
def handle(component_request: ComponentRequest, component: UnicornView, payload: Dict):
99
property_name = payload.get("name")
1010
property_value = payload.get("value")
11-
set_property_value(component, property_name, property_value, component_request.data)
11+
12+
call_resolved_method = True
13+
14+
# If there is more than one action then only call the resolved methods for the last action in the queue
15+
if len(component_request.action_queue) > 1:
16+
call_resolved_method = False
17+
last_action = component_request.action_queue[-1:][0]
18+
19+
if last_action.payload.get("name") == property_name and last_action.payload.get("value") == property_value:
20+
call_resolved_method = True
21+
22+
set_property_value(
23+
component, property_name, property_value, component_request.data, call_resolved_method=call_resolved_method
24+
)

django_unicorn/views/action_parsers/utils.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99
@timed
1010
def set_property_value(
1111
component: UnicornView,
12-
property_name: str,
12+
property_name: Optional[str],
1313
property_value: Any,
1414
data: Optional[Dict] = None,
15+
call_resolved_method=True, # noqa: FBT002
1516
) -> None:
1617
"""
1718
Sets properties on the component.
@@ -22,6 +23,7 @@ def set_property_value(
2223
param property_name: Name of the property.
2324
param property_value: Value to set on the property.
2425
param data: Dictionary that gets sent back with the response. Defaults to {}.
26+
call_resolved_method: Whether or not to call the resolved method. Defaults to True.
2527
"""
2628

2729
if property_name is None:
@@ -59,14 +61,16 @@ class TestView(UnicornView):
5961
component_or_field._set_property(
6062
property_name_part,
6163
property_value,
62-
call_updating_method=False,
64+
call_updating_method=False, # the updating method has already been called above
6365
call_updated_method=True,
66+
call_resolved_method=call_resolved_method,
6467
)
6568
else:
6669
# Handle calling the updating/updated method for nested properties
6770
property_name_snake_case = property_name.replace(".", "_")
6871
updating_function_name = f"updating_{property_name_snake_case}"
6972
updated_function_name = f"updated_{property_name_snake_case}"
73+
resolved_function_name = f"resolved_{property_name_snake_case}"
7074

7175
if hasattr(component, updating_function_name):
7276
getattr(component, updating_function_name)(property_value)
@@ -104,6 +108,9 @@ class TestView(UnicornView):
104108
if hasattr(component, updated_function_name):
105109
getattr(component, updated_function_name)(property_value)
106110

111+
if call_resolved_method and hasattr(component, resolved_function_name):
112+
getattr(component, resolved_function_name)(property_value)
113+
107114
data_or_dict[property_name_part] = property_value
108115
else:
109116
component_or_field = getattr(component_or_field, property_name_part)
@@ -120,12 +127,15 @@ class TestView(UnicornView):
120127
property_name_part_int = int(property_name_part)
121128

122129
if idx == len(property_name_parts) - 1:
123-
component_or_field[property_name_part_int] = property_value # type: ignore[index]
130+
component_or_field[property_name_part_int] = property_value # type: ignore[index]
124131
data_or_dict[property_name_part_int] = property_value
125132
else:
126-
component_or_field = component_or_field[property_name_part_int] # type: ignore[index]
133+
component_or_field = component_or_field[property_name_part_int] # type: ignore[index]
127134
data_or_dict = data_or_dict[property_name_part_int]
128135
else:
129136
break
130137

131138
component.updated(property_name, property_value)
139+
140+
if call_resolved_method:
141+
component.resolved(property_name, property_value)

docs/source/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 0.61.0
44

55
- Add [`template_html`](views.md#template_html) to specify inline template HTML on the component.
6+
- Add [`resolved`](views.md#resolved) method which only fires once even when there are multiple actions, e.g. during a debounce.
67

78
## 0.60.0
89

docs/source/views.md

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -305,21 +305,29 @@ class HelloWorldView(UnicornView):
305305
self.name = "hydrated"
306306
```
307307

308-
### updating(name, value)
308+
### updating(property_name, property_value)
309309

310-
Gets called before each property that will get set.
310+
Gets called before each property that will get set. This can be called multiple times in certain instances, e.g. during a debounce.
311311

312-
### updated(name, value)
312+
### updated(property_name, property_value)
313313

314-
Gets called after each property gets set.
314+
Gets called after each property gets set. This can be called multiple times in certain instances, e.g. during a debounce.
315315

316-
### updating\_{property_name}(value)
316+
### resolved(property_name, property_value)
317317

318-
Gets called before the specified property gets set.
318+
Gets called after the specified property gets set. This will only get called once.
319319

320-
### updated\_{property_name}(value)
320+
### updating\_{property_name}(property_value)
321321

322-
Gets called after the specified property gets set.
322+
Gets called before the specified property gets set. This can be called multiple times in certain instances, e.g. during a debounce.
323+
324+
### updated\_{property_name}(property_value)
325+
326+
Gets called after the specified property gets set. This can be called multiple times in certain instances, e.g. during a debounce.
327+
328+
### resolved\_{property_name}(property_value)
329+
330+
Gets called after the specified property gets set. This will only get called once.
323331

324332
### calling(name, args)
325333

tests/views/fake_components.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,28 @@ def updating_count(self, _):
174174
if count_updating >= 2:
175175
raise Exception("updating_count called more than once")
176176

177+
assert count_updating == 1
178+
177179
def updated_count(self, _):
178180
global count_updated # noqa: PLW0603
179181
count_updated += 1
180182

181183
if count_updated >= 2:
182184
raise Exception("count_updated called more than once")
185+
186+
assert count_updated == 1
187+
188+
189+
count_resolved = 0
190+
191+
192+
class FakeComponentWithResolveMethods(UnicornView):
193+
template_name = "templates/test_component.html"
194+
195+
count = 0
196+
197+
def resolved_count(self, _):
198+
global count_resolved # noqa: PLW0603
199+
count_resolved += 1
200+
201+
assert count_resolved == 1, "count_resolved called more than once"

tests/views/message/test_set_property.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def test_setter(client):
3636

3737

3838
def test_setter_updated(client):
39-
data = {"count": 1, "count_updating": 0, "count_updated": 0}
39+
data = {"count": 1}
4040
message = {
4141
"actionQueue": [
4242
{"type": "callMethod", "payload": {"name": "count=2"}},
@@ -60,6 +60,32 @@ def test_setter_updated(client):
6060
# `FakeComponentWithUpdateMethods` will raise an exception
6161

6262

63+
def test_setter_resolved(client):
64+
data = {"count": 1}
65+
action_queue = [
66+
{"type": "syncInput", "payload": {"name": "count", "value": 2}, "partials": []},
67+
{"type": "syncInput", "payload": {"name": "count", "value": 3}, "partials": []},
68+
]
69+
message = {
70+
"actionQueue": action_queue,
71+
"data": data,
72+
"checksum": generate_checksum(str(data)),
73+
"id": shortuuid.uuid()[:8],
74+
"epoch": time.time(),
75+
}
76+
77+
body = _post_message_and_get_body(
78+
client,
79+
message,
80+
url="/message/tests.views.fake_components.FakeComponentWithResolveMethods",
81+
)
82+
83+
assert not body["errors"]
84+
assert body["data"]["count"] == 3
85+
86+
# If resolved_count is called more than once `FakeComponentWithResolveMethods` will raise an exception
87+
88+
6389
def test_nested_setter(client):
6490
data = {"nested": {"check": False}}
6591
message = {

0 commit comments

Comments
 (0)