Skip to content

Commit

Permalink
Add resolved method that will only get called once even when there are
Browse files Browse the repository at this point in the history
multiple actions.
  • Loading branch information
adamghill committed Jul 26, 2024
1 parent f981dcc commit a7237ca
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 14 deletions.
14 changes: 14 additions & 0 deletions django_unicorn/components/unicorn_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,12 @@ def updated(self, name, value):
"""
pass

def resolved(self, name, value):
"""
Hook that gets called when a component's data is resolved.
"""
pass

def calling(self, name, args):
"""
Hook that gets called when a component's method is about to get called.
Expand Down Expand Up @@ -611,6 +617,7 @@ def _set_property(
*,
call_updating_method: bool = False,
call_updated_method: bool = False,
call_resolved_method: bool = False,
) -> None:
# Get the correct value type by using the form if it is available
data = self._attributes()
Expand Down Expand Up @@ -642,6 +649,12 @@ def _set_property(

if hasattr(self, updated_function_name):
getattr(self, updated_function_name)(value)

if call_resolved_method:
resolved_function_name = f"resolved_{name}"

if hasattr(self, resolved_function_name):
getattr(self, resolved_function_name)(value)
except AttributeError:
raise

Expand Down Expand Up @@ -749,6 +762,7 @@ def _is_public(self, name: str) -> bool:
"get_frontend_context_variables",
"errors",
"updated",
"resolved",
"parent",
"children",
"call",
Expand Down
15 changes: 14 additions & 1 deletion django_unicorn/views/action_parsers/sync_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,17 @@
def handle(component_request: ComponentRequest, component: UnicornView, payload: Dict):
property_name = payload.get("name")
property_value = payload.get("value")
set_property_value(component, property_name, property_value, component_request.data)

call_resolved_method = True

# If there is more than one action then only call the resolved methods for the last action in the queue
if len(component_request.action_queue) > 1:
call_resolved_method = False
last_action = component_request.action_queue[-1:][0]

if last_action.payload.get("name") == property_name and last_action.payload.get("value") == property_value:
call_resolved_method = True

set_property_value(
component, property_name, property_value, component_request.data, call_resolved_method=call_resolved_method
)
18 changes: 14 additions & 4 deletions django_unicorn/views/action_parsers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
@timed
def set_property_value(
component: UnicornView,
property_name: str,
property_name: Optional[str],
property_value: Any,
data: Optional[Dict] = None,
call_resolved_method=True, # noqa: FBT002
) -> None:
"""
Sets properties on the component.
Expand All @@ -22,6 +23,7 @@ def set_property_value(
param property_name: Name of the property.
param property_value: Value to set on the property.
param data: Dictionary that gets sent back with the response. Defaults to {}.
call_resolved_method: Whether or not to call the resolved method. Defaults to True.
"""

if property_name is None:
Expand Down Expand Up @@ -59,14 +61,16 @@ class TestView(UnicornView):
component_or_field._set_property(
property_name_part,
property_value,
call_updating_method=False,
call_updating_method=False, # the updating method has already been called above
call_updated_method=True,
call_resolved_method=call_resolved_method,
)
else:
# Handle calling the updating/updated method for nested properties
property_name_snake_case = property_name.replace(".", "_")
updating_function_name = f"updating_{property_name_snake_case}"
updated_function_name = f"updated_{property_name_snake_case}"
resolved_function_name = f"resolved_{property_name_snake_case}"

if hasattr(component, updating_function_name):
getattr(component, updating_function_name)(property_value)
Expand Down Expand Up @@ -104,6 +108,9 @@ class TestView(UnicornView):
if hasattr(component, updated_function_name):
getattr(component, updated_function_name)(property_value)

if call_resolved_method and hasattr(component, resolved_function_name):
getattr(component, resolved_function_name)(property_value)

data_or_dict[property_name_part] = property_value
else:
component_or_field = getattr(component_or_field, property_name_part)
Expand All @@ -120,12 +127,15 @@ class TestView(UnicornView):
property_name_part_int = int(property_name_part)

if idx == len(property_name_parts) - 1:
component_or_field[property_name_part_int] = property_value # type: ignore[index]
component_or_field[property_name_part_int] = property_value # type: ignore[index]
data_or_dict[property_name_part_int] = property_value
else:
component_or_field = component_or_field[property_name_part_int] # type: ignore[index]
component_or_field = component_or_field[property_name_part_int] # type: ignore[index]
data_or_dict = data_or_dict[property_name_part_int]
else:
break

component.updated(property_name, property_value)

if call_resolved_method:
component.resolved(property_name, property_value)
1 change: 1 addition & 0 deletions docs/source/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 0.61.0

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

## 0.60.0

Expand Down
24 changes: 16 additions & 8 deletions docs/source/views.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,21 +305,29 @@ class HelloWorldView(UnicornView):
self.name = "hydrated"
```

### updating(name, value)
### updating(property_name, property_value)

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

### updated(name, value)
### updated(property_name, property_value)

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

### updating\_{property_name}(value)
### resolved(property_name, property_value)

Gets called before the specified property gets set.
Gets called after the specified property gets set. This will only get called once.

### updated\_{property_name}(value)
### updating\_{property_name}(property_value)

Gets called after the specified property gets set.
Gets called before the specified property gets set. This can be called multiple times in certain instances, e.g. during a debounce.

### updated\_{property_name}(property_value)

Gets called after the specified property gets set. This can be called multiple times in certain instances, e.g. during a debounce.

### resolved\_{property_name}(property_value)

Gets called after the specified property gets set. This will only get called once.

### calling(name, args)

Expand Down
19 changes: 19 additions & 0 deletions tests/views/fake_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,28 @@ def updating_count(self, _):
if count_updating >= 2:
raise Exception("updating_count called more than once")

assert count_updating == 1

def updated_count(self, _):
global count_updated # noqa: PLW0603
count_updated += 1

if count_updated >= 2:
raise Exception("count_updated called more than once")

assert count_updated == 1


count_resolved = 0


class FakeComponentWithResolveMethods(UnicornView):
template_name = "templates/test_component.html"

count = 0

def resolved_count(self, _):
global count_resolved # noqa: PLW0603
count_resolved += 1

assert count_resolved == 1, "count_resolved called more than once"
28 changes: 27 additions & 1 deletion tests/views/message/test_set_property.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def test_setter(client):


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


def test_setter_resolved(client):
data = {"count": 1}
action_queue = [
{"type": "syncInput", "payload": {"name": "count", "value": 2}, "partials": []},
{"type": "syncInput", "payload": {"name": "count", "value": 3}, "partials": []},
]
message = {
"actionQueue": action_queue,
"data": data,
"checksum": generate_checksum(str(data)),
"id": shortuuid.uuid()[:8],
"epoch": time.time(),
}

body = _post_message_and_get_body(
client,
message,
url="/message/tests.views.fake_components.FakeComponentWithResolveMethods",
)

assert not body["errors"]
assert body["data"]["count"] == 3

# If resolved_count is called more than once `FakeComponentWithResolveMethods` will raise an exception


def test_nested_setter(client):
data = {"nested": {"check": False}}
message = {
Expand Down

0 comments on commit a7237ca

Please sign in to comment.