mirror of
https://github.com/django-components/django-components.git
synced 2025-09-15 18:34:59 +00:00
Add required kwd to slot tag and add test
Move required slot check to SlotNode.render(); clean up needed Remove unused code; drop caching Update docs Incorporate PR feedback
This commit is contained in:
parent
a8dfcce24e
commit
898d148382
9 changed files with 143 additions and 138 deletions
13
README.md
13
README.md
|
@ -323,16 +323,21 @@ This makes it possible to organize your front-end around reusable components. In
|
||||||
|
|
||||||
# Using slots in templates
|
# Using slots in templates
|
||||||
|
|
||||||
_New in version 0.26_ (__breaking change__): Defining slots and passing content to them used to be achieved by a single block tag `{% slot %}`. To make component nesting easier these functions have been split across two separate tags. Now, `{% slot %}` serves only to declare/define/open new slots inside the component template. The function of passing in content to a slot has been moved to a newly introduced `{% fill %}` tag.
|
_New in version 0.26_:
|
||||||
|
|
||||||
Components support something called 'slots'.
|
- The `slot` tag now serves only to declare new slots inside the component template.
|
||||||
|
- To override the content of a declared slot, use the newly introduced `fill` tag instead.
|
||||||
|
- Whereas unfilled slots used to raise a warning, filling a slot is now optional by default.
|
||||||
|
- To indicate that a slot must be filled, the new keyword `required` should be added at the end of the `slot` tag.
|
||||||
|
|
||||||
|
Components support something called 'slots'.
|
||||||
When a component is used inside another template, slots allow the parent template to override specific parts of the child component by passing in different content.
|
When a component is used inside another template, slots allow the parent template to override specific parts of the child component by passing in different content.
|
||||||
This mechanism makes components more reusable and composable.
|
This mechanism makes components more reusable and composable.
|
||||||
|
|
||||||
In the example below we introduce two block tags that work hand in hand to make this work. These are...
|
In the example below we introduce two block tags that work hand in hand to make this work. These are...
|
||||||
|
|
||||||
- `{% slot <name> %}`/`{% endslot %}`: Declare new slot on component template.
|
- `{% slot <name> %}`/`{% endslot %}`: Declares a new slot in the component template.
|
||||||
- `{% fill <name> %}`/`{% endfill %}`: Used inside component block. The content of this block is injected into the slot with the same name.
|
- `{% fill <name> %}`/`{% endfill %}`: (Used inside a component block.) Fills a declared slot with the specified content.
|
||||||
|
|
||||||
Let's update our calendar component to support more customization by updating our calendar.html template.
|
Let's update our calendar component to support more customization by updating our calendar.html template.
|
||||||
|
|
||||||
|
|
|
@ -17,11 +17,5 @@ class AppSettings:
|
||||||
def TEMPLATE_CACHE_SIZE(self):
|
def TEMPLATE_CACHE_SIZE(self):
|
||||||
return self.settings.setdefault("template_cache_size", 128)
|
return self.settings.setdefault("template_cache_size", 128)
|
||||||
|
|
||||||
@property
|
|
||||||
def STRICT_SLOTS(self):
|
|
||||||
"""If True, component slots that are declared must be explicitly filled; else
|
|
||||||
a TemplateSyntaxError is raised."""
|
|
||||||
return self.settings.setdefault("strict_slots", False)
|
|
||||||
|
|
||||||
|
|
||||||
app_settings = AppSettings()
|
app_settings = AppSettings()
|
||||||
|
|
|
@ -1,29 +1,23 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import warnings
|
import copy
|
||||||
from collections import defaultdict
|
|
||||||
from contextlib import contextmanager
|
|
||||||
from functools import lru_cache
|
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
ClassVar,
|
ClassVar,
|
||||||
Dict,
|
Dict,
|
||||||
|
Iterator,
|
||||||
List,
|
List,
|
||||||
Optional,
|
Optional,
|
||||||
Tuple,
|
|
||||||
TypeVar,
|
TypeVar,
|
||||||
)
|
)
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.forms.widgets import MediaDefiningClass
|
from django.forms.widgets import MediaDefiningClass
|
||||||
from django.template import TemplateSyntaxError
|
from django.template import Context, TemplateSyntaxError
|
||||||
from django.template.base import Node, NodeList, Template
|
from django.template.base import Node, NodeList, Template
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from django_components.app_settings import app_settings
|
|
||||||
|
|
||||||
# Allow "component.AlreadyRegistered" instead of having to import these everywhere
|
# Allow "component.AlreadyRegistered" instead of having to import these everywhere
|
||||||
from django_components.component_registry import ( # noqa
|
from django_components.component_registry import ( # noqa
|
||||||
AlreadyRegistered,
|
AlreadyRegistered,
|
||||||
|
@ -104,61 +98,26 @@ class Component(metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
"""Render only JS dependencies available in the media class."""
|
"""Render only JS dependencies available in the media class."""
|
||||||
return mark_safe("\n".join(self.media.render_js()))
|
return mark_safe("\n".join(self.media.render_js()))
|
||||||
|
|
||||||
@classmethod
|
def get_declared_slots(
|
||||||
@lru_cache(maxsize=app_settings.TEMPLATE_CACHE_SIZE)
|
self, context: Context, template: Optional[Template] = None
|
||||||
def fetch_and_analyze_template(
|
) -> List[SlotNode]:
|
||||||
cls, template_name: str
|
if template is None:
|
||||||
) -> Tuple[Template, Dict[str, SlotNode]]:
|
template = self.get_template(context)
|
||||||
template: Template = get_template(template_name).template
|
return list(
|
||||||
slots = {}
|
dfs_iter_slots_in_nodelist(template.nodelist, template.name)
|
||||||
for slot in iter_slots_in_nodelist(template.nodelist, template.name):
|
)
|
||||||
slot.component_cls = cls
|
|
||||||
slots[slot.name] = slot
|
|
||||||
return template, slots
|
|
||||||
|
|
||||||
def get_processed_template(self, context):
|
def get_template(self, context, template_name: Optional[str] = None):
|
||||||
template_name = self.get_template_name(context)
|
if template_name is None:
|
||||||
# Note: return of method below is cached.
|
template_name = self.get_template_name(context)
|
||||||
template, slots = self.fetch_and_analyze_template(template_name)
|
template = get_template(template_name).template
|
||||||
self._raise_if_fills_do_not_match_slots(
|
|
||||||
slots, self.instance_fills, self._component_name
|
|
||||||
)
|
|
||||||
self._raise_if_declared_slots_are_unfilled(
|
|
||||||
slots, self.instance_fills, self._component_name
|
|
||||||
)
|
|
||||||
return template
|
return template
|
||||||
|
|
||||||
@staticmethod
|
def set_instance_fills(self, fills: Dict[str, FillNode]) -> None:
|
||||||
def _raise_if_declared_slots_are_unfilled(
|
self._instance_fills = fills
|
||||||
slots: Dict[str, SlotNode], fills: Dict[str, FillNode], comp_name: str
|
|
||||||
):
|
|
||||||
# 'unconditional_slots' are slots that were encountered within an 'if_filled'
|
|
||||||
# context. They are exempt from filling checks.
|
|
||||||
unconditional_slots = {
|
|
||||||
slot.name for slot in slots.values() if not slot.is_conditional
|
|
||||||
}
|
|
||||||
unused_slots = unconditional_slots - fills.keys()
|
|
||||||
if unused_slots:
|
|
||||||
msg = (
|
|
||||||
f"Component '{comp_name}' declares slots that "
|
|
||||||
f"are not filled: '{unused_slots}'"
|
|
||||||
)
|
|
||||||
if app_settings.STRICT_SLOTS:
|
|
||||||
raise TemplateSyntaxError(msg)
|
|
||||||
elif settings.DEBUG:
|
|
||||||
warnings.warn(msg)
|
|
||||||
|
|
||||||
@staticmethod
|
def set_outer_context(self, context):
|
||||||
def _raise_if_fills_do_not_match_slots(
|
self._outer_context = context
|
||||||
slots: Dict[str, SlotNode], fills: Dict[str, FillNode], comp_name: str
|
|
||||||
):
|
|
||||||
unmatchable_fills = fills.keys() - slots.keys()
|
|
||||||
if unmatchable_fills:
|
|
||||||
msg = (
|
|
||||||
f"Component '{comp_name}' passed fill(s) "
|
|
||||||
f"refering to undefined slot(s). Bad fills: {list(unmatchable_fills)}."
|
|
||||||
)
|
|
||||||
raise TemplateSyntaxError(msg)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def instance_fills(self):
|
def instance_fills(self):
|
||||||
|
@ -168,28 +127,56 @@ class Component(metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
def outer_context(self):
|
def outer_context(self):
|
||||||
return self._outer_context or {}
|
return self._outer_context or {}
|
||||||
|
|
||||||
@contextmanager
|
def get_updated_fill_stacks(self, context):
|
||||||
def assign(
|
current_fill_stacks = context.get(FILLED_SLOTS_CONTEXT_KEY, None)
|
||||||
self: T,
|
updated_fill_stacks = (
|
||||||
fills: Optional[Dict[str, FillNode]] = None,
|
copy.deepcopy(current_fill_stacks)
|
||||||
outer_context: Optional[dict] = None,
|
if current_fill_stacks is not None
|
||||||
) -> T:
|
else {}
|
||||||
if fills is not None:
|
|
||||||
self._instance_fills = fills
|
|
||||||
if outer_context is not None:
|
|
||||||
self._outer_context = outer_context
|
|
||||||
yield self
|
|
||||||
self._instance_fills = None
|
|
||||||
self._outer_context = None
|
|
||||||
|
|
||||||
def render(self, context):
|
|
||||||
template = self.get_processed_template(context)
|
|
||||||
current_fills_stack = context.get(
|
|
||||||
FILLED_SLOTS_CONTEXT_KEY, defaultdict(list)
|
|
||||||
)
|
)
|
||||||
for name, fill in self.instance_fills.items():
|
for name, fill in self.instance_fills.items():
|
||||||
current_fills_stack[name].append(fill)
|
if name in updated_fill_stacks:
|
||||||
with context.update({FILLED_SLOTS_CONTEXT_KEY: current_fills_stack}):
|
updated_fill_stacks[name].append(fill)
|
||||||
|
else:
|
||||||
|
updated_fill_stacks[name] = [fill]
|
||||||
|
return updated_fill_stacks
|
||||||
|
|
||||||
|
def validate_fills_and_slots_(
|
||||||
|
self,
|
||||||
|
context,
|
||||||
|
template: Template,
|
||||||
|
fills: Optional[Dict[str, FillNode]] = None,
|
||||||
|
) -> None:
|
||||||
|
if fills is None:
|
||||||
|
fills = self.instance_fills
|
||||||
|
all_slots: List[SlotNode] = self.get_declared_slots(context, template)
|
||||||
|
slots: Dict[str, SlotNode] = {}
|
||||||
|
# Each declared slot must have a unique name.
|
||||||
|
for slot in all_slots:
|
||||||
|
slot_name = slot.name
|
||||||
|
if slot_name in slots:
|
||||||
|
raise TemplateSyntaxError(
|
||||||
|
f"Encountered non-unique slot '{slot_name}' in template "
|
||||||
|
f"'{template.name}' of component '{self._component_name}'."
|
||||||
|
)
|
||||||
|
slots[slot_name] = slot
|
||||||
|
# All fill nodes must correspond to a declared slot.
|
||||||
|
unmatchable_fills = fills.keys() - slots.keys()
|
||||||
|
if unmatchable_fills:
|
||||||
|
msg = (
|
||||||
|
f"Component '{self._component_name}' passed fill(s) "
|
||||||
|
f"refering to undefined slot(s). Bad fills: {list(unmatchable_fills)}."
|
||||||
|
)
|
||||||
|
raise TemplateSyntaxError(msg)
|
||||||
|
# Note: Requirement that 'required' slots be filled is enforced
|
||||||
|
# in SlotNode.render().
|
||||||
|
|
||||||
|
def render(self, context):
|
||||||
|
template_name = self.get_template_name(context)
|
||||||
|
template = self.get_template(context, template_name)
|
||||||
|
self.validate_fills_and_slots_(context, template)
|
||||||
|
updated_fill_stacks = self.get_updated_fill_stacks(context)
|
||||||
|
with context.update({FILLED_SLOTS_CONTEXT_KEY: updated_fill_stacks}):
|
||||||
return template.render(context)
|
return template.render(context)
|
||||||
|
|
||||||
class Media:
|
class Media:
|
||||||
|
@ -197,23 +184,15 @@ class Component(metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
js = []
|
js = []
|
||||||
|
|
||||||
|
|
||||||
def iter_slots_in_nodelist(nodelist: NodeList, template_name: str = None):
|
def dfs_iter_slots_in_nodelist(
|
||||||
|
nodelist: NodeList, template_name: str = None
|
||||||
|
) -> Iterator[SlotNode]:
|
||||||
from django_components.templatetags.component_tags import SlotNode
|
from django_components.templatetags.component_tags import SlotNode
|
||||||
|
|
||||||
nodes: List[Node] = list(nodelist)
|
nodes: List[Node] = list(nodelist)
|
||||||
slot_names = set()
|
|
||||||
while nodes:
|
while nodes:
|
||||||
node = nodes.pop()
|
node = nodes.pop()
|
||||||
if isinstance(node, SlotNode):
|
if isinstance(node, SlotNode):
|
||||||
slot_name = node.name
|
|
||||||
if slot_name in slot_names:
|
|
||||||
context = (
|
|
||||||
f" in template '{template_name}'" if template_name else ""
|
|
||||||
)
|
|
||||||
raise TemplateSyntaxError(
|
|
||||||
f"Encountered non-unique slot '{slot_name}'{context}"
|
|
||||||
)
|
|
||||||
slot_names.add(slot_name)
|
|
||||||
yield node
|
yield node
|
||||||
for nodelist_name in node.child_nodelists:
|
for nodelist_name in node.child_nodelists:
|
||||||
nodes.extend(reversed(getattr(node, nodelist_name, [])))
|
nodes.extend(reversed(getattr(node, nodelist_name, [])))
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, DefaultDict, List, Optional, Tuple
|
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -164,11 +164,13 @@ class UserSlotVar:
|
||||||
|
|
||||||
|
|
||||||
class SlotNode(Node):
|
class SlotNode(Node):
|
||||||
def __init__(self, name, nodelist):
|
def __init__(
|
||||||
|
self, name, nodelist, template_name: str = "", required=False
|
||||||
|
):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.nodelist = nodelist
|
self.nodelist = nodelist
|
||||||
self.component_cls = None
|
self.template_name = template_name
|
||||||
self.is_conditional: bool = False
|
self.is_required = required
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Slot Node: {self.name}. Contents: {repr(self.nodelist)}>"
|
return f"<Slot Node: {self.name}. Contents: {repr(self.nodelist)}>"
|
||||||
|
@ -178,13 +180,19 @@ class SlotNode(Node):
|
||||||
raise TemplateSyntaxError(
|
raise TemplateSyntaxError(
|
||||||
f"Attempted to render SlotNode '{self.name}' outside a parent component."
|
f"Attempted to render SlotNode '{self.name}' outside a parent component."
|
||||||
)
|
)
|
||||||
filled_slots: DefaultDict[str, List[FillNode]] = context[
|
filled_slots: Dict[str, List[FillNode]] = context[
|
||||||
FILLED_SLOTS_CONTEXT_KEY
|
FILLED_SLOTS_CONTEXT_KEY
|
||||||
]
|
]
|
||||||
fill_node_stack = filled_slots[self.name]
|
fill_node_stack = filled_slots.get(self.name, None)
|
||||||
extra_context = {}
|
extra_context = {}
|
||||||
if not fill_node_stack: # if []
|
if not fill_node_stack: # if None or []
|
||||||
nodelist = self.nodelist
|
nodelist = self.nodelist
|
||||||
|
# Raise if slot is 'required'
|
||||||
|
if self.is_required:
|
||||||
|
raise TemplateSyntaxError(
|
||||||
|
f"Slot '{self.name}' is marked as 'required' (i.e. non-optional), "
|
||||||
|
f"yet no fill is provided. Check template '{self.template_name}'"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
fill_node = fill_node_stack.pop()
|
fill_node = fill_node_stack.pop()
|
||||||
nodelist = fill_node.nodelist
|
nodelist = fill_node.nodelist
|
||||||
|
@ -204,6 +212,16 @@ def do_slot(parser, token):
|
||||||
# e.g. {% slot <name> %}
|
# e.g. {% slot <name> %}
|
||||||
if len(args) == 1:
|
if len(args) == 1:
|
||||||
slot_name: str = args[0]
|
slot_name: str = args[0]
|
||||||
|
required = False
|
||||||
|
elif len(args) == 2:
|
||||||
|
slot_name: str = args[0]
|
||||||
|
required_keyword = args[1]
|
||||||
|
if required_keyword != "required":
|
||||||
|
raise TemplateSyntaxError(
|
||||||
|
f"'{bits[0]}' only accepts 'required' keyword as optional second argument"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
required = True
|
||||||
else:
|
else:
|
||||||
raise TemplateSyntaxError(
|
raise TemplateSyntaxError(
|
||||||
f"{bits[0]}' tag takes only one argument (the slot name)"
|
f"{bits[0]}' tag takes only one argument (the slot name)"
|
||||||
|
@ -220,7 +238,8 @@ def do_slot(parser, token):
|
||||||
nodelist = parser.parse(parse_until=["endslot"])
|
nodelist = parser.parse(parse_until=["endslot"])
|
||||||
parser.delete_first_token()
|
parser.delete_first_token()
|
||||||
|
|
||||||
return SlotNode(slot_name, nodelist)
|
template_name = parser.origin.template_name
|
||||||
|
return SlotNode(slot_name, nodelist, template_name, required)
|
||||||
|
|
||||||
|
|
||||||
class FillNode(Node):
|
class FillNode(Node):
|
||||||
|
@ -328,17 +347,16 @@ class ComponentNode(Node):
|
||||||
for fill_node in self.fill_nodes
|
for fill_node in self.fill_nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create a fresh isolated context if requested w 'only' keyword.
|
component.set_instance_fills(resolved_fills)
|
||||||
with component.assign(
|
component.set_outer_context(context)
|
||||||
fills=resolved_fills, outer_context=context.flatten()
|
|
||||||
):
|
component_context = component.get_context_data(
|
||||||
component_context = component.get_context_data(
|
*resolved_context_args, **resolved_context_kwargs
|
||||||
*resolved_context_args, **resolved_context_kwargs
|
)
|
||||||
)
|
if self.isolated_context:
|
||||||
if self.isolated_context:
|
context = context.new()
|
||||||
context = context.new()
|
with context.update(component_context):
|
||||||
with context.update(component_context):
|
rendered_component = component.render(context)
|
||||||
rendered_component = component.render(context)
|
|
||||||
|
|
||||||
if is_dependency_middleware_active():
|
if is_dependency_middleware_active():
|
||||||
return (
|
return (
|
||||||
|
@ -432,7 +450,9 @@ def fill_tokens(parser):
|
||||||
not is_whitespace(token) and token.token_type != TokenType.COMMENT
|
not is_whitespace(token) and token.token_type != TokenType.COMMENT
|
||||||
):
|
):
|
||||||
raise TemplateSyntaxError(
|
raise TemplateSyntaxError(
|
||||||
f"Content tokens in component blocks must be placed inside 'fill' tags: {token}"
|
"Component block EITHER contains illegal tokens tag that are not "
|
||||||
|
"{{% fill ... %}} tags OR the proper closing tag -- "
|
||||||
|
"{{% endcomponent_block %}} -- is missing."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -536,7 +556,6 @@ class IfSlotFilledNode(Node):
|
||||||
):
|
):
|
||||||
# [(<slot name var | None (= condition)>, nodelist, <is_positive>)]
|
# [(<slot name var | None (= condition)>, nodelist, <is_positive>)]
|
||||||
self.branches = branches
|
self.branches = branches
|
||||||
self.visit_and_mark_slots_as_conditional_()
|
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
for _, nodelist, _ in self.branches:
|
for _, nodelist, _ in self.branches:
|
||||||
|
@ -546,15 +565,6 @@ class IfSlotFilledNode(Node):
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__}>"
|
return f"<{self.__class__.__name__}>"
|
||||||
|
|
||||||
def visit_and_mark_slots_as_conditional_(self):
|
|
||||||
stack = list(self.nodelist)
|
|
||||||
while stack:
|
|
||||||
node = stack.pop()
|
|
||||||
if isinstance(node, SlotNode):
|
|
||||||
node.is_conditional = True
|
|
||||||
for nodelist_name in node.child_nodelists:
|
|
||||||
stack.extend(getattr(node, nodelist_name, ()))
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def nodelist(self):
|
def nodelist(self):
|
||||||
return NodeList(self)
|
return NodeList(self)
|
||||||
|
|
|
@ -10,12 +10,11 @@ if not settings.configured:
|
||||||
"DIRS": ["tests/templates/"],
|
"DIRS": ["tests/templates/"],
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
COMPONENTS={"template_cache_size": 128, "strict_slots": False},
|
COMPONENTS={"template_cache_size": 128},
|
||||||
MIDDLEWARE=[
|
MIDDLEWARE=[
|
||||||
"django_components.middleware.ComponentDependencyMiddleware"
|
"django_components.middleware.ComponentDependencyMiddleware"
|
||||||
],
|
],
|
||||||
DATABASES={},
|
DATABASES={},
|
||||||
# DEBUG=True
|
|
||||||
)
|
)
|
||||||
|
|
||||||
django.setup()
|
django.setup()
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
{% endcomponent_block %}
|
{% endcomponent_block %}
|
||||||
<ol>
|
<ol>
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
<li>{{ item }}</li>{% endfor %}
|
<li>{{ item }}</li>
|
||||||
|
{% endfor %}
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
|
|
5
tests/templates/slotted_template_with_required_slot.html
Normal file
5
tests/templates/slotted_template_with_required_slot.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{% load component_tags %}
|
||||||
|
<div class="header-box">
|
||||||
|
<h1>{% slot "title" required %}{% endslot %}</h1>
|
||||||
|
<h2>{% slot "subtitle" %}{% endslot %}</h2>
|
||||||
|
</div>
|
|
@ -1,4 +1,4 @@
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
<header>{% slot "header" %}Default header{% endslot %}</header>
|
<header>{% slot "header" %}Default header{% endslot %}</header>
|
||||||
<main>{% slot "header" %}Default main header{% endslot %}</main> {# <- whoops! slot name 'header' used twice.
|
<main>{% slot "header" %}Default main header{% endslot %}</main> {# <- whoops! slot name 'header' used twice. #}
|
||||||
<footer>{% slot "footer" %}Default footer{% endslot %}</footer>
|
<footer>{% slot "footer" %}Default footer{% endslot %}</footer>
|
|
@ -334,6 +334,21 @@ class ComponentSlottedTemplateTagTest(SimpleTestCase):
|
||||||
"""
|
"""
|
||||||
self.assertHTMLEqual(rendered, expected)
|
self.assertHTMLEqual(rendered, expected)
|
||||||
|
|
||||||
|
def test_missing_required_slot_raises_error(self):
|
||||||
|
class Component(component.Component):
|
||||||
|
template_name = "slotted_template_with_required_slot.html"
|
||||||
|
|
||||||
|
component.registry.register("test", Component)
|
||||||
|
template = Template(
|
||||||
|
"""
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component_block 'test' %}
|
||||||
|
{% endcomponent_block %}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
with self.assertRaises(TemplateSyntaxError):
|
||||||
|
template.render(Context({}))
|
||||||
|
|
||||||
|
|
||||||
class SlottedTemplateRegressionTests(SimpleTestCase):
|
class SlottedTemplateRegressionTests(SimpleTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -902,9 +917,6 @@ class ComponentNestingTests(SimpleTestCase):
|
||||||
{% endcomponent_block %}
|
{% endcomponent_block %}
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
import sys
|
|
||||||
|
|
||||||
sys.setrecursionlimit(100)
|
|
||||||
rendered = template.render(Context({"items": [1, 2]}))
|
rendered = template.render(Context({"items": [1, 2]}))
|
||||||
expected = """
|
expected = """
|
||||||
<div class="dashboard-component">
|
<div class="dashboard-component">
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue