Skip to content

Commit 6d257d2

Browse files
committed
Handle arguments that get passed into a component. Clarify the use of __init__ vs mount in component views.
1 parent 9188748 commit 6d257d2

File tree

6 files changed

+147
-51
lines changed

6 files changed

+147
-51
lines changed

django_unicorn/call_method_parser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def _get_expr_string(expr: ast.expr) -> str:
6767
@lru_cache(maxsize=128, typed=True)
6868
def eval_value(value):
6969
"""
70-
Uses `ast.literal_eval` to parse strings into an appropriate Python primative.
70+
Uses `ast.literal_eval` to parse strings into an appropriate Python primitive.
7171
7272
Also returns an appropriate object for strings that look like they represent datetime,
7373
date, time, duration, or UUID.

django_unicorn/components/unicorn_view.py

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import pickle
55
import sys
66
from functools import lru_cache
7-
from typing import Any, Callable, Dict, List, Sequence, Tuple, Type
7+
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Type
88

99
from django.apps import AppConfig
1010
from django.conf import settings
@@ -50,6 +50,17 @@
5050
constructed_views_cache = LRUCache(maxsize=100)
5151
COMPONENTS_MODULE_CACHE_ENABLED = "pytest" not in sys.modules
5252

53+
STANDARD_COMPONENT_KWARG_KEYS = set(
54+
[
55+
"id",
56+
"component_id",
57+
"component_name",
58+
"component_key",
59+
"parent",
60+
"request",
61+
]
62+
)
63+
5364

5465
def convert_to_snake_case(s: str) -> str:
5566
# TODO: Better handling of dash->snake
@@ -136,6 +147,7 @@ def construct_component(
136147
component_key,
137148
parent,
138149
request,
150+
component_args,
139151
**kwargs,
140152
):
141153
"""
@@ -147,6 +159,7 @@ def construct_component(
147159
component_key=component_key,
148160
parent=parent,
149161
request=request,
162+
component_args=component_args,
150163
**kwargs,
151164
)
152165

@@ -166,8 +179,10 @@ class UnicornView(TemplateView):
166179
component_name: str = ""
167180
component_key: str = ""
168181
component_id: str = ""
182+
component_args: List = None
183+
component_kwargs: Dict = None
169184

170-
def __init__(self, **kwargs):
185+
def __init__(self, component_args: Optional[List] = None, **kwargs):
171186
self.response_class = UnicornTemplateResponse
172187

173188
self.component_name: str = ""
@@ -212,6 +227,14 @@ def __init__(self, **kwargs):
212227
if self.parent and self not in self.parent.children:
213228
self.parent.children.append(self)
214229

230+
# Set component args
231+
self.component_args = component_args if component_args is not None else []
232+
233+
# Only include custom kwargs since the standard kwargs are available
234+
# as instance variables
235+
custom_kwargs = set(kwargs.keys()) - STANDARD_COMPONENT_KWARG_KEYS
236+
self.component_kwargs = {k: kwargs[k] for k in list(custom_kwargs)}
237+
215238
self._init_script: str = ""
216239
self._validate_called = False
217240
self.errors = {}
@@ -371,25 +394,32 @@ def dispatch(self, request, *args, **kwargs):
371394
self.mount()
372395
self.hydrate()
373396

374-
self._cache_component(request, **kwargs)
397+
self._cache_component(request, *args, **kwargs)
375398

376399
return self.render_to_response(
377400
context=self.get_context_data(),
378401
component=self,
379402
init_js=True,
380403
)
381404

382-
def _cache_component(self, request: HttpRequest, parent=None, **kwargs):
405+
def _cache_component(
406+
self, request: HttpRequest, parent=None, component_args=None, **kwargs
407+
):
383408
"""
384-
Cache the component in the module and the Django cache. Re-set the `request` that got
385-
removed to make the component cacheable.
409+
Cache the component in the module and the Django cache. Re-set the `request`
410+
that got removed to make the component cacheable.
386411
"""
387412

