mirror of
https://github.com/django-components/django-components.git
synced 2025-09-27 16:09:07 +00:00
Improve slot handling to allow nested components, conditional slots, and slot.super (Fixes #33, #34, #37)
Co-authored-by: rbeard0330 <@dul2k3BKW6m>
This commit is contained in:
parent
c4db1646db
commit
070b754d24
7 changed files with 459 additions and 69 deletions
|
@ -91,8 +91,7 @@ class RenderBenchmarks(SimpleTestCase):
|
||||||
"{% component 'inner_component' variable='foo' %}{% endslot %}{% endcomponent_block %}",
|
"{% component 'inner_component' variable='foo' %}{% endslot %}{% endcomponent_block %}",
|
||||||
name='root')
|
name='root')
|
||||||
# Sanity tests
|
# Sanity tests
|
||||||
response = create_and_process_template_response(template)
|
response_content = create_and_process_template_response(template)
|
||||||
response_content = response.content.decode('utf-8')
|
|
||||||
self.assertNotIn(CSS_DEPENDENCY_PLACEHOLDER, response_content)
|
self.assertNotIn(CSS_DEPENDENCY_PLACEHOLDER, response_content)
|
||||||
self.assertNotIn(JS_DEPENDENCY_PLACEHOLDER, response_content)
|
self.assertNotIn(JS_DEPENDENCY_PLACEHOLDER, response_content)
|
||||||
self.assertIn('style.css', response_content)
|
self.assertIn('style.css', response_content)
|
||||||
|
@ -109,8 +108,7 @@ class RenderBenchmarks(SimpleTestCase):
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
|
|
||||||
template = get_template('mdn_complete_page.html')
|
template = get_template('mdn_complete_page.html')
|
||||||
response = create_and_process_template_response(template, {})
|
response_content = create_and_process_template_response(template, {})
|
||||||
response_content = response.content.decode('utf-8')
|
|
||||||
self.assertNotIn(CSS_DEPENDENCY_PLACEHOLDER, response_content)
|
self.assertNotIn(CSS_DEPENDENCY_PLACEHOLDER, response_content)
|
||||||
self.assertNotIn(JS_DEPENDENCY_PLACEHOLDER, response_content)
|
self.assertNotIn(JS_DEPENDENCY_PLACEHOLDER, response_content)
|
||||||
self.assertIn('test.css', response_content)
|
self.assertIn('test.css', response_content)
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import warnings
|
import warnings
|
||||||
from copy import copy
|
from copy import copy, deepcopy
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from itertools import chain
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.forms.widgets import MediaDefiningClass
|
from django.forms.widgets import MediaDefiningClass
|
||||||
from django.template.base import NodeList, TokenType
|
from django.template.base import Node, NodeList, TokenType
|
||||||
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
|
||||||
|
|
||||||
|
@ -14,6 +13,7 @@ from django_components.component_registry import AlreadyRegistered, ComponentReg
|
||||||
|
|
||||||
TEMPLATE_CACHE_SIZE = getattr(settings, "COMPONENTS", {}).get('TEMPLATE_CACHE_SIZE', 128)
|
TEMPLATE_CACHE_SIZE = getattr(settings, "COMPONENTS", {}).get('TEMPLATE_CACHE_SIZE', 128)
|
||||||
|
|
||||||
|
|
||||||
class SimplifiedInterfaceMediaDefiningClass(MediaDefiningClass):
|
class SimplifiedInterfaceMediaDefiningClass(MediaDefiningClass):
|
||||||
def __new__(mcs, name, bases, attrs):
|
def __new__(mcs, name, bases, attrs):
|
||||||
if "Media" in attrs:
|
if "Media" in attrs:
|
||||||
|
@ -39,6 +39,7 @@ class SimplifiedInterfaceMediaDefiningClass(MediaDefiningClass):
|
||||||
|
|
||||||
return super().__new__(mcs, name, bases, attrs)
|
return super().__new__(mcs, name, bases, attrs)
|
||||||
|
|
||||||
|
|
||||||
class Component(metaclass=SimplifiedInterfaceMediaDefiningClass):
|
class Component(metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
|
|
||||||
def __init__(self, component_name):
|
def __init__(self, component_name):
|
||||||
|
@ -73,45 +74,54 @@ class Component(metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_slot_node(node):
|
def is_slot_node(node):
|
||||||
return node.token.token_type == TokenType.BLOCK and node.token.split_contents()[0] == "slot"
|
return (isinstance(node, Node)
|
||||||
|
and node.token.token_type == TokenType.BLOCK
|
||||||
|
and node.token.split_contents()[0] == "slot")
|
||||||
|
|
||||||
@lru_cache(maxsize=TEMPLATE_CACHE_SIZE)
|
@lru_cache(maxsize=TEMPLATE_CACHE_SIZE)
|
||||||
def compile_instance_template(self, template_name):
|
def get_processed_template(self, template_name):
|
||||||
"""Use component's base template and the slots used for this instance to compile
|
"""Retrieve the requested template and add a link to this component to each SlotNode in the template."""
|
||||||
a unified template for this instance."""
|
|
||||||
|
|
||||||
component_template = get_template(template_name)
|
source_template = get_template(template_name)
|
||||||
slots_in_template = Component.slots_in_template(component_template)
|
|
||||||
|
|
||||||
defined_slot_names = set(slots_in_template.keys())
|
# The template may be shared with another component (e.g., due to caching). To ensure that each
|
||||||
filled_slot_names = set(self.slots.keys())
|
# SlotNode is unique between components, we have to copy the nodes in the template nodelist and
|
||||||
unexpected_slots = filled_slot_names - defined_slot_names
|
# any contained nodelists.
|
||||||
if unexpected_slots:
|
component_template = copy(source_template.template)
|
||||||
|
cloned_nodelist = [duplicate_node(node) for node in component_template.nodelist]
|
||||||
|
component_template.nodelist = NodeList(cloned_nodelist)
|
||||||
|
|
||||||
|
# Traverse template nodes and descendants, and give each slot node a reference to this component.
|
||||||
|
visited_nodes = set()
|
||||||
|
nodes_to_visit = list(component_template.nodelist)
|
||||||
|
slots_seen = set()
|
||||||
|
while nodes_to_visit:
|
||||||
|
current_node = nodes_to_visit.pop()
|
||||||
|
if current_node in visited_nodes:
|
||||||
|
continue
|
||||||
|
visited_nodes.add(current_node)
|
||||||
|
for nodelist_name in current_node.child_nodelists:
|
||||||
|
nodes_to_visit.extend(getattr(current_node, nodelist_name, []))
|
||||||
|
if self.is_slot_node(current_node):
|
||||||
|
slots_seen.add(current_node.name)
|
||||||
|
current_node.parent_component = self
|
||||||
|
|
||||||
|
# Check and warn for unknown slots
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
filled_slot_names = set(self.slots.keys())
|
||||||
|
unused_slots = filled_slot_names - slots_seen
|
||||||
|
if unused_slots:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"Component {} was provided with unexpected slots: {}".format(
|
"Component {} was provided with slots that were not used in a template: {}".format(
|
||||||
self._component_name, unexpected_slots
|
self._component_name, unused_slots
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
for unexpected_slot in unexpected_slots:
|
|
||||||
del self.slots[unexpected_slot]
|
|
||||||
|
|
||||||
combined_slots = dict(slots_in_template, **self.slots)
|
return component_template
|
||||||
if combined_slots:
|
|
||||||
# Replace slot nodes with their nodelists, then combine into a single, flat nodelist
|
|
||||||
node_iterator = ([node] if not Component.is_slot_node(node) else combined_slots[node.name]
|
|
||||||
for node in component_template.template.nodelist)
|
|
||||||
|
|
||||||
instance_template = copy(component_template.template)
|
|
||||||
instance_template.nodelist = NodeList(chain.from_iterable(node_iterator))
|
|
||||||
else:
|
|
||||||
instance_template = component_template.template
|
|
||||||
|
|
||||||
return instance_template
|
|
||||||
|
|
||||||
def render(self, context):
|
def render(self, context):
|
||||||
template_name = self.template(context)
|
template_name = self.template(context)
|
||||||
instance_template = self.compile_instance_template(template_name)
|
instance_template = self.get_processed_template(template_name)
|
||||||
return instance_template.render(context)
|
return instance_template.render(context)
|
||||||
|
|
||||||
class Media:
|
class Media:
|
||||||
|
@ -122,6 +132,25 @@ class Component(metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
# This variable represents the global component registry
|
# This variable represents the global component registry
|
||||||
registry = ComponentRegistry()
|
registry = ComponentRegistry()
|
||||||
|
|
||||||
|
|
||||||
|
def duplicate_node(source_node):
|
||||||
|
"""Perform a shallow copy of source_node and then recursively copy over each of source_node's nodelists.
|
||||||
|
|
||||||
|
If a nodelist is a dynamic property that cannot be set, fall back to a deepcopy of source_node."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
clone = copy(source_node)
|
||||||
|
for nodelist_name in source_node.child_nodelists:
|
||||||
|
nodelist = getattr(source_node, nodelist_name, NodeList())
|
||||||
|
nodelist_contents = [duplicate_node(n) for n in nodelist]
|
||||||
|
setattr(clone, nodelist_name, type(nodelist)(nodelist_contents))
|
||||||
|
return clone
|
||||||
|
except AttributeError:
|
||||||
|
# AttributeError is raised if an attribute cannot be set (e.g., IfNode's nodelist,
|
||||||
|
# which is a read-only property).
|
||||||
|
return deepcopy(source_node)
|
||||||
|
|
||||||
|
|
||||||
def register(name):
|
def register(name):
|
||||||
"""Class decorator to register a component.
|
"""Class decorator to register a component.
|
||||||
|
|
||||||
|
@ -133,6 +162,7 @@ def register(name):
|
||||||
...
|
...
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(component):
|
def decorator(component):
|
||||||
registry.register(name=name, component=component)
|
registry.register(name=name, component=component)
|
||||||
return component
|
return component
|
||||||
|
|
|
@ -11,7 +11,7 @@ from django_components.middleware import CSS_DEPENDENCY_PLACEHOLDER, JS_DEPENDEN
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
COMPONENT_CONTEXT_KEY = "component_context"
|
SLOT_CONTEXT_KEY = "__slot_context"
|
||||||
|
|
||||||
|
|
||||||
def get_components_from_registry(registry):
|
def get_components_from_registry(registry):
|
||||||
|
@ -79,14 +79,28 @@ def do_component(parser, token):
|
||||||
class SlotNode(Node):
|
class SlotNode(Node):
|
||||||
def __init__(self, name, nodelist, component=None):
|
def __init__(self, name, nodelist, component=None):
|
||||||
self.name, self.nodelist, self.component = name, nodelist, component
|
self.name, self.nodelist, self.component = name, nodelist, component
|
||||||
|
self.component = None
|
||||||
|
self.parent_component = None
|
||||||
|
self.context = None
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Slot Node: %s. Contents: %r>" % (self.name, self.nodelist)
|
return "<Slot Node: %s. Contents: %r>" % (self.name, self.nodelist)
|
||||||
|
|
||||||
def render(self, context):
|
def render(self, context):
|
||||||
# This method should only be called if a slot tag is used outside of a component.
|
# Thread safety: storing the context as a property of the cloned SlotNode without using
|
||||||
assert self.component is None
|
# the render_context facility should be thread-safe, since each cloned_node
|
||||||
return self.nodelist.render(context)
|
# is only used for a single render.
|
||||||
|
cloned_node = SlotNode(self.name, self.nodelist, self.component)
|
||||||
|
cloned_node.parent_component = self.parent_component
|
||||||
|
cloned_node.context = context
|
||||||
|
|
||||||
|
assert not NodeList(), "Logic in SlotNode.render method assumes that empty nodelists are falsy."
|
||||||
|
with context.update({'slot': cloned_node}):
|
||||||
|
return cloned_node.parent_component.slots.get(cloned_node.name, cloned_node.nodelist).render(context)
|
||||||
|
|
||||||
|
def super(self):
|
||||||
|
"""Render default slot content."""
|
||||||
|
return mark_safe(self.nodelist.render(self.context))
|
||||||
|
|
||||||
|
|
||||||
@register.tag("slot")
|
@register.tag("slot")
|
||||||
|
@ -103,19 +117,24 @@ def do_slot(parser, token, component=None):
|
||||||
|
|
||||||
|
|
||||||
class ComponentNode(Node):
|
class ComponentNode(Node):
|
||||||
|
class InvalidSlot:
|
||||||
|
def super(self):
|
||||||
|
raise TemplateSyntaxError('slot.super may only be called within a {% slot %}/{% endslot %} block.')
|
||||||
|
|
||||||
def __init__(self, component, context_args, context_kwargs, slots=None, isolated_context=False):
|
def __init__(self, component, context_args, context_kwargs, slots=None, isolated_context=False):
|
||||||
self.context_args = context_args or []
|
self.context_args = context_args or []
|
||||||
self.context_kwargs = context_kwargs or {}
|
self.context_kwargs = context_kwargs or {}
|
||||||
self.component, self.isolated_context = component, isolated_context
|
self.component, self.isolated_context = component, isolated_context
|
||||||
slot_dict = defaultdict(NodeList)
|
|
||||||
if slots:
|
# Group slot notes by name and concatenate their nodelists
|
||||||
for slot in slots:
|
self.component.slots = defaultdict(NodeList)
|
||||||
slot_dict[slot.name].extend(slot.nodelist)
|
for slot in slots or []:
|
||||||
self.component.slots = slot_dict
|
self.component.slots[slot.name].extend(slot.nodelist)
|
||||||
self.should_render_dependencies = is_dependency_middleware_active()
|
self.should_render_dependencies = is_dependency_middleware_active()
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Component Node: %s. Contents: %r>" % (self.component, self.component.instance_template.nodelist)
|
return "<Component Node: %s. Contents: %r>" % (self.component,
|
||||||
|
getattr(self.component.instance_template, 'nodelist', None))
|
||||||
|
|
||||||
def render(self, context):
|
def render(self, context):
|
||||||
self.component.outer_context = context.flatten()
|
self.component.outer_context = context.flatten()
|
||||||
|
@ -123,9 +142,7 @@ class ComponentNode(Node):
|
||||||
# Resolve FilterExpressions and Variables that were passed as args to the component, then call component's
|
# Resolve FilterExpressions and Variables that were passed as args to the component, then call component's
|
||||||
# context method to get values to insert into the context
|
# context method to get values to insert into the context
|
||||||
resolved_context_args = [safe_resolve(arg, context) for arg in self.context_args]
|
resolved_context_args = [safe_resolve(arg, context) for arg in self.context_args]
|
||||||
resolved_context_kwargs = {
|
resolved_context_kwargs = {key: safe_resolve(kwarg, context) for key, kwarg in self.context_kwargs.items()}
|
||||||
key: safe_resolve(kwarg, context) for key, kwarg in self.context_kwargs.items()
|
|
||||||
}
|
|
||||||
component_context = self.component.context(*resolved_context_args, **resolved_context_kwargs)
|
component_context = self.component.context(*resolved_context_args, **resolved_context_kwargs)
|
||||||
|
|
||||||
# Create a fresh context if requested
|
# Create a fresh context if requested
|
||||||
|
@ -133,6 +150,7 @@ class ComponentNode(Node):
|
||||||
context = context.new()
|
context = context.new()
|
||||||
|
|
||||||
with context.update(component_context):
|
with context.update(component_context):
|
||||||
|
context.render_context[SLOT_CONTEXT_KEY] = {}
|
||||||
rendered_component = self.component.render(context)
|
rendered_component = self.component.render(context)
|
||||||
if self.should_render_dependencies:
|
if self.should_render_dependencies:
|
||||||
return f'<!-- _RENDERED {self.component._component_name} -->' + rendered_component
|
return f'<!-- _RENDERED {self.component._component_name} -->' + rendered_component
|
||||||
|
@ -158,31 +176,36 @@ def do_component_block(parser, token):
|
||||||
bits = token.split_contents()
|
bits = token.split_contents()
|
||||||
bits, isolated_context = check_for_isolated_context_keyword(bits)
|
bits, isolated_context = check_for_isolated_context_keyword(bits)
|
||||||
|
|
||||||
tag_name, token = next_block_token(parser)
|
|
||||||
component, context_args, context_kwargs = parse_component_with_args(parser, bits, 'component_block')
|
component, context_args, context_kwargs = parse_component_with_args(parser, bits, 'component_block')
|
||||||
|
|
||||||
slots_filled = NodeList()
|
return ComponentNode(component, context_args, context_kwargs,
|
||||||
while tag_name != "endcomponent_block":
|
slots=[do_slot(parser, slot_token, component=component)
|
||||||
if tag_name == "slot":
|
for slot_token in slot_tokens(parser)],
|
||||||
slots_filled += do_slot(parser, token, component=component)
|
|
||||||
tag_name, token = next_block_token(parser)
|
|
||||||
|
|
||||||
return ComponentNode(component, context_args, context_kwargs, slots=slots_filled,
|
|
||||||
isolated_context=isolated_context)
|
isolated_context=isolated_context)
|
||||||
|
|
||||||
|
|
||||||
def next_block_token(parser):
|
def slot_tokens(parser):
|
||||||
"""Return tag and token for next block token.
|
"""Yield each 'slot' token appearing before the next 'endcomponent_block' token.
|
||||||
|
|
||||||
Raises IndexError if there are not more block tokens in the remainder of the template."""
|
Raises TemplateSyntaxError if there are other content tokens or if there is no endcomponent_block token."""
|
||||||
|
|
||||||
|
def is_whitespace(token):
|
||||||
|
return token.token_type == TokenType.TEXT and not token.contents.strip()
|
||||||
|
|
||||||
|
def is_block_tag(token, /, name):
|
||||||
|
return token.token_type == TokenType.BLOCK and token.split_contents()[0] == name
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
try:
|
||||||
token = parser.next_token()
|
token = parser.next_token()
|
||||||
if token.token_type != TokenType.BLOCK:
|
except IndexError:
|
||||||
continue
|
raise TemplateSyntaxError('Unclosed component_block tag')
|
||||||
|
if is_block_tag(token, name='endcomponent_block'):
|
||||||
tag_name = token.split_contents()[0]
|
return
|
||||||
return tag_name, token
|
elif is_block_tag(token, name='slot'):
|
||||||
|
yield token
|
||||||
|
elif not is_whitespace(token) and token.token_type != TokenType.COMMENT:
|
||||||
|
raise TemplateSyntaxError(f'Content tokens in component blocks must be inside of slot tags: {token}')
|
||||||
|
|
||||||
|
|
||||||
def check_for_isolated_context_keyword(bits):
|
def check_for_isolated_context_keyword(bits):
|
||||||
|
|
6
tests/templates/conditional_template.html
Normal file
6
tests/templates/conditional_template.html
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{% load component_tags %}
|
||||||
|
{% if branch == 'a' %}
|
||||||
|
<p id="a">{% slot 'a' %}Default A{% endslot %}</p>
|
||||||
|
{% elif branch == 'b' %}
|
||||||
|
<p id="b">{% slot 'b' %}Default B{% endslot %}</p>
|
||||||
|
{% endif %}
|
4
tests/templates/nested_slot_template.html
Normal file
4
tests/templates/nested_slot_template.html
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{% load component_tags %}
|
||||||
|
{% slot 'outer' %}
|
||||||
|
<div id="outer">{% slot 'inner' %}Default{% endslot %}</div>
|
||||||
|
{% endslot %}
|
|
@ -1,11 +1,11 @@
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
|
||||||
from django.template import Context
|
from django.template import Context, Template
|
||||||
|
|
||||||
from .django_test_setup import * # NOQA
|
from .django_test_setup import * # NOQA
|
||||||
from .testutils import Django30CompatibleSimpleTestCase as SimpleTestCase
|
|
||||||
|
|
||||||
from django_components import component
|
from django_components import component
|
||||||
|
from .testutils import Django30CompatibleSimpleTestCase as SimpleTestCase
|
||||||
|
|
||||||
|
|
||||||
class ComponentTest(SimpleTestCase):
|
class ComponentTest(SimpleTestCase):
|
||||||
|
@ -98,6 +98,8 @@ class ComponentTest(SimpleTestCase):
|
||||||
""")
|
""")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentMediaTests(SimpleTestCase):
|
||||||
def test_component_media_with_strings(self):
|
def test_component_media_with_strings(self):
|
||||||
class SimpleComponent(component.Component):
|
class SimpleComponent(component.Component):
|
||||||
class Media:
|
class Media:
|
||||||
|
@ -164,3 +166,52 @@ class ComponentTest(SimpleTestCase):
|
||||||
<script src="path/to/script.js"></script>
|
<script src="path/to/script.js"></script>
|
||||||
""")
|
""")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentIsolationTests(SimpleTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
class SlottedComponent(component.Component):
|
||||||
|
def template(self, context):
|
||||||
|
return "slotted_template.html"
|
||||||
|
|
||||||
|
component.registry.register('test', SlottedComponent)
|
||||||
|
|
||||||
|
def test_instances_of_component_do_not_share_slots(self):
|
||||||
|
template = Template(
|
||||||
|
"""
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component_block "test" %}
|
||||||
|
{% slot "header" %}Override header{% endslot %}
|
||||||
|
{% endcomponent_block %}
|
||||||
|
{% component_block "test" %}
|
||||||
|
{% slot "main" %}Override main{% endslot %}
|
||||||
|
{% endcomponent_block %}
|
||||||
|
{% component_block "test" %}
|
||||||
|
{% slot "footer" %}Override footer{% endslot %}
|
||||||
|
{% endcomponent_block %}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
template.render(Context({}))
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
<custom-template>
|
||||||
|
<header>Override header</header>
|
||||||
|
<main>Default main</main>
|
||||||
|
<footer>Default footer</footer>
|
||||||
|
</custom-template>
|
||||||
|
<custom-template>
|
||||||
|
<header>Default header</header>
|
||||||
|
<main>Override main</main>
|
||||||
|
<footer>Default footer</footer>
|
||||||
|
</custom-template>
|
||||||
|
<custom-template>
|
||||||
|
<header>Default header</header>
|
||||||
|
<main>Default main</main>
|
||||||
|
<footer>Override footer</footer>
|
||||||
|
</custom-template>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
|
||||||
from django.template import Context, Template
|
from django.template import Context, Template, TemplateSyntaxError
|
||||||
|
|
||||||
from .django_test_setup import * # NOQA
|
from .django_test_setup import * # NOQA
|
||||||
from django_components import component
|
from django_components import component
|
||||||
|
@ -356,3 +356,281 @@ class TemplateInstrumentationTest(SimpleTestCase):
|
||||||
templates_used = self.templates_used_to_render(template)
|
templates_used = self.templates_used_to_render(template)
|
||||||
self.assertIn('slotted_template.html', templates_used)
|
self.assertIn('slotted_template.html', templates_used)
|
||||||
self.assertIn('simple_template.html', templates_used)
|
self.assertIn('simple_template.html', templates_used)
|
||||||
|
|
||||||
|
|
||||||
|
class NestedSlotTests(SimpleTestCase):
|
||||||
|
class NestedComponent(component.Component):
|
||||||
|
def context(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def template(self, context):
|
||||||
|
return "nested_slot_template.html"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
component.registry.clear()
|
||||||
|
component.registry.register('test', cls.NestedComponent)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
super().tearDownClass()
|
||||||
|
component.registry.clear()
|
||||||
|
|
||||||
|
def test_default_slots_render_correctly(self):
|
||||||
|
template = Template(
|
||||||
|
"""
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component_block 'test' %}{% endcomponent_block %}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
self.assertHTMLEqual(rendered, '<div id="outer">Default</div>')
|
||||||
|
|
||||||
|
def test_inner_slot_overriden(self):
|
||||||
|
template = Template(
|
||||||
|
"""
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component_block 'test' %}{% slot 'inner' %}Override{% endslot %}{% endcomponent_block %}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
self.assertHTMLEqual(rendered, '<div id="outer">Override</div>')
|
||||||
|
|
||||||
|
def test_outer_slot_overriden(self):
|
||||||
|
template = Template(
|
||||||
|
"""
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component_block 'test' %}{% slot 'outer' %}<p>Override</p>{% endslot %}{% endcomponent_block %}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
self.assertHTMLEqual(rendered, '<p>Override</p>')
|
||||||
|
|
||||||
|
def test_both_overriden_and_inner_removed(self):
|
||||||
|
template = Template(
|
||||||
|
"""
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component_block 'test' %}
|
||||||
|
{% slot 'outer' %}<p>Override</p>{% endslot %}
|
||||||
|
{% slot 'inner' %}<p>Will not appear</p>{% endslot %}
|
||||||
|
{% endcomponent_block %}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
self.assertHTMLEqual(rendered, '<p>Override</p>')
|
||||||
|
|
||||||
|
|
||||||
|
class ConditionalSlotTests(SimpleTestCase):
|
||||||
|
class ConditionalComponent(component.Component):
|
||||||
|
def context(self, branch=None):
|
||||||
|
return {'branch': branch}
|
||||||
|
|
||||||
|
def template(self, context):
|
||||||
|
return "conditional_template.html"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
component.registry.clear()
|
||||||
|
component.registry.register('test', cls.ConditionalComponent)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
super().tearDownClass()
|
||||||
|
component.registry.clear()
|
||||||
|
|
||||||
|
def test_no_content_if_branches_are_false(self):
|
||||||
|
template = Template(
|
||||||
|
"""
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component_block 'test' %}
|
||||||
|
{% slot 'a' %}Override A{% endslot %}
|
||||||
|
{% slot 'b' %}Override B{% endslot %}
|
||||||
|
{% endcomponent_block %}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
self.assertHTMLEqual(rendered, '')
|
||||||
|
|
||||||
|
def test_default_content_if_no_slots(self):
|
||||||
|
template = Template(
|
||||||
|
"""
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component 'test' branch='a' %}
|
||||||
|
{% component 'test' branch='b' %}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
self.assertHTMLEqual(rendered, '<p id="a">Default A</p><p id="b">Default B</p>')
|
||||||
|
|
||||||
|
def test_one_slot_overridden(self):
|
||||||
|
template = Template(
|
||||||
|
"""
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component_block 'test' branch='a' %}
|
||||||
|
{% slot 'b' %}Override B{% endslot %}
|
||||||
|
{% endcomponent_block %}
|
||||||
|
{% component_block 'test' branch='b' %}
|
||||||
|
{% slot 'b' %}Override B{% endslot %}
|
||||||
|
{% endcomponent_block %}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
self.assertHTMLEqual(rendered, '<p id="a">Default A</p><p id="b">Override B</p>')
|
||||||
|
|
||||||
|
def test_both_slots_overridden(self):
|
||||||
|
template = Template(
|
||||||
|
"""
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component_block 'test' branch='a' %}
|
||||||
|
{% slot 'a' %}Override A{% endslot %}
|
||||||
|
{% slot 'b' %}Override B{% endslot %}
|
||||||
|
{% endcomponent_block %}
|
||||||
|
{% component_block 'test' branch='b' %}
|
||||||
|
{% slot 'a' %}Override A{% endslot %}
|
||||||
|
{% slot 'b' %}Override B{% endslot %}
|
||||||
|
{% endcomponent_block %}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
self.assertHTMLEqual(rendered, '<p id="a">Override A</p><p id="b">Override B</p>')
|
||||||
|
|
||||||
|
|
||||||
|
class SlotSuperTests(SimpleTestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
component.registry.clear()
|
||||||
|
component.registry.register('test', SlottedComponent)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
super().tearDownClass()
|
||||||
|
component.registry.clear()
|
||||||
|
|
||||||
|
def test_basic_super_functionality(self):
|
||||||
|
template = Template(
|
||||||
|
"""
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component_block "test" %}
|
||||||
|
{% slot "header" %}Before: {{ slot.super }}{% endslot %}
|
||||||
|
{% slot "main" %}{{ slot.super }}{% endslot %}
|
||||||
|
{% slot "footer" %}{{ slot.super }}, after{% endslot %}
|
||||||
|
{% endcomponent_block %}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
<custom-template>
|
||||||
|
<header>Before: Default header</header>
|
||||||
|
<main>Default main</main>
|
||||||
|
<footer>Default footer, after</footer>
|
||||||
|
</custom-template>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_multiple_super_calls(self):
|
||||||
|
template = Template(
|
||||||
|
"""
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component_block "test" %}
|
||||||
|
{% slot "header" %}First: {{ slot.super }}; Second: {{ slot.super }}{% endslot %}
|
||||||
|
{% endcomponent_block %}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
<custom-template>
|
||||||
|
<header>First: Default header; Second: Default header</header>
|
||||||
|
<main>Default main</main>
|
||||||
|
<footer>Default footer</footer>
|
||||||
|
</custom-template>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_super_under_if_node(self):
|
||||||
|
template = Template(
|
||||||
|
"""
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component_block "test" %}
|
||||||
|
{% slot "header" %}
|
||||||
|
{% for i in range %}
|
||||||
|
{% if forloop.first %}First {{slot.super}}
|
||||||
|
{% else %}Later {{ slot.super }}
|
||||||
|
{% endif %}
|
||||||
|
{%endfor %}
|
||||||
|
{% endslot %}
|
||||||
|
{% endcomponent_block %}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
rendered = template.render(Context({'range': range(3)}))
|
||||||
|
|
||||||
|
self.assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
<custom-template>
|
||||||
|
<header>First Default header Later Default header Later Default header</header>
|
||||||
|
<main>Default main</main>
|
||||||
|
<footer>Default footer</footer>
|
||||||
|
</custom-template>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateSyntaxErrorTests(SimpleTestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
component.registry.register('test', SlottedComponent)
|
||||||
|
|
||||||
|
def test_variable_outside_slot_tag_is_error(self):
|
||||||
|
with self.assertRaises(TemplateSyntaxError):
|
||||||
|
Template(
|
||||||
|
"""
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component_block "test" %}
|
||||||
|
{{ slot.super }}
|
||||||
|
{% endcomponent_block %}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_text_outside_slot_tag_is_error(self):
|
||||||
|
with self.assertRaises(TemplateSyntaxError):
|
||||||
|
Template(
|
||||||
|
"""
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component_block "test" %}
|
||||||
|
Text
|
||||||
|
{% endcomponent_block %}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_nonslot_block_outside_slot_tag_is_error(self):
|
||||||
|
with self.assertRaises(TemplateSyntaxError):
|
||||||
|
Template(
|
||||||
|
"""
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component_block "test" %}
|
||||||
|
{% if True %}
|
||||||
|
{% slot "header" %}{% endslot %}
|
||||||
|
{% endif %}
|
||||||
|
{% endcomponent_block %}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_unclosed_component_block_is_error(self):
|
||||||
|
with self.assertRaises(TemplateSyntaxError):
|
||||||
|
Template(
|
||||||
|
"""
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component_block "test" %}
|
||||||
|
{% slot "header" %}{% endslot %}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue