mirror of
https://github.com/django-components/django-components.git
synced 2025-08-11 09:48:00 +00:00
160 lines
5.6 KiB
Python
160 lines
5.6 KiB
Python
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):
|
|
"""
|
|
This component is given a registered name or a reference to another component,
|
|
and behaves as if the other component was in its place.
|
|
|
|
The args, kwargs, and slot fills are all passed down to the underlying component.
|
|
|
|
Args:
|
|
is (str | Type[Component]): Component that should be rendered. Either a registered name of a component,
|
|
or a [Component](../api#django_components.Component) class directly. Required.
|
|
registry (ComponentRegistry, optional): Specify the [registry](../api#django_components.ComponentRegistry)\
|
|
to search for the registered name. If omitted, all registries are searched until the first match.
|
|
*args: Additional data passed to the component.
|
|
**kwargs: Additional data passed to the component.
|
|
|
|
**Slots:**
|
|
|
|
* Any slots, depending on the actual component.
|
|
|
|
**Examples:**
|
|
|
|
Django
|
|
```django
|
|
{% component "dynamic" is=table_comp data=table_data headers=table_headers %}
|
|
{% fill "pagination" %}
|
|
{% component "pagination" / %}
|
|
{% endfill %}
|
|
{% endcomponent %}
|
|
```
|
|
|
|
Python
|
|
```py
|
|
from django_components import DynamicComponent
|
|
|
|
DynamicComponent.render(
|
|
kwargs={
|
|
"is": table_comp,
|
|
"data": table_data,
|
|
"headers": table_headers,
|
|
},
|
|
slots={
|
|
"pagination": PaginationComponent.render(
|
|
render_dependencies=False,
|
|
),
|
|
},
|
|
)
|
|
```
|
|
|
|
# Use cases
|
|
|
|
Dynamic components are suitable if you are writing something like a form component. You may design
|
|
it such that users give you a list of input types, and you render components depending on the input types.
|
|
|
|
While you could handle this with a series of if / else statements, that's not an extensible approach.
|
|
Instead, you can use the dynamic component in place of normal components.
|
|
|
|
# Component name
|
|
|
|
By default, the dynamic component is registered under the name `"dynamic"`. In case of a conflict,
|
|
you can set the
|
|
[`COMPONENTS.dynamic_component_name`](../settings#django_components.app_settings.ComponentsSettings.dynamic_component_name)
|
|
setting to change the name used for the dynamic components.
|
|
|
|
```py
|
|
# settings.py
|
|
COMPONENTS = ComponentsSettings(
|
|
dynamic_component_name="my_dynamic",
|
|
)
|
|
```
|
|
|
|
After which you will be able to use the dynamic component with the new name:
|
|
```django
|
|
{% component "my_dynamic" is=table_comp data=table_data headers=table_headers %}
|
|
{% fill "pagination" %}
|
|
{% component "pagination" / %}
|
|
{% endfill %}
|
|
{% endcomponent %}
|
|
```
|
|
"""
|
|
|
|
_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)
|
|
|
|
# NOTE: Slots are passed at component instantiation
|
|
comp = comp_class(
|
|
registered_name=self.registered_name,
|
|
component_id=self.component_id,
|
|
outer_context=self.outer_context,
|
|
registry=self.registry,
|
|
)
|
|
output = comp.render(
|
|
context=self.input.context,
|
|
args=args,
|
|
kwargs=kwargs,
|
|
slots=self.input.slots,
|
|
# NOTE: Since we're accessing slots as `self.input.slots`, the content of slot functions
|
|
# was already escaped (if set so).
|
|
escape_slots_content=False,
|
|
type=self.input.type,
|
|
render_dependencies=self.input.render_dependencies,
|
|
)
|
|
|
|
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
|