388413
# Put the location for the component name in a module cache
389414
location_cache[self.component_name] = (self.__module__, self.__class__.__name__)
390415

391416
# Put the component's class in a module cache
392-
views_cache[self.component_id] = (self.__class__, parent, kwargs)
417+
views_cache[self.component_id] = (
418+
self.__class__,
419+
parent,
420+
component_args,
421+
kwargs,
422+
)
393423

394424
# Put the instantiated component into a module cache and the Django cache
395425
try:
@@ -732,6 +762,8 @@ def _is_public(self, name: str) -> bool:
732762
"call",
733763
"calls",
734764
"component_cache_key",
765+
"component_kwargs",
766+
"component_args",
735767
)
736768
excludes = []
737769

@@ -764,7 +796,8 @@ def create(
764796
parent: "UnicornView" = None,
765797
request: HttpRequest = None,
766798
use_cache=True,
767-
kwargs: Dict[str, Any] = {},
799+
component_args: List = None,
800+
kwargs: Dict[str, Any] = None,
768801
) -> "UnicornView":
769802
"""
770803
Find and instantiate a component class based on `component_name`.
@@ -776,6 +809,7 @@ def create(
776809
param component_key: Key of the component to allow multiple components of the same name
777810
to be differentiated. Optional.
778811
param parent: The parent component of the current component.
812+
param component_args: Arguments for the component passed in from the template. Defaults to `[]`.
779813
param kwargs: Keyword arguments for the component passed in from the template. Defaults to `{}`.
780814
781815
Returns:
@@ -785,6 +819,9 @@ def create(
785819
assert component_id, "Component id is required"
786820
assert component_name, "Component name is required"
787821

822+
component_args = component_args if component_args is not None else []
823+
kwargs = kwargs if kwargs is not None else {}
824+
788825
@timed
789826
def _get_component_class(
790827
module_name: str, class_name: str
@@ -816,7 +853,9 @@ def _get_component_class(
816853
return cached_component
817854

818855
if component_id in views_cache:
819-
(component_class, parent, kwargs) = views_cache[component_id]
856+
(component_class, parent, component_args, kwargs) = views_cache[
857+
component_id
858+
]
820859

821860
component = construct_component(
822861
component_class=component_class,
@@ -825,6 +864,7 @@ def _get_component_class(
825864
component_key=component_key,
826865
parent=parent,
827866
request=request,
867+
component_args=component_args,
828868
**kwargs,
829869
)
830870
logger.debug(f"Retrieve {component_id} from views cache")
@@ -841,20 +881,24 @@ def _get_component_class(
841881
class_name_not_found = None
842882
attribute_exception = None
843883

844-
for (module_name, class_name) in locations:
884+
for module_name, class_name in locations:
845885
try:
846886
component_class = _get_component_class(module_name, class_name)
887+
847888
component = construct_component(
848889
component_class=component_class,
849890
component_id=component_id,
850891
component_name=component_name,
851892
component_key=component_key,
852893
parent=parent,
853894
request=request,
895+
component_args=component_args,
854896
**kwargs,
855897
)
856898

857-
component._cache_component(request, parent, **kwargs)
899+
component._cache_component(
900+
request, parent=parent, component_args=component_args, **kwargs
901+
)
858902

859903
return component
860904
except ModuleNotFoundError as e:

django_unicorn/templatetags/unicorn.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Dict
1+
from typing import Dict, List
22

33
from django import template
44
from django.conf import settings
@@ -50,22 +50,28 @@ def unicorn(parser, token):
5050

5151
component_name = parser.compile_filter(contents[1])
5252

53+
args = []
5354
kwargs = {}
5455

5556
for arg in contents[2:]:
5657
try:
57-
kwarg = parse_kwarg(arg)
58-
kwargs.update(kwarg)
58+
parsed_kwarg = parse_kwarg(arg)
59+
kwargs.update(parsed_kwarg)
5960
except InvalidKwarg:
60-
pass
61+
# Assume it's an arg if invalid kwarg and kwargs is empty
62+
if not kwargs:
63+
args.append(arg)
6164

62-
return UnicornNode(component_name, kwargs)
65+
return UnicornNode(component_name, args, kwargs)
6366

6467

6568
class UnicornNode(template.Node):
66-
def __init__(self, component_name: FilterExpression, kwargs: Dict = {}):
69+
def __init__(
70+
self, component_name: FilterExpression, args: List = None, kwargs: Dict = None
71+
):
6772
self.component_name = component_name
68-
self.kwargs = kwargs
73+
self.args = args if args is not None else []
74+
self.kwargs = kwargs if kwargs is not None else {}
6975
self.component_key = ""
7076
self.parent = None
7177

@@ -75,10 +81,16 @@ def render(self, context):
7581
if hasattr(context, "request"):
7682
request = context.request
7783

78-
resolved_kwargs = {}
79-
8084
from ..components import UnicornView
8185

86+
resolved_args = []
87+
88+
for value in self.args:
89+
resolved_arg = template.Variable(value).resolve(context)
90+
resolved_args.append(resolved_arg)
91+
92+
resolved_kwargs = {}
93+
8294
for key, value in self.kwargs.items():
8395
try:
8496
resolved_value = template.Variable(value).resolve(context)
@@ -178,8 +190,9 @@ def render(self, context):
178190
component_name=component_name,
179191
component_key=self.component_key,
180192
parent=self.parent,
181-
kwargs=resolved_kwargs,
182193
request=request,
194+
component_args=resolved_args,
195+
kwargs=resolved_kwargs,
183196
)
184197

185198
extra_context = {}

docs/source/advanced.md

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,56 @@ class HelloWorldView(UnicornView):
1616

1717
## Instance properties
1818

19+
### component_args
20+
21+
The arguments passed into the component.
22+
23+
```html
24+
<!-- index.html -->
25+
{% unicorn 'hello-arg' 'World' %}
26+
```
27+
28+
```python
29+
# hello_arg.py
30+
from django_unicorn.components import UnicornView
31+
32+
class HelloArgView(UnicornView):
33+
def mount(self):
34+
assert self.component_args[0] == "World"
35+
```
36+
37+
### component_kwargs
38+
39+
The keyword arguments passed into the component.
40+
41+
```html
42+
<!-- index.html -->
43+
{% unicorn 'hello-kwarg' hello='World' %}
44+
```
45+
46+
```python
47+
# hello_kwarg.py
48+
from django_unicorn.components import UnicornView
49+
50+
class HelloKwargView(UnicornView):
51+
def mount(self):
52+
assert self.component_kwargs["hello"] == "World"
53+
```
54+
1955
### request
2056

21-
The current `request` is available on `self` in the component's methods.
57+
The current `request`.
2258

2359
```python
2460
# hello_world.py
2561
from django_unicorn.components import UnicornView
2662

2763
class HelloWorldView(UnicornView):
28-
def __init__(self, *args, **kwargs):
29-
super.__init__(**kwargs)
64+
def mount(self):
3065
print("Initial request that rendered the component", self.request)
3166

3267
def test(self):
33-
print("callMethod request to re-render the component", self.request)
68+
print("AJAX request that re-renders the component", self.request)
3469
```
3570

3671
## Custom methods
@@ -76,22 +111,6 @@ class StateView(UnicornView):
76111

77112
## Instance methods
78113

79-
### \_\_init\_\_()
80-
81-
Gets called when the component gets constructed for the very first time. Note that constructed components get cached to reduce the amount of time discovering and instantiating them, so `__init__` only gets called the very first time the component gets rendered.
82-
83-
```python
84-
# hello_world.py
85-
from django_unicorn.components import UnicornView
86-
87-
class HelloWorldView(UnicornView):
88-
name = "original"
89-
90-
def __init__(self, *args, **kwargs):
91-
super().__init__(**kwargs)
92-
self.name = "initialized"
93-
```
94-
95114
### mount()
96115

97116
Gets called when the component gets initialized or [reset](actions.md#reset).
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<div>
2+
<b>{{ hello }}</b>
3+
</div>

0 commit comments

Comments
 (0)