mirror of
https://github.com/django-components/django-components.git
synced 2025-07-16 04:54:59 +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
|
@ -1,11 +1,10 @@
|
|||
import warnings
|
||||
from copy import copy
|
||||
from copy import copy, deepcopy
|
||||
from functools import lru_cache
|
||||
from itertools import chain
|
||||
|
||||
from django.conf import settings
|
||||
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.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)
|
||||
|
||||
|
||||
class SimplifiedInterfaceMediaDefiningClass(MediaDefiningClass):
|
||||
def __new__(mcs, name, bases, attrs):
|
||||
if "Media" in attrs:
|
||||
|
@ -39,6 +39,7 @@ class SimplifiedInterfaceMediaDefiningClass(MediaDefiningClass):
|
|||
|
||||
return super().__new__(mcs, name, bases, attrs)
|
||||
|
||||
|
||||
class Component(metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||
|
||||
def __init__(self, component_name):
|
||||
|
@ -73,45 +74,54 @@ class Component(metaclass=SimplifiedInterfaceMediaDefiningClass):
|
|||
|
||||
@staticmethod
|
||||
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)
|
||||
def compile_instance_template(self, template_name):
|
||||
"""Use component's base template and the slots used for this instance to compile
|
||||
a unified template for this instance."""
|
||||
def get_processed_template(self, template_name):
|
||||
"""Retrieve the requested template and add a link to this component to each SlotNode in the template."""
|
||||
|
||||
component_template = get_template(template_name)
|
||||
slots_in_template = Component.slots_in_template(component_template)
|
||||
source_template = get_template(template_name)
|
||||
|
||||
defined_slot_names = set(slots_in_template.keys())
|
||||
filled_slot_names = set(self.slots.keys())
|
||||
unexpected_slots = filled_slot_names - defined_slot_names
|
||||
if unexpected_slots:
|
||||
if settings.DEBUG:
|
||||
# The template may be shared with another component (e.g., due to caching). To ensure that each
|
||||
# SlotNode is unique between components, we have to copy the nodes in the template nodelist and
|
||||
# any contained nodelists.
|
||||
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:
|
||||
filled_slot_names = set(self.slots.keys())
|
||||
unused_slots = filled_slot_names - slots_seen
|
||||
if unused_slots:
|
||||
warnings.warn(
|
||||
"Component {} was provided with unexpected slots: {}".format(
|
||||
self._component_name, unexpected_slots
|
||||
"Component {} was provided with slots that were not used in a template: {}".format(
|
||||
self._component_name, unused_slots
|
||||
)
|
||||
)
|
||||
for unexpected_slot in unexpected_slots:
|
||||
del self.slots[unexpected_slot]
|
||||
|
||||
combined_slots = dict(slots_in_template, **self.slots)
|
||||
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
|
||||
return component_template
|
||||
|
||||
def render(self, 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)
|
||||
|
||||
class Media:
|
||||
|
@ -122,6 +132,25 @@ class Component(metaclass=SimplifiedInterfaceMediaDefiningClass):
|
|||
# This variable represents the global component registry
|
||||
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):
|
||||
"""Class decorator to register a component.
|
||||
|
||||
|
@ -133,6 +162,7 @@ def register(name):
|
|||
...
|
||||
|
||||
"""
|
||||
|
||||
def decorator(component):
|
||||
registry.register(name=name, component=component)
|
||||
return component
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue