mirror of
https://github.com/django-components/django-components.git
synced 2025-08-17 12:40:15 +00:00
feat: dynamic slots, fills, and provides (#609)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
6793aec9b4
commit
b90961b4a7
8 changed files with 521 additions and 140 deletions
118
README.md
118
README.md
|
@ -48,7 +48,7 @@ And this is what gets rendered (plus the CSS and Javascript you've specified):
|
|||
- [Use components as views](#use-components-as-views)
|
||||
- [Autodiscovery](#autodiscovery)
|
||||
- [Using slots in templates](#using-slots-in-templates)
|
||||
- [Passing data to components](#passing-data-to-components)
|
||||
- [Accessing data passed to the component](#accessing-data-passed-to-the-component)
|
||||
- [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)
|
||||
|
@ -68,6 +68,7 @@ And this is what gets rendered (plus the CSS and Javascript you've specified):
|
|||
**Version 0.93**
|
||||
- Spread operator `...dict` inside template tags. See [Spread operator](#spread-operator))
|
||||
- Use template tags inside string literals in component inputs. See [Use template tags inside component inputs](#use-template-tags-inside-component-inputs))
|
||||
- Dynamic slots, fills and provides - The `name` argument for these can now be a variable, a template expression, or via spread operator
|
||||
|
||||
🚨📢 **Version 0.92**
|
||||
- BREAKING CHANGE: `Component` class is no longer a subclass of `View`. To configure the `View` class, set the `Component.View` nested class. HTTP methods like `get` or `post` can still be defined directly on `Component` class, and `Component.as_view()` internally calls `Component.View.as_view()`. (See [Modifying the View class](#modifying-the-view-class))
|
||||
|
@ -812,7 +813,7 @@ class ComponentView(View):
|
|||
|
||||
If you want to define your own `View` class, you need to:
|
||||
1. Set the class as `Component.View`
|
||||
2. Subclass from `ComponentView`, so the View instance has access to the component class.
|
||||
2. Subclass from `ComponentView`, so the View instance has access to the component instance.
|
||||
|
||||
In the example below, we added extra logic into `View.setup()`.
|
||||
|
||||
|
@ -1440,18 +1441,77 @@ to the same name. This raises an error:
|
|||
{% endcomponent %}
|
||||
```
|
||||
|
||||
## Passing data to components
|
||||
### Dynamic slots and fills
|
||||
|
||||
As seen above, you can pass arguments to components like so:
|
||||
Until now, we were declaring slot and fill names statically, as a string literal, e.g.
|
||||
|
||||
```django
|
||||
<body>
|
||||
{% component "calendar" date="2015-06-19" %}
|
||||
{% endcomponent %}
|
||||
</body>
|
||||
{% slot "content" / %}
|
||||
```
|
||||
|
||||
### Accessing data passed to the component
|
||||
However, sometimes you may want to generate slots based on the given input. One example of this is [a table component like that of Vuetify](https://vuetifyjs.com/en/api/v-data-table/), which creates a header and an item slots for each user-defined column.
|
||||
|
||||
In django_components you can achieve the same, simply by using a variable (or a [template expression](#use-template-tags-inside-component-inputs)) instead of a string literal:
|
||||
|
||||
```django
|
||||
<table>
|
||||
<tr>
|
||||
{% for header in headers %}
|
||||
<th>
|
||||
{% slot "header-{{ header.key }}" value=header.title %}
|
||||
{{ header.title }}
|
||||
{% endslot %}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</table>
|
||||
```
|
||||
|
||||
When using the component, you can either set the fill explicitly:
|
||||
|
||||
```django
|
||||
{% component "table" headers=headers items=items %}
|
||||
{% fill "header-name" data="data" %}
|
||||
<b>{{ data.value }}</b>
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
```
|
||||
|
||||
Or also use a variable:
|
||||
|
||||
```django
|
||||
{% component "table" headers=headers items=items %}
|
||||
{# Make only the active column bold #}
|
||||
{% fill "header-{{ active_header_name }}" data="data" %}
|
||||
<b>{{ data.value }}</b>
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
```
|
||||
|
||||
> NOTE: It's better to use static slot names whenever possible for clarity. The dynamic slot names should be reserved for advanced use only.
|
||||
|
||||
Lastly, in rare cases, you can also pass the slot name via [the spread operator](#spread-operator). This is possible, because the slot name argument is actually a shortcut for a `name` keyword argument.
|
||||
|
||||
So this:
|
||||
|
||||
```django
|
||||
{% slot "content" / %}
|
||||
```
|
||||
|
||||
is the same as:
|
||||
|
||||
```django
|
||||
{% slot name="content" / %}
|
||||
```
|
||||
|
||||
So it's possible to define a `name` key on a dictionary, and then spread that onto the slot tag:
|
||||
|
||||
```django
|
||||
{# slot_props = {"name": "content"} #}
|
||||
{% slot ...slot_props / %}
|
||||
```
|
||||
|
||||
## 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`.
|
||||
|
||||
|
@ -1862,7 +1922,23 @@ attributes_to_string(attrs)
|
|||
## 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.
|
||||
support extra syntax that makes it possible to write components like in Vue or React (JSX).
|
||||
|
||||
### Self-closing tags
|
||||
|
||||
When you have a tag like `{% component %}` or `{% slot %}`, but it has no content, you can simply append a forward slash `/` at the end, instead of writing out the closing tags like `{% endcomponent %}` or `{% endslot %}`:
|
||||
|
||||
So this:
|
||||
|
||||
```django
|
||||
{% component "button" %}{% endcomponent %}
|
||||
```
|
||||
|
||||
becomes
|
||||
|
||||
```django
|
||||
{% component "button" / %}
|
||||
```
|
||||
|
||||
### Special characters
|
||||
|
||||
|
@ -1960,6 +2036,7 @@ Instead, template tags in django_components (`{% component %}`, `{% slot %}`, `{
|
|||
"As positional arg {# yay #}"
|
||||
title="{{ person.first_name }} {{ person.last_name }}"
|
||||
id="{% random_int 10 20 %}"
|
||||
readonly="{{ editable|not }}"
|
||||
author="John Wick {# TODO: parametrize #}"
|
||||
/ %}
|
||||
```
|
||||
|
@ -1968,6 +2045,7 @@ In the example above:
|
|||
- Component `test` receives a positional argument with value `"As positional arg "`. The comment is omitted.
|
||||
- Kwarg `title` is passed as a string, e.g. `John Doe`
|
||||
- Kwarg `id` is passed as `int`, e.g. `15`
|
||||
- Kwarg `readonly` is passed as `bool`, e.g. `False`
|
||||
- Kwarg `author` is passed as a string, e.g. `John Wick ` (Comment omitted)
|
||||
|
||||
This is inspired by [django-cotton](https://github.com/wrabit/django-cotton#template-expressions-in-attributes).
|
||||
|
@ -2028,7 +2106,7 @@ Similar is possible with [`django-expr`](https://pypi.org/project/django-expr/),
|
|||
/ %}
|
||||
```
|
||||
|
||||
> Note: Never use this feature to mix business logic and template logic. Business logic should still be in the template!
|
||||
> Note: Never use this feature to mix business logic and template logic. Business logic should still be in the view!
|
||||
|
||||
### Pass dictonary by its key-value pairs
|
||||
|
||||
|
@ -2147,10 +2225,7 @@ First we use the `{% provide %}` tag to define the data we want to "provide" (ma
|
|||
|
||||
Notice that the `provide` tag REQUIRES a name as a first argument. This is the _key_ by which we can then access the data passed to this tag.
|
||||
|
||||
`provide` tag _key_, similarly to the _name_ argument in `component` or `slot` tags, has these requirements:
|
||||
|
||||
- The _key_ must be a string literal
|
||||
- It must be a valid identifier (AKA a valid Python variable name)
|
||||
`provide` tag name must resolve to a valid identifier (AKA a valid Python variable name).
|
||||
|
||||
Once you've set the name, you define the data you want to "provide" by passing it as keyword arguments. This is similar to how you pass data to the `{% with %}` tag.
|
||||
|
||||
|
@ -2163,6 +2238,15 @@ Once you've set the name, you define the data you want to "provide" by passing i
|
|||
> {% endprovide %}
|
||||
> ```
|
||||
|
||||
Similarly to [slots and fills](#dynamic-slots-and-fills), also provide's name argument can be set dynamically via a variable, a template expression, or a spread operator:
|
||||
|
||||
```django
|
||||
{% provide name=name ... %}
|
||||
...
|
||||
{% provide %}
|
||||
</table>
|
||||
```
|
||||
|
||||
### Using `inject()` method
|
||||
|
||||
To "inject" (access) the data defined on the `provide` tag, you can use the `inject()` method inside of `get_context_data()`.
|
||||
|
@ -2180,7 +2264,7 @@ class ChildComponent(Component):
|
|||
return {}
|
||||
```
|
||||
|
||||
First argument to `inject` is the _key_ of the provided data. This
|
||||
First argument to `inject` is the _key_ (or _name_) of the provided data. This
|
||||
must match the string that you used in the `provide` tag. If no provider
|
||||
with given key is found, `inject` raises a `KeyError`.
|
||||
|
||||
|
@ -2237,7 +2321,7 @@ With this in mind, the `{% component %}` tag behaves similarly to `{% include %}
|
|||
And just like with `{% include %}`, if you don't want a specific component template to have access to the parent context, add `only` to the `{% component %}` tag:
|
||||
|
||||
```htmldjango
|
||||
{% component "calendar" date="2015-06-19" only %}{% endcomponent %}
|
||||
{% component "calendar" date="2015-06-19" only / %}
|
||||
```
|
||||
|
||||
NOTE: `{% csrf_token %}` tags need access to the top-level context, and they will not function properly if they are rendered in a component that is called with the `only` modifier.
|
||||
|
|
|
@ -25,7 +25,6 @@ from django.forms.widgets import Media
|
|||
from django.http import HttpRequest, HttpResponse
|
||||
from django.template.base import NodeList, Template, TextNode
|
||||
from django.template.context import Context
|
||||
from django.template.exceptions import TemplateSyntaxError
|
||||
from django.template.loader import get_template
|
||||
from django.template.loader_tags import BLOCK_CONTEXT_KEY
|
||||
from django.utils.html import conditional_escape
|
||||
|
@ -48,8 +47,6 @@ from django_components.middleware import is_dependency_middleware_active
|
|||
from django_components.node import BaseNode
|
||||
from django_components.slots import (
|
||||
DEFAULT_SLOT_KEY,
|
||||
SLOT_DATA_KWARG,
|
||||
SLOT_DEFAULT_KWARG,
|
||||
FillContent,
|
||||
FillNode,
|
||||
SlotContent,
|
||||
|
@ -57,6 +54,7 @@ from django_components.slots import (
|
|||
SlotRef,
|
||||
SlotResult,
|
||||
_nodelist_to_slot_render_func,
|
||||
resolve_fill_nodes,
|
||||
resolve_slots,
|
||||
)
|
||||
from django_components.utils import gen_id
|
||||
|
@ -634,23 +632,7 @@ class ComponentNode(BaseNode):
|
|||
),
|
||||
}
|
||||
else:
|
||||
fill_content = {}
|
||||
for fill_node in self.fill_nodes:
|
||||
# Note that outer component context is used to resolve variables in
|
||||
# fill tag.
|
||||
resolved_name = fill_node.name.resolve(context)
|
||||
if resolved_name in fill_content:
|
||||
raise TemplateSyntaxError(
|
||||
f"Multiple fill tags cannot target the same slot name: "
|
||||
f"Detected duplicate fill tag name '{resolved_name}'."
|
||||
)
|
||||
|
||||
fill_kwargs = fill_node.resolve_kwargs(context, self.name)
|
||||
fill_content[resolved_name] = FillContent(
|
||||
content_func=_nodelist_to_slot_render_func(fill_node.nodelist),
|
||||
slot_default_var=fill_kwargs[SLOT_DEFAULT_KWARG],
|
||||
slot_data_var=fill_kwargs[SLOT_DATA_KWARG],
|
||||
)
|
||||
fill_content = resolve_fill_nodes(context, self.fill_nodes, self.name)
|
||||
|
||||
component: Component = component_cls(
|
||||
registered_name=self.name,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Optional
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
from django.template import Context
|
||||
from django.template.base import NodeList
|
||||
|
@ -10,6 +10,8 @@ from django_components.logger import trace_msg
|
|||
from django_components.node import BaseNode
|
||||
from django_components.utils import gen_id
|
||||
|
||||
PROVIDE_NAME_KWARG = "name"
|
||||
|
||||
|
||||
class ProvideNode(BaseNode):
|
||||
"""
|
||||
|
@ -19,34 +21,43 @@ class ProvideNode(BaseNode):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
nodelist: NodeList,
|
||||
trace_id: str,
|
||||
node_id: Optional[str] = None,
|
||||
kwargs: Optional[RuntimeKwargs] = None,
|
||||
):
|
||||
super().__init__(nodelist=nodelist, args=None, kwargs=kwargs, node_id=node_id)
|
||||
|
||||
self.name = name
|
||||
self.nodelist = nodelist
|
||||
self.node_id = node_id or gen_id()
|
||||
self.trace_id = trace_id
|
||||
self.kwargs = kwargs or RuntimeKwargs({})
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Provide Node: {self.name}. Contents: {repr(self.nodelist)}. Data: {self.provide_kwargs.kwargs}>"
|
||||
return f"<Provide Node: {self.node_id}. Contents: {repr(self.nodelist)}>"
|
||||
|
||||
def render(self, context: Context) -> SafeString:
|
||||
trace_msg("RENDR", "PROVIDE", self.name, self.node_id)
|
||||
trace_msg("RENDR", "PROVIDE", self.trace_id, self.node_id)
|
||||
|
||||
kwargs = self.kwargs.resolve(context)
|
||||
name, kwargs = self.resolve_kwargs(context)
|
||||
|
||||
# NOTE: The "provided" kwargs are meant to be shared privately, meaning that components
|
||||
# have to explicitly opt in by using the `Component.inject()` method. That's why we don't
|
||||
# add the provided kwargs into the Context.
|
||||
with context.update({}):
|
||||
# "Provide" the data to child nodes
|
||||
set_provided_context_var(context, self.name, kwargs)
|
||||
set_provided_context_var(context, name, kwargs)
|
||||
|
||||
output = self.nodelist.render(context)
|
||||
|
||||
trace_msg("RENDR", "PROVIDE", self.name, self.node_id, msg="...Done!")
|
||||
trace_msg("RENDR", "PROVIDE", self.trace_id, self.node_id, msg="...Done!")
|
||||
return output
|
||||
|
||||
def resolve_kwargs(self, context: Context) -> Tuple[str, Dict[str, Optional[str]]]:
|
||||
kwargs = self.kwargs.resolve(context)
|
||||
name = kwargs.pop(PROVIDE_NAME_KWARG, None)
|
||||
|
||||
if not name:
|
||||
raise RuntimeError("Provide tag kwarg 'name' is missing")
|
||||
|
||||
return (name, kwargs)
|
||||
|
|
|
@ -25,6 +25,7 @@ TSlotData = TypeVar("TSlotData", bound=Mapping, contravariant=True)
|
|||
|
||||
DEFAULT_SLOT_KEY = "_DJANGO_COMPONENTS_DEFAULT_SLOT"
|
||||
SLOT_DATA_KWARG = "data"
|
||||
SLOT_NAME_KWARG = "name"
|
||||
SLOT_DEFAULT_KWARG = "default"
|
||||
SLOT_REQUIRED_KEYWORD = "required"
|
||||
SLOT_DEFAULT_KEYWORD = "default"
|
||||
|
@ -127,8 +128,8 @@ class SlotRef:
|
|||
class SlotNode(BaseNode):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
nodelist: NodeList,
|
||||
trace_id: str,
|
||||
node_id: Optional[str] = None,
|
||||
kwargs: Optional[RuntimeKwargs] = None,
|
||||
is_required: bool = False,
|
||||
|
@ -136,9 +137,9 @@ class SlotNode(BaseNode):
|
|||
):
|
||||
super().__init__(nodelist=nodelist, args=None, kwargs=kwargs, node_id=node_id)
|
||||
|
||||
self.name = name
|
||||
self.is_required = is_required
|
||||
self.is_default = is_default
|
||||
self.trace_id = trace_id
|
||||
|
||||
@property
|
||||
def active_flags(self) -> List[str]:
|
||||
|
@ -150,14 +151,16 @@ class SlotNode(BaseNode):
|
|||
return flags
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Slot Node: {self.name}. Contents: {repr(self.nodelist)}. Options: {self.active_flags}>"
|
||||
return f"<Slot Node: {self.node_id}. Contents: {repr(self.nodelist)}. Options: {self.active_flags}>"
|
||||
|
||||
def render(self, context: Context) -> SafeString:
|
||||
trace_msg("RENDR", "SLOT", self.name, self.node_id)
|
||||
trace_msg("RENDR", "SLOT", self.trace_id, self.node_id)
|
||||
slots: Dict[SlotId, "SlotFill"] = context[_FILLED_SLOTS_CONTENT_CONTEXT_KEY]
|
||||
# NOTE: Slot entry MUST be present. If it's missing, there was an issue upstream.
|
||||
slot_fill = slots[self.node_id]
|
||||
|
||||
name, kwargs = self.resolve_kwargs(context)
|
||||
|
||||
extra_context: Dict[str, Any] = {}
|
||||
|
||||
# Irrespective of which context we use ("root" context or the one passed to this
|
||||
|
@ -177,19 +180,18 @@ class SlotNode(BaseNode):
|
|||
if default_var:
|
||||
if not default_var.isidentifier():
|
||||
raise TemplateSyntaxError(
|
||||
f"Slot default alias in fill '{self.name}' must be a valid identifier. Got '{default_var}'"
|
||||
f"Slot default alias in fill '{name}' must be a valid identifier. Got '{default_var}'"
|
||||
)
|
||||
extra_context[default_var] = slot_ref
|
||||
|
||||
# Expose the kwargs that were passed to the `{% slot %}` tag. These kwargs
|
||||
# are made available through a variable name that was set on the `{% fill %}`
|
||||
# tag.
|
||||
kwargs = self.kwargs.resolve(context)
|
||||
data_var = slot_fill.slot_data_var
|
||||
if data_var:
|
||||
if not data_var.isidentifier():
|
||||
raise TemplateSyntaxError(
|
||||
f"Slot data alias in fill '{self.name}' must be a valid identifier. Got '{data_var}'"
|
||||
f"Slot data alias in fill '{name}' must be a valid identifier. Got '{data_var}'"
|
||||
)
|
||||
extra_context[data_var] = kwargs
|
||||
|
||||
|
@ -202,7 +204,7 @@ class SlotNode(BaseNode):
|
|||
# the render function ALWAYS receives them.
|
||||
output = slot_fill.content_func(used_ctx, kwargs, slot_ref)
|
||||
|
||||
trace_msg("RENDR", "SLOT", self.name, self.node_id, msg="...Done!")
|
||||
trace_msg("RENDR", "SLOT", self.trace_id, self.node_id, msg="...Done!")
|
||||
return output
|
||||
|
||||
def _resolve_slot_context(self, context: Context, slot_fill: "SlotFill") -> Context:
|
||||
|
@ -220,6 +222,19 @@ class SlotNode(BaseNode):
|
|||
else:
|
||||
raise ValueError(f"Unknown value for CONTEXT_BEHAVIOR: '{app_settings.CONTEXT_BEHAVIOR}'")
|
||||
|
||||
def resolve_kwargs(
|
||||
self,
|
||||
context: Context,
|
||||
component_name: Optional[str] = None,
|
||||
) -> Tuple[str, Dict[str, Optional[str]]]:
|
||||
kwargs = self.kwargs.resolve(context)
|
||||
name = kwargs.pop(SLOT_NAME_KWARG, None)
|
||||
|
||||
if not name:
|
||||
raise RuntimeError(f"Slot tag kwarg 'name' is missing in component {component_name}")
|
||||
|
||||
return (name, kwargs)
|
||||
|
||||
|
||||
class FillNode(BaseNode):
|
||||
"""
|
||||
|
@ -230,16 +245,16 @@ class FillNode(BaseNode):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
name: FilterExpression,
|
||||
nodelist: NodeList,
|
||||
kwargs: RuntimeKwargs,
|
||||
trace_id: str,
|
||||
node_id: Optional[str] = None,
|
||||
is_implicit: bool = False,
|
||||
):
|
||||
super().__init__(nodelist=nodelist, args=None, kwargs=kwargs, node_id=node_id)
|
||||
|
||||
self.name = name
|
||||
self.is_implicit = is_implicit
|
||||
self.trace_id = trace_id
|
||||
self.component_id: Optional[str] = None
|
||||
|
||||
def render(self, context: Context) -> str:
|
||||
|
@ -250,22 +265,24 @@ class FillNode(BaseNode):
|
|||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{type(self)} Name: {self.name}. Contents: {repr(self.nodelist)}.>"
|
||||
return f"<{type(self)} ID: {self.node_id}. Contents: {repr(self.nodelist)}.>"
|
||||
|
||||
def resolve_kwargs(self, context: Context, component_name: Optional[str] = None) -> Dict[str, Optional[str]]:
|
||||
kwargs = self.kwargs.resolve(context)
|
||||
|
||||
name = self._resolve_kwarg(kwargs, SLOT_NAME_KWARG, "slot name", component_name, identifier=False)
|
||||
default_key = self._resolve_kwarg(kwargs, SLOT_DEFAULT_KWARG, "slot default", component_name)
|
||||
data_key = self._resolve_kwarg(kwargs, SLOT_DATA_KWARG, "slot data", component_name)
|
||||
|
||||
# data and default cannot be bound to the same variable
|
||||
if data_key and default_key and data_key == default_key:
|
||||
raise RuntimeError(
|
||||
f"Fill {self.name} received the same string for slot default ({SLOT_DEFAULT_KWARG}=...)"
|
||||
f"Fill '{name}' received the same string for slot default ({SLOT_DEFAULT_KWARG}=...)"
|
||||
f" and slot data ({SLOT_DATA_KWARG}=...)"
|
||||
)
|
||||
|
||||
return {
|
||||
SLOT_NAME_KWARG: name,
|
||||
SLOT_DEFAULT_KWARG: default_key,
|
||||
SLOT_DATA_KWARG: data_key,
|
||||
}
|
||||
|
@ -276,16 +293,19 @@ class FillNode(BaseNode):
|
|||
key: str,
|
||||
name: str,
|
||||
component_name: Optional[str] = None,
|
||||
identifier: bool = True,
|
||||
) -> Optional[str]:
|
||||
if key not in kwargs:
|
||||
return None
|
||||
|
||||
value = kwargs[key]
|
||||
if not is_identifier(value):
|
||||
if identifier and not is_identifier(value):
|
||||
raise RuntimeError(
|
||||
f"Fill tag {name} in component {component_name}"
|
||||
f"does not resolve to a valid Python identifier, got '{value}'"
|
||||
)
|
||||
elif not identifier and not value:
|
||||
raise RuntimeError(f"Fill tag {name} is missing value in component {component_name}")
|
||||
|
||||
return value
|
||||
|
||||
|
@ -340,15 +360,15 @@ def _try_parse_as_named_fill_tag_set(
|
|||
seen_names: Set[str] = set()
|
||||
for node in nodelist:
|
||||
if isinstance(node, FillNode):
|
||||
# Check that, after we've resolved the names, that there's still no duplicates.
|
||||
# This makes sure that if two different variables refer to same string, we detect
|
||||
# them.
|
||||
if node.name.token in seen_names:
|
||||
raise TemplateSyntaxError(
|
||||
f"Multiple fill tags cannot target the same slot name: "
|
||||
f"Detected duplicate fill tag name '{node.name}'."
|
||||
)
|
||||
seen_names.add(node.name.token)
|
||||
# If the fill name was defined statically, then check for no duplicates.
|
||||
maybe_fill_name = node.kwargs.kwargs.get(SLOT_NAME_KWARG)
|
||||
if isinstance(maybe_fill_name, FilterExpression):
|
||||
if maybe_fill_name.token in seen_names:
|
||||
raise TemplateSyntaxError(
|
||||
f"Multiple fill tags cannot target the same slot name: "
|
||||
f"Detected duplicate fill tag name '{maybe_fill_name.token}'."
|
||||
)
|
||||
seen_names.add(maybe_fill_name.token)
|
||||
result.append(node)
|
||||
elif isinstance(node, CommentNode):
|
||||
pass
|
||||
|
@ -378,13 +398,50 @@ def _try_parse_as_default_fill(
|
|||
return [
|
||||
FillNode(
|
||||
nodelist=nodelist,
|
||||
name=FilterExpression(json.dumps(DEFAULT_SLOT_KEY), Parser("")),
|
||||
kwargs=RuntimeKwargs({}),
|
||||
kwargs=RuntimeKwargs(
|
||||
{
|
||||
# Wrap the default slot name in quotes so it's treated as FilterExpression
|
||||
SLOT_NAME_KWARG: FilterExpression(json.dumps(DEFAULT_SLOT_KEY), Parser("")),
|
||||
}
|
||||
),
|
||||
is_implicit=True,
|
||||
trace_id="default",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def resolve_fill_nodes(
|
||||
context: Context,
|
||||
fill_nodes: List[FillNode],
|
||||
component_name: str,
|
||||
) -> Dict[str, FillContent]:
|
||||
fill_content: Dict[str, FillContent] = {}
|
||||
for fill_node in fill_nodes:
|
||||
# Note that outer component context is used to resolve variables in
|
||||
# fill tag.
|
||||
fill_kwargs = fill_node.resolve_kwargs(context, component_name)
|
||||
fill_name = fill_kwargs.get(SLOT_NAME_KWARG)
|
||||
|
||||
if SLOT_NAME_KWARG not in fill_kwargs:
|
||||
raise TemplateSyntaxError("Fill tag is missing the 'name' kwarg")
|
||||
|
||||
if not isinstance(fill_name, str):
|
||||
raise TemplateSyntaxError(f"Fill tag 'name' kwarg must resolve to a string, got {fill_name}")
|
||||
|
||||
if fill_name in fill_content:
|
||||
raise TemplateSyntaxError(
|
||||
f"Multiple fill tags cannot target the same slot name: "
|
||||
f"Detected duplicate fill tag name '{fill_name}'."
|
||||
)
|
||||
|
||||
fill_content[fill_name] = FillContent(
|
||||
content_func=_nodelist_to_slot_render_func(fill_node.nodelist),
|
||||
slot_default_var=fill_kwargs[SLOT_DEFAULT_KWARG],
|
||||
slot_data_var=fill_kwargs[SLOT_DATA_KWARG],
|
||||
)
|
||||
return fill_content
|
||||
|
||||
|
||||
####################
|
||||
# SLOT RESOLUTION
|
||||
####################
|
||||
|
@ -427,13 +484,15 @@ def resolve_slots(
|
|||
if not isinstance(node, SlotNode):
|
||||
return
|
||||
|
||||
slot_name, _ = node.resolve_kwargs(context, component_name)
|
||||
|
||||
# 1. Collect slots
|
||||
# Basically we take all the important info form the SlotNode, so the logic is
|
||||
# less coupled to Django's Template/Node. Plain tuples should also help with
|
||||
# troubleshooting.
|
||||
slot = Slot(
|
||||
id=node.node_id,
|
||||
name=node.name,
|
||||
name=slot_name,
|
||||
nodelist=node.nodelist,
|
||||
is_default=node.is_default,
|
||||
is_required=node.is_required,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from typing import TYPE_CHECKING, Callable, Dict, List, NamedTuple, Optional, Set, Union
|
||||
|
||||
import django.template
|
||||
from django.template.base import FilterExpression, NodeList, Parser, Token, TokenType
|
||||
from django.template.base import NodeList, Parser, Token, TokenType
|
||||
from django.template.exceptions import TemplateSyntaxError
|
||||
from django.utils.safestring import SafeString, mark_safe
|
||||
from django.utils.text import smart_split
|
||||
|
@ -25,7 +25,6 @@ from django_components.expression import (
|
|||
is_internal_spread_operator,
|
||||
is_kwarg,
|
||||
is_spread_operator,
|
||||
resolve_string,
|
||||
)
|
||||
from django_components.logger import trace_msg
|
||||
from django_components.middleware import (
|
||||
|
@ -33,11 +32,12 @@ from django_components.middleware import (
|
|||
JS_DEPENDENCY_PLACEHOLDER,
|
||||
is_dependency_middleware_active,
|
||||
)
|
||||
from django_components.provide import ProvideNode
|
||||
from django_components.provide import PROVIDE_NAME_KWARG, ProvideNode
|
||||
from django_components.slots import (
|
||||
SLOT_DATA_KWARG,
|
||||
SLOT_DEFAULT_KEYWORD,
|
||||
SLOT_DEFAULT_KWARG,
|
||||
SLOT_NAME_KWARG,
|
||||
SLOT_REQUIRED_KEYWORD,
|
||||
FillNode,
|
||||
SlotNode,
|
||||
|
@ -45,7 +45,7 @@ from django_components.slots import (
|
|||
)
|
||||
from django_components.tag_formatter import get_tag_formatter
|
||||
from django_components.template_parser import parse_bits
|
||||
from django_components.utils import gen_id, is_str_wrapped_in_quotes
|
||||
from django_components.utils import gen_id
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django_components.component import Component
|
||||
|
@ -139,27 +139,30 @@ def slot(parser: Parser, token: Token) -> SlotNode:
|
|||
"slot",
|
||||
parser,
|
||||
token,
|
||||
params=["name"],
|
||||
params=[SLOT_NAME_KWARG],
|
||||
optional_params=[SLOT_NAME_KWARG],
|
||||
flags=[SLOT_DEFAULT_KEYWORD, SLOT_REQUIRED_KEYWORD],
|
||||
keywordonly_kwargs=True,
|
||||
repeatable_kwargs=False,
|
||||
end_tag="endslot",
|
||||
)
|
||||
data = _parse_slot_args(parser, tag)
|
||||
|
||||
trace_msg("PARSE", "SLOT", data.name, tag.id)
|
||||
slot_name_kwarg = tag.kwargs.kwargs.get(SLOT_NAME_KWARG, None)
|
||||
trace_id = f"slot-id-{tag.id} ({slot_name_kwarg})" if slot_name_kwarg else f"slot-id-{tag.id}"
|
||||
|
||||
trace_msg("PARSE", "SLOT", trace_id, tag.id)
|
||||
|
||||
body = tag.parse_body()
|
||||
slot_node = SlotNode(
|
||||
name=data.name,
|
||||
nodelist=body,
|
||||
node_id=tag.id,
|
||||
kwargs=tag.kwargs,
|
||||
is_required=tag.flags[SLOT_REQUIRED_KEYWORD],
|
||||
is_default=tag.flags[SLOT_DEFAULT_KEYWORD],
|
||||
trace_id=trace_id,
|
||||
)
|
||||
|
||||
trace_msg("PARSE", "SLOT", data.name, tag.id, "...Done!")
|
||||
trace_msg("PARSE", "SLOT", trace_id, tag.id, "...Done!")
|
||||
return slot_node
|
||||
|
||||
|
||||
|
@ -177,24 +180,27 @@ def fill(parser: Parser, token: Token) -> FillNode:
|
|||
"fill",
|
||||
parser,
|
||||
token,
|
||||
params=["name"],
|
||||
params=[SLOT_NAME_KWARG],
|
||||
optional_params=[SLOT_NAME_KWARG],
|
||||
keywordonly_kwargs=[SLOT_DATA_KWARG, SLOT_DEFAULT_KWARG],
|
||||
repeatable_kwargs=False,
|
||||
end_tag="endfill",
|
||||
)
|
||||
slot_name = tag.named_args["name"]
|
||||
|
||||
trace_msg("PARSE", "FILL", str(slot_name), tag.id)
|
||||
fill_name_kwarg = tag.kwargs.kwargs.get(SLOT_NAME_KWARG, None)
|
||||
trace_id = f"fill-id-{tag.id} ({fill_name_kwarg})" if fill_name_kwarg else f"fill-id-{tag.id}"
|
||||
|
||||
trace_msg("PARSE", "FILL", trace_id, tag.id)
|
||||
|
||||
body = tag.parse_body()
|
||||
fill_node = FillNode(
|
||||
nodelist=body,
|
||||
name=slot_name,
|
||||
node_id=tag.id,
|
||||
kwargs=tag.kwargs,
|
||||
trace_id=trace_id,
|
||||
)
|
||||
|
||||
trace_msg("PARSE", "FILL", str(slot_name), tag.id, "...Done!")
|
||||
trace_msg("PARSE", "FILL", trace_id, tag.id, "...Done!")
|
||||
return fill_node
|
||||
|
||||
|
||||
|
@ -244,7 +250,7 @@ def component(parser: Parser, token: Token, tag_name: str) -> ComponentNode:
|
|||
|
||||
# Tag all fill nodes as children of this particular component instance
|
||||
for node in fill_nodes:
|
||||
trace_msg("ASSOC", "FILL", node.name, node.node_id, component_id=tag.id)
|
||||
trace_msg("ASSOC", "FILL", node.trace_id, node.node_id, component_id=tag.id)
|
||||
node.component_id = tag.id
|
||||
|
||||
component_node = ComponentNode(
|
||||
|
@ -267,25 +273,28 @@ def provide(parser: Parser, token: Token) -> ProvideNode:
|
|||
"provide",
|
||||
parser,
|
||||
token,
|
||||
params=["name"],
|
||||
params=[PROVIDE_NAME_KWARG],
|
||||
optional_params=[PROVIDE_NAME_KWARG],
|
||||
flags=[],
|
||||
keywordonly_kwargs=True,
|
||||
repeatable_kwargs=False,
|
||||
end_tag="endprovide",
|
||||
)
|
||||
data = _parse_provide_args(parser, tag)
|
||||
|
||||
trace_msg("PARSE", "PROVIDE", data.key, tag.id)
|
||||
name_kwarg = tag.kwargs.kwargs.get(PROVIDE_NAME_KWARG, None)
|
||||
trace_id = f"provide-id-{tag.id} ({name_kwarg})" if name_kwarg else f"fill-id-{tag.id}"
|
||||
|
||||
trace_msg("PARSE", "PROVIDE", trace_id, tag.id)
|
||||
|
||||
body = tag.parse_body()
|
||||
slot_node = ProvideNode(
|
||||
name=data.key,
|
||||
nodelist=body,
|
||||
node_id=tag.id,
|
||||
kwargs=tag.kwargs,
|
||||
trace_id=trace_id,
|
||||
)
|
||||
|
||||
trace_msg("PARSE", "PROVIDE", data.key, tag.id, "...Done!")
|
||||
trace_msg("PARSE", "PROVIDE", trace_id, tag.id, "...Done!")
|
||||
return slot_node
|
||||
|
||||
|
||||
|
@ -531,7 +540,9 @@ def _parse_tag(
|
|||
else:
|
||||
is_key_allowed = keywordonly_kwargs == True or key in keywordonly_kwargs # noqa: E712
|
||||
if not is_key_allowed:
|
||||
extra_keywords.add(key)
|
||||
is_optional = key in optional_params if optional_params else False
|
||||
if not is_optional:
|
||||
extra_keywords.add(key)
|
||||
|
||||
# Check for repeated keys
|
||||
if key in kwargs:
|
||||
|
@ -674,43 +685,3 @@ def _fix_nested_tags(parser: Parser, block_token: Token) -> None:
|
|||
|
||||
expects_text = True
|
||||
continue
|
||||
|
||||
|
||||
class ParsedSlotTag(NamedTuple):
|
||||
name: str
|
||||
|
||||
|
||||
def _parse_slot_args(
|
||||
parser: Parser,
|
||||
tag: ParsedTag,
|
||||
) -> ParsedSlotTag:
|
||||
slot_name_expr = tag.named_args["name"]
|
||||
if not isinstance(slot_name_expr, FilterExpression):
|
||||
raise TemplateSyntaxError(f"Slot name must be string literal, got {slot_name_expr}")
|
||||
slot_name = slot_name_expr.token
|
||||
if not is_str_wrapped_in_quotes(slot_name):
|
||||
raise TemplateSyntaxError(f"'{tag.name}' name must be a string 'literal', got {slot_name}.")
|
||||
|
||||
slot_name = resolve_string(slot_name, parser)
|
||||
|
||||
return ParsedSlotTag(name=slot_name)
|
||||
|
||||
|
||||
class ParsedProvideTag(NamedTuple):
|
||||
key: str
|
||||
|
||||
|
||||
def _parse_provide_args(
|
||||
parser: Parser,
|
||||
tag: ParsedTag,
|
||||
) -> ParsedProvideTag:
|
||||
provide_key_expr = tag.named_args["name"]
|
||||
if not isinstance(provide_key_expr, FilterExpression):
|
||||
raise TemplateSyntaxError(f"Provide key must be string literal, got {provide_key_expr}")
|
||||
provide_key = provide_key_expr.token
|
||||
if not is_str_wrapped_in_quotes(provide_key):
|
||||
raise TemplateSyntaxError(f"'{tag.name}' key must be a string 'literal', got {provide_key}")
|
||||
|
||||
provide_key = resolve_string(provide_key, parser)
|
||||
|
||||
return ParsedProvideTag(key=provide_key)
|
||||
|
|
|
@ -188,6 +188,38 @@ class HtmlAttrsTests(BaseTestCase):
|
|||
)
|
||||
self.assertNotIn("override-me", rendered)
|
||||
|
||||
def test_tag_spread(self):
|
||||
@register("test")
|
||||
class AttrsComponent(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
<div {% html_attrs ...props class="another-class" %}>
|
||||
content
|
||||
</div>
|
||||
""" # noqa: E501
|
||||
|
||||
def get_context_data(self, *args, attrs):
|
||||
return {
|
||||
"props": {
|
||||
"attrs": attrs,
|
||||
"defaults": {"class": "override-me"},
|
||||
"class": "added_class",
|
||||
"data-id": 123,
|
||||
},
|
||||
}
|
||||
|
||||
template = Template(self.template_str)
|
||||
rendered = template.render(Context({"class_var": "padding-top-8"}))
|
||||
self.assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
<div @click.stop="dispatch('click_event')" class="added_class another-class padding-top-8" data-id="123" x-data="{hello: 'world'}">
|
||||
content
|
||||
</div>
|
||||
""", # noqa: E501
|
||||
)
|
||||
self.assertNotIn("override-me", rendered)
|
||||
|
||||
def test_tag_aggregate_args(self):
|
||||
@register("test")
|
||||
class AttrsComponent(Component):
|
||||
|
|
|
@ -220,7 +220,7 @@ class ProvideTemplateTagTest(BaseTestCase):
|
|||
)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_provide_key_single_quotes(self):
|
||||
def test_provide_name_single_quotes(self):
|
||||
@register("injectee")
|
||||
class InjectComponent(Component):
|
||||
template: types.django_html = """
|
||||
|
@ -252,7 +252,87 @@ class ProvideTemplateTagTest(BaseTestCase):
|
|||
)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_provide_no_key_raises(self):
|
||||
def test_provide_name_as_var(self):
|
||||
@register("injectee")
|
||||
class InjectComponent(Component):
|
||||
template: types.django_html = """
|
||||
<div> injected: {{ var|safe }} </div>
|
||||
"""
|
||||
|
||||
def get_context_data(self):
|
||||
var = self.inject("my_provide", "default")
|
||||
return {"var": var}
|
||||
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% provide var_a key="hi" another=123 %}
|
||||
{% component "injectee" %}
|
||||
{% endcomponent %}
|
||||
{% endprovide %}
|
||||
{% component "injectee" %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
template = Template(template_str)
|
||||
rendered = template.render(
|
||||
Context(
|
||||
{
|
||||
"var_a": "my_provide",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
self.assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
<div> injected: DepInject(key='hi', another=123) </div>
|
||||
<div> injected: default </div>
|
||||
""",
|
||||
)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_provide_name_as_spread(self):
|
||||
@register("injectee")
|
||||
class InjectComponent(Component):
|
||||
template: types.django_html = """
|
||||
<div> injected: {{ var|safe }} </div>
|
||||
"""
|
||||
|
||||
def get_context_data(self):
|
||||
var = self.inject("my_provide", "default")
|
||||
return {"var": var}
|
||||
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% provide ...provide_props %}
|
||||
{% component "injectee" %}
|
||||
{% endcomponent %}
|
||||
{% endprovide %}
|
||||
{% component "injectee" %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
template = Template(template_str)
|
||||
rendered = template.render(
|
||||
Context(
|
||||
{
|
||||
"provide_props": {
|
||||
"name": "my_provide",
|
||||
"key": "hi",
|
||||
"another": 123,
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
self.assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
<div> injected: DepInject(key='hi', another=123) </div>
|
||||
<div> injected: default </div>
|
||||
""",
|
||||
)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_provide_no_name_raises(self):
|
||||
@register("injectee")
|
||||
class InjectComponent(Component):
|
||||
template: types.django_html = """
|
||||
|
@ -272,11 +352,11 @@ class ProvideTemplateTagTest(BaseTestCase):
|
|||
{% component "injectee" %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
with self.assertRaises(TemplateSyntaxError):
|
||||
Template(template_str)
|
||||
with self.assertRaisesMessage(RuntimeError, "Provide tag kwarg 'name' is missing"):
|
||||
Template(template_str).render(Context({}))
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_provide_key_must_be_string_literal(self):
|
||||
def test_provide_name_must_be_string_literal(self):
|
||||
@register("injectee")
|
||||
class InjectComponent(Component):
|
||||
template: types.django_html = """
|
||||
|
@ -296,11 +376,11 @@ class ProvideTemplateTagTest(BaseTestCase):
|
|||
{% component "injectee" %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
with self.assertRaises(TemplateSyntaxError):
|
||||
Template(template_str)
|
||||
with self.assertRaisesMessage(RuntimeError, "Provide tag kwarg 'name' is missing"):
|
||||
Template(template_str).render(Context({}))
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_provide_key_must_be_identifier(self):
|
||||
def test_provide_name_must_be_identifier(self):
|
||||
@register("injectee")
|
||||
class InjectComponent(Component):
|
||||
template: types.django_html = """
|
||||
|
|
|
@ -796,6 +796,80 @@ class ScopedSlotTest(BaseTestCase):
|
|||
"""
|
||||
self.assertHTMLEqual(rendered, expected)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_slot_data_with_variable(self):
|
||||
@register("test")
|
||||
class TestComponent(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
<div>
|
||||
{% slot slot_name abc=abc var123=var123 default required %}Default text{% endslot %}
|
||||
</div>
|
||||
"""
|
||||
|
||||
def get_context_data(self):
|
||||
return {
|
||||
"slot_name": "my_slot",
|
||||
"abc": "def",
|
||||
"var123": 456,
|
||||
}
|
||||
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component "test" %}
|
||||
{% fill "my_slot" data="slot_data_in_fill" %}
|
||||
{{ slot_data_in_fill.abc }}
|
||||
{{ slot_data_in_fill.var123 }}
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
rendered = Template(template).render(Context())
|
||||
expected = """
|
||||
<div>
|
||||
def
|
||||
456
|
||||
</div>
|
||||
"""
|
||||
self.assertHTMLEqual(rendered, expected)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_slot_data_with_spread(self):
|
||||
@register("test")
|
||||
class TestComponent(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
<div>
|
||||
{% slot ...slot_props default required %}Default text{% endslot %}
|
||||
</div>
|
||||
"""
|
||||
|
||||
def get_context_data(self):
|
||||
return {
|
||||
"slot_props": {
|
||||
"name": "my_slot",
|
||||
"abc": "def",
|
||||
"var123": 456,
|
||||
},
|
||||
}
|
||||
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component "test" %}
|
||||
{% fill "my_slot" data="slot_data_in_fill" %}
|
||||
{{ slot_data_in_fill.abc }}
|
||||
{{ slot_data_in_fill.var123 }}
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
rendered = Template(template).render(Context())
|
||||
expected = """
|
||||
<div>
|
||||
def
|
||||
456
|
||||
</div>
|
||||
"""
|
||||
self.assertHTMLEqual(rendered, expected)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_slot_data_raises_on_slot_data_and_slot_default_same_var(self):
|
||||
@register("test")
|
||||
|
@ -823,7 +897,7 @@ class ScopedSlotTest(BaseTestCase):
|
|||
"""
|
||||
with self.assertRaisesMessage(
|
||||
RuntimeError,
|
||||
'Fill "my_slot" received the same string for slot default (default=...) and slot data (data=...)',
|
||||
"Fill 'my_slot' received the same string for slot default (default=...) and slot data (data=...)",
|
||||
):
|
||||
Template(template).render(Context())
|
||||
|
||||
|
@ -905,6 +979,94 @@ class ScopedSlotTest(BaseTestCase):
|
|||
expected = "<div> Default text </div>"
|
||||
self.assertHTMLEqual(rendered, expected)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_slot_data_fill_with_variables(self):
|
||||
@register("test")
|
||||
class TestComponent(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
<div>
|
||||
{% slot "my_slot" abc=abc var123=var123 %}Default text{% endslot %}
|
||||
</div>
|
||||
"""
|
||||
|
||||
def get_context_data(self):
|
||||
return {
|
||||
"abc": "def",
|
||||
"var123": 456,
|
||||
}
|
||||
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component "test" %}
|
||||
{% fill fill_name data=data_var %}
|
||||
{{ slot_data_in_fill.abc }}
|
||||
{{ slot_data_in_fill.var123 }}
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
rendered = Template(template).render(
|
||||
Context(
|
||||
{
|
||||
"fill_name": "my_slot",
|
||||
"data_var": "slot_data_in_fill",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
expected = """
|
||||
<div>
|
||||
def
|
||||
456
|
||||
</div>
|
||||
"""
|
||||
self.assertHTMLEqual(rendered, expected)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_slot_data_fill_with_spread(self):
|
||||
@register("test")
|
||||
class TestComponent(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
<div>
|
||||
{% slot "my_slot" abc=abc var123=var123 %}Default text{% endslot %}
|
||||
</div>
|
||||
"""
|
||||
|
||||
def get_context_data(self):
|
||||
return {
|
||||
"abc": "def",
|
||||
"var123": 456,
|
||||
}
|
||||
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component "test" %}
|
||||
{% fill ...fill_props %}
|
||||
{{ slot_data_in_fill.abc }}
|
||||
{{ slot_data_in_fill.var123 }}
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
rendered = Template(template).render(
|
||||
Context(
|
||||
{
|
||||
"fill_props": {
|
||||
"name": "my_slot",
|
||||
"data": "slot_data_in_fill",
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
expected = """
|
||||
<div>
|
||||
def
|
||||
456
|
||||
</div>
|
||||
"""
|
||||
self.assertHTMLEqual(rendered, expected)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_nested_fills(self):
|
||||
@register("test")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue