mirror of
https://github.com/django-components/django-components.git
synced 2025-09-26 15:39:08 +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)
|
- [Using single-file components](#using-single-file-components)
|
||||||
- [Use components in templates](#use-components-in-templates)
|
- [Use components in templates](#use-components-in-templates)
|
||||||
- [Use components outside of templates](#use-components-outside-of-templates)
|
- [Use components outside of templates](#use-components-outside-of-templates)
|
||||||
- [Registering components](#registering-components)
|
|
||||||
- [Use components as views](#use-components-as-views)
|
- [Use components as views](#use-components-as-views)
|
||||||
|
- [Pre-defined components](#pre-defined-components)
|
||||||
|
- [Registering components](#registering-components)
|
||||||
- [Autodiscovery](#autodiscovery)
|
- [Autodiscovery](#autodiscovery)
|
||||||
- [Using slots in templates](#using-slots-in-templates)
|
- [Using slots in templates](#using-slots-in-templates)
|
||||||
- [Accessing data passed to the component](#accessing-data-passed-to-the-component)
|
- [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
|
## 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**
|
**Version 0.94**
|
||||||
- django_components now automatically configures Django to support multi-line tags. (See [Multi-line tags](#multi-line-tags))
|
- 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))
|
- 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)
|
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
|
## Registering components
|
||||||
|
|
||||||
In previous examples you could repeatedly see us using `@register()` to "register"
|
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_name`
|
||||||
- `get_template_string`
|
- `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`:
|
`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 = {
|
COMPONENTS = {
|
||||||
"autodiscover": True,
|
"autodiscover": True,
|
||||||
"context_behavior": "django", # "django" | "isolated"
|
"context_behavior": "django", # "django" | "isolated"
|
||||||
|
"dynamic_component_name": "dynamic",
|
||||||
"libraries": [], # ["mysite.components.forms", ...]
|
"libraries": [], # ["mysite.components.forms", ...]
|
||||||
"multiline_tags": True,
|
"multiline_tags": True,
|
||||||
"reload_on_template_change": False,
|
"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
|
### `multiline_tags` - Enable/Disable multiline support
|
||||||
|
|
||||||
If `True`, template tags can span multiple lines. Default: `True`
|
If `True`, template tags can span multiple lines. Default: `True`
|
||||||
|
|
|
@ -22,6 +22,7 @@ from django_components.component_registry import (
|
||||||
register as register,
|
register as register,
|
||||||
registry as registry,
|
registry as registry,
|
||||||
)
|
)
|
||||||
|
from django_components.components import DynamicComponent as DynamicComponent
|
||||||
from django_components.library import TagProtectedError as TagProtectedError
|
from django_components.library import TagProtectedError as TagProtectedError
|
||||||
from django_components.slots import (
|
from django_components.slots import (
|
||||||
SlotContent as SlotContent,
|
SlotContent as SlotContent,
|
||||||
|
|
|
@ -99,7 +99,11 @@ class AppSettings:
|
||||||
return self.settings.get("autodiscover", True)
|
return self.settings.get("autodiscover", True)
|
||||||
|
|
||||||
@property
|
@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", [])
|
return self.settings.get("libraries", [])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -11,6 +11,8 @@ class ComponentsConfig(AppConfig):
|
||||||
def ready(self) -> None:
|
def ready(self) -> None:
|
||||||
from django_components.app_settings import app_settings
|
from django_components.app_settings import app_settings
|
||||||
from django_components.autodiscover import autodiscover, get_dirs, import_libraries, search_dirs
|
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
|
from django_components.utils import watch_files_for_autoreload
|
||||||
|
|
||||||
# Import modules set in `COMPONENTS.libraries` setting
|
# Import modules set in `COMPONENTS.libraries` setting
|
||||||
|
@ -27,19 +29,22 @@ class ComponentsConfig(AppConfig):
|
||||||
component_filepaths = search_dirs(dirs, "**/*")
|
component_filepaths = search_dirs(dirs, "**/*")
|
||||||
watch_files_for_autoreload(component_filepaths)
|
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:
|
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
|
from django.template import base
|
||||||
|
|
||||||
base.tag_re = re.compile(base.tag_re.pattern, re.DOTALL)
|
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__
|
return self.registered_name or self.__class__.__name__
|
||||||
|
|
||||||
@property
|
@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
|
Input holds the data (like arg, kwargs, slots) that were passsed to
|
||||||
the current execution of the `render` method.
|
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`,
|
# 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.
|
# 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:
|
def get_context_data(self, *args: Any, **kwargs: Any) -> DataType:
|
||||||
return cast(DataType, {})
|
return cast(DataType, {})
|
||||||
|
@ -526,6 +529,8 @@ class Component(Generic[ArgsType, KwargsType, DataType, SlotsType], metaclass=Co
|
||||||
component_name=self.name,
|
component_name=self.name,
|
||||||
context_data=slot_context_data,
|
context_data=slot_context_data,
|
||||||
fill_content=fill_content,
|
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
|
# 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
|
from django.template import Library
|
||||||
|
|
||||||
|
@ -44,6 +44,12 @@ class InternalRegistrySettings(NamedTuple):
|
||||||
TAG_FORMATTER: Union["TagFormatterABC", str]
|
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:
|
class ComponentRegistry:
|
||||||
"""
|
"""
|
||||||
Manages which components can be used in the template tags.
|
Manages which components can be used in the template tags.
|
||||||
|
@ -88,6 +94,8 @@ class ComponentRegistry:
|
||||||
self._settings_input = settings
|
self._settings_input = settings
|
||||||
self._settings: Optional[Callable[[], InternalRegistrySettings]] = None
|
self._settings: Optional[Callable[[], InternalRegistrySettings]] = None
|
||||||
|
|
||||||
|
all_registries.append(self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def library(self) -> Library:
|
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],
|
component_name: Optional[str],
|
||||||
context_data: Mapping[str, Any],
|
context_data: Mapping[str, Any],
|
||||||
fill_content: Dict[SlotName, FillContent],
|
fill_content: Dict[SlotName, FillContent],
|
||||||
|
is_dynamic_component: bool = False,
|
||||||
) -> Tuple[Dict[SlotId, Slot], Dict[SlotId, SlotFill]]:
|
) -> Tuple[Dict[SlotId, Slot], Dict[SlotId, SlotFill]]:
|
||||||
"""
|
"""
|
||||||
Search the template for all SlotNodes, and associate the slots
|
Search the template for all SlotNodes, and associate the slots
|
||||||
|
@ -546,10 +547,14 @@ def resolve_slots(
|
||||||
component_name=component_name,
|
component_name=component_name,
|
||||||
slots=slots,
|
slots=slots,
|
||||||
slot_fills=slot_fills,
|
slot_fills=slot_fills,
|
||||||
|
is_dynamic_component=is_dynamic_component,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 4. Detect any errors with slots/fills
|
# 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
|
# 5. Find roots of the slot relationships
|
||||||
top_level_slot_ids: List[SlotId] = []
|
top_level_slot_ids: List[SlotId] = []
|
||||||
|
@ -598,6 +603,7 @@ def _resolve_default_slot(
|
||||||
component_name: Optional[str],
|
component_name: Optional[str],
|
||||||
slots: Dict[SlotId, Slot],
|
slots: Dict[SlotId, Slot],
|
||||||
slot_fills: Dict[SlotName, SlotFill],
|
slot_fills: Dict[SlotName, SlotFill],
|
||||||
|
is_dynamic_component: bool,
|
||||||
) -> Dict[SlotName, SlotFill]:
|
) -> Dict[SlotName, SlotFill]:
|
||||||
"""Figure out which slot the default fill refers to, and perform checks."""
|
"""Figure out which slot the default fill refers to, and perform checks."""
|
||||||
named_fills = slot_fills.copy()
|
named_fills = slot_fills.copy()
|
||||||
|
@ -637,7 +643,7 @@ def _resolve_default_slot(
|
||||||
|
|
||||||
# Check: Only component templates that include a 'default' slot
|
# Check: Only component templates that include a 'default' slot
|
||||||
# can be invoked with implicit filling.
|
# 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(
|
raise TemplateSyntaxError(
|
||||||
f"Component '{component_name}' passed default fill content '{default_fill.name}'"
|
f"Component '{component_name}' passed default fill content '{default_fill.name}'"
|
||||||
f"(i.e. without explicit 'fill' tag), "
|
f"(i.e. without explicit 'fill' tag), "
|
||||||
|
|
|
@ -2,12 +2,12 @@ import textwrap
|
||||||
|
|
||||||
from django.template import Context, Template, TemplateSyntaxError
|
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 .django_test_setup import setup_test_config
|
||||||
from .testutils import BaseTestCase, parametrize_context_behavior
|
from .testutils import BaseTestCase, parametrize_context_behavior
|
||||||
|
|
||||||
setup_test_config()
|
setup_test_config({"autodiscover": False})
|
||||||
|
|
||||||
|
|
||||||
class SlottedComponent(Component):
|
class SlottedComponent(Component):
|
||||||
|
@ -75,18 +75,6 @@ class ComponentTemplateTagTest(BaseTestCase):
|
||||||
rendered = template.render(Context({}))
|
rendered = template.render(Context({}))
|
||||||
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
|
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"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
def test_call_with_invalid_name(self):
|
def test_call_with_invalid_name(self):
|
||||||
registry.register(name="test_one", component=self.SimpleComponent)
|
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):
|
class MultiComponentTests(BaseTestCase):
|
||||||
def register_components(self):
|
def register_components(self):
|
||||||
registry.register("first_component", SlottedComponent)
|
registry.register("first_component", SlottedComponent)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue