mirror of
https://github.com/django-components/django-components.git
synced 2025-09-19 12:19:44 +00:00
refactor: prepare registry for custom template tags and docs (#566)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
c202c5a901
commit
d6dec450ed
11 changed files with 491 additions and 94 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -64,7 +64,6 @@ target/
|
||||||
# lock file is not needed for development
|
# lock file is not needed for development
|
||||||
# as project supports variety of Django versions
|
# as project supports variety of Django versions
|
||||||
poetry.lock
|
poetry.lock
|
||||||
pyproject.toml
|
|
||||||
|
|
||||||
# PyCharm
|
# PyCharm
|
||||||
.idea/
|
.idea/
|
||||||
|
|
122
README.md
122
README.md
|
@ -29,6 +29,7 @@ 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)
|
||||||
- [Autodiscovery](#autodiscovery)
|
- [Autodiscovery](#autodiscovery)
|
||||||
- [Using slots in templates](#using-slots-in-templates)
|
- [Using slots in templates](#using-slots-in-templates)
|
||||||
|
@ -643,6 +644,127 @@ Note: slots content are automatically escaped by default to prevent XSS attacks.
|
||||||
|
|
||||||
If you're planning on passing an HTML string, check Django's use of [`format_html`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.html.format_html) and [`mark_safe`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.safestring.mark_safe).
|
If you're planning on passing an HTML string, check Django's use of [`format_html`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.html.format_html) and [`mark_safe`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.safestring.mark_safe).
|
||||||
|
|
||||||
|
## Registering components
|
||||||
|
|
||||||
|
In previous examples you could repeatedly see us using `@register()` to "register"
|
||||||
|
the components. In this section we dive deeper into what it actually means and how you can
|
||||||
|
manage (add or remove) components.
|
||||||
|
|
||||||
|
As a reminder, we may have a component like this:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from django_components import Component, register
|
||||||
|
|
||||||
|
@register("calendar")
|
||||||
|
class Calendar(Component):
|
||||||
|
template_name = "template.html"
|
||||||
|
|
||||||
|
# This component takes one parameter, a date string to show in the template
|
||||||
|
def get_context_data(self, date):
|
||||||
|
return {
|
||||||
|
"date": date,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
which we then render in the template as:
|
||||||
|
|
||||||
|
```django
|
||||||
|
{% component "calendar" date="1970-01-01" %}
|
||||||
|
{% endcomponent %}
|
||||||
|
```
|
||||||
|
|
||||||
|
As you can see, `@register` links up the component class
|
||||||
|
with the `{% component %}` template tag. So when the template tag comes across
|
||||||
|
a component called `"calendar"`, it can look up it's class and instantiate it.
|
||||||
|
|
||||||
|
### What is ComponentRegistry
|
||||||
|
|
||||||
|
The `@register` decorator is a shortcut for working with the `ComponentRegistry`.
|
||||||
|
|
||||||
|
`ComponentRegistry` manages which components can be used in the template tags.
|
||||||
|
|
||||||
|
Each `ComponentRegistry` instance is associated with an instance
|
||||||
|
of Django's `Library`. And Libraries are inserted into Django template
|
||||||
|
using the `{% load %}` tags.
|
||||||
|
|
||||||
|
The `@register` decorator accepts an optional kwarg `registry`, which specifies, the `ComponentRegistry` to register components into.
|
||||||
|
If omitted, the default `ComponentRegistry` instance defined in django_components is used.
|
||||||
|
|
||||||
|
```py
|
||||||
|
my_registry = ComponentRegistry()
|
||||||
|
|
||||||
|
@register(registry=my_registry)
|
||||||
|
class MyComponent(Component):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
The default `ComponentRegistry` is associated with the `Library` that
|
||||||
|
you load when you call `{% load component_tags %}` inside your template, or when you
|
||||||
|
add `django_components.templatetags.component_tags` to the template builtins.
|
||||||
|
|
||||||
|
So when you register or unregister a component to/from a component registry,
|
||||||
|
then behind the scenes the registry automatically adds/removes the component's
|
||||||
|
template tags to/from the Library, so you can call the component from within the templates
|
||||||
|
such as `{% component "my_comp" %}`.
|
||||||
|
|
||||||
|
### Working with ComponentRegistry
|
||||||
|
|
||||||
|
The default `ComponentRegistry` instance can be imported as:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from django_components import registry
|
||||||
|
```
|
||||||
|
|
||||||
|
You can use the registry to manually add/remove/get components:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from django_components import registry
|
||||||
|
|
||||||
|
# Register components
|
||||||
|
registry.register("button", ButtonComponent)
|
||||||
|
registry.register("card", CardComponent)
|
||||||
|
|
||||||
|
# Get all or single
|
||||||
|
registry.all() # {"button": ButtonComponent, "card": CardComponent}
|
||||||
|
registry.get("card") # CardComponent
|
||||||
|
|
||||||
|
# Unregister single component
|
||||||
|
registry.unregister("card")
|
||||||
|
|
||||||
|
# Unregister all components
|
||||||
|
registry.clear()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Registering components to custom ComponentRegistry
|
||||||
|
|
||||||
|
In rare cases, you may want to manage your own instance of `ComponentRegistry`,
|
||||||
|
or register components onto a different `Library` instance than the default one.
|
||||||
|
|
||||||
|
The `Library` instance can be set at instantiation of `ComponentRegistry`. If omitted,
|
||||||
|
then the default Library instance from django_components is used.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from django.template import Library
|
||||||
|
from django_components import ComponentRegistry
|
||||||
|
|
||||||
|
my_library = Library(...)
|
||||||
|
my_registry = ComponentRegistry(library=my_library)
|
||||||
|
```
|
||||||
|
|
||||||
|
When you have defined your own `ComponentRegistry`, you can either register the components
|
||||||
|
with `my_registry.register()`, or pass the registry to the `@component.register()` decorator
|
||||||
|
via the `registry` kwarg:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from path.to.my.registry import my_registry
|
||||||
|
|
||||||
|
@register("my_component", registry=my_registry)
|
||||||
|
class MyComponent(Component):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
NOTE: The Library instance can be accessed under `library` attribute of `ComponentRegistry`.
|
||||||
|
|
||||||
## Autodiscovery
|
## Autodiscovery
|
||||||
|
|
||||||
Every component that you want to use in the template with the `{% component %}` tag needs to be registered with the ComponentRegistry. Normally, we use the `@register` decorator for that:
|
Every component that you want to use in the template with the `{% component %}` tag needs to be registered with the ComponentRegistry. Normally, we use the `@register` decorator for that:
|
||||||
|
|
|
@ -1,9 +1,23 @@
|
||||||
from typing import TYPE_CHECKING, Callable, Dict, Type, TypeVar
|
from typing import TYPE_CHECKING, Callable, Dict, List, NamedTuple, Optional, Set, Type, TypeVar
|
||||||
|
|
||||||
|
from django.template import Library
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from django_components import component
|
from django_components.component import Component
|
||||||
|
|
||||||
_TC = TypeVar("_TC", bound=Type["component.Component"])
|
_TComp = TypeVar("_TComp", bound=Type["Component"])
|
||||||
|
|
||||||
|
|
||||||
|
PROTECTED_TAGS = [
|
||||||
|
"component",
|
||||||
|
"component_dependencies",
|
||||||
|
"component_css_dependencies",
|
||||||
|
"component_js_dependencies",
|
||||||
|
"fill",
|
||||||
|
"html_attrs",
|
||||||
|
"provide",
|
||||||
|
"slot",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class AlreadyRegistered(Exception):
|
class AlreadyRegistered(Exception):
|
||||||
|
@ -14,50 +28,311 @@ class NotRegistered(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ComponentRegistry:
|
# Why do we store the tags with the component?
|
||||||
def __init__(self) -> None:
|
#
|
||||||
self._registry: Dict[str, Type["component.Component"]] = {} # component name -> component_class mapping
|
# Each component may be associated with two template tags - One for "block"
|
||||||
|
# and one for "inline" usage. E.g. in the following snippets, the template
|
||||||
|
# tags are `component` and `#component`:
|
||||||
|
#
|
||||||
|
# `{% component "abc" %}{% endcomponent %}`
|
||||||
|
# `{% #component "abc" %}`
|
||||||
|
#
|
||||||
|
# (NOTE: While `endcomponent` also looks like a template tag, we don't have to register
|
||||||
|
# it, because it simply marks the end of body.)
|
||||||
|
#
|
||||||
|
# With the component tag formatter (configurable tags per component class),
|
||||||
|
# each component may have a unique set of template tags.
|
||||||
|
#
|
||||||
|
# For user's convenience, we automatically add/remove the tags from Django's tag Library,
|
||||||
|
# when a component is (un)registered.
|
||||||
|
#
|
||||||
|
# Thus we need to remember which component used which template tags.
|
||||||
|
class ComponentRegistryEntry(NamedTuple):
|
||||||
|
cls: Type["Component"]
|
||||||
|
block_tag: str
|
||||||
|
inline_tag: str
|
||||||
|
|
||||||
def register(self, name: str, component: Type["component.Component"]) -> None:
|
@property
|
||||||
|
def tags(self) -> List[str]:
|
||||||
|
return [self.block_tag, self.inline_tag]
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentRegistry:
|
||||||
|
"""
|
||||||
|
Manages which components can be used in the template tags.
|
||||||
|
|
||||||
|
Each ComponentRegistry instance is associated with an instance
|
||||||
|
of Django's Library. So when you register or unregister a component
|
||||||
|
to/from a component registry, behind the scenes the registry
|
||||||
|
automatically adds/removes the component's template tag to/from
|
||||||
|
the Library.
|
||||||
|
|
||||||
|
The Library instance can be set at instantiation. If omitted, then
|
||||||
|
the default Library instance from django_components is used. The
|
||||||
|
Library instance can be accessed under `library` attribute.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```py
|
||||||
|
# Use with default Library
|
||||||
|
registry = ComponentRegistry()
|
||||||
|
|
||||||
|
# Or a custom one
|
||||||
|
my_lib = Library()
|
||||||
|
registry = ComponentRegistry(library=my_lib)
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
registry.register("button", ButtonComponent)
|
||||||
|
registry.register("card", CardComponent)
|
||||||
|
registry.all()
|
||||||
|
registry.clear()
|
||||||
|
registry.get()
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, library: Optional[Library] = None) -> None:
|
||||||
|
self._registry: Dict[str, ComponentRegistryEntry] = {} # component name -> component_entry mapping
|
||||||
|
self._tags: Dict[str, Set[str]] = {} # tag -> list[component names]
|
||||||
|
self._library = library
|
||||||
|
|
||||||
|
@property
|
||||||
|
def library(self) -> Library:
|
||||||
|
"""
|
||||||
|
The template tag library with which the component registry is associated.
|
||||||
|
"""
|
||||||
|
# Lazily use the default library if none was passed
|
||||||
|
if self._library is not None:
|
||||||
|
lib = self._library
|
||||||
|
else:
|
||||||
|
from django_components.templatetags.component_tags import register as tag_library
|
||||||
|
|
||||||
|
# For the default library, we want to protect our template tags from
|
||||||
|
# being overriden.
|
||||||
|
# On the other hand, if user provided their own Library instance,
|
||||||
|
# it is up to the user to use `mark_protected_tags` if they want
|
||||||
|
# to protect any tags.
|
||||||
|
mark_protected_tags(tag_library, PROTECTED_TAGS)
|
||||||
|
lib = self._library = tag_library
|
||||||
|
return lib
|
||||||
|
|
||||||
|
def register(self, name: str, component: Type["Component"]) -> None:
|
||||||
|
"""
|
||||||
|
Register a component with this registry under the given name.
|
||||||
|
|
||||||
|
A component MUST be registered before it can be used in a template such as:
|
||||||
|
```django
|
||||||
|
{% component "my_comp" %}{% endcomponent %}
|
||||||
|
```
|
||||||
|
|
||||||
|
Raises `AlreadyRegistered` if a different component was already registered
|
||||||
|
under the same name.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```py
|
||||||
|
registry.register("button", ButtonComponent)
|
||||||
|
```
|
||||||
|
"""
|
||||||
existing_component = self._registry.get(name)
|
existing_component = self._registry.get(name)
|
||||||
if existing_component and existing_component._class_hash != component._class_hash:
|
if existing_component and existing_component.cls._class_hash != component._class_hash:
|
||||||
raise AlreadyRegistered('The component "%s" has already been registered' % name)
|
raise AlreadyRegistered('The component "%s" has already been registered' % name)
|
||||||
self._registry[name] = component
|
|
||||||
|
block_tag = "component"
|
||||||
|
inline_tag = "#component"
|
||||||
|
|
||||||
|
entry = ComponentRegistryEntry(
|
||||||
|
cls=component,
|
||||||
|
block_tag=block_tag,
|
||||||
|
inline_tag=inline_tag,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Keep track of which components use which tags, because multiple components may
|
||||||
|
# use the same tag.
|
||||||
|
for tag in entry.tags:
|
||||||
|
if tag not in self._tags:
|
||||||
|
self._tags[tag] = set()
|
||||||
|
self._tags[tag].add(name)
|
||||||
|
|
||||||
|
self._registry[name] = entry
|
||||||
|
|
||||||
def unregister(self, name: str) -> None:
|
def unregister(self, name: str) -> None:
|
||||||
|
"""
|
||||||
|
Unlinks a previously-registered component from the registry under the given name.
|
||||||
|
|
||||||
|
Once a component is unregistered, it CANNOT be used in a template anymore.
|
||||||
|
Following would raise an error:
|
||||||
|
```django
|
||||||
|
{% component "my_comp" %}{% endcomponent %}
|
||||||
|
```
|
||||||
|
|
||||||
|
Raises `NotRegistered` if the given name is not registered.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```py
|
||||||
|
# First register component
|
||||||
|
registry.register("button", ButtonComponent)
|
||||||
|
# Then unregister
|
||||||
|
registry.unregister("button")
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
# Validate
|
||||||
self.get(name)
|
self.get(name)
|
||||||
|
|
||||||
|
entry = self._registry[name]
|
||||||
|
|
||||||
|
# Unregister the tag from library if this was the last component using this tag
|
||||||
|
for tag in entry.tags:
|
||||||
|
# Unlink component from tag
|
||||||
|
self._tags[tag].remove(name)
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
is_tag_empty = not len(self._tags[tag])
|
||||||
|
if is_tag_empty:
|
||||||
|
del self._tags[tag]
|
||||||
|
|
||||||
|
# Do NOT unregister tag if it's protected
|
||||||
|
is_protected = is_tag_protected(self.library, tag)
|
||||||
|
if is_protected:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Unregister the tag from library if this was the last component using this tag
|
||||||
|
if is_tag_empty and tag in self.library.tags:
|
||||||
|
del self.library.tags[tag]
|
||||||
|
|
||||||
del self._registry[name]
|
del self._registry[name]
|
||||||
|
|
||||||
def get(self, name: str) -> Type["component.Component"]:
|
def get(self, name: str) -> Type["Component"]:
|
||||||
|
"""
|
||||||
|
Retrieve a component class registered under the given name.
|
||||||
|
|
||||||
|
Raises `NotRegistered` if the given name is not registered.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```py
|
||||||
|
# First register component
|
||||||
|
registry.register("button", ButtonComponent)
|
||||||
|
# Then get
|
||||||
|
registry.get("button")
|
||||||
|
# > ButtonComponent
|
||||||
|
```
|
||||||
|
"""
|
||||||
if name not in self._registry:
|
if name not in self._registry:
|
||||||
raise NotRegistered('The component "%s" is not registered' % name)
|
raise NotRegistered('The component "%s" is not registered' % name)
|
||||||
|
|
||||||
return self._registry[name]
|
return self._registry[name].cls
|
||||||
|
|
||||||
def all(self) -> Dict[str, Type["component.Component"]]:
|
def all(self) -> Dict[str, Type["Component"]]:
|
||||||
return self._registry
|
"""
|
||||||
|
Retrieve all registered component classes.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```py
|
||||||
|
# First register components
|
||||||
|
registry.register("button", ButtonComponent)
|
||||||
|
registry.register("card", CardComponent)
|
||||||
|
# Then get all
|
||||||
|
registry.all()
|
||||||
|
# > {
|
||||||
|
# > "button": ButtonComponent,
|
||||||
|
# > "card": CardComponent,
|
||||||
|
# > }
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
comps = {key: entry.cls for key, entry in self._registry.items()}
|
||||||
|
return comps
|
||||||
|
|
||||||
def clear(self) -> None:
|
def clear(self) -> None:
|
||||||
|
"""
|
||||||
|
Clears the registry, unregistering all components.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```py
|
||||||
|
# First register components
|
||||||
|
registry.register("button", ButtonComponent)
|
||||||
|
registry.register("card", CardComponent)
|
||||||
|
# Then clear
|
||||||
|
registry.clear()
|
||||||
|
# Then get all
|
||||||
|
registry.all()
|
||||||
|
# > {}
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
all_comp_names = list(self._registry.keys())
|
||||||
|
for comp_name in all_comp_names:
|
||||||
|
self.unregister(comp_name)
|
||||||
|
|
||||||
self._registry = {}
|
self._registry = {}
|
||||||
|
self._tags = {}
|
||||||
|
|
||||||
|
|
||||||
# This variable represents the global component registry
|
# This variable represents the global component registry
|
||||||
registry: ComponentRegistry = ComponentRegistry()
|
registry: ComponentRegistry = ComponentRegistry()
|
||||||
|
"""
|
||||||
|
The default and global component registry. Use this instance to directly
|
||||||
|
register or remove components:
|
||||||
|
|
||||||
|
```py
|
||||||
|
# Register components
|
||||||
|
registry.register("button", ButtonComponent)
|
||||||
|
registry.register("card", CardComponent)
|
||||||
|
# Get single
|
||||||
|
registry.get("button")
|
||||||
|
# Get all
|
||||||
|
registry.all()
|
||||||
|
# Unregister single
|
||||||
|
registry.unregister("button")
|
||||||
|
# Unregister all
|
||||||
|
registry.clear()
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
# NOTE: Aliased so that the arg to `@register` can also be called `registry`
|
||||||
|
_the_registry = registry
|
||||||
|
|
||||||
|
|
||||||
def register(name: str) -> Callable[[_TC], _TC]:
|
def register(name: str, registry: Optional[ComponentRegistry] = None) -> Callable[[_TComp], _TComp]:
|
||||||
"""Class decorator to register a component.
|
"""
|
||||||
|
Class decorator to register a component.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|
||||||
|
```py
|
||||||
@register("my_component")
|
@register("my_component")
|
||||||
class MyComponent(component.Component):
|
class MyComponent(Component):
|
||||||
...
|
...
|
||||||
"""
|
```
|
||||||
|
|
||||||
def decorator(component: _TC) -> _TC:
|
Optionally specify which `ComponentRegistry` the component should be registered to by
|
||||||
|
setting the `registry` kwarg:
|
||||||
|
|
||||||
|
```py
|
||||||
|
my_lib = django.template.Library()
|
||||||
|
my_reg = ComponentRegistry(library=my_lib)
|
||||||
|
|
||||||
|
@register("my_component", registry=my_reg)
|
||||||
|
class MyComponent(Component):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
if registry is None:
|
||||||
|
registry = _the_registry
|
||||||
|
|
||||||
|
def decorator(component: _TComp) -> _TComp:
|
||||||
registry.register(name=name, component=component)
|
registry.register(name=name, component=component)
|
||||||
return component
|
return component
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def mark_protected_tags(lib: Library, tags: List[str]) -> None:
|
||||||
|
# By marking the library as default,
|
||||||
|
lib._protected_tags = [*tags]
|
||||||
|
|
||||||
|
|
||||||
|
def is_tag_protected(lib: Library, tag: str) -> bool:
|
||||||
|
protected_tags = getattr(lib, "_protected_tags", [])
|
||||||
|
return tag in protected_tags
|
||||||
|
|
|
@ -73,10 +73,7 @@ class AppendAttributesTest(BaseTestCase):
|
||||||
|
|
||||||
|
|
||||||
class HtmlAttrsTests(BaseTestCase):
|
class HtmlAttrsTests(BaseTestCase):
|
||||||
def setUp(self):
|
template_str: types.django_html = """
|
||||||
super().setUp()
|
|
||||||
|
|
||||||
self.template_str: types.django_html = """
|
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component "test" attrs:@click.stop="dispatch('click_event')" attrs:x-data="{hello: 'world'}" attrs:class=class_var %}
|
{% component "test" attrs:@click.stop="dispatch('click_event')" attrs:x-data="{hello: 'world'}" attrs:class=class_var %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
|
|
|
@ -615,10 +615,6 @@ class ContextVarsIsFilledTests(BaseTestCase):
|
||||||
)
|
)
|
||||||
registry.register("negated_conditional_slot", self.ComponentWithNegatedConditionalSlot)
|
registry.register("negated_conditional_slot", self.ComponentWithNegatedConditionalSlot)
|
||||||
|
|
||||||
def tearDown(self) -> None:
|
|
||||||
super().tearDown()
|
|
||||||
registry.clear()
|
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
def test_is_filled_vars(self):
|
def test_is_filled_vars(self):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
|
|
|
@ -67,10 +67,6 @@ class MultistyleComponent(Component):
|
||||||
|
|
||||||
@override_settings(COMPONENTS={"RENDER_DEPENDENCIES": True})
|
@override_settings(COMPONENTS={"RENDER_DEPENDENCIES": True})
|
||||||
class ComponentMediaRenderingTests(BaseTestCase):
|
class ComponentMediaRenderingTests(BaseTestCase):
|
||||||
def setUp(self):
|
|
||||||
# NOTE: registry is global, so need to clear before each test
|
|
||||||
registry.clear()
|
|
||||||
|
|
||||||
def test_no_dependencies_when_no_components_used(self):
|
def test_no_dependencies_when_no_components_used(self):
|
||||||
registry.register(name="test", component=SimpleComponent)
|
registry.register(name="test", component=SimpleComponent)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
from django.template import Library
|
||||||
|
|
||||||
from django_components import AlreadyRegistered, Component, ComponentRegistry, NotRegistered, register, registry
|
from django_components import AlreadyRegistered, Component, ComponentRegistry, NotRegistered, register, registry
|
||||||
|
|
||||||
from .django_test_setup import setup_test_config
|
from .django_test_setup import setup_test_config
|
||||||
|
@ -22,6 +24,7 @@ class MockComponentView(Component):
|
||||||
|
|
||||||
class ComponentRegistryTest(unittest.TestCase):
|
class ComponentRegistryTest(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
self.registry = ComponentRegistry()
|
self.registry = ComponentRegistry()
|
||||||
|
|
||||||
def test_register_class_decorator(self):
|
def test_register_class_decorator(self):
|
||||||
|
@ -31,6 +34,23 @@ class ComponentRegistryTest(unittest.TestCase):
|
||||||
|
|
||||||
self.assertEqual(registry.get("decorated_component"), TestComponent)
|
self.assertEqual(registry.get("decorated_component"), TestComponent)
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
registry.unregister("decorated_component")
|
||||||
|
|
||||||
|
def test_register_class_decorator_custom_registry(self):
|
||||||
|
my_lib = Library()
|
||||||
|
my_reg = ComponentRegistry(library=my_lib)
|
||||||
|
|
||||||
|
self.assertDictEqual(my_reg.all(), {})
|
||||||
|
self.assertDictEqual(registry.all(), {})
|
||||||
|
|
||||||
|
@register("decorated_component", registry=my_reg)
|
||||||
|
class TestComponent(Component):
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.assertDictEqual(my_reg.all(), {"decorated_component": TestComponent})
|
||||||
|
self.assertDictEqual(registry.all(), {})
|
||||||
|
|
||||||
def test_simple_register(self):
|
def test_simple_register(self):
|
||||||
self.registry.register(name="testcomponent", component=MockComponent)
|
self.registry.register(name="testcomponent", component=MockComponent)
|
||||||
self.assertEqual(self.registry.all(), {"testcomponent": MockComponent})
|
self.assertEqual(self.registry.all(), {"testcomponent": MockComponent})
|
||||||
|
@ -46,6 +66,44 @@ class ComponentRegistryTest(unittest.TestCase):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_unregisters_only_unused_tags(self):
|
||||||
|
self.assertDictEqual(self.registry._tags, {})
|
||||||
|
# NOTE: We preserve the default component tags
|
||||||
|
self.assertIn("component", self.registry.library.tags)
|
||||||
|
|
||||||
|
# Register two components that use the same tag
|
||||||
|
self.registry.register(name="testcomponent", component=MockComponent)
|
||||||
|
self.registry.register(name="testcomponent2", component=MockComponent)
|
||||||
|
|
||||||
|
self.assertDictEqual(
|
||||||
|
self.registry._tags,
|
||||||
|
{
|
||||||
|
"#component": {"testcomponent", "testcomponent2"},
|
||||||
|
"component": {"testcomponent", "testcomponent2"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn("component", self.registry.library.tags)
|
||||||
|
|
||||||
|
# Unregister only one of the components. The tags should remain
|
||||||
|
self.registry.unregister(name="testcomponent")
|
||||||
|
|
||||||
|
self.assertDictEqual(
|
||||||
|
self.registry._tags,
|
||||||
|
{
|
||||||
|
"#component": {"testcomponent2"},
|
||||||
|
"component": {"testcomponent2"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn("component", self.registry.library.tags)
|
||||||
|
|
||||||
|
# Unregister the second components. The tags should be removed
|
||||||
|
self.registry.unregister(name="testcomponent2")
|
||||||
|
|
||||||
|
self.assertDictEqual(self.registry._tags, {})
|
||||||
|
self.assertIn("component", self.registry.library.tags)
|
||||||
|
|
||||||
def test_prevent_registering_different_components_with_the_same_name(self):
|
def test_prevent_registering_different_components_with_the_same_name(self):
|
||||||
self.registry.register(name="testcomponent", component=MockComponent)
|
self.registry.register(name="testcomponent", component=MockComponent)
|
||||||
with self.assertRaises(AlreadyRegistered):
|
with self.assertRaises(AlreadyRegistered):
|
||||||
|
|
|
@ -25,6 +25,7 @@ class TemplateInstrumentationTest(BaseTestCase):
|
||||||
saved_render_method: Callable # Assigned during setup.
|
saved_render_method: Callable # Assigned during setup.
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
Template._render = self.saved_render_method
|
Template._render = self.saved_render_method
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -92,14 +93,6 @@ class TemplateInstrumentationTest(BaseTestCase):
|
||||||
|
|
||||||
|
|
||||||
class BlockCompatTests(BaseTestCase):
|
class BlockCompatTests(BaseTestCase):
|
||||||
def setUp(self):
|
|
||||||
registry.clear()
|
|
||||||
super().setUp()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
super().tearDown()
|
|
||||||
registry.clear()
|
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
def test_slots_inside_extends(self):
|
def test_slots_inside_extends(self):
|
||||||
registry.register("slotted_component", SlottedComponent)
|
registry.register("slotted_component", SlottedComponent)
|
||||||
|
|
|
@ -47,10 +47,6 @@ class ComponentTemplateTagTest(BaseTestCase):
|
||||||
css = "style.css"
|
css = "style.css"
|
||||||
js = "script.js"
|
js = "script.js"
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
# NOTE: registry is global, so need to clear before each test
|
|
||||||
registry.clear()
|
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
def test_single_component(self):
|
def test_single_component(self):
|
||||||
registry.register(name="test", component=self.SimpleComponent)
|
registry.register(name="test", component=self.SimpleComponent)
|
||||||
|
@ -190,9 +186,6 @@ class ComponentTemplateTagTest(BaseTestCase):
|
||||||
|
|
||||||
|
|
||||||
class MultiComponentTests(BaseTestCase):
|
class MultiComponentTests(BaseTestCase):
|
||||||
def setUp(self):
|
|
||||||
registry.clear()
|
|
||||||
|
|
||||||
def register_components(self):
|
def register_components(self):
|
||||||
registry.register("first_component", SlottedComponent)
|
registry.register("first_component", SlottedComponent)
|
||||||
registry.register("second_component", SlottedComponentWithContext)
|
registry.register("second_component", SlottedComponentWithContext)
|
||||||
|
@ -265,7 +258,6 @@ class MultiComponentTests(BaseTestCase):
|
||||||
|
|
||||||
|
|
||||||
class ComponentIsolationTests(BaseTestCase):
|
class ComponentIsolationTests(BaseTestCase):
|
||||||
def setUp(self):
|
|
||||||
class SlottedComponent(Component):
|
class SlottedComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
|
@ -276,7 +268,9 @@ class ComponentIsolationTests(BaseTestCase):
|
||||||
</custom-template>
|
</custom-template>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
registry.register("test", SlottedComponent)
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
registry.register("test", self.SlottedComponent)
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
def test_instances_of_component_do_not_share_slots(self):
|
def test_instances_of_component_do_not_share_slots(self):
|
||||||
|
@ -358,10 +352,6 @@ class ComponentTemplateSyntaxErrorTests(BaseTestCase):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
registry.register("test", SlottedComponent)
|
registry.register("test", SlottedComponent)
|
||||||
|
|
||||||
def tearDown(self) -> None:
|
|
||||||
super().tearDown()
|
|
||||||
registry.clear()
|
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
def test_variable_outside_fill_tag_compiles_w_out_error(self):
|
def test_variable_outside_fill_tag_compiles_w_out_error(self):
|
||||||
# As of v0.28 this is valid, provided the component registered under "test"
|
# As of v0.28 this is valid, provided the component registered under "test"
|
||||||
|
|
|
@ -32,10 +32,6 @@ class SlottedComponentWithContext(SlottedComponent):
|
||||||
|
|
||||||
|
|
||||||
class ComponentSlottedTemplateTagTest(BaseTestCase):
|
class ComponentSlottedTemplateTagTest(BaseTestCase):
|
||||||
def setUp(self):
|
|
||||||
# NOTE: registry is global, so need to clear before each test
|
|
||||||
registry.clear()
|
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
def test_slotted_template_basic(self):
|
def test_slotted_template_basic(self):
|
||||||
registry.register(name="test1", component=SlottedComponent)
|
registry.register(name="test1", component=SlottedComponent)
|
||||||
|
@ -477,10 +473,6 @@ class ComponentSlottedTemplateTagTest(BaseTestCase):
|
||||||
|
|
||||||
|
|
||||||
class SlottedTemplateRegressionTests(BaseTestCase):
|
class SlottedTemplateRegressionTests(BaseTestCase):
|
||||||
def setUp(self):
|
|
||||||
# NOTE: registry is global, so need to clear before each test
|
|
||||||
registry.clear()
|
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
def test_slotted_template_that_uses_missing_variable(self):
|
def test_slotted_template_that_uses_missing_variable(self):
|
||||||
@register("test")
|
@register("test")
|
||||||
|
@ -517,13 +509,8 @@ class SlottedTemplateRegressionTests(BaseTestCase):
|
||||||
class SlotDefaultTests(BaseTestCase):
|
class SlotDefaultTests(BaseTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
registry.clear()
|
|
||||||
registry.register("test", SlottedComponent)
|
registry.register("test", SlottedComponent)
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
super().tearDown()
|
|
||||||
registry.clear()
|
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
def test_basic(self):
|
def test_basic(self):
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
|
@ -1115,10 +1102,6 @@ class SlotFillTemplateSyntaxErrorTests(BaseTestCase):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
registry.register("test", SlottedComponent)
|
registry.register("test", SlottedComponent)
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
super().tearDown()
|
|
||||||
registry.clear()
|
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
def test_fill_with_no_parent_is_error(self):
|
def test_fill_with_no_parent_is_error(self):
|
||||||
with self.assertRaises(TemplateSyntaxError):
|
with self.assertRaises(TemplateSyntaxError):
|
||||||
|
|
|
@ -178,13 +178,8 @@ class ConditionalSlotTests(BaseTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
registry.clear()
|
|
||||||
registry.register("test", self.ConditionalComponent)
|
registry.register("test", self.ConditionalComponent)
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
super().tearDown()
|
|
||||||
registry.clear()
|
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
def test_no_content_if_branches_are_false(self):
|
def test_no_content_if_branches_are_false(self):
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
|
@ -260,9 +255,6 @@ class SlotIterationTest(BaseTestCase):
|
||||||
"objects": objects,
|
"objects": objects,
|
||||||
}
|
}
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
registry.clear()
|
|
||||||
|
|
||||||
# NOTE: Second arg in tuple is expected result. In isolated mode, loops should NOT leak.
|
# NOTE: Second arg in tuple is expected result. In isolated mode, loops should NOT leak.
|
||||||
@parametrize_context_behavior(
|
@parametrize_context_behavior(
|
||||||
[
|
[
|
||||||
|
@ -627,10 +619,6 @@ class ComponentNestingTests(BaseTestCase):
|
||||||
registry.register("complex_child", self.ComplexChildComponent)
|
registry.register("complex_child", self.ComplexChildComponent)
|
||||||
registry.register("complex_parent", self.ComplexParentComponent)
|
registry.register("complex_parent", self.ComplexParentComponent)
|
||||||
|
|
||||||
def tearDown(self) -> None:
|
|
||||||
super().tearDown()
|
|
||||||
registry.clear()
|
|
||||||
|
|
||||||
# NOTE: Second arg in tuple are expected names in nested fills. In "django" mode,
|
# NOTE: Second arg in tuple are expected names in nested fills. In "django" mode,
|
||||||
# the value should be overridden by the component, while in "isolated" it should
|
# the value should be overridden by the component, while in "isolated" it should
|
||||||
# remain top-level context.
|
# remain top-level context.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue