mirror of
https://github.com/django-components/django-components.git
synced 2025-09-21 21:22:26 +00:00
refactor: Rename {% fill default=... %}
to {% fill fallback=... %}
(#1190)
This commit is contained in:
parent
0d05ef4cb2
commit
b6b574d875
7 changed files with 167 additions and 91 deletions
22
CHANGELOG.md
22
CHANGELOG.md
|
@ -322,10 +322,30 @@
|
||||||
ctx.template_data["my_template_var"] = "my_value"
|
ctx.template_data["my_template_var"] = "my_value"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Miscellaneous**
|
**Slots**
|
||||||
|
|
||||||
- `SlotContent` was renamed to `SlotInput`. The old name is deprecated and will be removed in v1.
|
- `SlotContent` was renamed to `SlotInput`. The old name is deprecated and will be removed in v1.
|
||||||
|
|
||||||
|
- `SlotRef` was renamed to `SlotFallback`. The old name is deprecated and will be removed in v1.
|
||||||
|
|
||||||
|
- The `default` kwarg in `{% fill %}` tag was renamed to `fallback`. The old name is deprecated and will be removed in v1.
|
||||||
|
|
||||||
|
Before:
|
||||||
|
|
||||||
|
```django
|
||||||
|
{% fill "footer" default="footer" %}
|
||||||
|
{{ footer }}
|
||||||
|
{% endfill %}
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
```django
|
||||||
|
{% fill "footer" fallback="footer" %}
|
||||||
|
{{ footer }}
|
||||||
|
{% endfill %}
|
||||||
|
```
|
||||||
|
|
||||||
#### Feat
|
#### Feat
|
||||||
|
|
||||||
- New method to render template variables - `get_template_data()`
|
- New method to render template variables - `get_template_data()`
|
||||||
|
|
|
@ -50,7 +50,7 @@ from django_components.extensions.defaults import ComponentDefaults, Default
|
||||||
from django_components.extensions.view import ComponentView, get_component_url
|
from django_components.extensions.view import ComponentView, get_component_url
|
||||||
from django_components.library import TagProtectedError
|
from django_components.library import TagProtectedError
|
||||||
from django_components.node import BaseNode, template_tag
|
from django_components.node import BaseNode, template_tag
|
||||||
from django_components.slots import Slot, SlotContent, SlotFunc, SlotInput, SlotRef, SlotResult
|
from django_components.slots import Slot, SlotContent, SlotFallback, SlotFunc, SlotInput, SlotRef, SlotResult
|
||||||
from django_components.tag_formatter import (
|
from django_components.tag_formatter import (
|
||||||
ComponentFormatter,
|
ComponentFormatter,
|
||||||
ShorthandComponentFormatter,
|
ShorthandComponentFormatter,
|
||||||
|
@ -125,6 +125,7 @@ __all__ = [
|
||||||
"ShorthandComponentFormatter",
|
"ShorthandComponentFormatter",
|
||||||
"Slot",
|
"Slot",
|
||||||
"SlotContent",
|
"SlotContent",
|
||||||
|
"SlotFallback",
|
||||||
"SlotFunc",
|
"SlotFunc",
|
||||||
"SlotInput",
|
"SlotInput",
|
||||||
"SlotRef",
|
"SlotRef",
|
||||||
|
|
|
@ -67,11 +67,11 @@ from django_components.perfutil.provide import register_provide_reference, unreg
|
||||||
from django_components.provide import get_injected_context_var
|
from django_components.provide import get_injected_context_var
|
||||||
from django_components.slots import (
|
from django_components.slots import (
|
||||||
Slot,
|
Slot,
|
||||||
|
SlotFallback,
|
||||||
SlotFunc,
|
SlotFunc,
|
||||||
SlotInput,
|
SlotInput,
|
||||||
SlotIsFilled,
|
SlotIsFilled,
|
||||||
SlotName,
|
SlotName,
|
||||||
SlotRef,
|
|
||||||
SlotResult,
|
SlotResult,
|
||||||
_is_extracting_fill,
|
_is_extracting_fill,
|
||||||
resolve_fills,
|
resolve_fills,
|
||||||
|
@ -2723,8 +2723,8 @@ class Component(metaclass=ComponentMeta):
|
||||||
# so we can assign metadata to our internal copies.
|
# so we can assign metadata to our internal copies.
|
||||||
if not isinstance(content, Slot) or not content.escaped:
|
if not isinstance(content, Slot) or not content.escaped:
|
||||||
# We wrap the original function so we post-process it by escaping the result.
|
# We wrap the original function so we post-process it by escaping the result.
|
||||||
def content_fn(ctx: Context, slot_data: Dict, slot_ref: SlotRef) -> SlotResult:
|
def content_fn(ctx: Context, slot_data: Dict, fallback: SlotFallback) -> SlotResult:
|
||||||
rendered = content(ctx, slot_data, slot_ref)
|
rendered = content(ctx, slot_data, fallback)
|
||||||
return conditional_escape(rendered) if escape_content else rendered
|
return conditional_escape(rendered) if escape_content else rendered
|
||||||
|
|
||||||
content_func = cast(SlotFunc, content_fn)
|
content_func = cast(SlotFunc, content_fn)
|
||||||
|
|
|
@ -45,7 +45,7 @@ SLOT_NAME_KWARG = "name"
|
||||||
SLOT_REQUIRED_FLAG = "required"
|
SLOT_REQUIRED_FLAG = "required"
|
||||||
SLOT_DEFAULT_FLAG = "default"
|
SLOT_DEFAULT_FLAG = "default"
|
||||||
FILL_DATA_KWARG = "data"
|
FILL_DATA_KWARG = "data"
|
||||||
FILL_DEFAULT_KWARG = "default"
|
FILL_FALLBACK_KWARG = "fallback"
|
||||||
|
|
||||||
|
|
||||||
# Public types
|
# Public types
|
||||||
|
@ -54,7 +54,7 @@ SlotResult = Union[str, SafeString]
|
||||||
|
|
||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
class SlotFunc(Protocol, Generic[TSlotData]):
|
class SlotFunc(Protocol, Generic[TSlotData]):
|
||||||
def __call__(self, ctx: Context, slot_data: TSlotData, slot_ref: "SlotRef") -> SlotResult: ... # noqa E704
|
def __call__(self, ctx: Context, slot_data: TSlotData, slot_ref: "SlotFallback") -> SlotResult: ... # noqa E704
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -102,7 +102,7 @@ class Slot(Generic[TSlotData]):
|
||||||
raise ValueError(f"Slot content must be a callable, got: {self.content_func}")
|
raise ValueError(f"Slot content must be a callable, got: {self.content_func}")
|
||||||
|
|
||||||
# Allow to treat the instances as functions
|
# Allow to treat the instances as functions
|
||||||
def __call__(self, ctx: Context, slot_data: TSlotData, slot_ref: "SlotRef") -> SlotResult:
|
def __call__(self, ctx: Context, slot_data: TSlotData, slot_ref: "SlotFallback") -> SlotResult:
|
||||||
return self.content_func(ctx, slot_data, slot_ref)
|
return self.content_func(ctx, slot_data, slot_ref)
|
||||||
|
|
||||||
# Make Django pass the instances of this class within the templates without calling
|
# Make Django pass the instances of this class within the templates without calling
|
||||||
|
@ -125,7 +125,7 @@ class Slot(Generic[TSlotData]):
|
||||||
nodelist=NodeList([TextNode(contents)]),
|
nodelist=NodeList([TextNode(contents)]),
|
||||||
contents=contents,
|
contents=contents,
|
||||||
data_var=None,
|
data_var=None,
|
||||||
default_var=None,
|
fallback_var=None,
|
||||||
)
|
)
|
||||||
return slot.contents, slot.nodelist, slot.content_func
|
return slot.contents, slot.nodelist, slot.content_func
|
||||||
|
|
||||||
|
@ -191,25 +191,36 @@ class SlotFill(Generic[TSlotData]):
|
||||||
slot: Slot[TSlotData]
|
slot: Slot[TSlotData]
|
||||||
|
|
||||||
|
|
||||||
class SlotRef:
|
class SlotFallback:
|
||||||
"""
|
"""
|
||||||
SlotRef allows to treat a slot as a variable. The slot is rendered only once
|
SlotFallback allows to treat a slot fallback as a variable. The slot is rendered only once
|
||||||
the instance is coerced to string.
|
the instance is coerced to string.
|
||||||
|
|
||||||
This is used to access slots as variables inside the templates. When a SlotRef
|
This is used to access slots as variables inside the templates. When a `SlotFallback`
|
||||||
is rendered in the template with `{{ my_lazy_slot }}`, it will output the contents
|
is rendered in the template with `{{ my_lazy_slot }}`, it will output the contents
|
||||||
of the slot.
|
of the slot.
|
||||||
|
|
||||||
|
Usage in slot functions:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def slot_function(self, ctx: Context, slot_data: TSlotData, fallback: SlotFallback):
|
||||||
|
return f"Hello, {fallback}!"
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, slot: "SlotNode", context: Context):
|
def __init__(self, slot: "SlotNode", context: Context):
|
||||||
self._slot = slot
|
self._slot = slot
|
||||||
self._context = context
|
self._context = context
|
||||||
|
|
||||||
# Render the slot when the template coerces SlotRef to string
|
# Render the slot when the template coerces SlotFallback to string
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return mark_safe(self._slot.nodelist.render(self._context))
|
return mark_safe(self._slot.nodelist.render(self._context))
|
||||||
|
|
||||||
|
|
||||||
|
# TODO_v1 - REMOVE - superseded by SlotFallback
|
||||||
|
SlotRef = SlotFallback
|
||||||
|
|
||||||
|
|
||||||
class SlotIsFilled(dict):
|
class SlotIsFilled(dict):
|
||||||
"""
|
"""
|
||||||
Dictionary that returns `True` if the slot is filled (key is found), `False` otherwise.
|
Dictionary that returns `True` if the slot is filled (key is found), `False` otherwise.
|
||||||
|
@ -312,13 +323,13 @@ class SlotNode(BaseNode):
|
||||||
\"\"\"
|
\"\"\"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Accessing default slot content
|
### Accessing fallback slot content
|
||||||
|
|
||||||
The content between the `{% slot %}..{% endslot %}` tags is the default content that
|
The content between the `{% slot %}..{% endslot %}` tags is the fallback content that
|
||||||
will be rendered if no fill is given for the slot.
|
will be rendered if no fill is given for the slot.
|
||||||
|
|
||||||
This default content can then be accessed from within the [`{% fill %}`](#fill) tag using
|
This fallback content can then be accessed from within the [`{% fill %}`](#fill) tag using
|
||||||
the fill's `default` kwarg.
|
the fill's `fallback` kwarg.
|
||||||
This is useful if you need to wrap / prepend / append the original slot's content.
|
This is useful if you need to wrap / prepend / append the original slot's content.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
@ -327,7 +338,7 @@ class SlotNode(BaseNode):
|
||||||
template = \"\"\"
|
template = \"\"\"
|
||||||
<div>
|
<div>
|
||||||
{% slot "content" %}
|
{% slot "content" %}
|
||||||
This is default content!
|
This is fallback content!
|
||||||
{% endslot %}
|
{% endslot %}
|
||||||
</div>
|
</div>
|
||||||
\"\"\"
|
\"\"\"
|
||||||
|
@ -337,10 +348,10 @@ class SlotNode(BaseNode):
|
||||||
@register("parent")
|
@register("parent")
|
||||||
class Parent(Component):
|
class Parent(Component):
|
||||||
template = \"\"\"
|
template = \"\"\"
|
||||||
{# Parent can access the slot's default content #}
|
{# Parent can access the slot's fallback content #}
|
||||||
{% component "child" %}
|
{% component "child" %}
|
||||||
{% fill "content" default="default" %}
|
{% fill "content" fallback="fallback" %}
|
||||||
{{ default }}
|
{{ fallback }}
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
\"\"\"
|
\"\"\"
|
||||||
|
@ -547,7 +558,7 @@ class SlotNode(BaseNode):
|
||||||
slot=slot_fill_fn,
|
slot=slot_fill_fn,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# No fill was supplied, render the slot's default content
|
# No fill was supplied, render the slot's fallback content
|
||||||
slot_fill = SlotFill(
|
slot_fill = SlotFill(
|
||||||
name=slot_name,
|
name=slot_name,
|
||||||
is_filled=False,
|
is_filled=False,
|
||||||
|
@ -557,7 +568,7 @@ class SlotNode(BaseNode):
|
||||||
nodelist=self.nodelist,
|
nodelist=self.nodelist,
|
||||||
contents=self.contents,
|
contents=self.contents,
|
||||||
data_var=None,
|
data_var=None,
|
||||||
default_var=None,
|
fallback_var=None,
|
||||||
# Escaped because this was defined in the template
|
# Escaped because this was defined in the template
|
||||||
escaped=True,
|
escaped=True,
|
||||||
),
|
),
|
||||||
|
@ -625,7 +636,7 @@ class SlotNode(BaseNode):
|
||||||
if key.startswith(_INJECT_CONTEXT_KEY_PREFIX):
|
if key.startswith(_INJECT_CONTEXT_KEY_PREFIX):
|
||||||
extra_context[key] = value
|
extra_context[key] = value
|
||||||
|
|
||||||
slot_ref = SlotRef(self, context)
|
slot_ref = SlotFallback(self, context)
|
||||||
|
|
||||||
# For the user-provided slot fill, we want to use the context of where the slot
|
# For the user-provided slot fill, we want to use the context of where the slot
|
||||||
# came from (or current context if configured so)
|
# came from (or current context if configured so)
|
||||||
|
@ -644,7 +655,7 @@ class SlotNode(BaseNode):
|
||||||
with used_ctx.render_context.push(render_ctx_layer):
|
with used_ctx.render_context.push(render_ctx_layer):
|
||||||
with add_slot_to_error_message(component_name, slot_name):
|
with add_slot_to_error_message(component_name, slot_name):
|
||||||
# Render slot as a function
|
# Render slot as a function
|
||||||
# NOTE: While `{% fill %}` tag has to opt in for the `default` and `data` variables,
|
# NOTE: While `{% fill %}` tag has to opt in for the `fallback` and `data` variables,
|
||||||
# the render function ALWAYS receives them.
|
# the render function ALWAYS receives them.
|
||||||
output = slot_fill.slot(used_ctx, kwargs, slot_ref)
|
output = slot_fill.slot(used_ctx, kwargs, slot_ref)
|
||||||
|
|
||||||
|
@ -669,7 +680,7 @@ class SlotNode(BaseNode):
|
||||||
component_ctx: "ComponentContext",
|
component_ctx: "ComponentContext",
|
||||||
) -> Context:
|
) -> Context:
|
||||||
"""Prepare the context used in a slot fill based on the settings."""
|
"""Prepare the context used in a slot fill based on the settings."""
|
||||||
# If slot is NOT filled, we use the slot's default AKA content between
|
# If slot is NOT filled, we use the slot's fallback AKA content between
|
||||||
# the `{% slot %}` tags. These should be evaluated as if the `{% slot %}`
|
# the `{% slot %}` tags. These should be evaluated as if the `{% slot %}`
|
||||||
# tags weren't even there, which means that we use the current context.
|
# tags weren't even there, which means that we use the current context.
|
||||||
if not slot_fill.is_filled:
|
if not slot_fill.is_filled:
|
||||||
|
@ -696,11 +707,10 @@ class FillNode(BaseNode):
|
||||||
|
|
||||||
- `name` (str, required): Name of the slot to insert this content into. Use `"default"` for
|
- `name` (str, required): Name of the slot to insert this content into. Use `"default"` for
|
||||||
the default slot.
|
the default slot.
|
||||||
- `default` (str, optional): This argument allows you to access the original content of the slot
|
- `fallback` (str, optional): This argument allows you to access the original content of the slot
|
||||||
under the specified variable name. See
|
under the specified variable name. See [Slot fallback](../../concepts/fundamentals/slots#slot-fallback).
|
||||||
[Accessing original content of slots](../../concepts/fundamentals/slots#accessing-original-content-of-slots)
|
|
||||||
- `data` (str, optional): This argument allows you to access the data passed to the slot
|
- `data` (str, optional): This argument allows you to access the data passed to the slot
|
||||||
under the specified variable name. See [Scoped slots](../../concepts/fundamentals/slots#scoped-slots)
|
under the specified variable name. See [Slot data](../../concepts/fundamentals/slots#slot-data).
|
||||||
|
|
||||||
**Examples:**
|
**Examples:**
|
||||||
|
|
||||||
|
@ -713,7 +723,7 @@ class FillNode(BaseNode):
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Accessing slot's default content with the `default` kwarg
|
### Accessing slot's fallback content with the `fallback` kwarg
|
||||||
|
|
||||||
```django
|
```django
|
||||||
{# my_table.html #}
|
{# my_table.html #}
|
||||||
|
@ -727,9 +737,9 @@ class FillNode(BaseNode):
|
||||||
|
|
||||||
```django
|
```django
|
||||||
{% component "my_table" %}
|
{% component "my_table" %}
|
||||||
{% fill "pagination" default="default_pag" %}
|
{% fill "pagination" fallback="fallback" %}
|
||||||
<div class="my-class">
|
<div class="my-class">
|
||||||
{{ default_pag }}
|
{{ fallback }}
|
||||||
</div>
|
</div>
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
|
@ -759,16 +769,16 @@ class FillNode(BaseNode):
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Accessing slot data and default content on the default slot
|
### Accessing slot data and fallback content on the default slot
|
||||||
|
|
||||||
To access slot data and the default slot content on the default slot,
|
To access slot data and the fallback slot content on the default slot,
|
||||||
use `{% fill %}` with `name` set to `"default"`:
|
use `{% fill %}` with `name` set to `"default"`:
|
||||||
|
|
||||||
```django
|
```django
|
||||||
{% component "button" %}
|
{% component "button" %}
|
||||||
{% fill name="default" data="slot_data" default="default_slot" %}
|
{% fill name="default" data="slot_data" fallback="slot_fallback" %}
|
||||||
You clicked me {{ slot_data.count }} times!
|
You clicked me {{ slot_data.count }} times!
|
||||||
{{ default_slot }}
|
{{ slot_fallback }}
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
```
|
```
|
||||||
|
@ -778,7 +788,25 @@ class FillNode(BaseNode):
|
||||||
end_tag = "endfill"
|
end_tag = "endfill"
|
||||||
allowed_flags = []
|
allowed_flags = []
|
||||||
|
|
||||||
def render(self, context: Context, name: str, *, data: Optional[str] = None, default: Optional[str] = None) -> str:
|
def render(
|
||||||
|
self,
|
||||||
|
context: Context,
|
||||||
|
name: str,
|
||||||
|
*,
|
||||||
|
data: Optional[str] = None,
|
||||||
|
fallback: Optional[str] = None,
|
||||||
|
# TODO_V1: Use `fallback` kwarg instead of `default`
|
||||||
|
default: Optional[str] = None,
|
||||||
|
) -> str:
|
||||||
|
# TODO_V1: Use `fallback` kwarg instead of `default`
|
||||||
|
if fallback is not None and default is not None:
|
||||||
|
raise TemplateSyntaxError(
|
||||||
|
f"Fill tag received both 'default' and '{FILL_FALLBACK_KWARG}' kwargs. "
|
||||||
|
f"Use '{FILL_FALLBACK_KWARG}' instead."
|
||||||
|
)
|
||||||
|
elif fallback is None and default is not None:
|
||||||
|
fallback = default
|
||||||
|
|
||||||
if not _is_extracting_fill(context):
|
if not _is_extracting_fill(context):
|
||||||
raise TemplateSyntaxError(
|
raise TemplateSyntaxError(
|
||||||
"FillNode.render() (AKA {% fill ... %} block) cannot be rendered outside of a Component context. "
|
"FillNode.render() (AKA {% fill ... %} block) cannot be rendered outside of a Component context. "
|
||||||
|
@ -797,28 +825,28 @@ class FillNode(BaseNode):
|
||||||
f"Fill tag kwarg '{FILL_DATA_KWARG}' does not resolve to a valid Python identifier, got '{data}'"
|
f"Fill tag kwarg '{FILL_DATA_KWARG}' does not resolve to a valid Python identifier, got '{data}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
if default is not None:
|
if fallback is not None:
|
||||||
if not isinstance(default, str):
|
if not isinstance(fallback, str):
|
||||||
raise TemplateSyntaxError(
|
raise TemplateSyntaxError(
|
||||||
f"Fill tag '{FILL_DEFAULT_KWARG}' kwarg must resolve to a string, got {default}"
|
f"Fill tag '{FILL_FALLBACK_KWARG}' kwarg must resolve to a string, got {fallback}"
|
||||||
)
|
)
|
||||||
if not is_identifier(default):
|
if not is_identifier(fallback):
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Fill tag kwarg '{FILL_DEFAULT_KWARG}' does not resolve to a valid Python identifier,"
|
f"Fill tag kwarg '{FILL_FALLBACK_KWARG}' does not resolve to a valid Python identifier,"
|
||||||
f" got '{default}'"
|
f" got '{fallback}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
# data and default cannot be bound to the same variable
|
# data and fallback cannot be bound to the same variable
|
||||||
if data and default and data == default:
|
if data and fallback and data == fallback:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Fill '{name}' received the same string for slot default ({FILL_DEFAULT_KWARG}=...)"
|
f"Fill '{name}' received the same string for slot fallback ({FILL_FALLBACK_KWARG}=...)"
|
||||||
f" and slot data ({FILL_DATA_KWARG}=...)"
|
f" and slot data ({FILL_DATA_KWARG}=...)"
|
||||||
)
|
)
|
||||||
|
|
||||||
fill_data = FillWithData(
|
fill_data = FillWithData(
|
||||||
fill=self,
|
fill=self,
|
||||||
name=name,
|
name=name,
|
||||||
default_var=default,
|
fallback_var=fallback,
|
||||||
data_var=data,
|
data_var=data,
|
||||||
extra_context={},
|
extra_context={},
|
||||||
)
|
)
|
||||||
|
@ -905,7 +933,7 @@ class FillNode(BaseNode):
|
||||||
class FillWithData(NamedTuple):
|
class FillWithData(NamedTuple):
|
||||||
fill: FillNode
|
fill: FillNode
|
||||||
name: str
|
name: str
|
||||||
default_var: Optional[str]
|
fallback_var: Optional[str]
|
||||||
data_var: Optional[str]
|
data_var: Optional[str]
|
||||||
extra_context: Dict[str, Any]
|
extra_context: Dict[str, Any]
|
||||||
|
|
||||||
|
@ -989,7 +1017,7 @@ def resolve_fills(
|
||||||
nodelist=nodelist,
|
nodelist=nodelist,
|
||||||
contents=contents,
|
contents=contents,
|
||||||
data_var=None,
|
data_var=None,
|
||||||
default_var=None,
|
fallback_var=None,
|
||||||
# Escaped because this was defined in the template
|
# Escaped because this was defined in the template
|
||||||
escaped=True,
|
escaped=True,
|
||||||
)
|
)
|
||||||
|
@ -1005,7 +1033,7 @@ def resolve_fills(
|
||||||
nodelist=fill.fill.nodelist,
|
nodelist=fill.fill.nodelist,
|
||||||
contents=fill.fill.contents,
|
contents=fill.fill.contents,
|
||||||
data_var=fill.data_var,
|
data_var=fill.data_var,
|
||||||
default_var=fill.default_var,
|
fallback_var=fill.fallback_var,
|
||||||
extra_context=fill.extra_context,
|
extra_context=fill.extra_context,
|
||||||
# Escaped because this was defined in the template
|
# Escaped because this was defined in the template
|
||||||
escaped=True,
|
escaped=True,
|
||||||
|
@ -1080,7 +1108,7 @@ def _nodelist_to_slot(
|
||||||
nodelist: NodeList,
|
nodelist: NodeList,
|
||||||
contents: Optional[str] = None,
|
contents: Optional[str] = None,
|
||||||
data_var: Optional[str] = None,
|
data_var: Optional[str] = None,
|
||||||
default_var: Optional[str] = None,
|
fallback_var: Optional[str] = None,
|
||||||
escaped: bool = False,
|
escaped: bool = False,
|
||||||
extra_context: Optional[Dict[str, Any]] = None,
|
extra_context: Optional[Dict[str, Any]] = None,
|
||||||
) -> Slot:
|
) -> Slot:
|
||||||
|
@ -1090,10 +1118,10 @@ def _nodelist_to_slot(
|
||||||
f"Slot data alias in fill '{slot_name}' must be a valid identifier. Got '{data_var}'"
|
f"Slot data alias in fill '{slot_name}' must be a valid identifier. Got '{data_var}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
if default_var:
|
if fallback_var:
|
||||||
if not default_var.isidentifier():
|
if not fallback_var.isidentifier():
|
||||||
raise TemplateSyntaxError(
|
raise TemplateSyntaxError(
|
||||||
f"Slot default alias in fill '{slot_name}' must be a valid identifier. Got '{default_var}'"
|
f"Slot fallback alias in fill '{slot_name}' must be a valid identifier. Got '{fallback_var}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
# We use Template.render() to render the nodelist, so that Django correctly sets up
|
# We use Template.render() to render the nodelist, so that Django correctly sets up
|
||||||
|
@ -1103,17 +1131,17 @@ def _nodelist_to_slot(
|
||||||
# This allows the template to access current RenderContext layer.
|
# This allows the template to access current RenderContext layer.
|
||||||
template._djc_is_component_nested = True
|
template._djc_is_component_nested = True
|
||||||
|
|
||||||
def render_func(ctx: Context, slot_data: Dict[str, Any], slot_ref: SlotRef) -> SlotResult:
|
def render_func(ctx: Context, slot_data: Dict[str, Any], slot_ref: SlotFallback) -> SlotResult:
|
||||||
# Expose the kwargs that were passed to the `{% slot %}` tag. These kwargs
|
# 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 %}`
|
# are made available through a variable name that was set on the `{% fill %}`
|
||||||
# tag.
|
# tag.
|
||||||
if data_var:
|
if data_var:
|
||||||
ctx[data_var] = slot_data
|
ctx[data_var] = slot_data
|
||||||
|
|
||||||
# If slot fill is using `{% fill "myslot" default="abc" %}`, then set the "abc" to
|
# If slot fill is using `{% fill "myslot" fallback="abc" %}`, then set the "abc" to
|
||||||
# the context, so users can refer to the default slot from within the fill content.
|
# the context, so users can refer to the fallback slot from within the fill content.
|
||||||
if default_var:
|
if fallback_var:
|
||||||
ctx[default_var] = slot_ref
|
ctx[fallback_var] = slot_ref
|
||||||
|
|
||||||
# NOTE: If a `{% fill %}` tag inside a `{% component %}` tag is inside a forloop,
|
# NOTE: If a `{% fill %}` tag inside a `{% component %}` tag is inside a forloop,
|
||||||
# the `extra_context` contains the forloop variables. We want to make these available
|
# the `extra_context` contains the forloop variables. We want to make these available
|
||||||
|
|
|
@ -12,7 +12,7 @@ from django.template.base import NodeList, TextNode
|
||||||
from pytest_django.asserts import assertHTMLEqual
|
from pytest_django.asserts import assertHTMLEqual
|
||||||
|
|
||||||
from django_components import Component, register, types
|
from django_components import Component, register, types
|
||||||
from django_components.slots import Slot, SlotRef
|
from django_components.slots import Slot, SlotFallback
|
||||||
|
|
||||||
from django_components.testing import djc_test
|
from django_components.testing import djc_test
|
||||||
from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
|
from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
|
||||||
|
@ -49,7 +49,7 @@ class TestSlot:
|
||||||
"kwargs": kwargs,
|
"kwargs": kwargs,
|
||||||
}
|
}
|
||||||
|
|
||||||
def first_slot(ctx: Context, slot_data: Dict, slot_ref: SlotRef):
|
def first_slot(ctx: Context, slot_data: Dict, slot_ref: SlotFallback):
|
||||||
assert isinstance(ctx, Context)
|
assert isinstance(ctx, Context)
|
||||||
# NOTE: Since the slot has access to the Context object, it should behave
|
# NOTE: Since the slot has access to the Context object, it should behave
|
||||||
# the same way as it does in templates - when in "isolated" mode, then the
|
# the same way as it does in templates - when in "isolated" mode, then the
|
||||||
|
@ -72,7 +72,7 @@ class TestSlot:
|
||||||
}
|
}
|
||||||
assert slot_data_expected == slot_data
|
assert slot_data_expected == slot_data
|
||||||
|
|
||||||
assert isinstance(slot_ref, SlotRef)
|
assert isinstance(slot_ref, SlotFallback)
|
||||||
assert "SLOT_DEFAULT" == str(slot_ref).strip()
|
assert "SLOT_DEFAULT" == str(slot_ref).strip()
|
||||||
|
|
||||||
return f"FROM_INSIDE_FIRST_SLOT | {slot_ref}"
|
return f"FROM_INSIDE_FIRST_SLOT | {slot_ref}"
|
||||||
|
|
|
@ -895,8 +895,8 @@ class TestPassthroughSlots:
|
||||||
{% component "test" %}
|
{% component "test" %}
|
||||||
{% if slot_names %}
|
{% if slot_names %}
|
||||||
{% for slot in slot_names %}
|
{% for slot in slot_names %}
|
||||||
{% fill name=slot default="default" %}
|
{% fill name=slot fallback="fallback" %}
|
||||||
OVERRIDEN_SLOT "{{ slot }}" - INDEX {{ forloop.counter0 }} - ORIGINAL "{{ default }}"
|
OVERRIDEN_SLOT "{{ slot }}" - INDEX {{ forloop.counter0 }} - ORIGINAL "{{ fallback }}"
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -950,8 +950,8 @@ class TestPassthroughSlots:
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component "test" %}
|
{% component "test" %}
|
||||||
{% with slot="header" %}
|
{% with slot="header" %}
|
||||||
{% fill name=slot default="default" %}
|
{% fill name=slot fallback="fallback" %}
|
||||||
OVERRIDEN_SLOT "{{ slot }}" - ORIGINAL "{{ default }}"
|
OVERRIDEN_SLOT "{{ slot }}" - ORIGINAL "{{ fallback }}"
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
|
@ -996,7 +996,7 @@ class TestPassthroughSlots:
|
||||||
{% if slot_names %}
|
{% if slot_names %}
|
||||||
{% for slot in slot_names %}
|
{% for slot in slot_names %}
|
||||||
{{ forloop.counter0 }}
|
{{ forloop.counter0 }}
|
||||||
{% fill name=slot default="default" %}
|
{% fill name=slot fallback="fallback" %}
|
||||||
OVERRIDEN_SLOT
|
OVERRIDEN_SLOT
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -1365,9 +1365,11 @@ class TestSlottedTemplateRegression:
|
||||||
|
|
||||||
|
|
||||||
@djc_test
|
@djc_test
|
||||||
class TestSlotDefault:
|
class TestSlotFallback:
|
||||||
|
|
||||||
|
# TODO_v1 - REMOVE
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_basic(self, components_settings):
|
def test_basic_legacy(self, components_settings):
|
||||||
registry.register("test", _gen_slotted_component())
|
registry.register("test", _gen_slotted_component())
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
|
@ -1391,13 +1393,38 @@ class TestSlotDefault:
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
def test_basic(self, components_settings):
|
||||||
|
registry.register("test", _gen_slotted_component())
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "test" %}
|
||||||
|
{% fill "header" fallback="header" %}Before: {{ header }}{% endfill %}
|
||||||
|
{% fill "main" fallback="main" %}{{ main }}{% endfill %}
|
||||||
|
{% fill "footer" fallback="footer" %}{{ footer }}, after{% endfill %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
|
||||||
|
assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
<custom-template data-djc-id-ca1bc42>
|
||||||
|
<header>Before: Default header</header>
|
||||||
|
<main>Default main</main>
|
||||||
|
<footer>Default footer, after</footer>
|
||||||
|
</custom-template>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_multiple_calls(self, components_settings):
|
def test_multiple_calls(self, components_settings):
|
||||||
registry.register("test", _gen_slotted_component())
|
registry.register("test", _gen_slotted_component())
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component "test" %}
|
{% component "test" %}
|
||||||
{% fill "header" default="header" %}
|
{% fill "header" fallback="header" %}
|
||||||
First: {{ header }};
|
First: {{ header }};
|
||||||
Second: {{ header }}
|
Second: {{ header }}
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
|
@ -1423,7 +1450,7 @@ class TestSlotDefault:
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component "test" %}
|
{% component "test" %}
|
||||||
{% fill "header" default="header" %}
|
{% fill "header" fallback="header" %}
|
||||||
{% for i in range %}
|
{% for i in range %}
|
||||||
{% if forloop.first %}
|
{% if forloop.first %}
|
||||||
First {{ header }}
|
First {{ header }}
|
||||||
|
@ -1454,14 +1481,14 @@ class TestSlotDefault:
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component "test" %}
|
{% component "test" %}
|
||||||
{% fill "header" default="header1" %}
|
{% fill "header" fallback="header1" %}
|
||||||
header1_in_header1: {{ header1 }}
|
header1_in_header1: {{ header1 }}
|
||||||
{% component "test" %}
|
{% component "test" %}
|
||||||
{% fill "header" default="header2" %}
|
{% fill "header" fallback="header2" %}
|
||||||
header1_in_header2: {{ header1 }}
|
header1_in_header2: {{ header1 }}
|
||||||
header2_in_header2: {{ header2 }}
|
header2_in_header2: {{ header2 }}
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
{% fill "footer" default="footer2" %}
|
{% fill "footer" fallback="footer2" %}
|
||||||
header1_in_footer2: {{ header1 }}
|
header1_in_footer2: {{ header1 }}
|
||||||
footer2_in_footer2: {{ footer2 }}
|
footer2_in_footer2: {{ footer2 }}
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
|
@ -1570,7 +1597,7 @@ class TestScopedSlot:
|
||||||
assertHTMLEqual(rendered, expected)
|
assertHTMLEqual(rendered, expected)
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_slot_data_with_slot_default(self, components_settings):
|
def test_slot_data_with_slot_fallback(self, components_settings):
|
||||||
@register("test")
|
@register("test")
|
||||||
class TestComponent(Component):
|
class TestComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
|
@ -1589,8 +1616,8 @@ class TestScopedSlot:
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component "test" %}
|
{% component "test" %}
|
||||||
{% fill "my_slot" data="slot_data_in_fill" default="slot_var" %}
|
{% fill "my_slot" data="slot_data_in_fill" fallback="fallback" %}
|
||||||
{{ slot_var }}
|
{{ fallback }}
|
||||||
{{ slot_data_in_fill.abc }}
|
{{ slot_data_in_fill.abc }}
|
||||||
{{ slot_data_in_fill.var123 }}
|
{{ slot_data_in_fill.var123 }}
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
|
@ -1681,7 +1708,7 @@ class TestScopedSlot:
|
||||||
assertHTMLEqual(rendered, expected)
|
assertHTMLEqual(rendered, expected)
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_slot_data_and_default_on_default_slot(self, components_settings):
|
def test_slot_data_and_fallback_on_default_slot(self, components_settings):
|
||||||
@register("test")
|
@register("test")
|
||||||
class TestComponent(Component):
|
class TestComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
|
@ -1701,7 +1728,7 @@ class TestScopedSlot:
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component "test" %}
|
{% component "test" %}
|
||||||
{% fill name="default" data="slot_data_in_fill" default="slot_var" %}
|
{% fill name="default" data="slot_data_in_fill" fallback="slot_var" %}
|
||||||
{{ slot_data_in_fill.abc }}
|
{{ slot_data_in_fill.abc }}
|
||||||
{{ slot_var }}
|
{{ slot_var }}
|
||||||
{{ slot_data_in_fill.var123 }}
|
{{ slot_data_in_fill.var123 }}
|
||||||
|
@ -1718,7 +1745,7 @@ class TestScopedSlot:
|
||||||
assertHTMLEqual(rendered, expected)
|
assertHTMLEqual(rendered, expected)
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_slot_data_raises_on_slot_data_and_slot_default_same_var(self, components_settings):
|
def test_slot_data_raises_on_slot_data_and_slot_fallback_same_var(self, components_settings):
|
||||||
@register("test")
|
@register("test")
|
||||||
class TestComponent(Component):
|
class TestComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
|
@ -1737,14 +1764,14 @@ class TestScopedSlot:
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component "test" %}
|
{% component "test" %}
|
||||||
{% fill "my_slot" data="slot_var" default="slot_var" %}
|
{% fill "my_slot" data="slot_var" fallback="slot_var" %}
|
||||||
{{ slot_var }}
|
{{ slot_var }}
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
"""
|
"""
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
RuntimeError,
|
RuntimeError,
|
||||||
match=re.escape("Fill 'my_slot' received the same string for slot default (default=...) and slot data (data=...)"), # noqa: E501
|
match=re.escape("Fill 'my_slot' received the same string for slot fallback (fallback=...) and slot data (data=...)"), # noqa: E501
|
||||||
):
|
):
|
||||||
Template(template).render(Context())
|
Template(template).render(Context())
|
||||||
|
|
||||||
|
|
|
@ -445,7 +445,7 @@ class TestSlotIteration:
|
||||||
["django", "isolated"],
|
["django", "isolated"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
def test_inner_slot_iteration_nested_with_slot_default(self, components_settings, expected):
|
def test_inner_slot_iteration_nested_with_slot_fallback(self, components_settings, expected):
|
||||||
registry.register("slot_in_a_loop", self._get_component_simple_slot_in_a_loop())
|
registry.register("slot_in_a_loop", self._get_component_simple_slot_in_a_loop())
|
||||||
|
|
||||||
objects = [
|
objects = [
|
||||||
|
@ -458,7 +458,7 @@ class TestSlotIteration:
|
||||||
{% component "slot_in_a_loop" objects=objects %}
|
{% component "slot_in_a_loop" objects=objects %}
|
||||||
{% fill "slot_inner" %}
|
{% fill "slot_inner" %}
|
||||||
{% component "slot_in_a_loop" objects=object.inner %}
|
{% component "slot_in_a_loop" objects=object.inner %}
|
||||||
{% fill "slot_inner" default="super_slot_inner" %}
|
{% fill "slot_inner" fallback="super_slot_inner" %}
|
||||||
{{ super_slot_inner }}
|
{{ super_slot_inner }}
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
|
@ -498,7 +498,7 @@ class TestSlotIteration:
|
||||||
["django", "isolated"],
|
["django", "isolated"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
def test_inner_slot_iteration_nested_with_slot_default_and_outer_scope_variable(
|
def test_inner_slot_iteration_nested_with_slot_fallback_and_outer_scope_variable(
|
||||||
self,
|
self,
|
||||||
components_settings,
|
components_settings,
|
||||||
expected,
|
expected,
|
||||||
|
@ -516,7 +516,7 @@ class TestSlotIteration:
|
||||||
{% fill "slot_inner" %}
|
{% fill "slot_inner" %}
|
||||||
{{ outer_scope_variable_1 }}
|
{{ outer_scope_variable_1 }}
|
||||||
{% component "slot_in_a_loop" objects=object.inner %}
|
{% component "slot_in_a_loop" objects=object.inner %}
|
||||||
{% fill "slot_inner" default="super_slot_inner" %}
|
{% fill "slot_inner" fallback="super_slot_inner" %}
|
||||||
{{ outer_scope_variable_2 }}
|
{{ outer_scope_variable_2 }}
|
||||||
{{ super_slot_inner }}
|
{{ super_slot_inner }}
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
|
@ -537,7 +537,7 @@ class TestSlotIteration:
|
||||||
assertHTMLEqual(rendered, expected)
|
assertHTMLEqual(rendered, expected)
|
||||||
|
|
||||||
@djc_test(components_settings={"context_behavior": "isolated"})
|
@djc_test(components_settings={"context_behavior": "isolated"})
|
||||||
def test_inner_slot_iteration_nested_with_slot_default_and_outer_scope_variable__isolated_2(
|
def test_inner_slot_iteration_nested_with_slot_fallback_and_outer_scope_variable__isolated_2(
|
||||||
self,
|
self,
|
||||||
):
|
):
|
||||||
registry.register("slot_in_a_loop", self._get_component_simple_slot_in_a_loop())
|
registry.register("slot_in_a_loop", self._get_component_simple_slot_in_a_loop())
|
||||||
|
@ -556,7 +556,7 @@ class TestSlotIteration:
|
||||||
{% fill "slot_inner" %}
|
{% fill "slot_inner" %}
|
||||||
{{ outer_scope_variable_1|safe }}
|
{{ outer_scope_variable_1|safe }}
|
||||||
{% component "slot_in_a_loop" objects=objects %}
|
{% component "slot_in_a_loop" objects=objects %}
|
||||||
{% fill "slot_inner" default="super_slot_inner" %}
|
{% fill "slot_inner" fallback="super_slot_inner" %}
|
||||||
{{ outer_scope_variable_2|safe }}
|
{{ outer_scope_variable_2|safe }}
|
||||||
{{ super_slot_inner }}
|
{{ super_slot_inner }}
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
|
@ -959,14 +959,14 @@ class TestComponentNesting:
|
||||||
["django", "isolated"],
|
["django", "isolated"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
def test_component_nesting_component_with_slot_default(self, components_settings, expected):
|
def test_component_nesting_component_with_slot_fallback(self, components_settings, expected):
|
||||||
registry.register("dashboard", self._get_dashboard_component())
|
registry.register("dashboard", self._get_dashboard_component())
|
||||||
registry.register("calendar", self._get_calendar_component())
|
registry.register("calendar", self._get_calendar_component())
|
||||||
|
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component "dashboard" %}
|
{% component "dashboard" %}
|
||||||
{% fill "header" default="h" %} Hello! {{ h }} {% endfill %}
|
{% fill "header" fallback="h" %} Hello! {{ h }} {% endfill %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
"""
|
"""
|
||||||
template = Template(template_str)
|
template = Template(template_str)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue