mirror of
https://github.com/django-components/django-components.git
synced 2025-07-24 08:43:43 +00:00
Rework of context handling (#18)
Co-authored-by: rbeard0330 <@dul2k3BKW6m>
This commit is contained in:
parent
88fe2fc3b7
commit
93b8a7404a
9 changed files with 305 additions and 59 deletions
|
@ -1,5 +1,4 @@
|
|||
from django.forms.widgets import MediaDefiningClass
|
||||
from django.template import Context
|
||||
from django.template.base import NodeList, TextNode
|
||||
from django.template.loader import get_template
|
||||
from django.utils.safestring import mark_safe
|
||||
|
@ -8,12 +7,6 @@ from six import with_metaclass
|
|||
# Allow "component.AlreadyRegistered" instead of having to import these everywhere
|
||||
from django_components.component_registry import AlreadyRegistered, ComponentRegistry, NotRegistered # noqa
|
||||
|
||||
# Python 2 compatibility
|
||||
try:
|
||||
from inspect import getfullargspec
|
||||
except ImportError:
|
||||
from inspect import getargspec as getfullargspec
|
||||
|
||||
# Django < 2.1 compatibility
|
||||
try:
|
||||
from django.template.base import TokenType
|
||||
|
@ -49,52 +42,40 @@ class Component(with_metaclass(MediaDefiningClass)):
|
|||
return mark_safe("\n".join(self.media.render_js()))
|
||||
|
||||
def slots_in_template(self, template):
|
||||
nodelist = NodeList()
|
||||
for node in template.template.nodelist:
|
||||
if (
|
||||
node.token.token_type == TokenType.BLOCK
|
||||
and node.token.split_contents()[0] == "slot"
|
||||
):
|
||||
nodelist.append(node)
|
||||
return NodeList(node for node in template.template.nodelist if is_slot_node(node))
|
||||
|
||||
return nodelist
|
||||
|
||||
def render(self, slots_filled=None, *args, **kwargs):
|
||||
def render(self, context, slots_filled=None):
|
||||
slots_filled = slots_filled or []
|
||||
context_args_variables = getfullargspec(self.context).args[1:]
|
||||
context_args = {
|
||||
key: kwargs[key] for key in context_args_variables if key in kwargs
|
||||
}
|
||||
context = self.context(**context_args)
|
||||
template = get_template(self.template(context))
|
||||
slots_in_template = self.slots_in_template(template)
|
||||
|
||||
if slots_in_template:
|
||||
valid_slot_names = set([slot.name for slot in slots_in_template])
|
||||
# If there are no slots, then we can simply render the template
|
||||
if not slots_in_template:
|
||||
return template.template.render(context)
|
||||
|
||||
# Otherwise, we need to assemble and render a nodelist containing the nodes from the template, slots that were
|
||||
# provided when the component was called (previously rendered by the component's render method) and the
|
||||
# unrendered default slots
|
||||
nodelist = NodeList()
|
||||
for node in template.template.nodelist:
|
||||
if (
|
||||
node.token.token_type == TokenType.BLOCK
|
||||
and node.token.split_contents()[0] == "slot"
|
||||
):
|
||||
if node.name in valid_slot_names and node.name in slots_filled:
|
||||
if is_slot_node(node):
|
||||
if node.name in slots_filled:
|
||||
nodelist.append(TextNode(slots_filled[node.name]))
|
||||
else:
|
||||
for node in node.nodelist:
|
||||
nodelist.append(node)
|
||||
nodelist.extend(node.nodelist)
|
||||
else:
|
||||
nodelist.append(node)
|
||||
|
||||
render_context = Context(context)
|
||||
with render_context.bind_template(template.template):
|
||||
return nodelist.render(render_context)
|
||||
|
||||
return template.render(context)
|
||||
return nodelist.render(context)
|
||||
|
||||
class Media:
|
||||
css = {}
|
||||
js = []
|
||||
|
||||
|
||||
def is_slot_node(node):
|
||||
return node.token.token_type == TokenType.BLOCK and node.token.split_contents()[0] == "slot"
|
||||
|
||||
|
||||
# This variable represents the global component registry
|
||||
registry = ComponentRegistry()
|
||||
|
|
|
@ -88,7 +88,8 @@ def component_js_dependencies_tag():
|
|||
def component_tag(name, *args, **kwargs):
|
||||
component_class = registry.get(name)
|
||||
component = component_class()
|
||||
return component.render(*args, **kwargs)
|
||||
context = template.Context(component.context(*args, **kwargs))
|
||||
return component.render(context)
|
||||
|
||||
|
||||
class SlotNode(Node):
|
||||
|
@ -129,27 +130,30 @@ def do_slot(parser, token, component=None):
|
|||
|
||||
|
||||
class ComponentNode(Node):
|
||||
def __init__(self, component, extra_context, slots):
|
||||
extra_context = extra_context or {}
|
||||
self.component, self.extra_context, self.slots = component, extra_context, slots
|
||||
def __init__(self, component, context_kwargs, slots):
|
||||
self.context_kwargs = context_kwargs or {}
|
||||
self.component, self.slots = component, slots
|
||||
|
||||
def __repr__(self):
|
||||
return "<Component Node: %s. Contents: %r>" % (self.component, self.slots)
|
||||
|
||||
def render(self, context):
|
||||
extra_context = {
|
||||
# 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
|
||||
resolved_context_kwargs = {
|
||||
key: context_item.resolve(context) if hasattr(context_item, 'resolve') else context_item
|
||||
for key, context_item in self.extra_context.items()
|
||||
for key, context_item in self.context_kwargs.items()
|
||||
}
|
||||
context.update(extra_context)
|
||||
extra_context = self.component.context(**resolved_context_kwargs)
|
||||
|
||||
with context.update(extra_context):
|
||||
self.slots.render(context)
|
||||
|
||||
if COMPONENT_CONTEXT_KEY in context.render_context:
|
||||
slots_filled = context.render_context[COMPONENT_CONTEXT_KEY][self.component]
|
||||
return self.component.render(slots_filled=slots_filled, **context.flatten())
|
||||
else:
|
||||
slots_filled = []
|
||||
|
||||
return self.component.render(**extra_context)
|
||||
return self.component.render(context, slots_filled=slots_filled)
|
||||
|
||||
|
||||
@register.tag("component_block")
|
||||
|
@ -187,9 +191,9 @@ def do_component(parser, token):
|
|||
component_class = registry.get(component_name)
|
||||
component = component_class()
|
||||
|
||||
extra_context = {}
|
||||
context_kwargs = {}
|
||||
if len(bits) > 2:
|
||||
extra_context = component.context(**token_kwargs(bits[2:], parser))
|
||||
context_kwargs = token_kwargs(bits[2:], parser)
|
||||
|
||||
slots_filled = NodeList()
|
||||
tag_name = bits[0]
|
||||
|
@ -205,4 +209,4 @@ def do_component(parser, token):
|
|||
elif tag_name == "endcomponent_block":
|
||||
break
|
||||
|
||||
return ComponentNode(component, extra_context, slots_filled)
|
||||
return ComponentNode(component, context_kwargs, slots_filled)
|
||||
|
|
0
tests/templates/child_template.html
Normal file
0
tests/templates/child_template.html
Normal file
2
tests/templates/incrementer.html
Normal file
2
tests/templates/incrementer.html
Normal file
|
@ -0,0 +1,2 @@
|
|||
{% load component_tags %}<p class="incrementer">value={{ value }};calls={{ calls }}</p>
|
||||
{% slot 'content' %}{% endslot %}
|
12
tests/templates/parent_template.html
Normal file
12
tests/templates/parent_template.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
{% load component_tags %}
|
||||
|
||||
<div>
|
||||
<h1>Parent content</h1>
|
||||
{% component name="variable_display" shadowing_variable='override' new_variable='unique_val' %}
|
||||
</div>
|
||||
<div>
|
||||
{% slot 'content' %}
|
||||
<h2>Slot content</h2>
|
||||
{% component name="variable_display" shadowing_variable='slot_default_override' new_variable='slot_default_unique' %}
|
||||
{% endslot %}
|
||||
</div>
|
12
tests/templates/parent_with_args_template.html
Normal file
12
tests/templates/parent_with_args_template.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
{% load component_tags %}
|
||||
|
||||
<div>
|
||||
<h1>Parent content</h1>
|
||||
{% component name="variable_display" shadowing_variable=inner_parent_value new_variable='unique_val' %}
|
||||
</div>
|
||||
<div>
|
||||
{% slot 'content' %}
|
||||
<h2>Slot content</h2>
|
||||
{% component name="variable_display" shadowing_variable='slot_default_override' new_variable=inner_parent_value %}
|
||||
{% endslot %}
|
||||
</div>
|
4
tests/templates/variable_display.html
Normal file
4
tests/templates/variable_display.html
Normal file
|
@ -0,0 +1,4 @@
|
|||
{% load component_tags %}
|
||||
|
||||
<h1>Shadowing variable = {{ shadowing_variable }}</h1>
|
||||
<h1>Uniquely named variable = {{ unique_variable }}</h1>
|
|
@ -1,5 +1,7 @@
|
|||
from textwrap import dedent
|
||||
|
||||
from django.template import Context
|
||||
|
||||
from django_components import component
|
||||
|
||||
from .django_test_setup import * # NOQA
|
||||
|
@ -29,13 +31,14 @@ class ComponentRegistryTest(SimpleTestCase):
|
|||
js = ["script.js"]
|
||||
|
||||
comp = SimpleComponent()
|
||||
context = Context(comp.context(variable="test"))
|
||||
|
||||
self.assertHTMLEqual(comp.render_dependencies(), dedent("""
|
||||
<link href="style.css" type="text/css" media="all" rel="stylesheet">
|
||||
<script src="script.js"></script>
|
||||
""").strip())
|
||||
|
||||
self.assertHTMLEqual(comp.render(variable="test"), dedent("""
|
||||
self.assertHTMLEqual(comp.render(context), dedent("""
|
||||
Variable: <strong>test</strong>
|
||||
""").lstrip())
|
||||
|
||||
|
@ -66,8 +69,9 @@ class ComponentRegistryTest(SimpleTestCase):
|
|||
return "filtered_template.html"
|
||||
|
||||
comp = FilteredComponent()
|
||||
context = Context(comp.context(var1="test1", var2="test2"))
|
||||
|
||||
self.assertHTMLEqual(comp.render(var1="test1", var2="test2"), dedent("""
|
||||
self.assertHTMLEqual(comp.render(context), dedent("""
|
||||
Var1: <strong>test1</strong>
|
||||
Var2 (uppercased): <strong>TEST2</strong>
|
||||
""").lstrip())
|
||||
|
|
227
tests/test_context.py
Normal file
227
tests/test_context.py
Normal file
|
@ -0,0 +1,227 @@
|
|||
from django.template import Context, Template
|
||||
|
||||
from django_components import component
|
||||
|
||||
from .django_test_setup import * # NOQA
|
||||
from .testutils import Django111CompatibleSimpleTestCase as SimpleTestCase
|
||||
|
||||
|
||||
class ParentComponent(component.Component):
|
||||
def context(self):
|
||||
return {
|
||||
"shadowing_variable": 'NOT SHADOWED'
|
||||
}
|
||||
|
||||
def template(self, context):
|
||||
return "parent_template.html"
|
||||
|
||||
|
||||
class ParentComponentWithArgs(component.Component):
|
||||
def context(self, parent_value):
|
||||
return {
|
||||
"inner_parent_value": parent_value
|
||||
}
|
||||
|
||||
def template(self, context):
|
||||
return "parent_with_args_template.html"
|
||||
|
||||
|
||||
class VariableDisplay(component.Component):
|
||||
def context(self, shadowing_variable, new_variable):
|
||||
return {
|
||||
"shadowing_variable": shadowing_variable,
|
||||
"unique_variable": new_variable
|
||||
}
|
||||
|
||||
def template(self, context):
|
||||
return "variable_display.html"
|
||||
|
||||
|
||||
class IncrementerComponent(component.Component):
|
||||
def context(self, value=0):
|
||||
value = int(value)
|
||||
if hasattr(self, 'call_count'):
|
||||
self.call_count += 1
|
||||
else:
|
||||
self.call_count = 1
|
||||
return {
|
||||
"value": value + 1,
|
||||
"calls": self.call_count
|
||||
}
|
||||
|
||||
def template(self, context):
|
||||
return "incrementer.html"
|
||||
|
||||
|
||||
component.registry.register(name='parent_component', component=ParentComponent)
|
||||
component.registry.register(name='parent_with_args', component=ParentComponentWithArgs)
|
||||
component.registry.register(name='variable_display', component=VariableDisplay)
|
||||
component.registry.register(name='incrementer', component=IncrementerComponent)
|
||||
|
||||
|
||||
class ContextTests(SimpleTestCase):
|
||||
def test_nested_component_context_shadows_parent_with_unfilled_slots_and_component_tag(self):
|
||||
template = Template("{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component 'parent_component' %}")
|
||||
rendered = template.render(Context())
|
||||
|
||||
self.assertIn('<h1>Shadowing variable = override</h1>', rendered, rendered)
|
||||
self.assertIn('<h1>Shadowing variable = slot_default_override</h1>', rendered, rendered)
|
||||
self.assertNotIn('<h1>Shadowing variable = NOT SHADOWED</h1>', rendered, rendered)
|
||||
|
||||
def test_nested_component_instances_have_unique_context_with_unfilled_slots_and_component_tag(self):
|
||||
template = Template("{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component name='parent_component' %}")
|
||||
rendered = template.render(Context())
|
||||
|
||||
self.assertIn('<h1>Uniquely named variable = unique_val</h1>', rendered, rendered)
|
||||
self.assertIn('<h1>Uniquely named variable = slot_default_unique</h1>', rendered, rendered)
|
||||
|
||||
def test_nested_component_context_shadows_parent_with_unfilled_slots_and_component_block_tag(self):
|
||||
template = Template("{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component_block 'parent_component' %}{% endcomponent_block %}")
|
||||
rendered = template.render(Context())
|
||||
|
||||
self.assertIn('<h1>Shadowing variable = override</h1>', rendered, rendered)
|
||||
self.assertIn('<h1>Shadowing variable = slot_default_override</h1>', rendered, rendered)
|
||||
self.assertNotIn('<h1>Shadowing variable = NOT SHADOWED</h1>', rendered, rendered)
|
||||
|
||||
def test_nested_component_instances_have_unique_context_with_unfilled_slots_and_component_block_tag(self):
|
||||
template = Template("{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component_block 'parent_component' %}{% endcomponent_block %}")
|
||||
rendered = template.render(Context())
|
||||
|
||||
self.assertIn('<h1>Uniquely named variable = unique_val</h1>', rendered, rendered)
|
||||
self.assertIn('<h1>Uniquely named variable = slot_default_unique</h1>', rendered, rendered)
|
||||
|
||||
def test_nested_component_context_shadows_parent_with_filled_slots(self):
|
||||
template = Template("{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component_block 'parent_component' %}"
|
||||
"{% slot 'content' %}{% component name='variable_display' "
|
||||
"shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}{% endslot %}"
|
||||
"{% endcomponent_block %}")
|
||||
rendered = template.render(Context())
|
||||
|
||||
self.assertIn('<h1>Shadowing variable = override</h1>', rendered, rendered)
|
||||
self.assertIn('<h1>Shadowing variable = shadow_from_slot</h1>', rendered, rendered)
|
||||
self.assertNotIn('<h1>Shadowing variable = NOT SHADOWED</h1>', rendered, rendered)
|
||||
|
||||
def test_nested_component_instances_have_unique_context_with_filled_slots(self):
|
||||
template = Template("{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component_block 'parent_component' %}"
|
||||
"{% slot 'content' %}{% component name='variable_display' "
|
||||
"shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}{% endslot %}"
|
||||
"{% endcomponent_block %}")
|
||||
rendered = template.render(Context())
|
||||
|
||||
self.assertIn('<h1>Uniquely named variable = unique_val</h1>', rendered, rendered)
|
||||
self.assertIn('<h1>Uniquely named variable = unique_from_slot</h1>', rendered, rendered)
|
||||
|
||||
def test_nested_component_context_shadows_outer_context_with_unfilled_slots_and_component_tag(self):
|
||||
template = Template("{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component name='parent_component' %}")
|
||||
rendered = template.render(Context({'shadowing_variable': 'NOT SHADOWED'}))
|
||||
|
||||
self.assertIn('<h1>Shadowing variable = override</h1>', rendered, rendered)
|
||||
self.assertIn('<h1>Shadowing variable = slot_default_override</h1>', rendered, rendered)
|
||||
self.assertNotIn('<h1>Shadowing variable = NOT SHADOWED</h1>', rendered, rendered)
|
||||
|
||||
def test_nested_component_context_shadows_outer_context_with_unfilled_slots_and_component_block_tag(self):
|
||||
template = Template("{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component_block 'parent_component' %}{% endcomponent_block %}")
|
||||
rendered = template.render(Context({'shadowing_variable': 'NOT SHADOWED'}))
|
||||
|
||||
self.assertIn('<h1>Shadowing variable = override</h1>', rendered, rendered)
|
||||
self.assertIn('<h1>Shadowing variable = slot_default_override</h1>', rendered, rendered)
|
||||
self.assertNotIn('<h1>Shadowing variable = NOT SHADOWED</h1>', rendered, rendered)
|
||||
|
||||
def test_nested_component_context_shadows_outer_context_with_filled_slots(self):
|
||||
template = Template("{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component_block 'parent_component' %}"
|
||||
"{% slot 'content' %}{% component name='variable_display' "
|
||||
"shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}{% endslot %}"
|
||||
"{% endcomponent_block %}")
|
||||
rendered = template.render(Context({'shadowing_variable': 'NOT SHADOWED'}))
|
||||
|
||||
self.assertIn('<h1>Shadowing variable = override</h1>', rendered, rendered)
|
||||
self.assertIn('<h1>Shadowing variable = shadow_from_slot</h1>', rendered, rendered)
|
||||
self.assertNotIn('<h1>Shadowing variable = NOT SHADOWED</h1>', rendered, rendered)
|
||||
|
||||
|
||||
class ParentArgsTests(SimpleTestCase):
|
||||
def test_parent_args_can_be_drawn_from_context(self):
|
||||
template = Template("{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component_block 'parent_with_args' parent_value=parent_value %}"
|
||||
"{% endcomponent_block %}")
|
||||
rendered = template.render(Context({'parent_value': 'passed_in'}))
|
||||
|
||||
self.assertIn('<h1>Shadowing variable = passed_in</h1>', rendered, rendered)
|
||||
self.assertIn('<h1>Uniquely named variable = passed_in</h1>', rendered, rendered)
|
||||
self.assertNotIn('<h1>Shadowing variable = NOT SHADOWED</h1>', rendered, rendered)
|
||||
|
||||
def test_parent_args_available_outside_slots(self):
|
||||
template = Template("{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component_block 'parent_with_args' parent_value='passed_in' %}{%endcomponent_block %}")
|
||||
rendered = template.render(Context())
|
||||
|
||||
self.assertIn('<h1>Shadowing variable = passed_in</h1>', rendered, rendered)
|
||||
self.assertIn('<h1>Uniquely named variable = passed_in</h1>', rendered, rendered)
|
||||
self.assertNotIn('<h1>Shadowing variable = NOT SHADOWED</h1>', rendered, rendered)
|
||||
|
||||
def test_parent_args_available_in_slots(self):
|
||||
template = Template("{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component_block 'parent_with_args' parent_value='passed_in' %}"
|
||||
"{% slot 'content' %}{% component name='variable_display' "
|
||||
"shadowing_variable='value_from_slot' new_variable=inner_parent_value %}{% endslot %}"
|
||||
"{%endcomponent_block %}")
|
||||
rendered = template.render(Context())
|
||||
|
||||
self.assertIn('<h1>Shadowing variable = value_from_slot</h1>', rendered, rendered)
|
||||
self.assertIn('<h1>Uniquely named variable = passed_in</h1>', rendered, rendered)
|
||||
self.assertNotIn('<h1>Shadowing variable = NOT SHADOWED</h1>', rendered, rendered)
|
||||
|
||||
|
||||
class ContextCalledOnceTests(SimpleTestCase):
|
||||
def test_one_context_call_with_simple_component(self):
|
||||
template = Template("{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component name='incrementer' %}")
|
||||
rendered = template.render(Context()).strip()
|
||||
|
||||
self.assertEqual(rendered, '<p class="incrementer">value=1;calls=1</p>', rendered)
|
||||
|
||||
def test_one_context_call_with_simple_component_and_arg(self):
|
||||
template = Template("{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component name='incrementer' value='2' %}")
|
||||
rendered = template.render(Context()).strip()
|
||||
|
||||
self.assertEqual(rendered, '<p class="incrementer">value=3;calls=1</p>', rendered)
|
||||
|
||||
def test_one_context_call_with_component_block(self):
|
||||
template = Template("{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component_block 'incrementer' %}{% endcomponent_block %}")
|
||||
rendered = template.render(Context()).strip()
|
||||
|
||||
self.assertEqual(rendered, '<p class="incrementer">value=1;calls=1</p>', rendered)
|
||||
|
||||
def test_one_context_call_with_component_block_and_arg(self):
|
||||
template = Template("{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component_block 'incrementer' value='3' %}{% endcomponent_block %}")
|
||||
rendered = template.render(Context()).strip()
|
||||
|
||||
self.assertEqual(rendered, '<p class="incrementer">value=4;calls=1</p>', rendered)
|
||||
|
||||
def test_one_context_call_with_slot(self):
|
||||
template = Template("{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component_block 'incrementer' %}{% slot 'content' %}"
|
||||
"<p>slot</p>{% endslot %}{% endcomponent_block %}")
|
||||
rendered = template.render(Context()).strip()
|
||||
|
||||
self.assertEqual(rendered, '<p class="incrementer">value=1;calls=1</p>\n<p>slot</p>', rendered)
|
||||
|
||||
def test_one_context_call_with_slot_and_arg(self):
|
||||
template = Template("{% load component_tags %}{% component_dependencies %}"
|
||||
"{% component_block 'incrementer' value='3' %}{% slot 'content' %}"
|
||||
"<p>slot</p>{% endslot %}{% endcomponent_block %}")
|
||||
rendered = template.render(Context()).strip()
|
||||
|
||||
self.assertEqual(rendered, '<p class="incrementer">value=4;calls=1</p>\n<p>slot</p>', rendered)
|
Loading…
Add table
Add a link
Reference in a new issue