mirror of
https://github.com/django-components/django-components.git
synced 2025-09-26 23:49:07 +00:00
refactor: change fill alias from "as var" to default=var (#504)
This commit is contained in:
parent
c07f0e6341
commit
edb2f347f2
5 changed files with 238 additions and 111 deletions
29
README.md
29
README.md
|
@ -44,6 +44,12 @@ Read on to learn about the details!
|
||||||
|
|
||||||
## Release notes
|
## Release notes
|
||||||
|
|
||||||
|
🚨📢 **Version 0.77** CHANGED the syntax for accessing default slot content.
|
||||||
|
- Previously, the syntax was
|
||||||
|
`{% fill "my_slot" as "alias" %}` and `{{ alias.default }}`.
|
||||||
|
- Now, the syntax is
|
||||||
|
`{% fill "my_slot" default="alias" %}` and `{{ alias }}`.
|
||||||
|
|
||||||
**Version 0.74** introduces `html_attrs` tag and `prefix:key=val` construct for passing dicts to components.
|
**Version 0.74** introduces `html_attrs` tag and `prefix:key=val` construct for passing dicts to components.
|
||||||
|
|
||||||
🚨📢 **Version 0.70**
|
🚨📢 **Version 0.70**
|
||||||
|
@ -627,13 +633,26 @@ Which you can then use are regular default slot:
|
||||||
|
|
||||||
_Added in version 0.26_
|
_Added in version 0.26_
|
||||||
|
|
||||||
Certain properties of a slot can be accessed from within a 'fill' context. They are provided as attributes on a user-defined alias of the targeted slot. For instance, let's say you're filling a slot called 'body'. To access properties of this slot, alias it using the 'as' keyword to a new name -- or keep the original name. With the new slot alias, you can call `<alias>.default` to insert the default content.
|
> NOTE: In version 0.77, the syntax was changed from
|
||||||
|
> ```django
|
||||||
|
> {% fill "my_slot" as "alias" %} {{ alias.default }}
|
||||||
|
> ```
|
||||||
|
> to
|
||||||
|
> ```django
|
||||||
|
> {% fill "my_slot" default="slot_default" %} {{ slot_default }}
|
||||||
|
> ```
|
||||||
|
|
||||||
Notice the use of `as "body"` below:
|
Sometimes you may want to keep the original slot, but only wrap or prepend/append content to it. To do so, you can access the default slot via the `default` kwarg.
|
||||||
|
|
||||||
|
Similarly to the `data` attribute, you specify the variable name through which the default slot will be made available.
|
||||||
|
|
||||||
|
For instance, let's say you're filling a slot called 'body'. To render the original slot, assign it to a variable using the `'default'` keyword. You then render this variable to insert the default content:
|
||||||
|
|
||||||
```htmldjango
|
```htmldjango
|
||||||
{% component "calendar" date="2020-06-06" %}
|
{% component "calendar" date="2020-06-06" %}
|
||||||
{% fill "body" as "body" %}{{ body.default }}. Have a great day!{% endfill %}
|
{% fill "body" default="body_default" %}
|
||||||
|
{{ body_default }}. Have a great day!
|
||||||
|
{% endfill %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -832,12 +851,12 @@ While this does not:
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: You cannot set the `data` attribute and
|
Note: You cannot set the `data` attribute and
|
||||||
[slot alias (`as var` syntax)](#accessing-original-content-of-slots)
|
[`default` attribute)](#accessing-original-content-of-slots)
|
||||||
to the same name. This raises an error:
|
to the same name. This raises an error:
|
||||||
|
|
||||||
```django
|
```django
|
||||||
{% component "my_comp" %}
|
{% component "my_comp" %}
|
||||||
{% fill "content" data="slot_var" as "slot_var" %}
|
{% fill "content" data="slot_var" default="slot_var" %}
|
||||||
{{ slot_var.input }}
|
{{ slot_var.input }}
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
|
|
|
@ -358,8 +358,8 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
slot_fills = {
|
slot_fills = {
|
||||||
slot_name: FillContent(
|
slot_name: FillContent(
|
||||||
nodes=NodeList([TextNode(escape(content) if escape_content else content)]),
|
nodes=NodeList([TextNode(escape(content) if escape_content else content)]),
|
||||||
alias=None,
|
slot_default_var=None,
|
||||||
scope=None,
|
slot_data_var=None,
|
||||||
)
|
)
|
||||||
for (slot_name, content) in slots_data.items()
|
for (slot_name, content) in slots_data.items()
|
||||||
}
|
}
|
||||||
|
@ -422,12 +422,12 @@ class ComponentNode(Node):
|
||||||
f"Detected duplicate fill tag name '{resolved_name}'."
|
f"Detected duplicate fill tag name '{resolved_name}'."
|
||||||
)
|
)
|
||||||
|
|
||||||
resolved_fill_alias = fill_node.resolve_alias(context, resolved_component_name)
|
resolved_slot_default_var = fill_node.resolve_slot_default(context, resolved_component_name)
|
||||||
resolved_scope_var = fill_node.resolve_scope(context, resolved_component_name)
|
resolved_slot_data_var = fill_node.resolve_slot_data(context, resolved_component_name)
|
||||||
fill_content[resolved_name] = FillContent(
|
fill_content[resolved_name] = FillContent(
|
||||||
nodes=fill_node.nodelist,
|
nodes=fill_node.nodelist,
|
||||||
alias=resolved_fill_alias,
|
slot_default_var=resolved_slot_default_var,
|
||||||
scope=resolved_scope_var,
|
slot_data_var=resolved_slot_data_var,
|
||||||
)
|
)
|
||||||
|
|
||||||
component: Component = component_cls(
|
component: Component = component_cls(
|
||||||
|
|
|
@ -24,8 +24,8 @@ DEFAULT_SLOT_KEY = "_DJANGO_COMPONENTS_DEFAULT_SLOT"
|
||||||
|
|
||||||
SlotId = str
|
SlotId = str
|
||||||
SlotName = str
|
SlotName = str
|
||||||
AliasName = str
|
SlotDefaultName = str
|
||||||
ScopeName = str
|
SlotDataName = str
|
||||||
|
|
||||||
|
|
||||||
class FillContent(NamedTuple):
|
class FillContent(NamedTuple):
|
||||||
|
@ -44,8 +44,8 @@ class FillContent(NamedTuple):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
nodes: NodeList
|
nodes: NodeList
|
||||||
alias: Optional[AliasName]
|
slot_default_var: Optional[SlotDefaultName]
|
||||||
scope: Optional[ScopeName]
|
slot_data_var: Optional[SlotDataName]
|
||||||
|
|
||||||
|
|
||||||
class Slot(NamedTuple):
|
class Slot(NamedTuple):
|
||||||
|
@ -80,27 +80,26 @@ class SlotFill(NamedTuple):
|
||||||
is_filled: bool
|
is_filled: bool
|
||||||
nodelist: NodeList
|
nodelist: NodeList
|
||||||
context_data: Dict
|
context_data: Dict
|
||||||
alias: Optional[AliasName]
|
slot_default_var: Optional[SlotDefaultName]
|
||||||
scope: Optional[ScopeName]
|
slot_data_var: Optional[SlotDataName]
|
||||||
|
|
||||||
|
|
||||||
class UserSlotVar:
|
class LazySlot:
|
||||||
"""
|
"""
|
||||||
Extensible mechanism for offering 'fill' blocks in template access to properties
|
Bound a slot and a context, so we can move these around as a single variable,
|
||||||
of parent slot.
|
and lazily render the slot with given context only once coerced to string.
|
||||||
|
|
||||||
How it works: At render time, SlotNode(s) that have been aliased in the fill tag
|
This is used to access slots as variables inside the templates. When a LazySlot
|
||||||
of the component instance create an instance of UserSlotVar. This instance is made
|
is rendered in the template with `{{ my_lazy_slot }}`, it will output the contents
|
||||||
available to the rendering context on a key matching the slot alias (see
|
of the slot.
|
||||||
SlotNode.render() for implementation).
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@property
|
# Render the slot when the template coerces LazySlot to string
|
||||||
def default(self) -> str:
|
def __str__(self) -> str:
|
||||||
return mark_safe(self._slot.nodelist.render(self._context))
|
return mark_safe(self._slot.nodelist.render(self._context))
|
||||||
|
|
||||||
|
|
||||||
|
@ -141,20 +140,28 @@ class SlotNode(Node):
|
||||||
|
|
||||||
extra_context: Dict[str, Any] = {}
|
extra_context: Dict[str, Any] = {}
|
||||||
|
|
||||||
# If slot is using alias `{% slot "myslot" as "abc" %}`, then set the "abc" to
|
# If slot fill is using `{% fill "myslot" default="abc" %}`, then set the "abc" to
|
||||||
# the context, so users can refer to the slot from within the slot.
|
# the context, so users can refer to the default slot from within the fill content.
|
||||||
if slot_fill.alias:
|
default_var = slot_fill.slot_default_var
|
||||||
if not slot_fill.alias.isidentifier():
|
if default_var:
|
||||||
raise TemplateSyntaxError(f"Invalid fill alias. Must be a valid identifier. Got '{slot_fill.alias}'")
|
if not default_var.isidentifier():
|
||||||
extra_context[slot_fill.alias] = UserSlotVar(self, context)
|
raise TemplateSyntaxError(
|
||||||
|
f"Slot default alias in fill '{self.name}' must be a valid identifier. Got '{default_var}'"
|
||||||
|
)
|
||||||
|
extra_context[default_var] = LazySlot(self, context)
|
||||||
|
|
||||||
# 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 slot_fill.scope:
|
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}'"
|
||||||
|
)
|
||||||
slot_kwargs = safe_resolve_dict(self.slot_kwargs, context)
|
slot_kwargs = safe_resolve_dict(self.slot_kwargs, context)
|
||||||
slot_kwargs = process_aggregate_kwargs(slot_kwargs)
|
slot_kwargs = process_aggregate_kwargs(slot_kwargs)
|
||||||
extra_context[slot_fill.scope] = slot_kwargs
|
extra_context[data_var] = slot_kwargs
|
||||||
|
|
||||||
# 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)
|
||||||
|
@ -192,17 +199,17 @@ class FillNode(Node):
|
||||||
self,
|
self,
|
||||||
nodelist: NodeList,
|
nodelist: NodeList,
|
||||||
name_fexp: FilterExpression,
|
name_fexp: FilterExpression,
|
||||||
alias_fexp: Optional[FilterExpression] = None,
|
slot_default_var_fexp: Optional[FilterExpression] = None,
|
||||||
scope_fexp: Optional[FilterExpression] = None,
|
slot_data_var_fexp: Optional[FilterExpression] = None,
|
||||||
is_implicit: bool = False,
|
is_implicit: bool = False,
|
||||||
node_id: Optional[str] = None,
|
node_id: Optional[str] = None,
|
||||||
):
|
):
|
||||||
self.node_id = node_id or gen_id()
|
self.node_id = node_id or gen_id()
|
||||||
self.nodelist = nodelist
|
self.nodelist = nodelist
|
||||||
self.name_fexp = name_fexp
|
self.name_fexp = name_fexp
|
||||||
self.alias_fexp = alias_fexp
|
self.slot_default_var_fexp = slot_default_var_fexp
|
||||||
self.is_implicit = is_implicit
|
self.is_implicit = is_implicit
|
||||||
self.scope_fexp = scope_fexp
|
self.slot_data_var_fexp = slot_data_var_fexp
|
||||||
self.component_id: Optional[str] = None
|
self.component_id: Optional[str] = None
|
||||||
|
|
||||||
def render(self, context: Context) -> str:
|
def render(self, context: Context) -> str:
|
||||||
|
@ -215,11 +222,11 @@ class FillNode(Node):
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<{type(self)} Name: {self.name_fexp}. Contents: {repr(self.nodelist)}.>"
|
return f"<{type(self)} Name: {self.name_fexp}. Contents: {repr(self.nodelist)}.>"
|
||||||
|
|
||||||
def resolve_alias(self, context: Context, component_name: Optional[str] = None) -> Optional[str]:
|
def resolve_slot_default(self, context: Context, component_name: Optional[str] = None) -> Optional[str]:
|
||||||
return self.resolve_fexp("alias", self.alias_fexp, context, component_name)
|
return self.resolve_fexp("slot default", self.slot_default_var_fexp, context, component_name)
|
||||||
|
|
||||||
def resolve_scope(self, context: Context, component_name: Optional[str] = None) -> Optional[str]:
|
def resolve_slot_data(self, context: Context, component_name: Optional[str] = None) -> Optional[str]:
|
||||||
return self.resolve_fexp("scope", self.scope_fexp, context, component_name)
|
return self.resolve_fexp("slot data", self.slot_data_var_fexp, context, component_name)
|
||||||
|
|
||||||
def resolve_fexp(
|
def resolve_fexp(
|
||||||
self,
|
self,
|
||||||
|
@ -232,14 +239,14 @@ class FillNode(Node):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resolved_alias = resolve_expression_as_identifier(context, fexp)
|
resolved_name = resolve_expression_as_identifier(context, fexp)
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
raise TemplateSyntaxError(
|
raise TemplateSyntaxError(
|
||||||
f"Fill tag {name} '{fexp.var}' in component {component_name}"
|
f"Fill tag {name} '{fexp.var}' in component {component_name}"
|
||||||
f"does not resolve to a valid Python identifier."
|
f"does not resolve to a valid Python identifier."
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
return resolved_alias
|
return resolved_name
|
||||||
|
|
||||||
|
|
||||||
def parse_slot_fill_nodes_from_component_nodelist(
|
def parse_slot_fill_nodes_from_component_nodelist(
|
||||||
|
@ -363,8 +370,8 @@ def resolve_slots(
|
||||||
is_filled=True,
|
is_filled=True,
|
||||||
nodelist=fill.nodes,
|
nodelist=fill.nodes,
|
||||||
context_data=context_data,
|
context_data=context_data,
|
||||||
alias=fill.alias,
|
slot_default_var=fill.slot_default_var,
|
||||||
scope=fill.scope,
|
slot_data_var=fill.slot_data_var,
|
||||||
)
|
)
|
||||||
for name, fill in fill_content.items()
|
for name, fill in fill_content.items()
|
||||||
}
|
}
|
||||||
|
@ -452,8 +459,8 @@ def resolve_slots(
|
||||||
is_filled=False,
|
is_filled=False,
|
||||||
nodelist=slot.nodelist,
|
nodelist=slot.nodelist,
|
||||||
context_data=context_data,
|
context_data=context_data,
|
||||||
alias=None,
|
slot_default_var=None,
|
||||||
scope=None,
|
slot_data_var=None,
|
||||||
)
|
)
|
||||||
# Since the slot's default CAN include other slots (because it's defined in
|
# Since the slot's default CAN include other slots (because it's defined in
|
||||||
# the same template), we need to enqueue the slot's children
|
# the same template), we need to enqueue the slot's children
|
||||||
|
@ -500,8 +507,8 @@ def _resolve_default_slot(
|
||||||
is_filled=default_fill.is_filled,
|
is_filled=default_fill.is_filled,
|
||||||
nodelist=default_fill.nodelist,
|
nodelist=default_fill.nodelist,
|
||||||
context_data=default_fill.context_data,
|
context_data=default_fill.context_data,
|
||||||
alias=default_fill.alias,
|
slot_default_var=default_fill.slot_default_var,
|
||||||
scope=default_fill.scope,
|
slot_data_var=default_fill.slot_data_var,
|
||||||
# Updated fields
|
# Updated fields
|
||||||
name=slot.name,
|
name=slot.name,
|
||||||
escaped_name=_escape_slot_name(slot.name),
|
escaped_name=_escape_slot_name(slot.name),
|
||||||
|
|
|
@ -31,6 +31,7 @@ register = django.template.Library()
|
||||||
SLOT_REQUIRED_OPTION_KEYWORD = "required"
|
SLOT_REQUIRED_OPTION_KEYWORD = "required"
|
||||||
SLOT_DEFAULT_OPTION_KEYWORD = "default"
|
SLOT_DEFAULT_OPTION_KEYWORD = "default"
|
||||||
SLOT_DATA_ATTR = "data"
|
SLOT_DATA_ATTR = "data"
|
||||||
|
SLOT_DEFAULT_ATTR = "default"
|
||||||
|
|
||||||
|
|
||||||
def get_components_from_registry(registry: ComponentRegistry) -> List["Component"]:
|
def get_components_from_registry(registry: ComponentRegistry) -> List["Component"]:
|
||||||
|
@ -147,7 +148,7 @@ def do_fill(parser: Parser, token: Token) -> FillNode:
|
||||||
"""
|
"""
|
||||||
# e.g. {% fill <name> %}
|
# e.g. {% fill <name> %}
|
||||||
tag_name, *args = token.split_contents()
|
tag_name, *args = token.split_contents()
|
||||||
slot_name_fexp, alias_fexp, scope_var_fexp = _parse_fill_args(parser, args, tag_name)
|
slot_name_fexp, slot_default_var_fexp, slot_data_var_fexp = _parse_fill_args(parser, args, tag_name)
|
||||||
|
|
||||||
# Use a unique ID to be able to tie the fill nodes with components and slots
|
# Use a unique ID to be able to tie the fill nodes with components and slots
|
||||||
# NOTE: MUST be called BEFORE `parser.parse()` to ensure predictable numbering
|
# NOTE: MUST be called BEFORE `parser.parse()` to ensure predictable numbering
|
||||||
|
@ -160,8 +161,8 @@ def do_fill(parser: Parser, token: Token) -> FillNode:
|
||||||
fill_node = FillNode(
|
fill_node = FillNode(
|
||||||
nodelist,
|
nodelist,
|
||||||
name_fexp=slot_name_fexp,
|
name_fexp=slot_name_fexp,
|
||||||
alias_fexp=alias_fexp,
|
slot_default_var_fexp=slot_default_var_fexp,
|
||||||
scope_fexp=scope_var_fexp,
|
slot_data_var_fexp=slot_data_var_fexp,
|
||||||
node_id=fill_id,
|
node_id=fill_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -382,19 +383,13 @@ def _parse_fill_args(
|
||||||
) -> Tuple[FilterExpression, Optional[FilterExpression], Optional[FilterExpression]]:
|
) -> Tuple[FilterExpression, Optional[FilterExpression], Optional[FilterExpression]]:
|
||||||
if not len(bits):
|
if not len(bits):
|
||||||
raise TemplateSyntaxError(
|
raise TemplateSyntaxError(
|
||||||
"'fill' tag does not match pattern " f"{{% fill <name> [{SLOT_DATA_ATTR}=val] [as alias] %}}. "
|
"'fill' tag does not match pattern "
|
||||||
|
f"{{% fill <name> [{SLOT_DATA_ATTR}=val] [{SLOT_DEFAULT_ATTR}=slot_var] %}} "
|
||||||
)
|
)
|
||||||
|
|
||||||
slot_name = bits.pop(0)
|
slot_name = bits.pop(0)
|
||||||
slot_name_fexp = parser.compile_filter(slot_name)
|
slot_name_fexp = parser.compile_filter(slot_name)
|
||||||
|
|
||||||
alias_fexp: Optional[FilterExpression] = None
|
|
||||||
# e.g. {% fill <name> as <alias> %}
|
|
||||||
if len(bits) >= 2 and bits[-2].lower() == "as":
|
|
||||||
alias = bits.pop()
|
|
||||||
bits.pop() # Remove the "as" keyword
|
|
||||||
alias_fexp = parser.compile_filter(alias)
|
|
||||||
|
|
||||||
# Even tho we want to parse only single kwarg, we use the same logic for parsing
|
# Even tho we want to parse only single kwarg, we use the same logic for parsing
|
||||||
# as we use for other tags, for consistency.
|
# as we use for other tags, for consistency.
|
||||||
_, tag_kwarg_pairs = parse_bits(
|
_, tag_kwarg_pairs = parse_bits(
|
||||||
|
@ -410,17 +405,29 @@ def _parse_fill_args(
|
||||||
raise TemplateSyntaxError(f"'{tag_name}' received multiple values for keyword argument '{key}'")
|
raise TemplateSyntaxError(f"'{tag_name}' received multiple values for keyword argument '{key}'")
|
||||||
tag_kwargs[key] = val
|
tag_kwargs[key] = val
|
||||||
|
|
||||||
scope_var_fexp: Optional[FilterExpression] = None
|
# Extract known kwargs
|
||||||
|
slot_data_var_fexp: Optional[FilterExpression] = None
|
||||||
if SLOT_DATA_ATTR in tag_kwargs:
|
if SLOT_DATA_ATTR in tag_kwargs:
|
||||||
scope_var_fexp = tag_kwargs.pop(SLOT_DATA_ATTR)
|
slot_data_var_fexp = tag_kwargs.pop(SLOT_DATA_ATTR)
|
||||||
if not is_wrapped_in_quotes(scope_var_fexp.token):
|
if not is_wrapped_in_quotes(slot_data_var_fexp.token):
|
||||||
raise TemplateSyntaxError(
|
raise TemplateSyntaxError(
|
||||||
f"Value of '{SLOT_DATA_ATTR}' in '{tag_name}' tag must be a string literal, got '{scope_var_fexp}'"
|
f"Value of '{SLOT_DATA_ATTR}' in '{tag_name}' tag must be a string literal, got '{slot_data_var_fexp}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
if scope_var_fexp and alias_fexp and scope_var_fexp.token == alias_fexp.token:
|
slot_default_var_fexp: Optional[FilterExpression] = None
|
||||||
|
if SLOT_DEFAULT_ATTR in tag_kwargs:
|
||||||
|
slot_default_var_fexp = tag_kwargs.pop(SLOT_DEFAULT_ATTR)
|
||||||
|
if not is_wrapped_in_quotes(slot_default_var_fexp.token):
|
||||||
|
raise TemplateSyntaxError(
|
||||||
|
f"Value of '{SLOT_DEFAULT_ATTR}' in '{tag_name}' tag must be a string literal,"
|
||||||
|
f" got '{slot_default_var_fexp}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
# data and default cannot be bound to the same variable
|
||||||
|
if slot_data_var_fexp and slot_default_var_fexp and slot_data_var_fexp.token == slot_default_var_fexp.token:
|
||||||
raise TemplateSyntaxError(
|
raise TemplateSyntaxError(
|
||||||
f"'{tag_name}' received the same string for slot alias (as ...) and slot data ({SLOT_DATA_ATTR}=...)"
|
f"'{tag_name}' received the same string for slot default ({SLOT_DEFAULT_ATTR}=...)"
|
||||||
|
f" and slot data ({SLOT_DATA_ATTR}=...)"
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(tag_kwargs):
|
if len(tag_kwargs):
|
||||||
|
@ -428,7 +435,7 @@ def _parse_fill_args(
|
||||||
extra_keys = ", ".join(extra_keywords)
|
extra_keys = ", ".join(extra_keywords)
|
||||||
raise TemplateSyntaxError(f"'{tag_name}' received unexpected kwargs: {extra_keys}")
|
raise TemplateSyntaxError(f"'{tag_name}' received unexpected kwargs: {extra_keys}")
|
||||||
|
|
||||||
return slot_name_fexp, alias_fexp, scope_var_fexp
|
return slot_name_fexp, slot_default_var_fexp, slot_data_var_fexp
|
||||||
|
|
||||||
|
|
||||||
def _get_positional_param(
|
def _get_positional_param(
|
||||||
|
|
|
@ -912,7 +912,7 @@ class ConditionalSlotTests(BaseTestCase):
|
||||||
self.assertHTMLEqual(rendered, '<p id="a">Override A</p><p id="b">Override B</p>')
|
self.assertHTMLEqual(rendered, '<p id="a">Override A</p><p id="b">Override B</p>')
|
||||||
|
|
||||||
|
|
||||||
class SlotSuperTests(BaseTestCase):
|
class SlotDefaultTests(BaseTestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
|
@ -924,13 +924,13 @@ class SlotSuperTests(BaseTestCase):
|
||||||
super().tearDownClass()
|
super().tearDownClass()
|
||||||
component.registry.clear()
|
component.registry.clear()
|
||||||
|
|
||||||
def test_basic_super_functionality(self):
|
def test_basic(self):
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component "test" %}
|
{% component "test" %}
|
||||||
{% fill "header" as "header" %}Before: {{ header.default }}{% endfill %}
|
{% fill "header" default="header" %}Before: {{ header }}{% endfill %}
|
||||||
{% fill "main" as "main" %}{{ main.default }}{% endfill %}
|
{% fill "main" default="main" %}{{ main }}{% endfill %}
|
||||||
{% fill "footer" as "footer" %}{{ footer.default }}, after{% endfill %}
|
{% fill "footer" default="footer" %}{{ footer }}, after{% endfill %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
"""
|
"""
|
||||||
template = Template(template_str)
|
template = Template(template_str)
|
||||||
|
@ -947,13 +947,13 @@ class SlotSuperTests(BaseTestCase):
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_multiple_super_calls(self):
|
def test_multiple_calls(self):
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component "test" %}
|
{% component "test" %}
|
||||||
{% fill "header" as "header" %}
|
{% fill "header" default="header" %}
|
||||||
First: {{ header.default }};
|
First: {{ header }};
|
||||||
Second: {{ header.default }}
|
Second: {{ header }}
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
"""
|
"""
|
||||||
|
@ -971,14 +971,16 @@ class SlotSuperTests(BaseTestCase):
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_super_under_if_node(self):
|
def test_under_if_and_forloop(self):
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component "test" %}
|
{% component "test" %}
|
||||||
{% fill "header" as "header" %}
|
{% fill "header" default="header" %}
|
||||||
{% for i in range %}
|
{% for i in range %}
|
||||||
{% if forloop.first %}First {{ header.default }}
|
{% if forloop.first %}
|
||||||
{% else %}Later {{ header.default }}
|
First {{ header }}
|
||||||
|
{% else %}
|
||||||
|
Later {{ header }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
|
@ -998,6 +1000,52 @@ class SlotSuperTests(BaseTestCase):
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_nested_fills(self):
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "test" %}
|
||||||
|
{% fill "header" default="header1" %}
|
||||||
|
header1_in_header1: {{ header1 }}
|
||||||
|
{% component "test" %}
|
||||||
|
{% fill "header" default="header2" %}
|
||||||
|
header1_in_header2: {{ header1 }}
|
||||||
|
header2_in_header2: {{ header2 }}
|
||||||
|
{% endfill %}
|
||||||
|
{% fill "footer" default="footer2" %}
|
||||||
|
header1_in_footer2: {{ header1 }}
|
||||||
|
footer2_in_footer2: {{ footer2 }}
|
||||||
|
{% endfill %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endfill %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
<custom-template>
|
||||||
|
<header>
|
||||||
|
header1_in_header1: Default header
|
||||||
|
<custom-template>
|
||||||
|
<header>
|
||||||
|
header1_in_header2: Default header
|
||||||
|
header2_in_header2: Default header
|
||||||
|
</header>
|
||||||
|
<main>Default main</main>
|
||||||
|
<footer>
|
||||||
|
header1_in_footer2: Default header
|
||||||
|
footer2_in_footer2: Default footer
|
||||||
|
</footer>
|
||||||
|
</custom-template>
|
||||||
|
</header>
|
||||||
|
<main>Default main</main>
|
||||||
|
<footer>Default footer</footer>
|
||||||
|
</custom-template>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TemplateSyntaxErrorTests(BaseTestCase):
|
class TemplateSyntaxErrorTests(BaseTestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -1310,11 +1358,11 @@ class ComponentNestingTests(BaseTestCase):
|
||||||
self.assertHTMLEqual(rendered, expected)
|
self.assertHTMLEqual(rendered, expected)
|
||||||
|
|
||||||
@override_settings(COMPONENTS={"context_behavior": "django"})
|
@override_settings(COMPONENTS={"context_behavior": "django"})
|
||||||
def test_component_nesting_component_with_fill_and_super__django(self):
|
def test_component_nesting_component_with_slot_default__django(self):
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component "dashboard" %}
|
{% component "dashboard" %}
|
||||||
{% fill "header" as "h" %} Hello! {{ h.default }} {% endfill %}
|
{% fill "header" default="h" %} Hello! {{ h }} {% endfill %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
"""
|
"""
|
||||||
template = Template(template_str)
|
template = Template(template_str)
|
||||||
|
@ -1338,11 +1386,11 @@ class ComponentNestingTests(BaseTestCase):
|
||||||
self.assertHTMLEqual(rendered, expected)
|
self.assertHTMLEqual(rendered, expected)
|
||||||
|
|
||||||
@override_settings(COMPONENTS={"context_behavior": "isolated"})
|
@override_settings(COMPONENTS={"context_behavior": "isolated"})
|
||||||
def test_component_nesting_component_with_fill_and_super__isolated(self):
|
def test_component_nesting_component_with_slot_default__isolated(self):
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component "dashboard" %}
|
{% component "dashboard" %}
|
||||||
{% fill "header" as "h" %} Hello! {{ h.default }} {% endfill %}
|
{% fill "header" default="h" %} Hello! {{ h }} {% endfill %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
"""
|
"""
|
||||||
template = Template(template_str)
|
template = Template(template_str)
|
||||||
|
@ -2194,8 +2242,8 @@ class IterationFillTest(BaseTestCase):
|
||||||
{% 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" as "super_slot_inner" %}
|
{% fill "slot_inner" default="super_slot_inner" %}
|
||||||
{{ super_slot_inner.default }}
|
{{ super_slot_inner }}
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
|
@ -2228,8 +2276,8 @@ class IterationFillTest(BaseTestCase):
|
||||||
{% 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" as "super_slot_inner" %}
|
{% fill "slot_inner" default="super_slot_inner" %}
|
||||||
{{ super_slot_inner.default }}
|
{{ super_slot_inner }}
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
|
@ -2256,9 +2304,9 @@ class IterationFillTest(BaseTestCase):
|
||||||
{% 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" as "super_slot_inner" %}
|
{% fill "slot_inner" default="super_slot_inner" %}
|
||||||
{{ outer_scope_variable_2 }}
|
{{ outer_scope_variable_2 }}
|
||||||
{{ super_slot_inner.default }}
|
{{ super_slot_inner }}
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
|
@ -2311,9 +2359,9 @@ class IterationFillTest(BaseTestCase):
|
||||||
{% 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" as "super_slot_inner" %}
|
{% fill "slot_inner" default="super_slot_inner" %}
|
||||||
{{ outer_scope_variable_2 }}
|
{{ outer_scope_variable_2 }}
|
||||||
{{ super_slot_inner.default }}
|
{{ super_slot_inner }}
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
|
@ -2358,9 +2406,9 @@ class IterationFillTest(BaseTestCase):
|
||||||
{% 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" as "super_slot_inner" %}
|
{% fill "slot_inner" default="super_slot_inner" %}
|
||||||
{{ outer_scope_variable_2|safe }}
|
{{ outer_scope_variable_2|safe }}
|
||||||
{{ super_slot_inner.default }}
|
{{ super_slot_inner }}
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
|
@ -2401,7 +2449,7 @@ class ScopedSlotTest(BaseTestCase):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
<div>
|
<div>
|
||||||
{% slot "my_slot" abc=abc def=var123 %}Default text{% endslot %}
|
{% slot "my_slot" abc=abc var123=var123 %}Default text{% endslot %}
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -2416,7 +2464,7 @@ class ScopedSlotTest(BaseTestCase):
|
||||||
{% component "test" %}
|
{% component "test" %}
|
||||||
{% fill "my_slot" data="slot_data_in_fill" %}
|
{% fill "my_slot" data="slot_data_in_fill" %}
|
||||||
{{ slot_data_in_fill.abc }}
|
{{ slot_data_in_fill.abc }}
|
||||||
{{ slot_data_in_fill.def }}
|
{{ slot_data_in_fill.var123 }}
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
"""
|
"""
|
||||||
|
@ -2435,7 +2483,7 @@ class ScopedSlotTest(BaseTestCase):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
<div>
|
<div>
|
||||||
{% slot "my_slot" default abc=abc 123=var123 required %}Default text{% endslot %}
|
{% slot "my_slot" default abc=abc var123=var123 required %}Default text{% endslot %}
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -2450,7 +2498,7 @@ class ScopedSlotTest(BaseTestCase):
|
||||||
{% component "test" %}
|
{% component "test" %}
|
||||||
{% fill "my_slot" data="slot_data_in_fill" %}
|
{% fill "my_slot" data="slot_data_in_fill" %}
|
||||||
{{ slot_data_in_fill.abc }}
|
{{ slot_data_in_fill.abc }}
|
||||||
{{ slot_data_in_fill.123 }}
|
{{ slot_data_in_fill.var123 }}
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
"""
|
"""
|
||||||
|
@ -2463,13 +2511,13 @@ class ScopedSlotTest(BaseTestCase):
|
||||||
"""
|
"""
|
||||||
self.assertHTMLEqual(rendered, expected)
|
self.assertHTMLEqual(rendered, expected)
|
||||||
|
|
||||||
def test_slot_data_fill_with_as(self):
|
def test_slot_data_with_slot_default(self):
|
||||||
@component.register("test")
|
@component.register("test")
|
||||||
class TestComponent(component.Component):
|
class TestComponent(component.Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
<div>
|
<div>
|
||||||
{% slot "my_slot" abc=abc 123=var123 %}Default text{% endslot %}
|
{% slot "my_slot" abc=abc var123=var123 %}Default text{% endslot %}
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -2482,10 +2530,10 @@ class ScopedSlotTest(BaseTestCase):
|
||||||
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" as "slot_var" %}
|
{% fill "my_slot" data="slot_data_in_fill" default="slot_var" %}
|
||||||
{{ slot_var.default }}
|
{{ slot_var }}
|
||||||
{{ slot_data_in_fill.abc }}
|
{{ slot_data_in_fill.abc }}
|
||||||
{{ slot_data_in_fill.123 }}
|
{{ slot_data_in_fill.var123 }}
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
"""
|
"""
|
||||||
|
@ -2499,13 +2547,13 @@ class ScopedSlotTest(BaseTestCase):
|
||||||
"""
|
"""
|
||||||
self.assertHTMLEqual(rendered, expected)
|
self.assertHTMLEqual(rendered, expected)
|
||||||
|
|
||||||
def test_slot_data_raises_on_slot_data_and_as_same_var(self):
|
def test_slot_data_raises_on_slot_data_and_slot_default_same_var(self):
|
||||||
@component.register("test")
|
@component.register("test")
|
||||||
class TestComponent(component.Component):
|
class TestComponent(component.Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
<div>
|
<div>
|
||||||
{% slot "my_slot" abc=abc 123=var123 %}Default text{% endslot %}
|
{% slot "my_slot" abc=abc var123=var123 %}Default text{% endslot %}
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -2518,14 +2566,14 @@ class ScopedSlotTest(BaseTestCase):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component "test" %}
|
{% component "test" %}
|
||||||
{% fill "my_slot" data="slot_var" as "slot_var" %}
|
{% fill "my_slot" data="slot_var" default="slot_var" %}
|
||||||
{{ slot_var.default }}
|
{{ slot_var }}
|
||||||
{% endfill %}
|
{% endfill %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
"""
|
"""
|
||||||
with self.assertRaisesMessage(
|
with self.assertRaisesMessage(
|
||||||
TemplateSyntaxError,
|
TemplateSyntaxError,
|
||||||
"'fill' received the same string for slot alias (as ...) and slot data (data=...)",
|
"'fill' received the same string for slot default (default=...) and slot data (data=...)",
|
||||||
):
|
):
|
||||||
Template(template).render(Context())
|
Template(template).render(Context())
|
||||||
|
|
||||||
|
@ -2535,7 +2583,7 @@ class ScopedSlotTest(BaseTestCase):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
<div>
|
<div>
|
||||||
{% slot "my_slot" abc=abc 123=var123 %}Default text{% endslot %}
|
{% slot "my_slot" abc=abc var123=var123 %}Default text{% endslot %}
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -2585,7 +2633,7 @@ class ScopedSlotTest(BaseTestCase):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
<div>
|
<div>
|
||||||
{% slot "my_slot" abc=abc 123=var123 %}Default text{% endslot %}
|
{% slot "my_slot" abc=abc var123=var123 %}Default text{% endslot %}
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -2603,3 +2651,49 @@ class ScopedSlotTest(BaseTestCase):
|
||||||
rendered = Template(template).render(Context())
|
rendered = Template(template).render(Context())
|
||||||
expected = "<div> Default text </div>"
|
expected = "<div> Default text </div>"
|
||||||
self.assertHTMLEqual(rendered, expected)
|
self.assertHTMLEqual(rendered, expected)
|
||||||
|
|
||||||
|
def test_nested_fills(self):
|
||||||
|
@component.register("test")
|
||||||
|
class TestComponent(component.Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
<div>
|
||||||
|
{% slot "my_slot" abc=abc input=input %}Default text{% endslot %}
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_context_data(self, input):
|
||||||
|
return {
|
||||||
|
"abc": "def",
|
||||||
|
"input": input,
|
||||||
|
}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "test" input=1 %}
|
||||||
|
{% fill "my_slot" data="data1" %}
|
||||||
|
data1_in_slot1: {{ data1|safe }}
|
||||||
|
{% component "test" input=2 %}
|
||||||
|
{% fill "my_slot" data="data2" %}
|
||||||
|
data1_in_slot2: {{ data1|safe }}
|
||||||
|
data2_in_slot2: {{ data2|safe }}
|
||||||
|
{% endfill %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endfill %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
<div>
|
||||||
|
data1_in_slot1: {'abc': 'def', 'input': 1}
|
||||||
|
<div>
|
||||||
|
data1_in_slot2: {'abc': 'def', 'input': 1}
|
||||||
|
data2_in_slot2: {'abc': 'def', 'input': 2}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue