mirror of
https://github.com/django-components/django-components.git
synced 2025-07-13 03:45:00 +00:00
feat: add spread operator (#596)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
36b8fcfbe6
commit
d6ec62c6be
4 changed files with 637 additions and 108 deletions
243
README.md
243
README.md
|
@ -50,6 +50,7 @@ And this is what gets rendered (plus the CSS and Javascript you've specified):
|
|||
- [Using slots in templates](#using-slots-in-templates)
|
||||
- [Passing data to components](#passing-data-to-components)
|
||||
- [Rendering HTML attributes](#rendering-html-attributes)
|
||||
- [Template tag syntax](#template-tag-syntax)
|
||||
- [Prop drilling and dependency injection (provide / inject)](#prop-drilling-and-dependency-injection-provide--inject)
|
||||
- [Component context and scope](#component-context-and-scope)
|
||||
- [Customizing component tags with TagFormatter](#customizing-component-tags-with-tagformatter)
|
||||
|
@ -1446,105 +1447,6 @@ As seen above, you can pass arguments to components like so:
|
|||
</body>
|
||||
```
|
||||
|
||||
### Special characters
|
||||
|
||||
_New in version 0.71_:
|
||||
|
||||
Keyword arguments can contain special characters `# @ . - _`, so keywords like
|
||||
so are still valid:
|
||||
|
||||
```django
|
||||
<body>
|
||||
{% component "calendar" my-date="2015-06-19" @click.native=do_something #some_id=True / %}
|
||||
</body>
|
||||
```
|
||||
|
||||
These can then be accessed inside `get_context_data` so:
|
||||
|
||||
```py
|
||||
@register("calendar")
|
||||
class Calendar(Component):
|
||||
# Since # . @ - are not valid identifiers, we have to
|
||||
# use `**kwargs` so the method can accept these args.
|
||||
def get_context_data(self, **kwargs):
|
||||
return {
|
||||
"date": kwargs["my-date"],
|
||||
"id": kwargs["#some_id"],
|
||||
"on_click": kwargs["@click.native"]
|
||||
}
|
||||
```
|
||||
|
||||
### Pass dictonary by its key-value pairs
|
||||
|
||||
_New in version 0.74_:
|
||||
|
||||
Sometimes, a component may expect a dictionary as one of its inputs.
|
||||
|
||||
Most commonly, this happens when a component accepts a dictionary
|
||||
of HTML attributes (usually called `attrs`) to pass to the underlying template.
|
||||
|
||||
In such cases, we may want to define some HTML attributes statically, and other dynamically.
|
||||
But for that, we need to define this dictionary on Python side:
|
||||
|
||||
```py
|
||||
@register("my_comp")
|
||||
class MyComp(Component):
|
||||
template = """
|
||||
{% component "other" attrs=attrs / %}
|
||||
"""
|
||||
|
||||
def get_context_data(self, some_id: str):
|
||||
attrs = {
|
||||
"class": "pa-4 flex",
|
||||
"data-some-id": some_id,
|
||||
"@click.stop": "onClickHandler",
|
||||
}
|
||||
return {"attrs": attrs}
|
||||
```
|
||||
|
||||
But as you can see in the case above, the event handler `@click.stop` and styling `pa-4 flex`
|
||||
are disconnected from the template. If the component grew in size and we moved the HTML
|
||||
to a separate file, we would have hard time reasoning about the component's template.
|
||||
|
||||
Luckily, there's a better way.
|
||||
|
||||
When we want to pass a dictionary to a component, we can define individual key-value pairs
|
||||
as component kwargs, so we can keep all the relevant information in the template. For that,
|
||||
we prefix the key with the name of the dict and `:`. So key `class` of input `attrs` becomes
|
||||
`attrs:class`. And our example becomes:
|
||||
|
||||
```py
|
||||
@register("my_comp")
|
||||
class MyComp(Component):
|
||||
template = """
|
||||
{% component "other"
|
||||
attrs:class="pa-4 flex"
|
||||
attrs:data-some-id=some_id
|
||||
attrs:@click.stop="onClickHandler"
|
||||
/ %}
|
||||
"""
|
||||
|
||||
def get_context_data(self, some_id: str):
|
||||
return {"some_id": some_id}
|
||||
```
|
||||
|
||||
Sweet! Now all the relevant HTML is inside the template, and we can move it to a separate file with confidence:
|
||||
|
||||
```django
|
||||
{% component "other"
|
||||
attrs:class="pa-4 flex"
|
||||
attrs:data-some-id=some_id
|
||||
attrs:@click.stop="onClickHandler"
|
||||
/ %}
|
||||
```
|
||||
|
||||
> Note: It is NOT possible to define nested dictionaries, so
|
||||
> `attrs:my_key:two=2` would be interpreted as:
|
||||
>
|
||||
> ```py
|
||||
> {"attrs": {"my_key:two": 2}}
|
||||
> ```
|
||||
|
||||
### Accessing data passed to the component
|
||||
|
||||
When you call `Component.render` or `Component.render_to_response`, the inputs to these methods can be accessed from within the instance under `self.input`.
|
||||
|
@ -1953,6 +1855,149 @@ attributes_to_string(attrs)
|
|||
# 'class="my-class text-red pa-4" data-id="123" required'
|
||||
```
|
||||
|
||||
## Template tag syntax
|
||||
|
||||
All template tags in django_component, like `{% component %}` or `{% slot %}`, and so on,
|
||||
support extra syntax that makes it possible to write components like in Vue or React.
|
||||
|
||||
### Special characters
|
||||
|
||||
_New in version 0.71_:
|
||||
|
||||
Keyword arguments can contain special characters `# @ . - _`, so keywords like
|
||||
so are still valid:
|
||||
|
||||
```django
|
||||
<body>
|
||||
{% component "calendar" my-date="2015-06-19" @click.native=do_something #some_id=True / %}
|
||||
</body>
|
||||
```
|
||||
|
||||
These can then be accessed inside `get_context_data` so:
|
||||
|
||||
```py
|
||||
@register("calendar")
|
||||
class Calendar(Component):
|
||||
# Since # . @ - are not valid identifiers, we have to
|
||||
# use `**kwargs` so the method can accept these args.
|
||||
def get_context_data(self, **kwargs):
|
||||
return {
|
||||
"date": kwargs["my-date"],
|
||||
"id": kwargs["#some_id"],
|
||||
"on_click": kwargs["@click.native"]
|
||||
}
|
||||
```
|
||||
|
||||
### Spread operator
|
||||
|
||||
_New in version 0.93_:
|
||||
|
||||
Instead of passing keyword arguments one-by-one:
|
||||
|
||||
```django
|
||||
{% component "calendar" title="How to abc" date="2015-06-19" author="John Wick" / %}
|
||||
```
|
||||
|
||||
You can use a spread operator `...dict` to apply key-value pairs from a dictionary:
|
||||
|
||||
```py
|
||||
post_data = {
|
||||
"title": "How to...",
|
||||
"date": "2015-06-19",
|
||||
"author": "John Wick",
|
||||
}
|
||||
```
|
||||
|
||||
```django
|
||||
{% component "calendar" ...post_data / %}
|
||||
```
|
||||
|
||||
This behaves similar to [JSX's spread operator](https://kevinyckim33.medium.com/jsx-spread-operator-component-props-meaning-3c9bcadd2493)
|
||||
or [Vue's `v-bind`](https://vuejs.org/api/built-in-directives.html#v-bind).
|
||||
|
||||
Spread operators are treated as keyword arguments, which means that:
|
||||
1. Spread operators must come after positional arguments.
|
||||
2. You cannot use spread operators for [positional-only arguments](https://martinxpn.medium.com/positional-only-and-keyword-only-arguments-in-python-37-100-days-of-python-310c311657b0).
|
||||
|
||||
Other than that, you can use spread operators multiple times, and even put keyword arguments in-between or after them:
|
||||
|
||||
```django
|
||||
{% component "calendar" ...post_data id=post.id ...extra / %}
|
||||
```
|
||||
|
||||
In a case of conflicts, the values added later (right-most) overwrite previous values.
|
||||
|
||||
### Pass dictonary by its key-value pairs
|
||||
|
||||
_New in version 0.74_:
|
||||
|
||||
Sometimes, a component may expect a dictionary as one of its inputs.
|
||||
|
||||
Most commonly, this happens when a component accepts a dictionary
|
||||
of HTML attributes (usually called `attrs`) to pass to the underlying template.
|
||||
|
||||
In such cases, we may want to define some HTML attributes statically, and other dynamically.
|
||||
But for that, we need to define this dictionary on Python side:
|
||||
|
||||
```py
|
||||
@register("my_comp")
|
||||
class MyComp(Component):
|
||||
template = """
|
||||
{% component "other" attrs=attrs / %}
|
||||
"""
|
||||
|
||||
def get_context_data(self, some_id: str):
|
||||
attrs = {
|
||||
"class": "pa-4 flex",
|
||||
"data-some-id": some_id,
|
||||
"@click.stop": "onClickHandler",
|
||||
}
|
||||
return {"attrs": attrs}
|
||||
```
|
||||
|
||||
But as you can see in the case above, the event handler `@click.stop` and styling `pa-4 flex`
|
||||
are disconnected from the template. If the component grew in size and we moved the HTML
|
||||
to a separate file, we would have hard time reasoning about the component's template.
|
||||
|
||||
Luckily, there's a better way.
|
||||
|
||||
When we want to pass a dictionary to a component, we can define individual key-value pairs
|
||||
as component kwargs, so we can keep all the relevant information in the template. For that,
|
||||
we prefix the key with the name of the dict and `:`. So key `class` of input `attrs` becomes
|
||||
`attrs:class`. And our example becomes:
|
||||
|
||||
```py
|
||||
@register("my_comp")
|
||||
class MyComp(Component):
|
||||
template = """
|
||||
{% component "other"
|
||||
attrs:class="pa-4 flex"
|
||||
attrs:data-some-id=some_id
|
||||
attrs:@click.stop="onClickHandler"
|
||||
/ %}
|
||||
"""
|
||||
|
||||
def get_context_data(self, some_id: str):
|
||||
return {"some_id": some_id}
|
||||
```
|
||||
|
||||
Sweet! Now all the relevant HTML is inside the template, and we can move it to a separate file with confidence:
|
||||
|
||||
```django
|
||||
{% component "other"
|
||||
attrs:class="pa-4 flex"
|
||||
attrs:data-some-id=some_id
|
||||
attrs:@click.stop="onClickHandler"
|
||||
/ %}
|
||||
```
|
||||
|
||||
> Note: It is NOT possible to define nested dictionaries, so
|
||||
> `attrs:my_key:two=2` would be interpreted as:
|
||||
>
|
||||
> ```py
|
||||
> {"attrs": {"my_key:two": 2}}
|
||||
> ```
|
||||
|
||||
## Prop drilling and dependency injection (provide / inject)
|
||||
|
||||
_New in version 0.80_:
|
||||
|
|
|
@ -1,11 +1,36 @@
|
|||
import re
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Dict, List, Mapping, Optional, Tuple, Union
|
||||
|
||||
from django.template import Context, TemplateSyntaxError
|
||||
from django.template.base import FilterExpression, Parser
|
||||
|
||||
Expression = Union[FilterExpression]
|
||||
RuntimeKwargsInput = Dict[str, Expression]
|
||||
RuntimeKwargPairsInput = List[Tuple[str, Expression]]
|
||||
RuntimeKwargsInput = Dict[str, Union[Expression, "Operator"]]
|
||||
RuntimeKwargPairsInput = List[Tuple[str, Union[Expression, "Operator"]]]
|
||||
|
||||
|
||||
class Operator(ABC):
|
||||
"""
|
||||
Operator describes something that somehow changes the inputs
|
||||
to template tags (the `{% %}`).
|
||||
|
||||
For example, a SpreadOperator inserts one or more kwargs at the
|
||||
specified location.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def resolve(self, context: Context) -> Any: ... # noqa E704
|
||||
|
||||
|
||||
class SpreadOperator(Operator):
|
||||
"""Operator that inserts one or more kwargs at the specified location."""
|
||||
|
||||
def __init__(self, expr: Expression) -> None:
|
||||
self.expr = expr
|
||||
|
||||
def resolve(self, context: Context) -> Dict[str, Any]:
|
||||
return self.expr.resolve(context)
|
||||
|
||||
|
||||
class RuntimeKwargs:
|
||||
|
@ -24,6 +49,11 @@ class RuntimeKwargPairs:
|
|||
def resolve(self, context: Context) -> List[Tuple[str, Any]]:
|
||||
resolved_kwarg_pairs: List[Tuple[str, Any]] = []
|
||||
for key, kwarg in self.kwarg_pairs:
|
||||
if isinstance(kwarg, SpreadOperator):
|
||||
spread_kwargs = kwarg.resolve(context)
|
||||
for spread_key, spread_value in spread_kwargs.items():
|
||||
resolved_kwarg_pairs.append((spread_key, spread_value))
|
||||
else:
|
||||
resolved_kwarg_pairs.append((key, kwarg.resolve(context)))
|
||||
|
||||
return resolved_kwarg_pairs
|
||||
|
@ -43,11 +73,18 @@ def safe_resolve_list(context: Context, args: List[Expression]) -> List:
|
|||
|
||||
def safe_resolve_dict(
|
||||
context: Context,
|
||||
kwargs: Dict[str, Expression],
|
||||
kwargs: Dict[str, Union[Expression, "Operator"]],
|
||||
) -> Dict[str, Any]:
|
||||
result = {}
|
||||
|
||||
for key, kwarg in kwargs.items():
|
||||
# If we've come across a Spread Operator (...), we insert the kwargs from it here
|
||||
if isinstance(kwarg, SpreadOperator):
|
||||
spread_dict = kwarg.resolve(context)
|
||||
if spread_dict is not None:
|
||||
for spreadkey, spreadkwarg in spread_dict.items():
|
||||
result[spreadkey] = spreadkwarg
|
||||
else:
|
||||
result[key] = kwarg.resolve(context)
|
||||
return result
|
||||
|
||||
|
@ -72,6 +109,31 @@ def is_aggregate_key(key: str) -> bool:
|
|||
return ":" in key and not key.startswith(":")
|
||||
|
||||
|
||||
def is_spread_operator(value: Any) -> bool:
|
||||
if not isinstance(value, str) or not value:
|
||||
return False
|
||||
|
||||
return value.startswith("...")
|
||||
|
||||
|
||||
# A string that starts with `...1=`, `...29=`, etc.
|
||||
# We convert the spread syntax to this, so Django parses
|
||||
# it as a kwarg, so it remains in the original position.
|
||||
#
|
||||
# So from `...dict`, we make `...1=dict`
|
||||
#
|
||||
# That way it's trivial to merge the kwargs after the spread
|
||||
# operator is replaced with actual values.
|
||||
INTERNAL_SPREAD_OPERATOR_RE = re.compile(r"^\.\.\.\d+=")
|
||||
|
||||
|
||||
def is_internal_spread_operator(value: Any) -> bool:
|
||||
if not isinstance(value, str) or not value:
|
||||
return False
|
||||
|
||||
return bool(INTERNAL_SPREAD_OPERATOR_RE.match(value))
|
||||
|
||||
|
||||
def process_aggregate_kwargs(kwargs: Mapping[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
This function aggregates "prefixed" kwargs into dicts. "Prefixed" kwargs
|
||||
|
|
|
@ -12,11 +12,16 @@ from django_components.component_registry import ComponentRegistry
|
|||
from django_components.component_registry import registry as component_registry
|
||||
from django_components.expression import (
|
||||
Expression,
|
||||
Operator,
|
||||
RuntimeKwargPairs,
|
||||
RuntimeKwargPairsInput,
|
||||
RuntimeKwargs,
|
||||
RuntimeKwargsInput,
|
||||
SpreadOperator,
|
||||
is_aggregate_key,
|
||||
is_internal_spread_operator,
|
||||
is_kwarg,
|
||||
is_spread_operator,
|
||||
resolve_string,
|
||||
)
|
||||
from django_components.logger import trace_msg
|
||||
|
@ -389,6 +394,7 @@ def _parse_tag(
|
|||
else:
|
||||
seen_kwargs.add(key)
|
||||
|
||||
spread_count = 0
|
||||
for bit in bits:
|
||||
value = bit
|
||||
bit_is_kwarg = is_kwarg(bit)
|
||||
|
@ -411,6 +417,21 @@ def _parse_tag(
|
|||
parsed_flags[value] = True
|
||||
continue
|
||||
|
||||
# Extract spread operator (...dict)
|
||||
elif is_spread_operator(value):
|
||||
if value == "...":
|
||||
raise TemplateSyntaxError("Syntax operator is missing a value")
|
||||
|
||||
# Replace the leading `...` with `...=`, so the parser
|
||||
# interprets it as a kwargs, and keeps it in the correct
|
||||
# position.
|
||||
# Since there can be multiple spread operators, we suffix
|
||||
# them with an index, e.g. `...0=`
|
||||
internal_spread_bit = f"...{spread_count}={value[3:]}"
|
||||
bits_without_flags.append(internal_spread_bit)
|
||||
spread_count += 1
|
||||
continue
|
||||
|
||||
bits_without_flags.append(bit)
|
||||
|
||||
bits = bits_without_flags
|
||||
|
@ -450,13 +471,25 @@ def _parse_tag(
|
|||
params = [param for param in params_to_sort if param not in optional_params]
|
||||
|
||||
# Parse args/kwargs that will be passed to the fill
|
||||
args, kwarg_pairs = parse_bits(
|
||||
args, raw_kwarg_pairs = parse_bits(
|
||||
parser=parser,
|
||||
bits=bits,
|
||||
params=[] if isinstance(params, bool) else params,
|
||||
name=tag_name,
|
||||
)
|
||||
|
||||
# Post-process args/kwargs - Mark special cases like aggregate dicts
|
||||
# or dynamic expressions
|
||||
kwarg_pairs: RuntimeKwargPairsInput = []
|
||||
for key, val in raw_kwarg_pairs:
|
||||
is_spread_op = is_internal_spread_operator(key + "=")
|
||||
|
||||
if is_spread_op:
|
||||
expr = parser.compile_filter(val.token)
|
||||
kwarg_pairs.append((key, SpreadOperator(expr)))
|
||||
else:
|
||||
kwarg_pairs.append((key, val))
|
||||
|
||||
# Allow only as many positional args as given
|
||||
if params != True and len(args) > len(params): # noqa F712
|
||||
raise TemplateSyntaxError(f"Tag '{tag_name}' received unexpected positional arguments: {args[len(params):]}")
|
||||
|
@ -471,6 +504,11 @@ def _parse_tag(
|
|||
kwargs: RuntimeKwargsInput = {}
|
||||
extra_keywords: Set[str] = set()
|
||||
for key, val in kwarg_pairs:
|
||||
# Operators are resolved at render-time, so skip them
|
||||
if isinstance(val, Operator):
|
||||
kwargs[key] = val
|
||||
continue
|
||||
|
||||
# Check if key allowed
|
||||
if not keywordonly_kwargs:
|
||||
is_key_allowed = False
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
"""Catch-all for tests that use template tags and don't fit other files"""
|
||||
|
||||
from typing import Dict
|
||||
from typing import Any, Dict
|
||||
|
||||
from django.template import Context, Template
|
||||
from django.template import Context, Template, TemplateSyntaxError
|
||||
from django.template.base import Parser
|
||||
|
||||
from django_components import Component, register, types
|
||||
from django_components.expression import safe_resolve_dict, safe_resolve_list
|
||||
|
||||
from .django_test_setup import setup_test_config
|
||||
from .testutils import BaseTestCase
|
||||
from .testutils import BaseTestCase, parametrize_context_behavior
|
||||
|
||||
setup_test_config({"autodiscover": False})
|
||||
|
||||
|
@ -58,3 +59,386 @@ class ResolveTests(BaseTestCase):
|
|||
safe_resolve_dict(ctx, exprs),
|
||||
{"a": 123, "b": [{}, {}], "c": ""},
|
||||
)
|
||||
|
||||
|
||||
class SpreadOperatorTests(BaseTestCase):
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_component(self):
|
||||
captured = {}
|
||||
|
||||
@register("test")
|
||||
class SimpleComponent(Component):
|
||||
def get_context_data(
|
||||
self,
|
||||
pos_var1: Any,
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
):
|
||||
nonlocal captured
|
||||
captured = kwargs
|
||||
|
||||
return {
|
||||
"pos_var1": pos_var1,
|
||||
**kwargs,
|
||||
}
|
||||
|
||||
template: types.django_html = """
|
||||
<div>{{ pos_var1 }}</div>
|
||||
<div>{{ attrs }}</div>
|
||||
<div>{{ items }}</div>
|
||||
<div>{{ a }}</div>
|
||||
<div>{{ x }}</div>
|
||||
"""
|
||||
|
||||
template_str: types.django_html = (
|
||||
"""
|
||||
{% load component_tags %}
|
||||
{% component 'test'
|
||||
var_a
|
||||
...my_dict
|
||||
...item
|
||||
x=123
|
||||
/ %}
|
||||
""".replace(
|
||||
"\n", " "
|
||||
)
|
||||
)
|
||||
|
||||
template = Template(template_str)
|
||||
rendered = template.render(
|
||||
Context(
|
||||
{
|
||||
"var_a": "LoREM",
|
||||
"my_dict": {
|
||||
"attrs:@click": "() => {}",
|
||||
"attrs:style": "height: 20px",
|
||||
"items": [1, 2, 3],
|
||||
},
|
||||
"item": {"a": 1},
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
# Check that variables passed to the component are of correct type
|
||||
self.assertEqual(captured["attrs"], {"@click": "() => {}", "style": "height: 20px"})
|
||||
self.assertEqual(captured["items"], [1, 2, 3])
|
||||
self.assertEqual(captured["a"], 1)
|
||||
self.assertEqual(captured["x"], 123)
|
||||
|
||||
self.assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
<div>LoREM</div>
|
||||
<div>{'@click': '() => {}', 'style': 'height: 20px'}</div>
|
||||
<div>[1, 2, 3]</div>
|
||||
<div>1</div>
|
||||
<div>123</div>
|
||||
""",
|
||||
)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_slot(self):
|
||||
@register("test")
|
||||
class SimpleComponent(Component):
|
||||
def get_context_data(self):
|
||||
return {
|
||||
"my_dict": {
|
||||
"attrs:@click": "() => {}",
|
||||
"attrs:style": "height: 20px",
|
||||
"items": [1, 2, 3],
|
||||
},
|
||||
"item": {"a": 1},
|
||||
}
|
||||
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% slot "my_slot" ...my_dict ...item x=123 default / %}
|
||||
"""
|
||||
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component 'test' %}
|
||||
{% fill "my_slot" data="slot_data" %}
|
||||
{{ slot_data }}
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
template = Template(template_str)
|
||||
rendered = template.render(Context({}))
|
||||
|
||||
self.assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
{'items': [1, 2, 3], 'a': 1, 'x': 123, 'attrs': {'@click': '() => {}', 'style': 'height: 20px'}}
|
||||
""",
|
||||
)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_fill(self):
|
||||
@register("test")
|
||||
class SimpleComponent(Component):
|
||||
def get_context_data(self):
|
||||
return {
|
||||
"my_dict": {
|
||||
"attrs:@click": "() => {}",
|
||||
"attrs:style": "height: 20px",
|
||||
"items": [1, 2, 3],
|
||||
},
|
||||
"item": {"a": 1},
|
||||
}
|
||||
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% slot "my_slot" ...my_dict ...item x=123 default %}
|
||||
__SLOT_DEFAULT__
|
||||
{% endslot %}
|
||||
"""
|
||||
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component 'test' %}
|
||||
{% fill "my_slot" ...fill_data %}
|
||||
{{ slot_data }}
|
||||
{{ slot_default }}
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
template = Template(template_str)
|
||||
rendered = template.render(
|
||||
Context(
|
||||
{
|
||||
"fill_data": {
|
||||
"data": "slot_data",
|
||||
"default": "slot_default",
|
||||
},
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
self.assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
{'items': [1, 2, 3], 'a': 1, 'x': 123, 'attrs': {'@click': '() => {}', 'style': 'height: 20px'}}
|
||||
__SLOT_DEFAULT__
|
||||
""",
|
||||
)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_provide(self):
|
||||
@register("test")
|
||||
class SimpleComponent(Component):
|
||||
def get_context_data(self):
|
||||
data = self.inject("test")
|
||||
return {
|
||||
"attrs": data.attrs,
|
||||
"items": data.items,
|
||||
"a": data.a,
|
||||
}
|
||||
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
<div>{{ attrs }}</div>
|
||||
<div>{{ items }}</div>
|
||||
<div>{{ a }}</div>
|
||||
"""
|
||||
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% provide 'test' ...my_dict ...item %}
|
||||
{% component 'test' / %}
|
||||
{% endprovide %}
|
||||
"""
|
||||
template = Template(template_str)
|
||||
rendered = template.render(
|
||||
Context(
|
||||
{
|
||||
"my_dict": {
|
||||
"attrs:@click": "() => {}",
|
||||
"attrs:style": "height: 20px",
|
||||
"items": [1, 2, 3],
|
||||
},
|
||||
"item": {"a": 1},
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
self.assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
<div>{'@click': '() => {}', 'style': 'height: 20px'}</div>
|
||||
<div>[1, 2, 3]</div>
|
||||
<div>1</div>
|
||||
""",
|
||||
)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_html_attrs(self):
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
<div {% html_attrs defaults:test="hi" ...my_dict attrs:lol="123" %}>
|
||||
"""
|
||||
template = Template(template_str)
|
||||
rendered = template.render(
|
||||
Context(
|
||||
{
|
||||
"my_dict": {
|
||||
"attrs:style": "height: 20px",
|
||||
"class": "button",
|
||||
"defaults:class": "my-class",
|
||||
"defaults:style": "NONO",
|
||||
},
|
||||
}
|
||||
),
|
||||
)
|
||||
self.assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
<div test="hi" class="my-class button" style="height: 20px" lol="123">
|
||||
""",
|
||||
)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_later_spreads_overwrite_earlier(self):
|
||||
@register("test")
|
||||
class SimpleComponent(Component):
|
||||
def get_context_data(
|
||||
self,
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
):
|
||||
return {
|
||||
**kwargs,
|
||||
}
|
||||
|
||||
template: types.django_html = """
|
||||
<div>{{ attrs }}</div>
|
||||
<div>{{ items }}</div>
|
||||
<div>{{ a }}</div>
|
||||
<div>{{ x }}</div>
|
||||
"""
|
||||
|
||||
template_str: types.django_html = (
|
||||
"""
|
||||
{% load component_tags %}
|
||||
{% component 'test'
|
||||
...my_dict
|
||||
attrs:style="OVERWRITTEN"
|
||||
x=123
|
||||
...item
|
||||
/ %}
|
||||
""".replace(
|
||||
"\n", " "
|
||||
)
|
||||
)
|
||||
|
||||
template = Template(template_str)
|
||||
rendered = template.render(
|
||||
Context(
|
||||
{
|
||||
"my_dict": {
|
||||
"attrs:@click": "() => {}",
|
||||
"attrs:style": "height: 20px",
|
||||
"items": [1, 2, 3],
|
||||
},
|
||||
"item": {"a": 1, "x": "OVERWRITTEN_X"},
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
self.assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
<div>{'@click': '() => {}', 'style': 'OVERWRITTEN'}</div>
|
||||
<div>[1, 2, 3]</div>
|
||||
<div>1</div>
|
||||
<div>OVERWRITTEN_X</div>
|
||||
""",
|
||||
)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_raises_if_position_arg_after_spread(self):
|
||||
@register("test")
|
||||
class SimpleComponent(Component):
|
||||
pass
|
||||
|
||||
template_str: types.django_html = (
|
||||
"""
|
||||
{% load component_tags %}
|
||||
{% component 'test'
|
||||
...my_dict
|
||||
var_a
|
||||
...item
|
||||
x=123
|
||||
/ %}
|
||||
""".replace(
|
||||
"\n", " "
|
||||
)
|
||||
)
|
||||
|
||||
with self.assertRaisesMessage(
|
||||
TemplateSyntaxError, "'component' received some positional argument(s) after some keyword argument(s)"
|
||||
):
|
||||
Template(template_str)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_raises_on_missing_value(self):
|
||||
@register("test")
|
||||
class SimpleComponent(Component):
|
||||
pass
|
||||
|
||||
template_str: types.django_html = (
|
||||
"""
|
||||
{% load component_tags %}
|
||||
{% component 'test'
|
||||
var_a
|
||||
...
|
||||
/ %}
|
||||
""".replace(
|
||||
"\n", " "
|
||||
)
|
||||
)
|
||||
|
||||
with self.assertRaisesMessage(TemplateSyntaxError, "Syntax operator is missing a value"):
|
||||
Template(template_str)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_raises_on_non_dict(self):
|
||||
@register("test")
|
||||
class SimpleComponent(Component):
|
||||
pass
|
||||
|
||||
template_str: types.django_html = (
|
||||
"""
|
||||
{% load component_tags %}
|
||||
{% component 'test'
|
||||
var_a
|
||||
...var_b
|
||||
/ %}
|
||||
""".replace(
|
||||
"\n", " "
|
||||
)
|
||||
)
|
||||
|
||||
template = Template(template_str)
|
||||
|
||||
# List
|
||||
with self.assertRaisesMessage(AttributeError, "'list' object has no attribute 'items'"):
|
||||
template.render(
|
||||
Context(
|
||||
{
|
||||
"var_a": "abc",
|
||||
"var_b": [1, 2, 3],
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# String
|
||||
with self.assertRaisesMessage(AttributeError, "'str' object has no attribute 'items'"):
|
||||
template.render(
|
||||
Context(
|
||||
{
|
||||
"var_a": "abc",
|
||||
"var_b": "def",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue