mirror of
https://github.com/django-components/django-components.git
synced 2025-08-31 03:07:19 +00:00
feat: extensions (#1009)
* feat: extensions * refactor: remove support for passing in extensions as instances
This commit is contained in:
parent
cff252c566
commit
4d35bc97a2
24 changed files with 1884 additions and 57 deletions
|
@ -8,5 +8,6 @@ nav:
|
|||
- Typing and validation: typing_and_validation.md
|
||||
- Custom template tags: template_tags.md
|
||||
- Tag formatters: tag_formatter.md
|
||||
- Extensions: extensions.md
|
||||
- Testing: testing.md
|
||||
- Authoring component libraries: authoring_component_libraries.md
|
||||
|
|
330
docs/concepts/advanced/extensions.md
Normal file
330
docs/concepts/advanced/extensions.md
Normal file
|
@ -0,0 +1,330 @@
|
|||
_New in version 0.131_
|
||||
|
||||
Django-components functionality can be extended with "extensions". Extensions allow for powerful customization and integrations. They can:
|
||||
|
||||
- Tap into lifecycle events, such as when a component is created, deleted, registered, or unregistered.
|
||||
- Add new attributes and methods to the components under an extension-specific nested class.
|
||||
|
||||
## Setting up extensions
|
||||
|
||||
Extensions are configured in the Django settings under [`COMPONENTS.extensions`](../../../reference/settings#django_components.app_settings.ComponentsSettings.extensions).
|
||||
|
||||
Extensions can be set by either as an import string or by passing in a class:
|
||||
|
||||
```python
|
||||
# settings.py
|
||||
|
||||
class MyExtension(ComponentsExtension):
|
||||
name = "my_extension"
|
||||
|
||||
class ExtensionClass(BaseExtensionClass):
|
||||
...
|
||||
|
||||
COMPONENTS = ComponentsSettings(
|
||||
extensions=[
|
||||
MyExtension,
|
||||
"another_app.extensions.AnotherExtension",
|
||||
"my_app.extensions.ThirdExtension",
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
## Lifecycle hooks
|
||||
|
||||
Extensions can define methods to hook into lifecycle events, such as:
|
||||
|
||||
- Component creation or deletion
|
||||
- Un/registering a component
|
||||
- Creating or deleting a registry
|
||||
- Pre-processing data passed to a component on render
|
||||
- Post-processing data returned from [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data)
|
||||
and others.
|
||||
|
||||
See the full list in [Extension Hooks Reference](../../../reference/extension_hooks).
|
||||
|
||||
## Configuring extensions per component
|
||||
|
||||
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.
|
||||
|
||||
!!! note
|
||||
|
||||
**Accessing the component instance from inside the nested classes:**
|
||||
|
||||
Each method of the nested classes has access to the `component` attribute,
|
||||
which points to the component instance.
|
||||
|
||||
```python
|
||||
class MyTable(Component):
|
||||
class View:
|
||||
def get(self, request):
|
||||
# `self.component` points to the instance of `MyTable` Component.
|
||||
return self.component.get(request)
|
||||
```
|
||||
|
||||
### Example: Component as View
|
||||
|
||||
The [Components as Views](../../fundamentals/components_as_views) feature is actually implemented as an extension
|
||||
that is configured by a `View` nested class.
|
||||
|
||||
You can override the `get`, `post`, etc methods to customize the behavior of the component as a view:
|
||||
|
||||
```python
|
||||
class MyTable(Component):
|
||||
class View:
|
||||
def get(self, request):
|
||||
return self.component.get(request)
|
||||
|
||||
def post(self, request):
|
||||
return self.component.post(request)
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
### Example: Storybook integration
|
||||
|
||||
The Storybook integration (work in progress) is an extension that is configured by a `Storybook` nested class.
|
||||
|
||||
You can override methods such as `title`, `parameters`, etc, to customize how to generate a Storybook
|
||||
JSON file from the component.
|
||||
|
||||
```python
|
||||
class MyTable(Component):
|
||||
class Storybook:
|
||||
def title(self):
|
||||
return self.component.__class__.__name__
|
||||
|
||||
def parameters(self) -> Parameters:
|
||||
return {
|
||||
"server": {
|
||||
"id": self.component.__class__.__name__,
|
||||
}
|
||||
}
|
||||
|
||||
def stories(self) -> List[StoryAnnotations]:
|
||||
return []
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
## Accessing extensions in components
|
||||
|
||||
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.
|
||||
|
||||
For example, the View extension is available as `self.view`:
|
||||
|
||||
```python
|
||||
class MyTable(Component):
|
||||
def get_context_data(self, request):
|
||||
# `self.view` points to the instance of `View` extension.
|
||||
return {
|
||||
"view": self.view,
|
||||
}
|
||||
```
|
||||
|
||||
And the Storybook extension is available as `self.storybook`:
|
||||
|
||||
```python
|
||||
class MyTable(Component):
|
||||
def get_context_data(self, request):
|
||||
# `self.storybook` points to the instance of `Storybook` extension.
|
||||
return {
|
||||
"title": self.storybook.title(),
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
```python
|
||||
from django_components.extension import ComponentExtension, OnComponentClassCreatedContext
|
||||
|
||||
class MyExtension(ComponentExtension):
|
||||
name = "my_extension"
|
||||
|
||||
def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
|
||||
# Custom logic for when a component class is created
|
||||
ctx.component_cls.my_attr = "my_value"
|
||||
```
|
||||
|
||||
### Defining the extension class
|
||||
|
||||
In previous sections we've seen the `View` and `Storybook` extensions classes that were nested within the `Component` class:
|
||||
|
||||
```python
|
||||
class MyComponent(Component):
|
||||
class View:
|
||||
...
|
||||
|
||||
class Storybook:
|
||||
...
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
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:
|
||||
|
||||
```python
|
||||
from django_components.extension import ComponentExtension, BaseExtensionClass
|
||||
|
||||
class ViewExtension(ComponentExtension):
|
||||
name = "view"
|
||||
|
||||
# The default behavior of the `View` extension class.
|
||||
class ExtensionClass(BaseExtensionClass):
|
||||
def get(self, request):
|
||||
return self.component.get(request)
|
||||
|
||||
def post(self, request):
|
||||
return self.component.post(request)
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
In any component that then defines a nested `View` extension class, the `View` extension class will actually
|
||||
subclass from the `ViewExtension.ExtensionClass` class.
|
||||
|
||||
In other words, when you define a component like this:
|
||||
|
||||
```python
|
||||
class MyTable(Component):
|
||||
class View:
|
||||
def get(self, request):
|
||||
# Do something
|
||||
...
|
||||
```
|
||||
|
||||
It will actually be implemented as if the `View` class subclassed from base class `ViewExtension.ExtensionClass`:
|
||||
|
||||
```python
|
||||
class MyTable(Component):
|
||||
class View(ViewExtension.ExtensionClass):
|
||||
def get(self, request):
|
||||
# Do something
|
||||
...
|
||||
```
|
||||
|
||||
!!! warning
|
||||
|
||||
When writing an extension, the `ExtensionClass` MUST subclass the base class [`BaseExtensionClass`](../../../reference/api/#django_components.ComponentExtension.BaseExtensionClass).
|
||||
|
||||
This base class ensures that the extension class will have access to the component instance.
|
||||
|
||||
### Registering extensions
|
||||
|
||||
Once the extension is defined, it needs to be registered in the Django settings to be used by the application.
|
||||
|
||||
Extensions can be given either as an extension class, or its import string:
|
||||
|
||||
```python
|
||||
# settings.py
|
||||
COMPONENTS = {
|
||||
"extensions": [
|
||||
"my_app.extensions.MyExtension",
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
Or by reference:
|
||||
|
||||
```python
|
||||
# settings.py
|
||||
from my_app.extensions import MyExtension
|
||||
|
||||
COMPONENTS = {
|
||||
"extensions": [
|
||||
MyExtension,
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
### Full example: Custom logging extension
|
||||
|
||||
To tie it all together, here's an example of a custom logging extension that logs when components are created, deleted, or rendered:
|
||||
|
||||
- Each component can specify which color to use for the logging by setting `Component.ColorLogger.color`.
|
||||
- The extension will log the component name and color when the component is created, deleted, or rendered.
|
||||
|
||||
```python
|
||||
from django_components.extension import (
|
||||
ComponentExtension,
|
||||
OnComponentClassCreatedContext,
|
||||
OnComponentClassDeletedContext,
|
||||
OnComponentInputContext,
|
||||
)
|
||||
|
||||
class ColorLoggerExtensionClass(BaseExtensionClass):
|
||||
color: str
|
||||
|
||||
|
||||
class ColorLoggerExtension(ComponentExtension):
|
||||
name = "color_logger"
|
||||
|
||||
# All `Component.ColorLogger` classes will inherit from this class.
|
||||
ExtensionClass = ColorLoggerExtensionClass
|
||||
|
||||
# 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:
|
||||
log.info(
|
||||
f"Component {ctx.component_cls} created.",
|
||||
color=ctx.component_cls.ColorLogger.color,
|
||||
)
|
||||
|
||||
def on_component_class_deleted(self, ctx: OnComponentClassDeletedContext) -> None:
|
||||
log.info(
|
||||
f"Component {ctx.component_cls} deleted.",
|
||||
color=ctx.component_cls.ColorLogger.color,
|
||||
)
|
||||
|
||||
# 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:
|
||||
log.info(
|
||||
f"Rendering component {ctx.component_cls}.",
|
||||
color=ctx.component.color_logger.color,
|
||||
)
|
||||
```
|
||||
|
||||
To use the `ColorLoggerExtension`, add it to your settings:
|
||||
|
||||
```python
|
||||
# settings.py
|
||||
COMPONENTS = {
|
||||
"extensions": [
|
||||
ColorLoggerExtension,
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
Once registered, in any component, you can define a `ColorLogger` attribute:
|
||||
|
||||
```python
|
||||
class MyComponent(Component):
|
||||
class ColorLogger:
|
||||
color = "red"
|
||||
```
|
||||
|
||||
This will log the component name and color when the component is created, deleted, or rendered.
|
Loading…
Add table
Add a link
Reference in a new issue