mirror of
https://github.com/django-components/django-components.git
synced 2025-08-04 06:18:17 +00:00
feat: allow different template settings for ComponentRegistries (#615)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
ee5c92ba00
commit
3b1f6088a0
10 changed files with 478 additions and 44 deletions
288
README.md
288
README.md
|
@ -59,6 +59,7 @@ And this is what gets rendered (plus the CSS and Javascript you've specified):
|
|||
- [Available settings](#available-settings)
|
||||
- [Logging and debugging](#logging-and-debugging)
|
||||
- [Management Command](#management-command)
|
||||
- [Writing and sharing component libraries](#writing-and-sharing-component-libraries)
|
||||
- [Community examples](#community-examples)
|
||||
- [Running django-components project locally](#running-django-components-project-locally)
|
||||
- [Development guides](#development-guides)
|
||||
|
@ -69,6 +70,7 @@ And this is what gets rendered (plus the CSS and Javascript you've specified):
|
|||
- Spread operator `...dict` inside template tags. See [Spread operator](#spread-operator))
|
||||
- Use template tags inside string literals in component inputs. See [Use template tags inside component inputs](#use-template-tags-inside-component-inputs))
|
||||
- Dynamic slots, fills and provides - The `name` argument for these can now be a variable, a template expression, or via spread operator
|
||||
- Component library authors can now configure `CONTEXT_BEHAVIOR` and `TAG_FORMATTER` settings independently from user settings.
|
||||
|
||||
🚨📢 **Version 0.92**
|
||||
- BREAKING CHANGE: `Component` class is no longer a subclass of `View`. To configure the `View` class, set the `Component.View` nested class. HTTP methods like `get` or `post` can still be defined directly on `Component` class, and `Component.as_view()` internally calls `Component.View.as_view()`. (See [Modifying the View class](#modifying-the-view-class))
|
||||
|
@ -932,8 +934,8 @@ 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.
|
||||
If you are writing a component library to be shared with others, you may want to manage your own instance of `ComponentRegistry`
|
||||
and 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.
|
||||
|
@ -960,6 +962,34 @@ class MyComponent(Component):
|
|||
|
||||
NOTE: The Library instance can be accessed under `library` attribute of `ComponentRegistry`.
|
||||
|
||||
### ComponentRegistry settings
|
||||
|
||||
When you are creating an instance of `ComponentRegistry`, you can define the components' behavior within the template.
|
||||
|
||||
The registry accepts these settings:
|
||||
- `CONTEXT_BEHAVIOR`
|
||||
- `TAG_FORMATTER`
|
||||
|
||||
```py
|
||||
from django.template import Library
|
||||
from django_components import ComponentRegistry, RegistrySettings
|
||||
|
||||
register = library = django.template.Library()
|
||||
comp_registry = ComponentRegistry(
|
||||
library=library,
|
||||
settings=RegistrySettings(
|
||||
CONTEXT_BEHAVIOR="isolated",
|
||||
TAG_FORMATTER="django_components.component_formatter",
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
These settings are [the same as the ones you can set for django_components](#available-settings).
|
||||
|
||||
In fact, when you set `COMPONENT.tag_formatter` or `COMPONENT.context_behavior`, these are forwarded to the default `ComponentRegistry`.
|
||||
|
||||
This makes it possible to have multiple registries with different settings in one projects, and makes sharing of component libraries possible.
|
||||
|
||||
## 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:
|
||||
|
@ -1002,7 +1032,7 @@ If you are using autodiscovery, keep a few points in mind:
|
|||
- Components inside the auto-imported files still need to be registered with `@register()`
|
||||
- Auto-imported component files must be valid Python modules, they must use suffix `.py`, and module name should follow [PEP-8](https://peps.python.org/pep-0008/#package-and-module-names).
|
||||
|
||||
Autodiscovery can be disabled in the [settings](#disable-autodiscovery).
|
||||
Autodiscovery can be disabled in the [settings](#autodiscover---toggle-autodiscovery).
|
||||
|
||||
### Manually trigger autodiscovery
|
||||
|
||||
|
@ -2719,9 +2749,9 @@ COMPONENTS = {
|
|||
|
||||
All library settings are handled from a global `COMPONENTS` variable that is read from `settings.py`. By default you don't need it set, there are resonable defaults.
|
||||
|
||||
### Configure the module where components are loaded from
|
||||
### `libraries` - Load component modules
|
||||
|
||||
Configure the location where components are loaded. To do this, add a `COMPONENTS` variable to you `settings.py` with a list of python paths to load. This allows you to build a structure of components that are independent from your apps.
|
||||
Configure the locations where components are loaded. To do this, add a `COMPONENTS` variable to you `settings.py` with a list of python paths to load. This allows you to build a structure of components that are independent from your apps.
|
||||
|
||||
```python
|
||||
COMPONENTS = {
|
||||
|
@ -2761,7 +2791,7 @@ from django_components import import_libraries
|
|||
import_libraries()
|
||||
```
|
||||
|
||||
### Disable autodiscovery
|
||||
### `autodiscover` - Toggle autodiscovery
|
||||
|
||||
If you specify all the component locations with the setting above and have a lot of apps, you can (very) slightly speed things up by disabling autodiscovery.
|
||||
|
||||
|
@ -2771,9 +2801,11 @@ COMPONENTS = {
|
|||
}
|
||||
```
|
||||
|
||||
### Tune the template cache
|
||||
### `template_cache_size` - Tune the template cache
|
||||
|
||||
Each time a template is rendered it is cached to a global in-memory cache (using Python's lru_cache decorator). This speeds up the next render of the component. As the same component is often used many times on the same page, these savings add up. By default the cache holds 128 component templates in memory, which should be enough for most sites. But if you have a lot of components, or if you are using the `template` method of a component to render lots of dynamic templates, you can increase this number. To remove the cache limit altogether and cache everything, set template_cache_size to `None`.
|
||||
Each time a template is rendered it is cached to a global in-memory cache (using Python's `lru_cache` decorator). This speeds up the next render of the component. As the same component is often used many times on the same page, these savings add up.
|
||||
|
||||
By default the cache holds 128 component templates in memory, which should be enough for most sites. But if you have a lot of components, or if you are using the `template` method of a component to render lots of dynamic templates, you can increase this number. To remove the cache limit altogether and cache everything, set template_cache_size to `None`.
|
||||
|
||||
```python
|
||||
COMPONENTS = {
|
||||
|
@ -2781,7 +2813,7 @@ COMPONENTS = {
|
|||
}
|
||||
```
|
||||
|
||||
### Context behavior setting
|
||||
### `context_behavior` - Make components isolated (or not)
|
||||
|
||||
> NOTE: `context_behavior` and `slot_context_behavior` options were merged in v0.70.
|
||||
>
|
||||
|
@ -2886,9 +2918,8 @@ But since `"cheese"` is not defined there, it's empty.
|
|||
|
||||
Notice that the variables defined with the `{% with %}` tag are ignored inside the `{% fill %}` tag with the `"isolated"` mode.
|
||||
|
||||
### Tag formatter setting
|
||||
|
||||
Set the [`TagFormatter`](#available-tagformatters) instance.
|
||||
### `tag_formatter` - Change how components are used in templates
|
||||
Sets the [`TagFormatter`](#available-tagformatters) instance. See the section [Customizing component tags with TagFormatter](#customizing-component-tags-with-tagformatter).
|
||||
|
||||
Can be set either as direct reference, or as an import string;
|
||||
|
||||
|
@ -3012,6 +3043,239 @@ python manage.py startcomponent my_component --dry-run
|
|||
|
||||
This will simulate the creation of `my_component` without creating any files.
|
||||
|
||||
## Writing and sharing component libraries
|
||||
|
||||
You can publish and share your components for others to use. Here are the steps to do so:
|
||||
|
||||
### Writing component libraries
|
||||
|
||||
1. Create a Django project with the following structure:
|
||||
|
||||
```txt
|
||||
project/
|
||||
|-- myapp/
|
||||
|-- __init__.py
|
||||
|-- apps.py
|
||||
|-- templates/
|
||||
|-- table/
|
||||
|-- table.py
|
||||
|-- table.js
|
||||
|-- table.css
|
||||
|-- table.html
|
||||
|-- menu.py <--- single-file component
|
||||
|-- templatetags/
|
||||
|-- __init__.py
|
||||
|-- mytags.py
|
||||
```
|
||||
|
||||
2. Create custom `Library` and `ComponentRegistry` instances in `mytags.py`
|
||||
|
||||
This will be the entrypoint for using the components inside Django templates.
|
||||
|
||||
Remember that Django requires the `Library` instance to be accessible under the `register` variable ([See Django docs](https://docs.djangoproject.com/en/dev/howto/custom-template-tags)):
|
||||
|
||||
```py
|
||||
from django.template import Library
|
||||
from django_components import ComponentRegistry, RegistrySettings
|
||||
|
||||
register = library = django.template.Library()
|
||||
comp_registry = ComponentRegistry(
|
||||
library=library,
|
||||
settings=RegistrySettings(
|
||||
CONTEXT_BEHAVIOR="isolated",
|
||||
TAG_FORMATTER="django_components.component_formatter",
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
As you can see above, this is also the place where we configure how our components should behave, using the `settings` argument. If omitted, default settings are used.
|
||||
|
||||
For library authors, we recommend setting `CONTEXT_BEHAVIOR` to `"isolated"`, so that the state cannot leak into the components, and so the components' behavior is configured solely through the inputs. This means that the components will be more predictable and easier to debug.
|
||||
|
||||
Next, you can decide how will others use your components by settingt the `TAG_FORMATTER` options.
|
||||
|
||||
If omitted or set to `"django_components.component_formatter"`,
|
||||
your components will be used like this:
|
||||
|
||||
```django
|
||||
{% component "table" items=items headers=headers %}
|
||||
{% endcomponent %}
|
||||
```
|
||||
|
||||
Or you can use `"django_components.component_shorthand_formatter"`
|
||||
to use components like so:
|
||||
|
||||
```django
|
||||
{% table items=items headers=headers %}
|
||||
{% endtable %}
|
||||
```
|
||||
|
||||
Or you can define a [custom TagFormatter](#tagformatter).
|
||||
|
||||
Either way, these settings will be scoped only to your components. So, in the user code, there may be components side-by-side that use different formatters:
|
||||
|
||||
```django
|
||||
{% load mytags %}
|
||||
|
||||
{# Component from your library "mytags", using the "shorthand" formatter #}
|
||||
{% table items=items headers=header %}
|
||||
{% endtable %}
|
||||
|
||||
{# User-created components using the default settings #}
|
||||
{% component "my_comp" title="Abc..." %}
|
||||
{% endcomponent %}
|
||||
```
|
||||
|
||||
3. Write your components and register them with your instance of `ComponentRegistry`
|
||||
|
||||
There's one difference when you are writing components that are to be shared, and that's that the components must be explicitly registered with your instance of `ComponentRegistry` from the previous step.
|
||||
|
||||
For better user experience, you can also define the types for the args, kwargs, slots and data.
|
||||
|
||||
It's also a good idea to have a common prefix for your components, so they can be easily distinguished from users' components. In the example below, we use the prefix `my_` / `My`.
|
||||
|
||||
```py
|
||||
from typing import Dict, NotRequired, Optional, Tuple, TypedDict
|
||||
|
||||
from django_components import Component, SlotFunc, register, types
|
||||
|
||||
from myapp.templatetags.mytags import comp_registry
|
||||
|
||||
# Define the types
|
||||
class EmptyDict(TypedDict):
|
||||
pass
|
||||
|
||||
type MyMenuArgs = Tuple[int, str]
|
||||
|
||||
class MyMenuSlots(TypedDict):
|
||||
default: NotRequired[Optional[SlotFunc[EmptyDict]]]
|
||||
|
||||
class MyMenuProps(TypedDict):
|
||||
vertical: NotRequired[bool]
|
||||
klass: NotRequired[str]
|
||||
style: NotRequired[str]
|
||||
|
||||
# Define the component
|
||||
# NOTE: Don't forget to set the `registry`!
|
||||
@register("my_menu", registry=comp_registry)
|
||||
class MyMenu(Component[MyMenuArgs, MyMenuProps, MyMenuSlots, Any]):
|
||||
def get_context_data(
|
||||
self,
|
||||
*args,
|
||||
attrs: Optional[Dict] = None,
|
||||
):
|
||||
return {
|
||||
"attrs": attrs,
|
||||
}
|
||||
|
||||
template: types.django_html = """
|
||||
{# Load django_components template tags #}
|
||||
{% load component_tags %}
|
||||
|
||||
<div {% html_attrs attrs class="my-menu" %}>
|
||||
<div class="my-menu__content">
|
||||
{% slot "default" default / %}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
```
|
||||
|
||||
4. Import the components in `apps.py`
|
||||
|
||||
Normally, users rely on [autodiscovery](#autodiscovery) and `STATICFILES_DIRS` to load the component files.
|
||||
|
||||
Since you, as the library author, are not in control of the file system, it is recommended to load the components manually.
|
||||
|
||||
We recommend doing this in the `AppConfig.ready()` hook of your `apps.py`:
|
||||
|
||||
```py
|
||||
from django.apps import AppConfig
|
||||
|
||||
class MyAppConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "myapp"
|
||||
|
||||
# This is the code that gets run when user adds myapp
|
||||
# to Django's INSTALLED_APPS
|
||||
def ready(self) -> None:
|
||||
# Import the components that you want to make available
|
||||
# inside the templates.
|
||||
from myapp.templates import (
|
||||
menu,
|
||||
table,
|
||||
)
|
||||
```
|
||||
|
||||
Note that you can also include any other startup logic within `AppConfig.ready()`.
|
||||
|
||||
And that's it! The next step is to publish it.
|
||||
|
||||
### Publishing component libraries
|
||||
|
||||
Once you are ready to share your library, you need to build
|
||||
a distribution and then publish it to PyPI.
|
||||
|
||||
django_components uses the [`build`](https://build.pypa.io/en/stable/) utility to build a distribution:
|
||||
|
||||
```bash
|
||||
python -m build --sdist --wheel --outdir dist/ .
|
||||
```
|
||||
|
||||
And to publish to PyPI, you can use `twine` ([See Python user guide](https://packaging.python.org/en/latest/tutorials/packaging-projects/#uploading-the-distribution-archives))
|
||||
|
||||
```bash
|
||||
twine upload --repository pypi dist/* -u __token__ -p <PyPI_TOKEN>
|
||||
```
|
||||
|
||||
Notes on publishing:
|
||||
- The user of the package NEEDS to have installed and configured `django_components`.
|
||||
- If you use components where the HTML / CSS / JS files are separate, you may need to define `MANIFEST.in` to include those files with the distribution ([see user guide](https://setuptools.pypa.io/en/latest/userguide/miscellaneous.html)).
|
||||
|
||||
### Installing and using component libraries
|
||||
|
||||
After the package has been published, all that remains is to install it in other django projects:
|
||||
|
||||
1. Install the package:
|
||||
|
||||
```bash
|
||||
pip install myapp
|
||||
```
|
||||
|
||||
2. Add the package to `INSTALLED_APPS`
|
||||
|
||||
```py
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
"myapp",
|
||||
]
|
||||
```
|
||||
|
||||
3. Optionally add the template tags to the `builtins`, so you don't have to call `{% load mytags %}` in every template:
|
||||
|
||||
```py
|
||||
TEMPLATES = [
|
||||
{
|
||||
...,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
...
|
||||
],
|
||||
'builtins': [
|
||||
'myapp.templatetags.mytags',
|
||||
]
|
||||
},
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
4. And, at last, you can use the components in your own project!
|
||||
|
||||
```django
|
||||
{% my_menu title="Abc..." %}
|
||||
Hello World!
|
||||
{% endmy_menu %}
|
||||
```
|
||||
|
||||
## Community examples
|
||||
|
||||
One of our goals with `django-components` is to make it easy to share components between projects. If you have a set of components that you think would be useful to others, please open a pull request to add them to the list below.
|
||||
|
|
|
@ -5,6 +5,7 @@ import django
|
|||
|
||||
# Public API
|
||||
# isort: off
|
||||
from django_components.app_settings import ContextBehavior as ContextBehavior
|
||||
from django_components.autodiscover import (
|
||||
autodiscover as autodiscover,
|
||||
import_libraries as import_libraries,
|
||||
|
@ -17,6 +18,7 @@ from django_components.component_registry import (
|
|||
AlreadyRegistered as AlreadyRegistered,
|
||||
ComponentRegistry as ComponentRegistry,
|
||||
NotRegistered as NotRegistered,
|
||||
RegistrySettings as RegistrySettings,
|
||||
register as register,
|
||||
registry as registry,
|
||||
)
|
||||
|
|
|
@ -31,11 +31,14 @@ from django.utils.html import conditional_escape
|
|||
from django.utils.safestring import SafeString, mark_safe
|
||||
from django.views import View
|
||||
|
||||
from django_components.app_settings import ContextBehavior
|
||||
from django_components.component_media import ComponentMediaInput, MediaMeta
|
||||
from django_components.component_registry import registry
|
||||
from django_components.component_registry import ComponentRegistry
|
||||
from django_components.component_registry import registry as registry_
|
||||
from django_components.context import (
|
||||
_FILLED_SLOTS_CONTENT_CONTEXT_KEY,
|
||||
_PARENT_COMP_CONTEXT_KEY,
|
||||
_REGISTRY_CONTEXT_KEY,
|
||||
_ROOT_CTX_CONTEXT_KEY,
|
||||
get_injected_context_var,
|
||||
make_isolated_context_copy,
|
||||
|
@ -70,6 +73,7 @@ from django_components.component_registry import registry as registry # NOQA
|
|||
# isort: on
|
||||
|
||||
RENDERED_COMMENT_TEMPLATE = "<!-- _RENDERED {name} -->"
|
||||
COMP_ONLY_FLAG = "only"
|
||||
|
||||
# Define TypeVars for args and kwargs
|
||||
ArgsType = TypeVar("ArgsType", bound=tuple, contravariant=True)
|
||||
|
@ -170,6 +174,7 @@ class Component(Generic[ArgsType, KwargsType, DataType, SlotsType], metaclass=Co
|
|||
component_id: Optional[str] = None,
|
||||
outer_context: Optional[Context] = None,
|
||||
fill_content: Optional[Dict[str, FillContent]] = None,
|
||||
registry: Optional[ComponentRegistry] = None, # noqa F811
|
||||
):
|
||||
# When user first instantiates the component class before calling
|
||||
# `render` or `render_to_response`, then we want to allow the render
|
||||
|
@ -189,6 +194,7 @@ class Component(Generic[ArgsType, KwargsType, DataType, SlotsType], metaclass=Co
|
|||
self.outer_context: Context = outer_context or Context()
|
||||
self.fill_content = fill_content or {}
|
||||
self.component_id = component_id or gen_id()
|
||||
self.registry = registry or registry_
|
||||
self._render_stack: Deque[RenderInput[ArgsType, KwargsType, SlotsType]] = deque()
|
||||
|
||||
def __init_subclass__(cls, **kwargs: Any) -> None:
|
||||
|
@ -535,8 +541,10 @@ class Component(Generic[ArgsType, KwargsType, DataType, SlotsType], metaclass=Co
|
|||
|
||||
with context.update(
|
||||
{
|
||||
# Private context fields
|
||||
_ROOT_CTX_CONTEXT_KEY: self.outer_context,
|
||||
_FILLED_SLOTS_CONTENT_CONTEXT_KEY: updated_slots,
|
||||
_REGISTRY_CONTEXT_KEY: self.registry,
|
||||
# NOTE: Public API for variables accessible from within a component's template
|
||||
# See https://github.com/EmilStenstrom/django-components/issues/280#issuecomment-2081180940
|
||||
"component_vars": {
|
||||
|
@ -595,6 +603,7 @@ class ComponentNode(BaseNode):
|
|||
name: str,
|
||||
args: List[Expression],
|
||||
kwargs: RuntimeKwargs,
|
||||
registry: ComponentRegistry, # noqa F811
|
||||
isolated_context: bool = False,
|
||||
fill_nodes: Optional[List[FillNode]] = None,
|
||||
node_id: Optional[str] = None,
|
||||
|
@ -604,6 +613,7 @@ class ComponentNode(BaseNode):
|
|||
self.name = name
|
||||
self.isolated_context = isolated_context
|
||||
self.fill_nodes = fill_nodes or []
|
||||
self.registry = registry
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<ComponentNode: {}. Contents: {!r}>".format(
|
||||
|
@ -614,7 +624,7 @@ class ComponentNode(BaseNode):
|
|||
def render(self, context: Context) -> str:
|
||||
trace_msg("RENDR", "COMP", self.name, self.node_id)
|
||||
|
||||
component_cls: Type[Component] = registry.get(self.name)
|
||||
component_cls: Type[Component] = self.registry.get(self.name)
|
||||
|
||||
# Resolve FilterExpressions and Variables that were passed as args to the
|
||||
# component, then call component's context method
|
||||
|
@ -639,10 +649,11 @@ class ComponentNode(BaseNode):
|
|||
outer_context=context,
|
||||
fill_content=fill_content,
|
||||
component_id=self.node_id,
|
||||
registry=self.registry,
|
||||
)
|
||||
|
||||
# Prevent outer context from leaking into the template of the component
|
||||
if self.isolated_context:
|
||||
if self.isolated_context or self.registry.settings.CONTEXT_BEHAVIOR == ContextBehavior.ISOLATED:
|
||||
context = make_isolated_context_copy(context)
|
||||
|
||||
output = component._render(
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
from typing import TYPE_CHECKING, Callable, Dict, NamedTuple, Optional, Set, Type, TypeVar
|
||||
from typing import TYPE_CHECKING, Callable, Dict, NamedTuple, Optional, Set, Type, TypeVar, Union
|
||||
|
||||
from django.template import Library
|
||||
|
||||
from django_components.app_settings import ContextBehavior, app_settings
|
||||
from django_components.library import is_tag_protected, mark_protected_tags, register_tag_from_formatter
|
||||
from django_components.tag_formatter import get_tag_formatter
|
||||
from django_components.tag_formatter import TagFormatterABC, get_tag_formatter
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django_components.component import Component
|
||||
|
@ -33,6 +34,16 @@ class ComponentRegistryEntry(NamedTuple):
|
|||
tag: str
|
||||
|
||||
|
||||
class RegistrySettings(NamedTuple):
|
||||
CONTEXT_BEHAVIOR: Optional[ContextBehavior] = None
|
||||
TAG_FORMATTER: Optional[Union["TagFormatterABC", str]] = None
|
||||
|
||||
|
||||
class InternalRegistrySettings(NamedTuple):
|
||||
CONTEXT_BEHAVIOR: ContextBehavior
|
||||
TAG_FORMATTER: Union["TagFormatterABC", str]
|
||||
|
||||
|
||||
class ComponentRegistry:
|
||||
"""
|
||||
Manages which components can be used in the template tags.
|
||||
|
@ -66,10 +77,16 @@ class ComponentRegistry:
|
|||
```
|
||||
"""
|
||||
|
||||
def __init__(self, library: Optional[Library] = None) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
library: Optional[Library] = None,
|
||||
settings: Optional[Union[RegistrySettings, Callable[["ComponentRegistry"], RegistrySettings]]] = 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
|
||||
self._settings_input = settings
|
||||
self._settings: Optional[Callable[[], InternalRegistrySettings]] = None
|
||||
|
||||
@property
|
||||
def library(self) -> Library:
|
||||
|
@ -91,6 +108,37 @@ class ComponentRegistry:
|
|||
lib = self._library = tag_library
|
||||
return lib
|
||||
|
||||
@property
|
||||
def settings(self) -> InternalRegistrySettings:
|
||||
# This is run on subsequent calls
|
||||
if self._settings is not None:
|
||||
# NOTE: Registry's settings can be a function, so we always take
|
||||
# the latest value from Django's settings.
|
||||
settings = self._settings()
|
||||
|
||||
# First-time initialization
|
||||
# NOTE: We allow the settings to be given as a getter function
|
||||
# so the settings can respond to changes.
|
||||
# So we wrapp that in our getter, which assigns default values from the settings.
|
||||
else:
|
||||
|
||||
def get_settings() -> InternalRegistrySettings:
|
||||
if callable(self._settings_input):
|
||||
settings_input: Optional[RegistrySettings] = self._settings_input(self)
|
||||
else:
|
||||
settings_input = self._settings_input
|
||||
|
||||
return InternalRegistrySettings(
|
||||
CONTEXT_BEHAVIOR=(settings_input and settings_input.CONTEXT_BEHAVIOR)
|
||||
or app_settings.CONTEXT_BEHAVIOR,
|
||||
TAG_FORMATTER=(settings_input and settings_input.TAG_FORMATTER) or app_settings.TAG_FORMATTER,
|
||||
)
|
||||
|
||||
self._settings = get_settings
|
||||
settings = self._settings()
|
||||
|
||||
return settings
|
||||
|
||||
def register(self, name: str, component: Type["Component"]) -> None:
|
||||
"""
|
||||
Register a component with this registry under the given name.
|
||||
|
@ -243,8 +291,8 @@ class ComponentRegistry:
|
|||
# Lazily import to avoid circular dependencies
|
||||
from django_components.templatetags.component_tags import component as do_component
|
||||
|
||||
formatter = get_tag_formatter()
|
||||
tag = register_tag_from_formatter(self.library, do_component, formatter, comp_name)
|
||||
formatter = get_tag_formatter(self)
|
||||
tag = register_tag_from_formatter(self, do_component, formatter, comp_name)
|
||||
|
||||
return ComponentRegistryEntry(cls=component, tag=tag)
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ from django_components.utils import find_last_index
|
|||
|
||||
_FILLED_SLOTS_CONTENT_CONTEXT_KEY = "_DJANGO_COMPONENTS_FILLED_SLOTS"
|
||||
_ROOT_CTX_CONTEXT_KEY = "_DJANGO_COMPONENTS_ROOT_CTX"
|
||||
_REGISTRY_CONTEXT_KEY = "_DJANGO_COMPONENTS_REGISTRY"
|
||||
_PARENT_COMP_CONTEXT_KEY = "_DJANGO_COMPONENTS_PARENT_COMP"
|
||||
_CURRENT_COMP_CONTEXT_KEY = "_DJANGO_COMPONENTS_CURRENT_COMP"
|
||||
_INJECT_CONTEXT_KEY_PREFIX = "_DJANGO_COMPONENTS_INJECT__"
|
||||
|
@ -38,6 +39,7 @@ def make_isolated_context_copy(context: Context) -> Context:
|
|||
|
||||
# Pass through our internal keys
|
||||
context_copy[_FILLED_SLOTS_CONTENT_CONTEXT_KEY] = context.get(_FILLED_SLOTS_CONTENT_CONTEXT_KEY, {})
|
||||
context_copy[_REGISTRY_CONTEXT_KEY] = context.get(_REGISTRY_CONTEXT_KEY, None)
|
||||
if _ROOT_CTX_CONTEXT_KEY in context:
|
||||
context_copy[_ROOT_CTX_CONTEXT_KEY] = context[_ROOT_CTX_CONTEXT_KEY]
|
||||
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
"""Module for interfacing with Django's Library (`django.template.library`)"""
|
||||
|
||||
from typing import Callable, List, Optional
|
||||
from typing import TYPE_CHECKING, Callable, List, Optional
|
||||
|
||||
from django.template.base import Node, Parser, Token
|
||||
from django.template.library import Library
|
||||
|
||||
from django_components.tag_formatter import InternalTagFormatter
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django_components.component_registry import ComponentRegistry
|
||||
|
||||
|
||||
class TagProtectedError(Exception):
|
||||
pass
|
||||
|
@ -28,25 +31,25 @@ as they would conflict with other tags in the Library.
|
|||
|
||||
|
||||
def register_tag(
|
||||
library: Library,
|
||||
registry: "ComponentRegistry",
|
||||
tag: str,
|
||||
tag_fn: Callable[[Parser, Token, str], Node],
|
||||
tag_fn: Callable[[Parser, Token, "ComponentRegistry", str], Node],
|
||||
) -> None:
|
||||
# Register inline tag
|
||||
if is_tag_protected(library, tag):
|
||||
if is_tag_protected(registry.library, tag):
|
||||
raise TagProtectedError('Cannot register tag "%s", this tag name is protected' % tag)
|
||||
else:
|
||||
library.tag(tag, lambda parser, token: tag_fn(parser, token, tag))
|
||||
registry.library.tag(tag, lambda parser, token: tag_fn(parser, token, registry, tag))
|
||||
|
||||
|
||||
def register_tag_from_formatter(
|
||||
library: Library,
|
||||
tag_fn: Callable[[Parser, Token, str], Node],
|
||||
registry: "ComponentRegistry",
|
||||
tag_fn: Callable[[Parser, Token, "ComponentRegistry", str], Node],
|
||||
formatter: InternalTagFormatter,
|
||||
component_name: str,
|
||||
) -> str:
|
||||
tag = formatter.start_tag(component_name)
|
||||
register_tag(library, tag, tag_fn)
|
||||
register_tag(registry, tag, tag_fn)
|
||||
return tag
|
||||
|
||||
|
||||
|
|
|
@ -3,7 +3,22 @@ import json
|
|||
import re
|
||||
from collections import deque
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Generic, List, Mapping, NamedTuple, Optional, Protocol, Set, Tuple, Type, TypeVar, Union
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
Generic,
|
||||
List,
|
||||
Mapping,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Protocol,
|
||||
Set,
|
||||
Tuple,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
from django.template import Context, Template
|
||||
from django.template.base import FilterExpression, Node, NodeList, Parser, TextNode
|
||||
|
@ -11,16 +26,20 @@ from django.template.defaulttags import CommentNode
|
|||
from django.template.exceptions import TemplateSyntaxError
|
||||
from django.utils.safestring import SafeString, mark_safe
|
||||
|
||||
from django_components.app_settings import ContextBehavior, app_settings
|
||||
from django_components.app_settings import ContextBehavior
|
||||
from django_components.context import (
|
||||
_FILLED_SLOTS_CONTENT_CONTEXT_KEY,
|
||||
_INJECT_CONTEXT_KEY_PREFIX,
|
||||
_REGISTRY_CONTEXT_KEY,
|
||||
_ROOT_CTX_CONTEXT_KEY,
|
||||
)
|
||||
from django_components.expression import RuntimeKwargs, is_identifier
|
||||
from django_components.logger import trace_msg
|
||||
from django_components.node import BaseNode, NodeTraverse, nodelist_has_content, walk_nodelist
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django_components.component_registry import ComponentRegistry
|
||||
|
||||
TSlotData = TypeVar("TSlotData", bound=Mapping, contravariant=True)
|
||||
|
||||
DEFAULT_SLOT_KEY = "_DJANGO_COMPONENTS_DEFAULT_SLOT"
|
||||
|
@ -215,12 +234,13 @@ class SlotNode(BaseNode):
|
|||
if not slot_fill.is_filled:
|
||||
return context
|
||||
|
||||
if app_settings.CONTEXT_BEHAVIOR == ContextBehavior.DJANGO:
|
||||
registry: "ComponentRegistry" = context[_REGISTRY_CONTEXT_KEY]
|
||||
if registry.settings.CONTEXT_BEHAVIOR == ContextBehavior.DJANGO:
|
||||
return context
|
||||
elif app_settings.CONTEXT_BEHAVIOR == ContextBehavior.ISOLATED:
|
||||
elif registry.settings.CONTEXT_BEHAVIOR == ContextBehavior.ISOLATED:
|
||||
return context[_ROOT_CTX_CONTEXT_KEY]
|
||||
else:
|
||||
raise ValueError(f"Unknown value for CONTEXT_BEHAVIOR: '{app_settings.CONTEXT_BEHAVIOR}'")
|
||||
raise ValueError(f"Unknown value for CONTEXT_BEHAVIOR: '{registry.settings.CONTEXT_BEHAVIOR}'")
|
||||
|
||||
def resolve_kwargs(
|
||||
self,
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
import abc
|
||||
import re
|
||||
from typing import List, NamedTuple
|
||||
from typing import TYPE_CHECKING, List, NamedTuple
|
||||
|
||||
from django.template import TemplateSyntaxError
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
from django_components.app_settings import app_settings
|
||||
from django_components.expression import resolve_string
|
||||
from django_components.template_parser import VAR_CHARS
|
||||
from django_components.utils import is_str_wrapped_in_quotes
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django_components.component_registry import ComponentRegistry
|
||||
|
||||
|
||||
TAG_RE = re.compile(r"^[{chars}]+$".format(chars=VAR_CHARS))
|
||||
|
||||
|
||||
|
@ -201,10 +204,10 @@ class ShorthandComponentFormatter(TagFormatterABC):
|
|||
return TagResult(name, tokens)
|
||||
|
||||
|
||||
def get_tag_formatter() -> InternalTagFormatter:
|
||||
def get_tag_formatter(registry: "ComponentRegistry") -> InternalTagFormatter:
|
||||
"""Returns an instance of the currently configured component tag formatter."""
|
||||
# Allow users to configure the component TagFormatter
|
||||
formatter_cls_or_str = app_settings.TAG_FORMATTER
|
||||
formatter_cls_or_str = registry.settings.TAG_FORMATTER
|
||||
|
||||
if isinstance(formatter_cls_or_str, str):
|
||||
tag_formatter: TagFormatterABC = import_string(formatter_cls_or_str)
|
||||
|
|
|
@ -6,9 +6,8 @@ from django.template.exceptions import TemplateSyntaxError
|
|||
from django.utils.safestring import SafeString, mark_safe
|
||||
from django.utils.text import smart_split
|
||||
|
||||
from django_components.app_settings import ContextBehavior, app_settings
|
||||
from django_components.attributes import HTML_ATTRS_ATTRS_KEY, HTML_ATTRS_DEFAULTS_KEY, HtmlAttrsNode
|
||||
from django_components.component import RENDERED_COMMENT_TEMPLATE, ComponentNode
|
||||
from django_components.component import COMP_ONLY_FLAG, RENDERED_COMMENT_TEMPLATE, ComponentNode
|
||||
from django_components.component_registry import ComponentRegistry
|
||||
from django_components.component_registry import registry as component_registry
|
||||
from django_components.expression import (
|
||||
|
@ -204,7 +203,7 @@ def fill(parser: Parser, token: Token) -> FillNode:
|
|||
return fill_node
|
||||
|
||||
|
||||
def component(parser: Parser, token: Token, tag_name: str) -> ComponentNode:
|
||||
def component(parser: Parser, token: Token, registry: ComponentRegistry, tag_name: str) -> ComponentNode:
|
||||
"""
|
||||
To give the component access to the template context:
|
||||
```#!htmldjango {% component "name" positional_arg keyword_arg=value ... %}```
|
||||
|
@ -221,7 +220,7 @@ def component(parser: Parser, token: Token, tag_name: str) -> ComponentNode:
|
|||
bits = token.split_contents()
|
||||
|
||||
# Let the TagFormatter pre-process the tokens
|
||||
formatter = get_tag_formatter()
|
||||
formatter = get_tag_formatter(registry)
|
||||
result = formatter.parse([*bits])
|
||||
end_tag = formatter.end_tag(result.component_name)
|
||||
|
||||
|
@ -234,14 +233,14 @@ def component(parser: Parser, token: Token, tag_name: str) -> ComponentNode:
|
|||
parser,
|
||||
token,
|
||||
params=True, # Allow many args
|
||||
flags=["only"],
|
||||
flags=[COMP_ONLY_FLAG],
|
||||
keywordonly_kwargs=True,
|
||||
repeatable_kwargs=False,
|
||||
end_tag=end_tag,
|
||||
)
|
||||
|
||||
# Check for isolated context keyword
|
||||
isolated_context = tag.flags["only"] or app_settings.CONTEXT_BEHAVIOR == ContextBehavior.ISOLATED
|
||||
isolated_context = tag.flags[COMP_ONLY_FLAG]
|
||||
|
||||
trace_msg("PARSE", "COMP", result.component_name, tag.id)
|
||||
|
||||
|
@ -260,6 +259,7 @@ def component(parser: Parser, token: Token, tag_name: str) -> ComponentNode:
|
|||
isolated_context=isolated_context,
|
||||
fill_nodes=fill_nodes,
|
||||
node_id=tag.id,
|
||||
registry=registry,
|
||||
)
|
||||
|
||||
trace_msg("PARSE", "COMP", result.component_name, tag.id, "...Done!")
|
||||
|
|
|
@ -1,19 +1,25 @@
|
|||
import unittest
|
||||
|
||||
from django.template import Library
|
||||
from django.template import Context, Engine, Library, Template
|
||||
from django.test import override_settings
|
||||
|
||||
from django_components import (
|
||||
AlreadyRegistered,
|
||||
Component,
|
||||
ComponentRegistry,
|
||||
ContextBehavior,
|
||||
NotRegistered,
|
||||
RegistrySettings,
|
||||
TagProtectedError,
|
||||
component_formatter,
|
||||
component_shorthand_formatter,
|
||||
register,
|
||||
registry,
|
||||
types,
|
||||
)
|
||||
|
||||
from .django_test_setup import setup_test_config
|
||||
from .testutils import BaseTestCase, parametrize_context_behavior
|
||||
|
||||
setup_test_config({"autodiscover": False})
|
||||
|
||||
|
@ -133,6 +139,81 @@ class ComponentRegistryTest(unittest.TestCase):
|
|||
self.registry.unregister(name="testcomponent")
|
||||
|
||||
|
||||
class MultipleComponentRegistriesTest(BaseTestCase):
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_different_registries_have_different_settings(self):
|
||||
library_a = Library()
|
||||
registry_a = ComponentRegistry(
|
||||
library=library_a,
|
||||
settings=RegistrySettings(
|
||||
CONTEXT_BEHAVIOR=ContextBehavior.ISOLATED,
|
||||
TAG_FORMATTER=component_shorthand_formatter,
|
||||
),
|
||||
)
|
||||
|
||||
library_b = Library()
|
||||
registry_b = ComponentRegistry(
|
||||
library=library_b,
|
||||
settings=RegistrySettings(
|
||||
CONTEXT_BEHAVIOR=ContextBehavior.DJANGO,
|
||||
TAG_FORMATTER=component_formatter,
|
||||
),
|
||||
)
|
||||
|
||||
# NOTE: We cannot load the Libraries above using `{% load xxx %}` tag, because
|
||||
# for that we'd need to register a Django app and whatnot.
|
||||
# Instead, we insert the Libraries directly into the engine's builtins.
|
||||
engine = Engine.get_default()
|
||||
|
||||
# Add the custom template tags to Django's built-in tags
|
||||
engine.template_builtins.append(library_a)
|
||||
engine.template_builtins.append(library_b)
|
||||
|
||||
class SimpleComponent(Component):
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
Variable: <strong>{{ variable }}</strong>
|
||||
Slot: {% slot "default" default / %}
|
||||
"""
|
||||
|
||||
def get_context_data(self, variable=None):
|
||||
return {
|
||||
"variable": variable,
|
||||
}
|
||||
|
||||
registry_a.register("simple_a", SimpleComponent)
|
||||
registry_b.register("simple_b", SimpleComponent)
|
||||
|
||||
template_str: types.django_html = """
|
||||
{% simple_a variable=123 %}
|
||||
SLOT 123
|
||||
{% endsimple_a %}
|
||||
{% component "simple_b" variable=123 %}
|
||||
SLOT ABC
|
||||
{% endcomponent %}
|
||||
"""
|
||||
template = Template(template_str)
|
||||
|
||||
rendered = template.render(Context({}))
|
||||
|
||||
self.assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
Variable: <strong>123</strong>
|
||||
Slot:
|
||||
SLOT 123
|
||||
|
||||
Variable: <strong>123</strong>
|
||||
Slot:
|
||||
SLOT ABC
|
||||
""",
|
||||
)
|
||||
|
||||
# Remove the custom template tags to clean up after tests
|
||||
engine.template_builtins.remove(library_a)
|
||||
engine.template_builtins.remove(library_b)
|
||||
|
||||
|
||||
class ProtectedTagsTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue