mirror of
https://github.com/django-components/django-components.git
synced 2025-07-07 17:34:59 +00:00
feat: extension defaults + docs + API cleanup (#1215)
This commit is contained in:
parent
7df8019544
commit
bb129aefab
16 changed files with 858 additions and 144 deletions
125
CHANGELOG.md
125
CHANGELOG.md
|
@ -506,6 +506,65 @@ Summary:
|
|||
ctx.template_data["my_template_var"] = "my_value"
|
||||
```
|
||||
|
||||
- When creating extensions, the `ComponentExtension.ExtensionClass` attribute was renamed to `ComponentConfig`.
|
||||
|
||||
The old name is deprecated and will be removed in v1.
|
||||
|
||||
Before:
|
||||
|
||||
```py
|
||||
from django_components import ComponentExtension
|
||||
|
||||
class MyExtension(ComponentExtension):
|
||||
class ExtensionClass(ComponentExtension.ComponentConfig):
|
||||
pass
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```py
|
||||
from django_components import ComponentExtension, ExtensionComponentConfig
|
||||
|
||||
class MyExtension(ComponentExtension):
|
||||
class ComponentConfig(ExtensionComponentConfig):
|
||||
pass
|
||||
```
|
||||
|
||||
- When creating extensions, to access the Component class from within the methods of the extension nested classes,
|
||||
use `component_cls`.
|
||||
|
||||
Previously this field was named `component_class`. The old name is deprecated and will be removed in v1.
|
||||
|
||||
`ComponentExtension.ExtensionClass` attribute was renamed to `ComponentConfig`.
|
||||
|
||||
The old name is deprecated and will be removed in v1.
|
||||
|
||||
Before:
|
||||
|
||||
```py
|
||||
from django_components import ComponentExtension, ExtensionComponentConfig
|
||||
|
||||
class LoggerExtension(ComponentExtension):
|
||||
name = "logger"
|
||||
|
||||
class ComponentConfig(ExtensionComponentConfig):
|
||||
def log(self, msg: str) -> None:
|
||||
print(f"{self.component_class.__name__}: {msg}")
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```py
|
||||
from django_components import ComponentExtension, ExtensionComponentConfig
|
||||
|
||||
class LoggerExtension(ComponentExtension):
|
||||
name = "logger"
|
||||
|
||||
class ComponentConfig(ExtensionComponentConfig):
|
||||
def log(self, msg: str) -> None:
|
||||
print(f"{self.component_cls.__name__}: {msg}")
|
||||
```
|
||||
|
||||
**Slots**
|
||||
|
||||
- `SlotContent` was renamed to `SlotInput`. The old name is deprecated and will be removed in v1.
|
||||
|
@ -590,6 +649,47 @@ Summary:
|
|||
NOTE: `Component.is_filled` automatically escaped slot names, so that even slot names that are
|
||||
not valid python identifiers could be set as slot names. `Component.slots` no longer does that.
|
||||
|
||||
**Miscellaneous**
|
||||
|
||||
- The `debug_highlight_components` and `debug_highlight_slots` settings are deprecated.
|
||||
These will be removed in v1.
|
||||
|
||||
The debug highlighting feature was re-implemented as an extension.
|
||||
As such, the recommended way for enabling it has changed:
|
||||
|
||||
Before:
|
||||
|
||||
```python
|
||||
COMPONENTS = ComponentsSettings(
|
||||
debug_highlight_components=True,
|
||||
debug_highlight_slots=True,
|
||||
)
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
Set `extensions_defaults` in your `settings.py` file.
|
||||
|
||||
```python
|
||||
COMPONENTS = ComponentsSettings(
|
||||
extensions_defaults={
|
||||
"debug_highlight": {
|
||||
"highlight_components": True,
|
||||
"highlight_slots": True,
|
||||
},
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
Alternatively, you can enable highlighting for specific components by setting `Component.DebugHighlight.highlight_components` to `True`:
|
||||
|
||||
```python
|
||||
class MyComponent(Component):
|
||||
class DebugHighlight:
|
||||
highlight_components = True
|
||||
highlight_slots = True
|
||||
```
|
||||
|
||||
#### Feat
|
||||
|
||||
- New method to render template variables - `get_template_data()`
|
||||
|
@ -859,6 +959,31 @@ Summary:
|
|||
|
||||
See all [Extension hooks](https://django-components.github.io/django-components/0.140/reference/extension_hooks/).
|
||||
|
||||
- When creating extensions, the previous syntax with `ComponentExtension.ExtensionClass` was causing
|
||||
Mypy errors, because Mypy doesn't allow using class attributes as bases:
|
||||
|
||||
Before:
|
||||
|
||||
```py
|
||||
from django_components import ComponentExtension
|
||||
|
||||
class MyExtension(ComponentExtension):
|
||||
class ExtensionClass(ComponentExtension.ComponentConfig): # Error!
|
||||
pass
|
||||
```
|
||||
|
||||
Instead, you can import `ExtensionComponentConfig` directly:
|
||||
|
||||
After:
|
||||
|
||||
```py
|
||||
from django_components import ComponentExtension, ExtensionComponentConfig
|
||||
|
||||
class MyExtension(ComponentExtension):
|
||||
class ComponentConfig(ExtensionComponentConfig):
|
||||
pass
|
||||
```
|
||||
|
||||
#### Refactor
|
||||
|
||||
- When a component is being rendered, a proper `Component` instance is now created.
|
||||
|
|
|
@ -10,7 +10,7 @@ Django-components functionality can be extended with "extensions". Extensions al
|
|||
|
||||
- [djc-ext-pydantic](https://github.com/django-components/djc-ext-pydantic)
|
||||
|
||||
## Setting up extensions
|
||||
## Install extensions
|
||||
|
||||
Extensions are configured in the Django settings under [`COMPONENTS.extensions`](../../../reference/settings#django_components.app_settings.ComponentsSettings.extensions).
|
||||
|
||||
|
@ -22,7 +22,7 @@ Extensions can be set by either as an import string or by passing in a class:
|
|||
class MyExtension(ComponentExtension):
|
||||
name = "my_extension"
|
||||
|
||||
class ExtensionClass(ComponentExtension.ExtensionClass):
|
||||
class ComponentConfig(ExtensionComponentConfig):
|
||||
...
|
||||
|
||||
COMPONENTS = ComponentsSettings(
|
||||
|
@ -47,7 +47,7 @@ Extensions can define methods to hook into lifecycle events, such as:
|
|||
|
||||
See the full list in [Extension Hooks Reference](../../../reference/extension_hooks).
|
||||
|
||||
## Configuring extensions per component
|
||||
## Per-component configuration
|
||||
|
||||
Each extension has a corresponding nested class within the [`Component`](../../../reference/api#django_components.Component) class. These allow
|
||||
to configure the extensions on a per-component basis.
|
||||
|
@ -84,18 +84,15 @@ You can override the `get()`, `post()`, etc methods to customize the behavior of
|
|||
class MyTable(Component):
|
||||
class View:
|
||||
def get(self, request):
|
||||
# TO BE IMPLEMENTED BY USER
|
||||
# return self.component_cls.render_to_response(request=request)
|
||||
raise NotImplementedError("You must implement the `get` method.")
|
||||
return self.component_class.render_to_response(request=request)
|
||||
|
||||
def post(self, request):
|
||||
# TO BE IMPLEMENTED BY USER
|
||||
# return self.component_cls.render_to_response(request=request)
|
||||
raise NotImplementedError("You must implement the `post` method.")
|
||||
return self.component_class.render_to_response(request=request)
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
<!-- TODO - LINK TO IT ONCE RELEASED -->
|
||||
### Example: Storybook integration
|
||||
|
||||
The Storybook integration (work in progress) is an extension that is configured by a `Storybook` nested class.
|
||||
|
@ -122,12 +119,79 @@ class MyTable(Component):
|
|||
...
|
||||
```
|
||||
|
||||
## Accessing extensions in components
|
||||
### Extension defaults
|
||||
|
||||
Extensions are incredibly flexible, but configuring the same extension for every component can be a pain.
|
||||
|
||||
For this reason, django-components allows for extension defaults. This is like setting the extension config for every component.
|
||||
|
||||
To set extension defaults, use the [`COMPONENTS.extensions_defaults`](../../../reference/settings#django_components.app_settings.ComponentsSettings.extensions_defaults) setting.
|
||||
|
||||
The `extensions_defaults` setting is a dictionary where the key is the extension name and the value is a dictionary of config attributes:
|
||||
|
||||
```python
|
||||
COMPONENTS = ComponentsSettings(
|
||||
extensions=[
|
||||
"my_extension.MyExtension",
|
||||
"storybook.StorybookExtension",
|
||||
],
|
||||
extensions_defaults={
|
||||
"my_extension": {
|
||||
"key": "value",
|
||||
},
|
||||
"view": {
|
||||
"public": True,
|
||||
},
|
||||
"cache": {
|
||||
"ttl": 60,
|
||||
},
|
||||
"storybook": {
|
||||
"title": lambda self: self.component_cls.__name__,
|
||||
},
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
Which is equivalent to setting the following for every component:
|
||||
|
||||
```python
|
||||
class MyTable(Component):
|
||||
class MyExtension:
|
||||
key = "value"
|
||||
|
||||
class View:
|
||||
public = True
|
||||
|
||||
class Cache:
|
||||
ttl = 60
|
||||
|
||||
class Storybook:
|
||||
def title(self):
|
||||
return self.component_cls.__name__
|
||||
```
|
||||
|
||||
!!! info
|
||||
|
||||
If you define an attribute as a function, it is like defining a method on the extension class.
|
||||
|
||||
E.g. in the example above, `title` is a method on the `Storybook` extension class.
|
||||
|
||||
As the name suggests, these are defaults, and so you can still selectively override them on a per-component basis:
|
||||
|
||||
```python
|
||||
class MyTable(Component):
|
||||
class View:
|
||||
public = False
|
||||
```
|
||||
|
||||
### Extensions in component instances
|
||||
|
||||
Above, we've configured extensions `View` and `Storybook` for the `MyTable` component.
|
||||
|
||||
You can access the instances of these extension classes in the component instance.
|
||||
|
||||
Extensions are available under their names (e.g. `self.view`, `self.storybook`).
|
||||
|
||||
For example, the View extension is available as `self.view`:
|
||||
|
||||
```python
|
||||
|
@ -150,23 +214,23 @@ class MyTable(Component):
|
|||
}
|
||||
```
|
||||
|
||||
Thus, you can use extensions to add methods or attributes that will be available to all components
|
||||
in their component context.
|
||||
|
||||
## Writing extensions
|
||||
|
||||
Creating extensions in django-components involves defining a class that inherits from
|
||||
[`ComponentExtension`](../../../reference/api/#django_components.ComponentExtension).
|
||||
This class can implement various lifecycle hooks and define new attributes or methods to be added to components.
|
||||
|
||||
### Defining an extension
|
||||
### Extension class
|
||||
|
||||
To create an extension, define a class that inherits from [`ComponentExtension`](../../../reference/api/#django_components.ComponentExtension)
|
||||
and implement the desired hooks.
|
||||
|
||||
- Each extension MUST have a `name` attribute. The name MUST be a valid Python identifier.
|
||||
- The extension MAY implement any of the [hook methods](../../../reference/extension_hooks).
|
||||
- Each hook method receives a context object with relevant data.
|
||||
- The extension may implement any of the [hook methods](../../../reference/extension_hooks).
|
||||
|
||||
Each hook method receives a context object with relevant data.
|
||||
|
||||
- Extension may own [URLs](#extension-urls) or [CLI commands](#extension-commands).
|
||||
|
||||
```python
|
||||
from django_components.extension import ComponentExtension, OnComponentClassCreatedContext
|
||||
|
@ -187,9 +251,10 @@ class MyExtension(ComponentExtension):
|
|||
|
||||
So if you name an extension `render`, it will conflict with the [`render()`](../../../reference/api/#django_components.Component.render) method of the `Component` class.
|
||||
|
||||
### Defining the extension class
|
||||
### Component config
|
||||
|
||||
In previous sections we've seen the `View` and `Storybook` extensions classes that were nested within the `Component` class:
|
||||
In previous sections we've seen the `View` and `Storybook` extensions classes that were nested
|
||||
within the [`Component`](../../../reference/api/#django_components.Component) class:
|
||||
|
||||
```python
|
||||
class MyComponent(Component):
|
||||
|
@ -202,36 +267,30 @@ class MyComponent(Component):
|
|||
|
||||
These can be understood as component-specific overrides or configuration.
|
||||
|
||||
The nested extension classes like `View` or `Storybook` will actually subclass from a base extension
|
||||
class as defined on the [`ComponentExtension.ExtensionClass`](../../../reference/api/#django_components.ComponentExtension.ExtensionClass).
|
||||
Whether it's `Component.View` or `Component.Storybook`, their respective extensions
|
||||
defined how these nested classes will behave.
|
||||
|
||||
This is how extensions define the "default" behavior of their nested extension classes.
|
||||
|
||||
For example, the `View` base extension class defines the handlers for GET, POST, etc:
|
||||
For example, the View extension defines the API that users may override in `ViewExtension.ComponentConfig`:
|
||||
|
||||
```python
|
||||
from django_components.extension import ComponentExtension
|
||||
from django_components.extension import ComponentExtension, ExtensionComponentConfig
|
||||
|
||||
class ViewExtension(ComponentExtension):
|
||||
name = "view"
|
||||
|
||||
# The default behavior of the `View` extension class.
|
||||
class ExtensionClass(ComponentExtension.ExtensionClass):
|
||||
class ComponentConfig(ExtensionComponentConfig):
|
||||
def get(self, request):
|
||||
# TO BE IMPLEMENTED BY USER
|
||||
# return self.component_cls.render_to_response(request=request)
|
||||
raise NotImplementedError("You must implement the `get` method.")
|
||||
|
||||
def post(self, request):
|
||||
# TO BE IMPLEMENTED BY USER
|
||||
# return self.component_cls.render_to_response(request=request)
|
||||
raise NotImplementedError("You must implement the `post` method.")
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
In any component that then defines a nested `View` extension class, the `View` extension class will actually
|
||||
subclass from the `ViewExtension.ExtensionClass` class.
|
||||
In any component that then defines a nested `Component.View` extension class, the resulting `View` class
|
||||
will actually subclass from the `ViewExtension.ComponentConfig` class.
|
||||
|
||||
In other words, when you define a component like this:
|
||||
|
||||
|
@ -243,11 +302,11 @@ class MyTable(Component):
|
|||
...
|
||||
```
|
||||
|
||||
It will actually be implemented as if the `View` class subclassed from base class `ViewExtension.ExtensionClass`:
|
||||
Behind the scenes it is as if you defined the following:
|
||||
|
||||
```python
|
||||
class MyTable(Component):
|
||||
class View(ViewExtension.ExtensionClass):
|
||||
class View(ViewExtension.ComponentConfig):
|
||||
def get(self, request):
|
||||
# Do something
|
||||
...
|
||||
|
@ -255,13 +314,13 @@ class MyTable(Component):
|
|||
|
||||
!!! warning
|
||||
|
||||
When writing an extension, the `ExtensionClass` MUST subclass the base class [`ComponentExtension.ExtensionClass`](../../../reference/api/#django_components.ComponentExtension.ExtensionClass).
|
||||
When writing an extension, the `ComponentConfig` MUST subclass the base class [`ExtensionComponentConfig`](../../../reference/api/#django_components.ExtensionComponentConfig).
|
||||
|
||||
This base class ensures that the extension class will have access to the component instance.
|
||||
|
||||
### Registering extensions
|
||||
### Install your extension
|
||||
|
||||
Once the extension is defined, it needs to be registered in the Django settings to be used by the application.
|
||||
Once the extension is defined, it needs to be installed in the Django settings to be used by the application.
|
||||
|
||||
Extensions can be given either as an extension class, or its import string:
|
||||
|
||||
|
@ -297,30 +356,30 @@ To tie it all together, here's an example of a custom logging extension that log
|
|||
```python
|
||||
from django_components.extension import (
|
||||
ComponentExtension,
|
||||
ExtensionComponentConfig,
|
||||
OnComponentClassCreatedContext,
|
||||
OnComponentClassDeletedContext,
|
||||
OnComponentInputContext,
|
||||
)
|
||||
|
||||
class ColorLoggerExtensionClass(ComponentExtension.ExtensionClass):
|
||||
color: str
|
||||
|
||||
|
||||
class ColorLoggerExtension(ComponentExtension):
|
||||
name = "color_logger"
|
||||
|
||||
# All `Component.ColorLogger` classes will inherit from this class.
|
||||
ExtensionClass = ColorLoggerExtensionClass
|
||||
class ComponentConfig(ExtensionComponentConfig):
|
||||
color: str
|
||||
|
||||
# These hooks don't have access to the Component instance, only to the Component class,
|
||||
# so we access the color as `Component.ColorLogger.color`.
|
||||
def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
|
||||
# These hooks don't have access to the Component instance,
|
||||
# only to the Component class, so we access the color
|
||||
# as `Component.ColorLogger.color`.
|
||||
def on_component_class_created(self, ctx: OnComponentClassCreatedContext):
|
||||
log.info(
|
||||
f"Component {ctx.component_cls} created.",
|
||||
color=ctx.component_cls.ColorLogger.color,
|
||||
)
|
||||
|
||||
def on_component_class_deleted(self, ctx: OnComponentClassDeletedContext) -> None:
|
||||
def on_component_class_deleted(self, ctx: OnComponentClassDeletedContext):
|
||||
log.info(
|
||||
f"Component {ctx.component_cls} deleted.",
|
||||
color=ctx.component_cls.ColorLogger.color,
|
||||
|
@ -328,7 +387,7 @@ class ColorLoggerExtension(ComponentExtension):
|
|||
|
||||
# This hook has access to the Component instance, so we access the color
|
||||
# as `self.component.color_logger.color`.
|
||||
def on_component_input(self, ctx: OnComponentInputContext) -> None:
|
||||
def on_component_input(self, ctx: OnComponentInputContext):
|
||||
log.info(
|
||||
f"Rendering component {ctx.component_cls}.",
|
||||
color=ctx.component.color_logger.color,
|
||||
|
@ -346,7 +405,7 @@ COMPONENTS = {
|
|||
}
|
||||
```
|
||||
|
||||
Once registered, in any component, you can define a `ColorLogger` attribute:
|
||||
Once installed, in any component, you can define a `ColorLogger` attribute:
|
||||
|
||||
```python
|
||||
class MyComponent(Component):
|
||||
|
@ -363,45 +422,38 @@ django-components provides a few utility functions to help with writing extensio
|
|||
- [`all_components()`](../../../reference/api#django_components.all_components) - returns a list of all created component classes.
|
||||
- [`all_registries()`](../../../reference/api#django_components.all_registries) - returns a list of all created registry instances.
|
||||
|
||||
### Accessing the component class from within an extension
|
||||
### Access component class
|
||||
|
||||
When you are writing the extension class that will be nested inside a Component class, e.g.
|
||||
You can access the owner [`Component`](../../../reference/api/#django_components.Component) class (`MyTable`) from within methods
|
||||
of the extension class (`MyExtension`) by using
|
||||
the [`component_cls`](../../../reference/api/#django_components.ExtensionComponentConfig.component_cls) attribute:
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
class MyExtension:
|
||||
def some_method(self):
|
||||
...
|
||||
print(self.component_cls)
|
||||
```
|
||||
|
||||
You can access the owner Component class (`MyTable`) from within methods of the extension class (`MyExtension`) by using the `component_class` attribute:
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
class MyExtension:
|
||||
def some_method(self):
|
||||
print(self.component_class)
|
||||
```
|
||||
|
||||
Here is how the `component_class` attribute may be used with our `ColorLogger`
|
||||
Here is how the `component_cls` attribute may be used with our `ColorLogger`
|
||||
extension shown above:
|
||||
|
||||
```python
|
||||
class ColorLoggerExtensionClass(ComponentExtension.ExtensionClass):
|
||||
class ColorLoggerComponentConfig(ExtensionComponentConfig):
|
||||
color: str
|
||||
|
||||
def log(self, msg: str) -> None:
|
||||
print(f"{self.component_class.name}: {msg}")
|
||||
print(f"{self.component_cls.__name__}: {msg}")
|
||||
|
||||
|
||||
class ColorLoggerExtension(ComponentExtension):
|
||||
name = "color_logger"
|
||||
|
||||
# All `Component.ColorLogger` classes will inherit from this class.
|
||||
ExtensionClass = ColorLoggerExtensionClass
|
||||
ComponentConfig = ColorLoggerComponentConfig
|
||||
```
|
||||
|
||||
## Extension Commands
|
||||
## Extension commands
|
||||
|
||||
Extensions in django-components can define custom commands that can be executed via the Django management command interface. This allows for powerful automation and customization capabilities.
|
||||
|
||||
|
@ -418,7 +470,7 @@ Where:
|
|||
- `my_ext` - is the extension name
|
||||
- `hello` - is the command name
|
||||
|
||||
### Defining Commands
|
||||
### Define commands
|
||||
|
||||
To define a command, subclass from [`ComponentCommand`](../../../reference/extension_commands#django_components.ComponentCommand).
|
||||
This subclass should define:
|
||||
|
@ -442,7 +494,7 @@ class MyExt(ComponentExtension):
|
|||
commands = [HelloCommand]
|
||||
```
|
||||
|
||||
### Defining Command Arguments and Options
|
||||
### Define arguments and options
|
||||
|
||||
Commands can accept positional arguments and options (e.g. `--foo`), which are defined using the
|
||||
[`arguments`](../../../reference/extension_commands#django_components.ComponentCommand.arguments)
|
||||
|
@ -507,7 +559,7 @@ python manage.py components ext run my_ext hello John --shout
|
|||
If a command doesn't have the [`handle`](../../../reference/extension_commands#django_components.ComponentCommand.handle)
|
||||
method defined, the command will print a help message and exit.
|
||||
|
||||
### Grouping Arguments
|
||||
### Argument groups
|
||||
|
||||
Arguments can be grouped using [`CommandArgGroup`](../../../reference/extension_commands#django_components.CommandArgGroup)
|
||||
to provide better organization and help messages.
|
||||
|
@ -626,7 +678,7 @@ python manage.py components ext run parent child
|
|||
python manage.py components ext run parent child --foo --bar
|
||||
```
|
||||
|
||||
### Print command help
|
||||
### Help message
|
||||
|
||||
By default, all commands will print their help message when run with the `--help` / `-h` flag.
|
||||
|
||||
|
@ -636,7 +688,7 @@ python manage.py components ext run my_ext --help
|
|||
|
||||
The help message prints out all the arguments and options available for the command, as well as any subcommands.
|
||||
|
||||
### Testing Commands
|
||||
### Testing commands
|
||||
|
||||
Commands can be tested using Django's [`call_command()`](https://docs.djangoproject.com/en/5.2/ref/django-admin/#running-management-commands-from-your-code)
|
||||
function, which allows you to simulate running the command in tests.
|
||||
|
@ -721,7 +773,7 @@ class MyExtension(ComponentExtension):
|
|||
|
||||
As of v0.131, `URLRoute` objects are directly converted to Django's `URLPattern` and `URLResolver` objects.
|
||||
|
||||
### Accessing Extension URLs
|
||||
### URL paths
|
||||
|
||||
The URLs defined in an extension are available under the path
|
||||
|
||||
|
@ -766,7 +818,7 @@ In this example, the URL
|
|||
|
||||
would call the `my_view` handler with the parameter `name` set to `"John"`.
|
||||
|
||||
### Passing kwargs and other extra fields to URL routes
|
||||
### Extra URL data
|
||||
|
||||
The [`URLRoute`](../../../reference/extension_urls#django_components.URLRoute) class is framework-agnostic,
|
||||
so that extensions could be used with non-Django frameworks in the future.
|
||||
|
|
|
@ -4,20 +4,30 @@ As larger projects get more complex, it can be hard to debug issues. Django Comp
|
|||
|
||||
Django Components provides a visual debugging feature that helps you understand the structure and boundaries of your components and slots. When enabled, it adds a colored border and a label around each component and slot on your rendered page.
|
||||
|
||||
To enable component and slot highlighting, set
|
||||
[`debug_highlight_components`](../../../reference/settings/#django_components.app_settings.ComponentsSettings.debug_highlight_components)
|
||||
and/or [`debug_highlight_slots`](../../../reference/settings/#django_components.app_settings.ComponentsSettings.debug_highlight_slots)
|
||||
to `True` in your `settings.py` file:
|
||||
To enable component and slot highlighting for all components and slots, set `highlight_components` and `highlight_slots` to `True` in [extensions defaults](../../../reference/settings/#django_components.app_settings.ComponentsSettings.extensions_defaults) in your `settings.py` file:
|
||||
|
||||
```python
|
||||
from django_components import ComponentsSettings
|
||||
|
||||
COMPONENTS = ComponentsSettings(
|
||||
debug_highlight_components=True,
|
||||
debug_highlight_slots=True,
|
||||
extensions_defaults={
|
||||
"debug_highlight": {
|
||||
"highlight_slots": True,
|
||||
"highlight_components": True,
|
||||
},
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
Alternatively, you can enable highlighting for specific components by setting `Component.DebugHighlight.highlight_components` to `True`:
|
||||
|
||||
```python
|
||||
class MyComponent(Component):
|
||||
class DebugHighlight:
|
||||
highlight_components = True
|
||||
highlight_slots = True
|
||||
```
|
||||
|
||||
Components will be highlighted with a **blue** border and label:
|
||||
|
||||

|
||||
|
|
|
@ -19,6 +19,10 @@
|
|||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.ComponentDebugHighlight
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.ComponentDefaults
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
@ -75,6 +79,10 @@
|
|||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.ExtensionComponentConfig
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.RegistrySettings
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
|
|
@ -43,6 +43,7 @@ defaults = ComponentsSettings(
|
|||
debug_highlight_slots=False,
|
||||
dynamic_component_name="dynamic",
|
||||
extensions=[],
|
||||
extensions_defaults={},
|
||||
libraries=[], # E.g. ["mysite.components.forms", ...]
|
||||
multiline_tags=True,
|
||||
reload_on_file_change=False,
|
||||
|
@ -157,6 +158,16 @@ defaults = ComponentsSettings(
|
|||
show_if_no_docstring: true
|
||||
show_labels: false
|
||||
|
||||
::: django_components.app_settings.ComponentsSettings.extensions_defaults
|
||||
options:
|
||||
show_root_heading: true
|
||||
show_signature: true
|
||||
separate_signature: true
|
||||
show_symbol_type_heading: false
|
||||
show_symbol_type_toc: false
|
||||
show_if_no_docstring: true
|
||||
show_labels: false
|
||||
|
||||
::: django_components.app_settings.ComponentsSettings.forbidden_static_files
|
||||
options:
|
||||
show_root_heading: true
|
||||
|
|
|
@ -67,7 +67,7 @@ If you insert this tag multiple times, ALL JS scripts will be duplicately insert
|
|||
|
||||
|
||||
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L3164" target="_blank">See source code</a>
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L3172" target="_blank">See source code</a>
|
||||
|
||||
|
||||
|
||||
|
@ -169,7 +169,7 @@ COMPONENTS = {
|
|||
|
||||
|
||||
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L949" target="_blank">See source code</a>
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L948" target="_blank">See source code</a>
|
||||
|
||||
|
||||
|
||||
|
@ -467,7 +467,7 @@ user = self.inject("user_data")["user"]
|
|||
|
||||
|
||||
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L475" target="_blank">See source code</a>
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L474" target="_blank">See source code</a>
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ from django_components.components import DynamicComponent
|
|||
from django_components.dependencies import DependenciesStrategy, render_dependencies
|
||||
from django_components.extension import (
|
||||
ComponentExtension,
|
||||
ExtensionComponentConfig,
|
||||
OnComponentRegisteredContext,
|
||||
OnComponentUnregisteredContext,
|
||||
OnRegistryCreatedContext,
|
||||
|
@ -47,6 +48,7 @@ from django_components.extension import (
|
|||
)
|
||||
from django_components.extensions.cache import ComponentCache
|
||||
from django_components.extensions.defaults import ComponentDefaults, Default
|
||||
from django_components.extensions.debug_highlight import ComponentDebugHighlight
|
||||
from django_components.extensions.view import ComponentView, get_component_url
|
||||
from django_components.library import TagProtectedError
|
||||
from django_components.node import BaseNode, template_tag
|
||||
|
@ -93,6 +95,7 @@ __all__ = [
|
|||
"Component",
|
||||
"ComponentCache",
|
||||
"ComponentCommand",
|
||||
"ComponentDebugHighlight",
|
||||
"ComponentDefaults",
|
||||
"ComponentExtension",
|
||||
"ComponentFileEntry",
|
||||
|
@ -111,6 +114,7 @@ __all__ = [
|
|||
"DependenciesStrategy",
|
||||
"DynamicComponent",
|
||||
"Empty",
|
||||
"ExtensionComponentConfig",
|
||||
"format_attributes",
|
||||
"get_component_by_class_id",
|
||||
"get_component_dirs",
|
||||
|
|
|
@ -158,6 +158,10 @@ class ComponentsSettings(NamedTuple):
|
|||
- Python import path, e.g. `"path.to.my_extension.MyExtension"`.
|
||||
- Extension class, e.g. `my_extension.MyExtension`.
|
||||
|
||||
Read more about [extensions](../../concepts/advanced/extensions).
|
||||
|
||||
**Example:**
|
||||
|
||||
```python
|
||||
COMPONENTS = ComponentsSettings(
|
||||
extensions=[
|
||||
|
@ -168,6 +172,29 @@ class ComponentsSettings(NamedTuple):
|
|||
```
|
||||
"""
|
||||
|
||||
extensions_defaults: Optional[Dict[str, Any]] = None
|
||||
"""
|
||||
Global defaults for the extension classes.
|
||||
|
||||
Read more about [Extension defaults](../../concepts/advanced/extensions#extension-defaults).
|
||||
|
||||
**Example:**
|
||||
|
||||
```python
|
||||
COMPONENTS = ComponentsSettings(
|
||||
extensions_defaults={
|
||||
"my_extension": {
|
||||
"my_setting": "my_value",
|
||||
},
|
||||
"cache": {
|
||||
"enabled": True,
|
||||
"ttl": 60,
|
||||
},
|
||||
},
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
autodiscover: Optional[bool] = None
|
||||
"""
|
||||
Toggle whether to run [autodiscovery](../../concepts/fundamentals/autodiscovery) at the Django server startup.
|
||||
|
@ -282,8 +309,13 @@ class ComponentsSettings(NamedTuple):
|
|||
> [here](https://github.com/django-components/django-components/issues/498).
|
||||
"""
|
||||
|
||||
# TODO_v1 - remove. Users should use extension defaults instead.
|
||||
debug_highlight_components: Optional[bool] = None
|
||||
"""
|
||||
DEPRECATED. Use
|
||||
[`extensions_defaults`](../settings/#django_components.app_settings.ComponentsSettings.extensions_defaults)
|
||||
instead. Will be removed in v1.
|
||||
|
||||
Enable / disable component highlighting.
|
||||
See [Troubleshooting](../../guides/other/troubleshooting#component-highlighting) for more details.
|
||||
|
||||
|
@ -296,8 +328,13 @@ class ComponentsSettings(NamedTuple):
|
|||
```
|
||||
"""
|
||||
|
||||
# TODO_v1 - remove. Users should use extension defaults instead.
|
||||
debug_highlight_slots: Optional[bool] = None
|
||||
"""
|
||||
DEPRECATED. Use
|
||||
[`extensions_defaults`](../settings/#django_components.app_settings.ComponentsSettings.extensions_defaults)
|
||||
instead. Will be removed in v1.
|
||||
|
||||
Enable / disable slot highlighting.
|
||||
See [Troubleshooting](../../guides/other/troubleshooting#slot-highlighting) for more details.
|
||||
|
||||
|
@ -670,6 +707,7 @@ defaults = ComponentsSettings(
|
|||
debug_highlight_slots=False,
|
||||
dynamic_component_name="dynamic",
|
||||
extensions=[],
|
||||
extensions_defaults={},
|
||||
libraries=[], # E.g. ["mysite.components.forms", ...]
|
||||
multiline_tags=True,
|
||||
reload_on_file_change=False,
|
||||
|
@ -735,6 +773,7 @@ class InternalSettings:
|
|||
# NOTE: Internally we store the extensions as a list of instances, but the user
|
||||
# can pass in either a list of classes or a list of import strings.
|
||||
extensions=self._prepare_extensions(components_settings), # type: ignore[arg-type]
|
||||
extensions_defaults=default(components_settings.extensions_defaults, defaults.extensions_defaults),
|
||||
multiline_tags=default(components_settings.multiline_tags, defaults.multiline_tags),
|
||||
reload_on_file_change=self._prepare_reload_on_file_change(components_settings),
|
||||
template_cache_size=default(components_settings.template_cache_size, defaults.template_cache_size),
|
||||
|
@ -755,7 +794,15 @@ class InternalSettings:
|
|||
from django_components.extensions.defaults import DefaultsExtension
|
||||
from django_components.extensions.view import ViewExtension
|
||||
|
||||
extensions = [CacheExtension, DefaultsExtension, ViewExtension, DebugHighlightExtension] + list(extensions)
|
||||
extensions = cast(
|
||||
List[Type["ComponentExtension"]],
|
||||
[
|
||||
CacheExtension,
|
||||
DefaultsExtension,
|
||||
ViewExtension,
|
||||
DebugHighlightExtension,
|
||||
],
|
||||
) + list(extensions)
|
||||
|
||||
# Extensions may be passed in either as classes or import strings.
|
||||
extension_instances: List["ComponentExtension"] = []
|
||||
|
|
|
@ -56,6 +56,7 @@ from django_components.extension import (
|
|||
extensions,
|
||||
)
|
||||
from django_components.extensions.cache import ComponentCache
|
||||
from django_components.extensions.debug_highlight import ComponentDebugHighlight
|
||||
from django_components.extensions.defaults import ComponentDefaults
|
||||
from django_components.extensions.view import ComponentView, ViewFn
|
||||
from django_components.node import BaseNode
|
||||
|
@ -1727,6 +1728,13 @@ class Component(metaclass=ComponentMeta):
|
|||
"""
|
||||
Instance of [`ComponentView`](../api#django_components.ComponentView) available at component render time.
|
||||
"""
|
||||
DebugHighlight: ClassVar[Type[ComponentDebugHighlight]]
|
||||
"""
|
||||
The fields of this class are used to configure the component debug highlighting.
|
||||
|
||||
Read more about [Component debug highlighting](../../guides/other/troubleshooting#component-and-slot-highlighting).
|
||||
"""
|
||||
debug_highlight: ComponentDebugHighlight
|
||||
|
||||
# #####################################
|
||||
# MISC
|
||||
|
|
|
@ -1,5 +1,19 @@
|
|||
from functools import wraps
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, List, NamedTuple, Optional, Set, Tuple, Type, TypeVar, Union
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Dict,
|
||||
List,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
import django.urls
|
||||
from django.template import Context
|
||||
|
@ -154,43 +168,159 @@ class OnSlotRenderedContext(NamedTuple):
|
|||
################################################
|
||||
|
||||
|
||||
class BaseExtensionClass:
|
||||
"""Base class for all extension classes."""
|
||||
class ExtensionComponentConfig:
|
||||
"""
|
||||
`ExtensionComponentConfig` is the base class for all extension component configs.
|
||||
|
||||
Extensions can define nested classes on the component class,
|
||||
such as [`Component.View`](../api#django_components.Component.View) or
|
||||
[`Component.Cache`](../api#django_components.Component.Cache):
|
||||
|
||||
```py
|
||||
class MyComp(Component):
|
||||
class View:
|
||||
def get(self, request):
|
||||
...
|
||||
|
||||
class Cache:
|
||||
ttl = 60
|
||||
```
|
||||
|
||||
This allows users to configure extension behavior per component.
|
||||
|
||||
Behind the scenes, the nested classes that users define on their components
|
||||
are merged with the extension's "base" class.
|
||||
|
||||
So the example above is the same as:
|
||||
|
||||
```py
|
||||
class MyComp(Component):
|
||||
class View(ViewExtension.ComponentConfig):
|
||||
def get(self, request):
|
||||
...
|
||||
|
||||
class Cache(CacheExtension.ComponentConfig):
|
||||
ttl = 60
|
||||
```
|
||||
|
||||
Where both `ViewExtension.ComponentConfig` and `CacheExtension.ComponentConfig` are
|
||||
subclasses of `ExtensionComponentConfig`.
|
||||
"""
|
||||
|
||||
component_cls: Type["Component"]
|
||||
"""The [`Component`](../api#django_components.Component) class that this extension is defined on."""
|
||||
|
||||
# TODO_v1 - Remove, superseded by `component_cls`
|
||||
component_class: Type["Component"]
|
||||
"""The Component class that this extension is defined on."""
|
||||
"""The [`Component`](../api#django_components.Component) class that this extension is defined on."""
|
||||
|
||||
component: "Component"
|
||||
"""
|
||||
When a [`Component`](../api#django_components.Component) is instantiated,
|
||||
also the nested extension classes (such as `Component.View`) are instantiated,
|
||||
receiving the component instance as an argument.
|
||||
|
||||
This attribute holds the owner [`Component`](../api#django_components.Component) instance
|
||||
that this extension is defined on.
|
||||
"""
|
||||
|
||||
def __init__(self, component: "Component") -> None:
|
||||
self.component = component
|
||||
|
||||
|
||||
# TODO_v1 - Delete
|
||||
BaseExtensionClass = ExtensionComponentConfig
|
||||
"""
|
||||
Deprecated. Will be removed in v1.0. Use
|
||||
[`ComponentConfig`](../api#django_components.ExtensionComponentConfig) instead.
|
||||
"""
|
||||
|
||||
|
||||
# TODO_V1 - Delete, meta class was needed only for backwards support for ExtensionClass.
|
||||
class ExtensionMeta(type):
|
||||
def __new__(mcs, name: Any, bases: Tuple, attrs: Dict) -> Any:
|
||||
# Rename `ExtensionClass` to `ComponentConfig`
|
||||
if "ExtensionClass" in attrs:
|
||||
attrs["ComponentConfig"] = attrs.pop("ExtensionClass")
|
||||
|
||||
return super().__new__(mcs, name, bases, attrs)
|
||||
|
||||
|
||||
# NOTE: This class is used for generating documentation for the extension hooks API.
|
||||
# To be recognized, all hooks must start with `on_` prefix.
|
||||
class ComponentExtension:
|
||||
class ComponentExtension(metaclass=ExtensionMeta):
|
||||
"""
|
||||
Base class for all extensions.
|
||||
|
||||
Read more on [Extensions](../../concepts/advanced/extensions).
|
||||
|
||||
**Example:**
|
||||
|
||||
```python
|
||||
class ExampleExtension(ComponentExtension):
|
||||
name = "example"
|
||||
|
||||
# Component-level behavior and settings. User will be able to override
|
||||
# the attributes and methods defined here on the component classes.
|
||||
class ComponentConfig(ComponentExtension.ComponentConfig):
|
||||
foo = "1"
|
||||
bar = "2"
|
||||
|
||||
def baz(cls):
|
||||
return "3"
|
||||
|
||||
# URLs
|
||||
urls = [
|
||||
URLRoute(path="dummy-view/", handler=dummy_view, name="dummy"),
|
||||
URLRoute(path="dummy-view-2/<int:id>/<str:name>/", handler=dummy_view_2, name="dummy-2"),
|
||||
]
|
||||
|
||||
# Commands
|
||||
commands = [
|
||||
HelloWorldCommand,
|
||||
]
|
||||
|
||||
# Hooks
|
||||
def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
|
||||
print(ctx.component_cls.__name__)
|
||||
|
||||
def on_component_class_deleted(self, ctx: OnComponentClassDeletedContext) -> None:
|
||||
print(ctx.component_cls.__name__)
|
||||
```
|
||||
|
||||
Which users then can override on a per-component basis. E.g.:
|
||||
|
||||
```python
|
||||
class MyComp(Component):
|
||||
class Example:
|
||||
foo = "overridden"
|
||||
|
||||
def baz(self):
|
||||
return "overridden baz"
|
||||
```
|
||||
"""
|
||||
|
||||
###########################
|
||||
# USER INPUT
|
||||
###########################
|
||||
|
||||
name: str
|
||||
name: ClassVar[str]
|
||||
"""
|
||||
Name of the extension.
|
||||
|
||||
Name must be lowercase, and must be a valid Python identifier (e.g. `"my_extension"`).
|
||||
|
||||
The extension may add new features to the [`Component`](../api#django_components.Component)
|
||||
class by allowing users to define and access a nested class in the `Component` class.
|
||||
class by allowing users to define and access a nested class in
|
||||
the [`Component`](../api#django_components.Component) class.
|
||||
|
||||
The extension name determines the name of the nested class in the `Component` class, and the attribute
|
||||
The extension name determines the name of the nested class in
|
||||
the [`Component`](../api#django_components.Component) class, and the attribute
|
||||
under which the extension will be accessible.
|
||||
|
||||
E.g. if the extension name is `"my_extension"`, then the nested class in the `Component` class
|
||||
will be `MyExtension`, and the extension will be accessible as `MyComp.my_extension`.
|
||||
E.g. if the extension name is `"my_extension"`, then the nested class in
|
||||
the [`Component`](../api#django_components.Component) class will be
|
||||
`MyExtension`, and the extension will be accessible as `MyComp.my_extension`.
|
||||
|
||||
```python
|
||||
class MyComp(Component):
|
||||
|
@ -202,13 +332,20 @@ class ComponentExtension:
|
|||
"my_extension": self.my_extension.do_something(),
|
||||
}
|
||||
```
|
||||
|
||||
!!! info
|
||||
|
||||
The extension class name can be customized by setting
|
||||
the [`class_name`](../api#django_components.ComponentExtension.class_name) attribute.
|
||||
"""
|
||||
|
||||
class_name: str
|
||||
class_name: ClassVar[str]
|
||||
"""
|
||||
Name of the extension class.
|
||||
|
||||
By default, this is the same as `name`, but with snake_case converted to PascalCase.
|
||||
By default, this is set automatically at class creation. The class name is the same as
|
||||
the [`name`](../api#django_components.ComponentExtension.name) attribute, but with snake_case
|
||||
converted to PascalCase.
|
||||
|
||||
So if the extension name is `"my_extension"`, then the extension class name will be `"MyExtension"`.
|
||||
|
||||
|
@ -217,20 +354,41 @@ class ComponentExtension:
|
|||
class MyExtension: # <--- This is the extension class
|
||||
...
|
||||
```
|
||||
|
||||
To customize the class name, you can manually set the `class_name` attribute.
|
||||
|
||||
The class name must be a valid Python identifier.
|
||||
|
||||
**Example:**
|
||||
|
||||
```python
|
||||
class MyExt(ComponentExtension):
|
||||
name = "my_extension"
|
||||
class_name = "MyCustomExtension"
|
||||
```
|
||||
|
||||
This will make the extension class name `"MyCustomExtension"`.
|
||||
|
||||
```python
|
||||
class MyComp(Component):
|
||||
class MyCustomExtension: # <--- This is the extension class
|
||||
...
|
||||
```
|
||||
"""
|
||||
|
||||
ExtensionClass = BaseExtensionClass
|
||||
ComponentConfig: ClassVar[Type[ExtensionComponentConfig]] = ExtensionComponentConfig
|
||||
"""
|
||||
Base class that the "extension class" nested within a [`Component`](../api#django_components.Component)
|
||||
class will inherit from.
|
||||
Base class that the "component-level" extension config nested within
|
||||
a [`Component`](../api#django_components.Component) class will inherit from.
|
||||
|
||||
This is where you can define new methods and attributes that will be available to the component
|
||||
instance.
|
||||
|
||||
Background:
|
||||
|
||||
The extension may add new features to the `Component` class by allowing users to
|
||||
define and access a nested class in the `Component` class. E.g.:
|
||||
The extension may add new features to the [`Component`](../api#django_components.Component) class
|
||||
by allowing users to define and access a nested class in
|
||||
the [`Component`](../api#django_components.Component) class. E.g.:
|
||||
|
||||
```python
|
||||
class MyComp(Component):
|
||||
|
@ -243,19 +401,20 @@ class ComponentExtension:
|
|||
}
|
||||
```
|
||||
|
||||
When rendering a component, the nested extension class will be set as a subclass of `ExtensionClass`.
|
||||
So it will be same as if the user had directly inherited from `ExtensionClass`. E.g.:
|
||||
When rendering a component, the nested extension class will be set as a subclass of
|
||||
`ComponentConfig`. So it will be same as if the user had directly inherited from extension's
|
||||
`ComponentConfig`. E.g.:
|
||||
|
||||
```python
|
||||
class MyComp(Component):
|
||||
class MyExtension(ComponentExtension.ExtensionClass):
|
||||
class MyExtension(ComponentExtension.ComponentConfig):
|
||||
...
|
||||
```
|
||||
|
||||
This setting decides what the extension class will inherit from.
|
||||
"""
|
||||
|
||||
commands: List[Type[ComponentCommand]] = []
|
||||
commands: ClassVar[List[Type[ComponentCommand]]] = []
|
||||
"""
|
||||
List of commands that can be run by the extension.
|
||||
|
||||
|
@ -315,7 +474,7 @@ class ComponentExtension:
|
|||
```
|
||||
"""
|
||||
|
||||
urls: List[URLRoute] = []
|
||||
urls: ClassVar[List[URLRoute]] = []
|
||||
|
||||
###########################
|
||||
# Misc
|
||||
|
@ -629,19 +788,19 @@ class ExtensionManager:
|
|||
for extension in self.extensions:
|
||||
ext_class_name = extension.class_name
|
||||
|
||||
# If a Component class has an extension class, e.g.
|
||||
# If a Component class has a nested extension class, e.g.
|
||||
# ```python
|
||||
# class MyComp(Component):
|
||||
# class MyExtension:
|
||||
# ...
|
||||
# ```
|
||||
# then create a dummy class to make `MyComp.MyExtension` extend
|
||||
# the base class `extension.ExtensionClass`.
|
||||
# the base class `extension.ComponentConfig`.
|
||||
#
|
||||
# So it will be same as if the user had directly inherited from `extension.ExtensionClass`.
|
||||
# So it will be same as if the user had directly inherited from `extension.ComponentConfig`.
|
||||
# ```python
|
||||
# class MyComp(Component):
|
||||
# class MyExtension(MyExtension.ExtensionClass):
|
||||
# class MyExtension(MyExtension.ComponentConfig):
|
||||
# ...
|
||||
# ```
|
||||
component_ext_subclass = getattr(component_cls, ext_class_name, None)
|
||||
|
@ -649,11 +808,11 @@ class ExtensionManager:
|
|||
# Add escape hatch, so that user can override the extension class
|
||||
# from within the component class. E.g.:
|
||||
# ```python
|
||||
# class MyExtDifferentStillSame(MyExtension.ExtensionClass):
|
||||
# class MyExtDifferentButStillSame(MyExtension.ComponentConfig):
|
||||
# ...
|
||||
#
|
||||
# class MyComp(Component):
|
||||
# my_extension_class = MyExtDifferentStillSame
|
||||
# my_extension_class = MyExtDifferentButStillSame
|
||||
# class MyExtension:
|
||||
# ...
|
||||
# ```
|
||||
|
@ -661,20 +820,54 @@ class ExtensionManager:
|
|||
# Will be effectively the same as:
|
||||
# ```python
|
||||
# class MyComp(Component):
|
||||
# class MyExtension(MyExtDifferentStillSame):
|
||||
# class MyExtension(MyExtDifferentButStillSame):
|
||||
# ...
|
||||
# ```
|
||||
ext_class_override_attr = extension.name + "_class" # "my_extension_class"
|
||||
ext_base_class = getattr(component_cls, ext_class_override_attr, extension.ExtensionClass)
|
||||
ext_base_class = getattr(component_cls, ext_class_override_attr, extension.ComponentConfig)
|
||||
|
||||
# Extensions have 3 levels of configuration:
|
||||
# 1. Factory defaults - The values that the extension author set on the extension class
|
||||
# 2. User global defaults with `COMPONENTS.extensions_defaults`
|
||||
# 3. User component-level settings - The values that the user set on the component class
|
||||
#
|
||||
# The component-level settings override the global defaults, which in turn override
|
||||
# the factory defaults.
|
||||
#
|
||||
# To apply these defaults, we set them as bases for our new extension class.
|
||||
#
|
||||
# The final class will look like this:
|
||||
# ```
|
||||
# class MyExtension(MyComp.MyExtension, MyExtensionDefaults, MyExtensionBase):
|
||||
# component_cls = MyComp
|
||||
# ...
|
||||
# ```
|
||||
# Where:
|
||||
# - `MyComp.MyExtension` is the extension class that the user defined on the component class.
|
||||
# - `MyExtensionDefaults` is a dummy class that holds the extension defaults from settings.
|
||||
# - `MyExtensionBase` is the base class that the extension class inherits from.
|
||||
bases_list = [ext_base_class]
|
||||
|
||||
all_extensions_defaults = app_settings._settings.extensions_defaults or {}
|
||||
extension_defaults = all_extensions_defaults.get(extension.name, None)
|
||||
if extension_defaults:
|
||||
# Create dummy class that holds the extension defaults
|
||||
defaults_class = type(f"{ext_class_name}Defaults", tuple(), extension_defaults.copy())
|
||||
bases_list.insert(0, defaults_class)
|
||||
|
||||
if component_ext_subclass:
|
||||
bases: tuple[Type, ...] = (component_ext_subclass, ext_base_class)
|
||||
else:
|
||||
bases = (ext_base_class,)
|
||||
bases_list.insert(0, component_ext_subclass)
|
||||
|
||||
# Allow to extension class to access the owner `Component` class that via
|
||||
# `ExtensionClass.component_class`.
|
||||
component_ext_subclass = type(ext_class_name, bases, {"component_class": component_cls})
|
||||
bases: tuple[Type, ...] = tuple(bases_list)
|
||||
|
||||
# Allow component-level extension class to access the owner `Component` class that via
|
||||
# `component_cls`.
|
||||
component_ext_subclass = type(
|
||||
ext_class_name,
|
||||
bases,
|
||||
# TODO_v1 - Remove `component_class`, superseded by `component_cls`
|
||||
{"component_cls": component_cls, "component_class": component_cls},
|
||||
)
|
||||
|
||||
# Finally, reassign the new class extension class on the component class.
|
||||
setattr(component_cls, ext_class_name, component_ext_subclass)
|
||||
|
|
|
@ -5,6 +5,7 @@ from django.core.cache import BaseCache, caches
|
|||
|
||||
from django_components.extension import (
|
||||
ComponentExtension,
|
||||
ExtensionComponentConfig,
|
||||
OnComponentInputContext,
|
||||
OnComponentRenderedContext,
|
||||
)
|
||||
|
@ -15,7 +16,7 @@ from django_components.slots import Slot
|
|||
CACHE_KEY_PREFIX = "components:cache:"
|
||||
|
||||
|
||||
class ComponentCache(ComponentExtension.ExtensionClass): # type: ignore
|
||||
class ComponentCache(ExtensionComponentConfig):
|
||||
"""
|
||||
The interface for `Component.Cache`.
|
||||
|
||||
|
@ -172,7 +173,7 @@ class CacheExtension(ComponentExtension):
|
|||
|
||||
name = "cache"
|
||||
|
||||
ExtensionClass = ComponentCache
|
||||
ComponentConfig = ComponentCache
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any):
|
||||
self.render_id_to_cache_key: dict[str, str] = {}
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
from typing import Any, Literal, NamedTuple, Optional, Type
|
||||
|
||||
from django_components.app_settings import app_settings
|
||||
from django_components.extension import ComponentExtension, OnComponentRenderedContext, OnSlotRenderedContext
|
||||
from django_components.extension import (
|
||||
ComponentExtension,
|
||||
ExtensionComponentConfig,
|
||||
OnComponentRenderedContext,
|
||||
OnSlotRenderedContext,
|
||||
)
|
||||
from django_components.util.misc import gen_id
|
||||
|
||||
|
||||
|
@ -45,14 +50,6 @@ def apply_component_highlight(type: Literal["component", "slot"], output: str, n
|
|||
return output
|
||||
|
||||
|
||||
# TODO - Deprecate `DEBUG_HIGHLIGHT_SLOTS` and `DEBUG_HIGHLIGHT_COMPONENTS` (with removal in v1)
|
||||
# once `extension_defaults` is implemented.
|
||||
# That way people will be able to set the highlighting from single place.
|
||||
# At that point also document this extension in the docs:
|
||||
# - Exposing `ComponentDebugHighlight` from `__init__.py`
|
||||
# - Adding `Component.DebugHighlight` and `Component.debug_highlight` attributes to Component class
|
||||
# so it's easier to find.
|
||||
# - Check docstring of `ComponentDebugHighlight` in the docs and make sure it's correct.
|
||||
class HighlightComponentsDescriptor:
|
||||
def __get__(self, obj: Optional[Any], objtype: Type) -> bool:
|
||||
return app_settings.DEBUG_HIGHLIGHT_COMPONENTS
|
||||
|
@ -63,14 +60,14 @@ class HighlightSlotsDescriptor:
|
|||
return app_settings.DEBUG_HIGHLIGHT_SLOTS
|
||||
|
||||
|
||||
class ComponentDebugHighlight(ComponentExtension.ExtensionClass): # type: ignore
|
||||
class ComponentDebugHighlight(ExtensionComponentConfig):
|
||||
"""
|
||||
The interface for `Component.DebugHighlight`.
|
||||
|
||||
The fields of this class are used to configure the component debug highlighting for this component
|
||||
and its direct slots.
|
||||
|
||||
Read more about [Component debug highlighting](../../concepts/advanced/component_debug_highlighting).
|
||||
Read more about [Component debug highlighting](../../guides/other/troubleshooting#component-and-slot-highlighting).
|
||||
|
||||
**Example:**
|
||||
|
||||
|
@ -84,10 +81,22 @@ class ComponentDebugHighlight(ComponentExtension.ExtensionClass): # type: ignor
|
|||
```
|
||||
|
||||
To highlight ALL components and slots, set
|
||||
[`ComponentsSettings.DEBUG_HIGHLIGHT_SLOTS`](../../settings/components_settings.md#debug_highlight_slots) and
|
||||
[`ComponentsSettings.DEBUG_HIGHLIGHT_COMPONENTS`](../../settings/components_settings.md#debug_highlight_components)
|
||||
to `True`.
|
||||
"""
|
||||
[extension defaults](../../reference/settings/#django_components.app_settings.ComponentsSettings.extensions_defaults)
|
||||
in your settings:
|
||||
|
||||
```python
|
||||
from django_components import ComponentsSettings
|
||||
|
||||
COMPONENTS = ComponentsSettings(
|
||||
extensions_defaults={
|
||||
"debug_highlight": {
|
||||
"highlight_components": True,
|
||||
"highlight_slots": True,
|
||||
},
|
||||
},
|
||||
)
|
||||
```
|
||||
""" # noqa: E501
|
||||
|
||||
# TODO_v1 - Remove `DEBUG_HIGHLIGHT_COMPONENTS` and `DEBUG_HIGHLIGHT_SLOTS`
|
||||
# Instead set this as plain boolean fields.
|
||||
|
@ -112,7 +121,7 @@ class DebugHighlightExtension(ComponentExtension):
|
|||
"""
|
||||
|
||||
name = "debug_highlight"
|
||||
ExtensionClass = ComponentDebugHighlight
|
||||
ComponentConfig = ComponentDebugHighlight
|
||||
|
||||
# Apply highlight to the slot's rendered output
|
||||
def on_slot_rendered(self, ctx: OnSlotRenderedContext) -> Optional[str]:
|
||||
|
|
|
@ -3,7 +3,12 @@ from dataclasses import MISSING, Field, dataclass
|
|||
from typing import TYPE_CHECKING, Any, Callable, Dict, List, NamedTuple, Optional, Type
|
||||
from weakref import WeakKeyDictionary
|
||||
|
||||
from django_components.extension import ComponentExtension, OnComponentClassCreatedContext, OnComponentInputContext
|
||||
from django_components.extension import (
|
||||
ComponentExtension,
|
||||
ExtensionComponentConfig,
|
||||
OnComponentClassCreatedContext,
|
||||
OnComponentInputContext,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django_components.component import Component
|
||||
|
@ -62,7 +67,8 @@ def _extract_defaults(defaults: Optional[Type]) -> List[ComponentDefaultField]:
|
|||
for default_field_key in dir(defaults):
|
||||
# Iterate only over fields set by the user (so non-dunder fields).
|
||||
# Plus ignore `component_class` because that was set by the extension system.
|
||||
if default_field_key.startswith("__") or default_field_key == "component_class":
|
||||
# TODO_V1 - Remove `component_class`
|
||||
if default_field_key.startswith("__") or default_field_key in {"component_class", "component_cls"}:
|
||||
continue
|
||||
|
||||
default_field = getattr(defaults, default_field_key)
|
||||
|
@ -119,7 +125,7 @@ def _apply_defaults(kwargs: Dict, defaults: List[ComponentDefaultField]) -> None
|
|||
kwargs[default_field.key] = default_value
|
||||
|
||||
|
||||
class ComponentDefaults(ComponentExtension.ExtensionClass): # type: ignore[misc,valid-type]
|
||||
class ComponentDefaults(ExtensionComponentConfig):
|
||||
"""
|
||||
The interface for `Component.Defaults`.
|
||||
|
||||
|
@ -164,7 +170,7 @@ class DefaultsExtension(ComponentExtension):
|
|||
"""
|
||||
|
||||
name = "defaults"
|
||||
ExtensionClass = ComponentDefaults
|
||||
ComponentConfig = ComponentDefaults
|
||||
|
||||
# Preprocess the `Component.Defaults` class, if given, so we don't have to do it
|
||||
# each time a component is rendered.
|
||||
|
|
|
@ -8,6 +8,7 @@ from django.views.generic import View
|
|||
|
||||
from django_components.extension import (
|
||||
ComponentExtension,
|
||||
ExtensionComponentConfig,
|
||||
OnComponentClassCreatedContext,
|
||||
OnComponentClassDeletedContext,
|
||||
URLRoute,
|
||||
|
@ -74,7 +75,7 @@ def get_component_url(
|
|||
return format_url(url, query=query, fragment=fragment)
|
||||
|
||||
|
||||
class ComponentView(ComponentExtension.ExtensionClass, View): # type: ignore
|
||||
class ComponentView(ExtensionComponentConfig, View):
|
||||
"""
|
||||
The interface for `Component.View`.
|
||||
|
||||
|
@ -157,7 +158,7 @@ class ComponentView(ComponentExtension.ExtensionClass, View): # type: ignore
|
|||
"""
|
||||
|
||||
def __init__(self, component: "Component", **kwargs: Any) -> None:
|
||||
ComponentExtension.ExtensionClass.__init__(self, component)
|
||||
ComponentExtension.ComponentConfig.__init__(self, component)
|
||||
View.__init__(self, **kwargs)
|
||||
|
||||
@property
|
||||
|
@ -257,7 +258,7 @@ class ViewExtension(ComponentExtension):
|
|||
|
||||
name = "view"
|
||||
|
||||
ExtensionClass = ComponentView
|
||||
ComponentConfig = ComponentView
|
||||
|
||||
def __init__(self) -> None:
|
||||
# Remember which route belongs to which component
|
||||
|
|
|
@ -82,7 +82,13 @@ class TestComponentHighlight:
|
|||
assert COLORS["slot"].text_color in result
|
||||
assert COLORS["slot"].border_color in result
|
||||
|
||||
@djc_test(components_settings={"debug_highlight_components": True})
|
||||
@djc_test(
|
||||
components_settings={
|
||||
"extensions_defaults": {
|
||||
"debug_highlight": {"highlight_components": True},
|
||||
},
|
||||
}
|
||||
)
|
||||
def test_component_highlight_extension(self):
|
||||
template = _prepare_template()
|
||||
rendered = template.render(Context({"items": [1, 2]}))
|
||||
|
@ -151,7 +157,83 @@ class TestComponentHighlight:
|
|||
"""
|
||||
assertHTMLEqual(rendered, expected)
|
||||
|
||||
@djc_test(components_settings={"debug_highlight_slots": True})
|
||||
# TODO_v1 - Remove this test once we've removed the `debug_highlight_components` setting.
|
||||
@djc_test(components_settings={"debug_highlight_components": True})
|
||||
def test_component_highlight_extension__legacy(self):
|
||||
template = _prepare_template()
|
||||
rendered = template.render(Context({"items": [1, 2]}))
|
||||
|
||||
expected = """
|
||||
<div class="item">
|
||||
<style>
|
||||
.component-highlight-a1bc45::before {
|
||||
content: "outer (ca1bc3f): ";
|
||||
font-weight: bold;
|
||||
color: #2f14bb;
|
||||
}
|
||||
</style>
|
||||
<div class="component-highlight-a1bc45" style="border: 1px solid blue">
|
||||
<div class="outer" data-djc-id-ca1bc3f="">
|
||||
<style>
|
||||
.component-highlight-a1bc44::before {
|
||||
content: "inner (ca1bc41): ";
|
||||
font-weight: bold;
|
||||
color: #2f14bb;
|
||||
}
|
||||
</style>
|
||||
<div class="component-highlight-a1bc44" style="border: 1px solid blue">
|
||||
<div class="inner" data-djc-id-ca1bc41="">
|
||||
<div>
|
||||
1: 1
|
||||
</div>
|
||||
<div>
|
||||
2: 1
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<style>
|
||||
.component-highlight-a1bc49::before {
|
||||
content: "outer (ca1bc46): ";
|
||||
font-weight: bold;
|
||||
color: #2f14bb;
|
||||
}
|
||||
</style>
|
||||
<div class="component-highlight-a1bc49" style="border: 1px solid blue">
|
||||
<div class="outer" data-djc-id-ca1bc46="">
|
||||
<style>
|
||||
.component-highlight-a1bc48::before {
|
||||
content: "inner (ca1bc47): ";
|
||||
font-weight: bold;
|
||||
color: #2f14bb;
|
||||
}
|
||||
</style>
|
||||
<div class="component-highlight-a1bc48" style="border: 1px solid blue">
|
||||
<div class="inner" data-djc-id-ca1bc47="">
|
||||
<div>
|
||||
1: 2
|
||||
</div>
|
||||
<div>
|
||||
2: 2
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
assertHTMLEqual(rendered, expected)
|
||||
|
||||
@djc_test(
|
||||
components_settings={
|
||||
"extensions_defaults": {
|
||||
"debug_highlight": {"highlight_slots": True},
|
||||
},
|
||||
}
|
||||
)
|
||||
def test_slot_highlight_extension(self):
|
||||
template = _prepare_template()
|
||||
rendered = template.render(Context({"items": [1, 2]}))
|
||||
|
@ -224,6 +306,80 @@ class TestComponentHighlight:
|
|||
"""
|
||||
assertHTMLEqual(rendered, expected)
|
||||
|
||||
# TODO_v1 - Remove this test once we've removed the `debug_highlight_slots` setting.
|
||||
@djc_test(components_settings={"debug_highlight_slots": True})
|
||||
def test_slot_highlight_extension__legacy(self):
|
||||
template = _prepare_template()
|
||||
rendered = template.render(Context({"items": [1, 2]}))
|
||||
|
||||
expected = """
|
||||
<div class="item">
|
||||
<div class="outer" data-djc-id-ca1bc3f="">
|
||||
<div class="inner" data-djc-id-ca1bc41="">
|
||||
<div>
|
||||
1:
|
||||
<style>
|
||||
.slot-highlight-a1bc44::before {
|
||||
content: "InnerComponent - content: ";
|
||||
font-weight: bold;
|
||||
color: #bb1414;
|
||||
}
|
||||
</style>
|
||||
<div class="slot-highlight-a1bc44" style="border: 1px solid #e40c0c">
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
2:
|
||||
<style>
|
||||
.slot-highlight-a1bc45::before {
|
||||
content: "InnerComponent - content: ";
|
||||
font-weight: bold;
|
||||
color: #bb1414;
|
||||
}
|
||||
</style>
|
||||
<div class="slot-highlight-a1bc45" style="border: 1px solid #e40c0c">
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="outer" data-djc-id-ca1bc46="">
|
||||
<div class="inner" data-djc-id-ca1bc47="">
|
||||
<div>
|
||||
1:
|
||||
<style>
|
||||
.slot-highlight-a1bc48::before {
|
||||
content: "InnerComponent - content: ";
|
||||
font-weight: bold;
|
||||
color: #bb1414;
|
||||
}
|
||||
</style>
|
||||
<div class="slot-highlight-a1bc48" style="border: 1px solid #e40c0c">
|
||||
2
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
2:
|
||||
<style>
|
||||
.slot-highlight-a1bc49::before {
|
||||
content: "InnerComponent - content: ";
|
||||
font-weight: bold;
|
||||
color: #bb1414;
|
||||
}
|
||||
</style>
|
||||
<div class="slot-highlight-a1bc49" style="border: 1px solid #e40c0c">
|
||||
2
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
assertHTMLEqual(rendered, expected)
|
||||
|
||||
def test_highlight_on_component_class(self):
|
||||
@register("inner")
|
||||
class InnerComponent(Component):
|
||||
|
|
|
@ -12,6 +12,7 @@ from django_components.component_registry import ComponentRegistry
|
|||
from django_components.extension import (
|
||||
URLRoute,
|
||||
ComponentExtension,
|
||||
ExtensionComponentConfig,
|
||||
OnComponentClassCreatedContext,
|
||||
OnComponentClassDeletedContext,
|
||||
OnRegistryCreatedContext,
|
||||
|
@ -43,6 +44,19 @@ def dummy_view_2(request: HttpRequest, id: int, name: str):
|
|||
return HttpResponse(f"Hello, world! {id} {name}")
|
||||
|
||||
|
||||
# TODO_V1 - Remove
|
||||
class LegacyExtension(ComponentExtension):
|
||||
name = "legacy"
|
||||
|
||||
class ExtensionClass(ExtensionComponentConfig):
|
||||
foo = "1"
|
||||
bar = "2"
|
||||
|
||||
@classmethod
|
||||
def baz(cls):
|
||||
return "3"
|
||||
|
||||
|
||||
class DummyExtension(ComponentExtension):
|
||||
"""
|
||||
Test extension that tracks all hook calls and their arguments.
|
||||
|
@ -50,6 +64,14 @@ class DummyExtension(ComponentExtension):
|
|||
|
||||
name = "test_extension"
|
||||
|
||||
class ComponentConfig(ExtensionComponentConfig):
|
||||
foo = "1"
|
||||
bar = "2"
|
||||
|
||||
@classmethod
|
||||
def baz(cls):
|
||||
return "3"
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.calls: Dict[str, List[Any]] = {
|
||||
"on_component_class_created": [],
|
||||
|
@ -154,7 +176,7 @@ class TestExtension:
|
|||
return {"name": kwargs.get("name", "World")}
|
||||
|
||||
ext_class = TestAccessComp.TestExtension # type: ignore[attr-defined]
|
||||
assert issubclass(ext_class, ComponentExtension.ExtensionClass)
|
||||
assert issubclass(ext_class, ComponentExtension.ComponentConfig)
|
||||
assert ext_class.component_class is TestAccessComp
|
||||
|
||||
# NOTE: Required for test_component_class_lifecycle_hooks to work
|
||||
|
@ -371,3 +393,64 @@ class TestExtensionViews:
|
|||
response2 = client.get("/components/ext/test_nested_extension/nested-view/123/John/")
|
||||
assert response2.status_code == 200
|
||||
assert response2.content == b"Hello, world! 123 John"
|
||||
|
||||
|
||||
@djc_test
|
||||
class TestExtensionDefaults:
|
||||
@djc_test(
|
||||
components_settings={
|
||||
"extensions": [DummyExtension],
|
||||
"extensions_defaults": {
|
||||
"test_extension": {},
|
||||
},
|
||||
}
|
||||
)
|
||||
def test_no_defaults(self):
|
||||
class TestComponent(Component):
|
||||
template = "Hello"
|
||||
|
||||
dummy_ext_cls: DummyExtension.ComponentConfig = TestComponent.TestExtension # type: ignore[attr-defined]
|
||||
assert dummy_ext_cls.foo == "1"
|
||||
assert dummy_ext_cls.bar == "2"
|
||||
assert dummy_ext_cls.baz() == "3"
|
||||
|
||||
@djc_test(
|
||||
components_settings={
|
||||
"extensions": [DummyExtension],
|
||||
"extensions_defaults": {
|
||||
"test_extension": {
|
||||
"foo": "NEW_FOO",
|
||||
"baz": classmethod(lambda self: "OVERRIDEN"),
|
||||
},
|
||||
"nonexistent": {
|
||||
"1": "2",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
def test_defaults(self):
|
||||
class TestComponent(Component):
|
||||
template = "Hello"
|
||||
|
||||
dummy_ext_cls: DummyExtension.ComponentConfig = TestComponent.TestExtension # type: ignore[attr-defined]
|
||||
assert dummy_ext_cls.foo == "NEW_FOO"
|
||||
assert dummy_ext_cls.bar == "2"
|
||||
assert dummy_ext_cls.baz() == "OVERRIDEN"
|
||||
|
||||
|
||||
@djc_test
|
||||
class TestLegacyApi:
|
||||
# TODO_V1 - Remove
|
||||
@djc_test(
|
||||
components_settings={
|
||||
"extensions": [LegacyExtension],
|
||||
}
|
||||
)
|
||||
def test_extension_class(self):
|
||||
class TestComponent(Component):
|
||||
template = "Hello"
|
||||
|
||||
dummy_ext_cls: LegacyExtension.ExtensionClass = TestComponent.Legacy # type: ignore[attr-defined]
|
||||
assert dummy_ext_cls.foo == "1"
|
||||
assert dummy_ext_cls.bar == "2"
|
||||
assert dummy_ext_cls.baz() == "3"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue