mirror of
https://github.com/django-components/django-components.git
synced 2025-08-04 14:28:18 +00:00
feat: Add dynamic component (#627)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
8c5b088c31
commit
e76227b8df
10 changed files with 503 additions and 34 deletions
68
README.md
68
README.md
|
@ -44,8 +44,9 @@ And this is what gets rendered (plus the CSS and Javascript you've specified):
|
|||
- [Using single-file components](#using-single-file-components)
|
||||
- [Use components in templates](#use-components-in-templates)
|
||||
- [Use components outside of templates](#use-components-outside-of-templates)
|
||||
- [Registering components](#registering-components)
|
||||
- [Use components as views](#use-components-as-views)
|
||||
- [Pre-defined components](#pre-defined-components)
|
||||
- [Registering components](#registering-components)
|
||||
- [Autodiscovery](#autodiscovery)
|
||||
- [Using slots in templates](#using-slots-in-templates)
|
||||
- [Accessing data passed to the component](#accessing-data-passed-to-the-component)
|
||||
|
@ -67,6 +68,10 @@ And this is what gets rendered (plus the CSS and Javascript you've specified):
|
|||
|
||||
## Release notes
|
||||
|
||||
**Version 0.95**
|
||||
- Added support for dynamic components, where the component name is passed as a variable. (See [Dynamic components](#dynamic-components))
|
||||
- Changed `Component.input` to raise `RuntimeError` if accessed outside of render context. Previously it returned `None` if unset.
|
||||
|
||||
**Version 0.94**
|
||||
- django_components now automatically configures Django to support multi-line tags. (See [Multi-line tags](#multi-line-tags))
|
||||
- New setting `reload_on_template_change`. Set this to `True` to reload the dev server on changes to component template files. (See [Reload dev server on component file changes](#reload-dev-server-on-component-file-changes))
|
||||
|
@ -850,6 +855,54 @@ class MyComponent(Component):
|
|||
do_something_extra(request, *args, **kwargs)
|
||||
```
|
||||
|
||||
## Pre-defined components
|
||||
|
||||
### Dynamic components
|
||||
|
||||
If you are writing something like a form component, you may design it such that users
|
||||
give you the component names, and your component renders it.
|
||||
|
||||
While you can handle this with a series of if / else statements, this is not an extensible solution.
|
||||
|
||||
Instead, you can use **dynamic components**. Dynamic components are used in place of normal components.
|
||||
|
||||
```django
|
||||
{% load component_tags %}
|
||||
{% component "dynamic" is=component_name title="Cat Museum" %}
|
||||
{% fill "content" %}
|
||||
HELLO_FROM_SLOT_1
|
||||
{% endfill %}
|
||||
{% fill "sidebar" %}
|
||||
HELLO_FROM_SLOT_2
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
```
|
||||
|
||||
These behave same way as regular components. You pass it the same args, kwargs, and slots as you would
|
||||
to the component that you want to render.
|
||||
|
||||
The only exception is that also you supply 1-2 additional inputs:
|
||||
- `is` - Required - The component name or a component class to render
|
||||
- `registry` - Optional - The `ComponentRegistry` that will be searched if `is` is a component name. If omitted, ALL registries are searched.
|
||||
|
||||
By default, the dynamic component is registered under the name `"dynamic"`. In case of a conflict, you can change the name used for the dynamic components by defining the [`COMPONENTS.dynamic_component_name` setting](#dynamic_component_name).
|
||||
|
||||
If you need to use the dynamic components in Python, you can also import it from `django_components`:
|
||||
```py
|
||||
from django_components import DynamicComponent
|
||||
|
||||
comp = SimpleTableComp if is_readonly else TableComp
|
||||
|
||||
output = DynamicComponent.render(
|
||||
kwargs={
|
||||
"is": comp,
|
||||
# Other kwargs...
|
||||
},
|
||||
# args: [...],
|
||||
# slots: {...},
|
||||
)
|
||||
```
|
||||
|
||||
## Registering components
|
||||
|
||||
In previous examples you could repeatedly see us using `@register()` to "register"
|
||||
|
@ -1559,7 +1612,7 @@ This means that you can use `self.input` inside:
|
|||
- `get_template_name`
|
||||
- `get_template_string`
|
||||
|
||||
`self.input` is defined only for the duration of `Component.render`, and returns `None` when called outside of this.
|
||||
`self.input` is defined only for the duration of `Component.render`, and raises `RuntimeError` when called outside of this.
|
||||
|
||||
`self.input` has the same fields as the input to `Component.render`:
|
||||
|
||||
|
@ -2792,6 +2845,7 @@ Here's overview of all available settings and their defaults:
|
|||
COMPONENTS = {
|
||||
"autodiscover": True,
|
||||
"context_behavior": "django", # "django" | "isolated"
|
||||
"dynamic_component_name": "dynamic",
|
||||
"libraries": [], # ["mysite.components.forms", ...]
|
||||
"multiline_tags": True,
|
||||
"reload_on_template_change": False,
|
||||
|
@ -2852,6 +2906,16 @@ COMPONENTS = {
|
|||
}
|
||||
```
|
||||
|
||||
### `dynamic_component_name`
|
||||
|
||||
By default, the dynamic component is registered under the name `"dynamic"`. In case of a conflict, use this setting to change the name used for the dynamic components.
|
||||
|
||||
```python
|
||||
COMPONENTS = {
|
||||
"dynamic_component_name": "new_dynamic",
|
||||
}
|
||||
```
|
||||
|
||||
### `multiline_tags` - Enable/Disable multiline support
|
||||
|
||||
If `True`, template tags can span multiple lines. Default: `True`
|
||||
|
|
|
@ -22,6 +22,7 @@ from django_components.component_registry import (
|
|||
register as register,
|
||||
registry as registry,
|
||||
)
|
||||
from django_components.components import DynamicComponent as DynamicComponent
|
||||
from django_components.library import TagProtectedError as TagProtectedError
|
||||
from django_components.slots import (
|
||||
SlotContent as SlotContent,
|
||||
|
|
|
@ -99,7 +99,11 @@ class AppSettings:
|
|||
return self.settings.get("autodiscover", True)
|
||||
|
||||
@property
|
||||
def LIBRARIES(self) -> List:
|
||||
def DYNAMIC_COMPONENT_NAME(self) -> str:
|
||||
return self.settings.get("dynamic_component_name", "dynamic")
|
||||
|
||||
@property
|
||||
def LIBRARIES(self) -> List[str]:
|
||||
return self.settings.get("libraries", [])
|
||||
|
||||
@property
|
||||
|
|
|
@ -11,6 +11,8 @@ class ComponentsConfig(AppConfig):
|
|||
def ready(self) -> None:
|
||||
from django_components.app_settings import app_settings
|
||||
from django_components.autodiscover import autodiscover, get_dirs, import_libraries, search_dirs
|
||||
from django_components.component_registry import registry
|
||||
from django_components.components.dynamic import DynamicComponent
|
||||
from django_components.utils import watch_files_for_autoreload
|
||||
|
||||
# Import modules set in `COMPONENTS.libraries` setting
|
||||
|
@ -27,19 +29,22 @@ class ComponentsConfig(AppConfig):
|
|||
component_filepaths = search_dirs(dirs, "**/*")
|
||||
watch_files_for_autoreload(component_filepaths)
|
||||
|
||||
# Allow tags to span multiple lines. This makes it easier to work with
|
||||
# components inside Django templates, allowing us syntax like:
|
||||
# ```html
|
||||
# {% component "icon"
|
||||
# icon='outline_chevron_down'
|
||||
# size=16
|
||||
# color="text-gray-400"
|
||||
# attrs:class="ml-2"
|
||||
# %}{% endcomponent %}
|
||||
# ```
|
||||
#
|
||||
# See https://stackoverflow.com/a/54206609/9788634
|
||||
if app_settings.MULTILINE_TAGS:
|
||||
# Allow tags to span multiple lines. This makes it easier to work with
|
||||
# components inside Django templates, allowing us syntax like:
|
||||
# ```html
|
||||
# {% component "icon"
|
||||
# icon='outline_chevron_down'
|
||||
# size=16
|
||||
# color="text-gray-400"
|
||||
# attrs:class="ml-2"
|
||||
# %}{% endcomponent %}
|
||||
# ```
|
||||
#
|
||||
# See https://stackoverflow.com/a/54206609/9788634
|
||||
from django.template import base
|
||||
|
||||
base.tag_re = re.compile(base.tag_re.pattern, re.DOTALL)
|
||||
|
||||
# Register the dynamic component under the name as given in settings
|
||||
registry.register(app_settings.DYNAMIC_COMPONENT_NAME, DynamicComponent)
|
||||
|
|
|
@ -205,14 +205,17 @@ class Component(Generic[ArgsType, KwargsType, DataType, SlotsType], metaclass=Co
|
|||
return self.registered_name or self.__class__.__name__
|
||||
|
||||
@property
|
||||
def input(self) -> Optional[RenderInput[ArgsType, KwargsType, SlotsType]]:
|
||||
def input(self) -> RenderInput[ArgsType, KwargsType, SlotsType]:
|
||||
"""
|
||||
Input holds the data (like arg, kwargs, slots) that were passsed to
|
||||
the current execution of the `render` method.
|
||||
"""
|
||||
if not len(self._render_stack):
|
||||
raise RuntimeError(f"{self.name}: Tried to access Component input while outside of rendering execution")
|
||||
|
||||
# NOTE: Input is managed as a stack, so if `render` is called within another `render`,
|
||||
# the propertes below will return only the inner-most state.
|
||||
return self._render_stack[-1] if len(self._render_stack) else None
|
||||
return self._render_stack[-1]
|
||||
|
||||
def get_context_data(self, *args: Any, **kwargs: Any) -> DataType:
|
||||
return cast(DataType, {})
|
||||
|
@ -526,6 +529,8 @@ class Component(Generic[ArgsType, KwargsType, DataType, SlotsType], metaclass=Co
|
|||
component_name=self.name,
|
||||
context_data=slot_context_data,
|
||||
fill_content=fill_content,
|
||||
# Dynamic component has a special mark do it doesn't raise certain errors
|
||||
is_dynamic_component=getattr(self, "_is_dynamic_component", False),
|
||||
)
|
||||
|
||||
# Available slot fills - this is internal to us
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from typing import TYPE_CHECKING, Callable, Dict, NamedTuple, Optional, Set, Type, TypeVar, Union
|
||||
from typing import TYPE_CHECKING, Callable, Dict, List, NamedTuple, Optional, Set, Type, TypeVar, Union
|
||||
|
||||
from django.template import Library
|
||||
|
||||
|
@ -44,6 +44,12 @@ class InternalRegistrySettings(NamedTuple):
|
|||
TAG_FORMATTER: Union["TagFormatterABC", str]
|
||||
|
||||
|
||||
# We keep track of all registries that exist so that, when users want to
|
||||
# dynamically resolve component name to component class, they would be able
|
||||
# to search across all registries.
|
||||
all_registries: List["ComponentRegistry"] = []
|
||||
|
||||
|
||||
class ComponentRegistry:
|
||||
"""
|
||||
Manages which components can be used in the template tags.
|
||||
|
@ -88,6 +94,8 @@ class ComponentRegistry:
|
|||
self._settings_input = settings
|
||||
self._settings: Optional[Callable[[], InternalRegistrySettings]] = None
|
||||
|
||||
all_registries.append(self)
|
||||
|
||||
@property
|
||||
def library(self) -> Library:
|
||||
"""
|
||||
|
|
3
src/django_components/components/__init__.py
Normal file
3
src/django_components/components/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
# flake8: noqa F401
|
||||
|
||||
from django_components.components.dynamic import DynamicComponent as DynamicComponent
|
89
src/django_components/components/dynamic.py
Normal file
89
src/django_components/components/dynamic.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
import inspect
|
||||
from typing import Any, Dict, Optional, Type, Union, cast
|
||||
|
||||
from django_components import Component, ComponentRegistry, NotRegistered, types
|
||||
from django_components.component_registry import all_registries
|
||||
|
||||
|
||||
class DynamicComponent(Component):
|
||||
"""
|
||||
Dynamic component - This component takes inputs and renders the outputs depending on the
|
||||
`is` and `registry` arguments.
|
||||
|
||||
- `is` - required - The component class or registered name of the component that will be
|
||||
rendered in this place.
|
||||
|
||||
- `registry` - optional - Specify the registry to search for the registered name. If omitted,
|
||||
all registries are searched.
|
||||
"""
|
||||
|
||||
_is_dynamic_component = True
|
||||
|
||||
def get_context_data(
|
||||
self,
|
||||
*args: Any,
|
||||
registry: Optional[ComponentRegistry] = None,
|
||||
**kwargs: Any,
|
||||
) -> Dict:
|
||||
# NOTE: We have to access `is` via kwargs, because it's a special keyword in Python
|
||||
comp_name_or_class: Union[str, Type[Component]] = kwargs.pop("is", None)
|
||||
if not comp_name_or_class:
|
||||
raise TypeError(f"Component '{self.name}' is missing a required argument 'is'")
|
||||
|
||||
comp_class = self._resolve_component(comp_name_or_class, registry)
|
||||
|
||||
comp = comp_class(
|
||||
registered_name=self.registered_name,
|
||||
component_id=self.component_id,
|
||||
outer_context=self.outer_context,
|
||||
fill_content=self.fill_content,
|
||||
registry=self.registry,
|
||||
)
|
||||
output = comp.render(
|
||||
context=self.input.context,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
escape_slots_content=self.input.escape_slots_content,
|
||||
)
|
||||
|
||||
return {
|
||||
"output": output,
|
||||
}
|
||||
|
||||
template: types.django_html = """
|
||||
{{ output }}
|
||||
"""
|
||||
|
||||
def _resolve_component(
|
||||
self,
|
||||
comp_name_or_class: Union[str, Type[Component], Any],
|
||||
registry: Optional[ComponentRegistry] = None,
|
||||
) -> Type[Component]:
|
||||
component_cls: Optional[Type[Component]] = None
|
||||
|
||||
if not isinstance(comp_name_or_class, str):
|
||||
# NOTE: When Django template is resolving the variable that refers to the
|
||||
# component class, it may see that it's callable and evaluate it. Hence, we need
|
||||
# get check if we've got class or instance.
|
||||
if inspect.isclass(comp_name_or_class):
|
||||
component_cls = comp_name_or_class
|
||||
else:
|
||||
component_cls = cast(Type[Component], comp_name_or_class.__class__)
|
||||
|
||||
else:
|
||||
if registry:
|
||||
component_cls = registry.get(comp_name_or_class)
|
||||
else:
|
||||
# Search all registries for the first match
|
||||
for reg in all_registries:
|
||||
try:
|
||||
component_cls = reg.get(comp_name_or_class)
|
||||
break
|
||||
except NotRegistered:
|
||||
continue
|
||||
|
||||
# Raise if none found
|
||||
if not component_cls:
|
||||
raise NotRegistered(f"The component '{comp_name_or_class}' was not found")
|
||||
|
||||
return component_cls
|
|
@ -473,6 +473,7 @@ def resolve_slots(
|
|||
component_name: Optional[str],
|
||||
context_data: Mapping[str, Any],
|
||||
fill_content: Dict[SlotName, FillContent],
|
||||
is_dynamic_component: bool = False,
|
||||
) -> Tuple[Dict[SlotId, Slot], Dict[SlotId, SlotFill]]:
|
||||
"""
|
||||
Search the template for all SlotNodes, and associate the slots
|
||||
|
@ -546,10 +547,14 @@ def resolve_slots(
|
|||
component_name=component_name,
|
||||
slots=slots,
|
||||
slot_fills=slot_fills,
|
||||
is_dynamic_component=is_dynamic_component,
|
||||
)
|
||||
|
||||
# 4. Detect any errors with slots/fills
|
||||
_report_slot_errors(slots, slot_fills, component_name)
|
||||
# NOTE: We ignore errors for the dynamic component, as the underlying component
|
||||
# will deal with it
|
||||
if not is_dynamic_component:
|
||||
_report_slot_errors(slots, slot_fills, component_name)
|
||||
|
||||
# 5. Find roots of the slot relationships
|
||||
top_level_slot_ids: List[SlotId] = []
|
||||
|
@ -598,6 +603,7 @@ def _resolve_default_slot(
|
|||
component_name: Optional[str],
|
||||
slots: Dict[SlotId, Slot],
|
||||
slot_fills: Dict[SlotName, SlotFill],
|
||||
is_dynamic_component: bool,
|
||||
) -> Dict[SlotName, SlotFill]:
|
||||
"""Figure out which slot the default fill refers to, and perform checks."""
|
||||
named_fills = slot_fills.copy()
|
||||
|
@ -637,7 +643,7 @@ def _resolve_default_slot(
|
|||
|
||||
# Check: Only component templates that include a 'default' slot
|
||||
# can be invoked with implicit filling.
|
||||
if default_fill and not default_slot_encountered:
|
||||
if default_fill and not default_slot_encountered and not is_dynamic_component:
|
||||
raise TemplateSyntaxError(
|
||||
f"Component '{component_name}' passed default fill content '{default_fill.name}'"
|
||||
f"(i.e. without explicit 'fill' tag), "
|
||||
|
|
|
@ -2,12 +2,12 @@ import textwrap
|
|||
|
||||
from django.template import Context, Template, TemplateSyntaxError
|
||||
|
||||
from django_components import Component, NotRegistered, register, registry, types
|
||||
from django_components import AlreadyRegistered, Component, NotRegistered, register, registry, types
|
||||
|
||||
from .django_test_setup import setup_test_config
|
||||
from .testutils import BaseTestCase, parametrize_context_behavior
|
||||
|
||||
setup_test_config()
|
||||
setup_test_config({"autodiscover": False})
|
||||
|
||||
|
||||
class SlottedComponent(Component):
|
||||
|
@ -75,18 +75,6 @@ class ComponentTemplateTagTest(BaseTestCase):
|
|||
rendered = template.render(Context({}))
|
||||
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_raises_on_no_registered_components(self):
|
||||
# Note: No tag registered
|
||||
|
||||
simple_tag_template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component name="test" variable="variable" %}{% endcomponent %}
|
||||
"""
|
||||
|
||||
with self.assertRaisesMessage(TemplateSyntaxError, "Invalid block tag on line 3: 'component'"):
|
||||
Template(simple_tag_template)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_call_with_invalid_name(self):
|
||||
registry.register(name="test_one", component=self.SimpleComponent)
|
||||
|
@ -199,6 +187,302 @@ class ComponentTemplateTagTest(BaseTestCase):
|
|||
)
|
||||
|
||||
|
||||
class DynamicComponentTemplateTagTest(BaseTestCase):
|
||||
class SimpleComponent(Component):
|
||||
template: types.django_html = """
|
||||
Variable: <strong>{{ variable }}</strong>
|
||||
"""
|
||||
|
||||
def get_context_data(self, variable, variable2="default"):
|
||||
return {
|
||||
"variable": variable,
|
||||
"variable2": variable2,
|
||||
}
|
||||
|
||||
class Media:
|
||||
css = "style.css"
|
||||
js = "script.js"
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
# Run app installation so the `dynamic` component is defined
|
||||
from django_components.apps import ComponentsConfig
|
||||
|
||||
ComponentsConfig.ready(None) # type: ignore[arg-type]
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_basic(self):
|
||||
registry.register(name="test", component=self.SimpleComponent)
|
||||
|
||||
simple_tag_template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component "dynamic" is="test" variable="variable" %}{% endcomponent %}
|
||||
"""
|
||||
|
||||
template = Template(simple_tag_template)
|
||||
rendered = template.render(Context({}))
|
||||
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_call_with_invalid_name(self):
|
||||
registry.register(name="test", component=self.SimpleComponent)
|
||||
|
||||
simple_tag_template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component "dynamic" is="haber_der_baber" variable="variable" %}{% endcomponent %}
|
||||
"""
|
||||
|
||||
template = Template(simple_tag_template)
|
||||
with self.assertRaisesMessage(NotRegistered, "The component 'haber_der_baber' was not found"):
|
||||
template.render(Context({}))
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_component_called_with_variable_as_name(self):
|
||||
registry.register(name="test", component=self.SimpleComponent)
|
||||
|
||||
simple_tag_template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% with component_name="test" %}
|
||||
{% component "dynamic" is=component_name variable="variable" %}{% endcomponent %}
|
||||
{% endwith %}
|
||||
"""
|
||||
|
||||
template = Template(simple_tag_template)
|
||||
rendered = template.render(Context({}))
|
||||
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_component_called_with_variable_as_spread(self):
|
||||
registry.register(name="test", component=self.SimpleComponent)
|
||||
|
||||
simple_tag_template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component "dynamic" ...props %}{% endcomponent %}
|
||||
"""
|
||||
|
||||
template = Template(simple_tag_template)
|
||||
rendered = template.render(
|
||||
Context(
|
||||
{
|
||||
"props": {
|
||||
"is": "test",
|
||||
"variable": "variable",
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_component_as_class(self):
|
||||
registry.register(name="test", component=self.SimpleComponent)
|
||||
|
||||
simple_tag_template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component "dynamic" is=comp_cls variable="variable" %}{% endcomponent %}
|
||||
"""
|
||||
|
||||
template = Template(simple_tag_template)
|
||||
rendered = template.render(
|
||||
Context(
|
||||
{
|
||||
"comp_cls": self.SimpleComponent,
|
||||
}
|
||||
)
|
||||
)
|
||||
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
|
||||
|
||||
@parametrize_context_behavior(
|
||||
["django", "isolated"],
|
||||
settings={
|
||||
"COMPONENTS": {
|
||||
"tag_formatter": "django_components.component_shorthand_formatter",
|
||||
"autodiscover": False,
|
||||
},
|
||||
},
|
||||
)
|
||||
def test_shorthand_formatter(self):
|
||||
from django_components.apps import ComponentsConfig
|
||||
|
||||
ComponentsConfig.ready(None) # type: ignore[arg-type]
|
||||
|
||||
registry.register(name="test", component=self.SimpleComponent)
|
||||
|
||||
simple_tag_template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% dynamic is="test" variable="variable" %}{% enddynamic %}
|
||||
"""
|
||||
|
||||
template = Template(simple_tag_template)
|
||||
rendered = template.render(Context({}))
|
||||
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
|
||||
|
||||
@parametrize_context_behavior(
|
||||
["django", "isolated"],
|
||||
settings={
|
||||
"COMPONENTS": {
|
||||
"dynamic_component_name": "uno_reverse",
|
||||
"tag_formatter": "django_components.component_shorthand_formatter",
|
||||
"autodiscover": False,
|
||||
},
|
||||
},
|
||||
)
|
||||
def test_component_name_is_configurable(self):
|
||||
from django_components.apps import ComponentsConfig
|
||||
|
||||
ComponentsConfig.ready(None) # type: ignore[arg-type]
|
||||
|
||||
registry.register(name="test", component=self.SimpleComponent)
|
||||
|
||||
simple_tag_template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% uno_reverse is="test" variable="variable" %}{% enduno_reverse %}
|
||||
"""
|
||||
|
||||
template = Template(simple_tag_template)
|
||||
rendered = template.render(Context({}))
|
||||
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_raises_already_registered_on_name_conflict(self):
|
||||
with self.assertRaisesMessage(AlreadyRegistered, 'The component "dynamic" has already been registered'):
|
||||
registry.register(name="dynamic", component=self.SimpleComponent)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_component_called_with_default_slot(self):
|
||||
class SimpleSlottedComponent(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
Variable: <strong>{{ variable }}</strong>
|
||||
Slot: {% slot "default" default / %}
|
||||
"""
|
||||
|
||||
def get_context_data(self, variable, variable2="default"):
|
||||
return {
|
||||
"variable": variable,
|
||||
"variable2": variable2,
|
||||
}
|
||||
|
||||
registry.register(name="test", component=SimpleSlottedComponent)
|
||||
|
||||
simple_tag_template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% with component_name="test" %}
|
||||
{% component "dynamic" is=component_name variable="variable" %}
|
||||
HELLO_FROM_SLOT
|
||||
{% endcomponent %}
|
||||
{% endwith %}
|
||||
"""
|
||||
|
||||
template = Template(simple_tag_template)
|
||||
rendered = template.render(Context({}))
|
||||
self.assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
Variable: <strong>variable</strong>
|
||||
Slot: HELLO_FROM_SLOT
|
||||
""",
|
||||
)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_component_called_with_named_slots(self):
|
||||
class SimpleSlottedComponent(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
Variable: <strong>{{ variable }}</strong>
|
||||
Slot 1: {% slot "default" default / %}
|
||||
Slot 2: {% slot "two" / %}
|
||||
"""
|
||||
|
||||
def get_context_data(self, variable, variable2="default"):
|
||||
return {
|
||||
"variable": variable,
|
||||
"variable2": variable2,
|
||||
}
|
||||
|
||||
registry.register(name="test", component=SimpleSlottedComponent)
|
||||
|
||||
simple_tag_template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% with component_name="test" %}
|
||||
{% component "dynamic" is=component_name variable="variable" %}
|
||||
{% fill "default" %}
|
||||
HELLO_FROM_SLOT_1
|
||||
{% endfill %}
|
||||
{% fill "two" %}
|
||||
HELLO_FROM_SLOT_2
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
{% endwith %}
|
||||
"""
|
||||
|
||||
template = Template(simple_tag_template)
|
||||
rendered = template.render(Context({}))
|
||||
self.assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
Variable: <strong>variable</strong>
|
||||
Slot 1: HELLO_FROM_SLOT_1
|
||||
Slot 2: HELLO_FROM_SLOT_2
|
||||
""",
|
||||
)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_raises_on_invalid_slots(self):
|
||||
class SimpleSlottedComponent(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
Variable: <strong>{{ variable }}</strong>
|
||||
Slot 1: {% slot "default" default / %}
|
||||
Slot 2: {% slot "two" / %}
|
||||
"""
|
||||
|
||||
def get_context_data(self, variable, variable2="default"):
|
||||
return {
|
||||
"variable": variable,
|
||||
"variable2": variable2,
|
||||
}
|
||||
|
||||
registry.register(name="test", component=SimpleSlottedComponent)
|
||||
|
||||
simple_tag_template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% with component_name="test" %}
|
||||
{% component "dynamic" is=component_name variable="variable" %}
|
||||
{% fill "default" %}
|
||||
HELLO_FROM_SLOT_1
|
||||
{% endfill %}
|
||||
{% fill "three" %}
|
||||
HELLO_FROM_SLOT_2
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
{% endwith %}
|
||||
"""
|
||||
|
||||
template = Template(simple_tag_template)
|
||||
|
||||
with self.assertRaisesMessage(
|
||||
TemplateSyntaxError, "Component \\'dynamic\\' passed fill that refers to undefined slot: \\'three\\'"
|
||||
):
|
||||
template.render(Context({}))
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_raises_on_invalid_args(self):
|
||||
registry.register(name="test", component=self.SimpleComponent)
|
||||
|
||||
simple_tag_template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% with component_name="test" %}
|
||||
{% component "dynamic" is=component_name invalid_variable="variable" %}{% endcomponent %}
|
||||
{% endwith %}
|
||||
"""
|
||||
|
||||
template = Template(simple_tag_template)
|
||||
with self.assertRaisesMessage(TypeError, "got an unexpected keyword argument \\'invalid_variable\\'"):
|
||||
template.render(Context({}))
|
||||
|
||||
|
||||
class MultiComponentTests(BaseTestCase):
|
||||
def register_components(self):
|
||||
registry.register("first_component", SlottedComponent)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue