Extensions
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.
- Define custom commands that can be executed via the Django management command interface.
Setting up extensionsยค
Extensions are configured in the Django settings under COMPONENTS.extensions
.
Extensions can be set by either as an import string or by passing in a class:
# settings.py
class MyExtension(ComponentExtension):
name = "my_extension"
class ExtensionClass(ComponentExtension.ExtensionClass):
...
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()
and others.
See the full list in Extension Hooks Reference.
Configuring extensions per componentยค
Each extension has a corresponding nested class within the 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.
Example: Component as Viewยค
The 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:
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.
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
:
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
:
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
. 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
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.
- Each hook method receives a context object with relevant data.
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:
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
.
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:
from django_components.extension import ComponentExtension
class ViewExtension(ComponentExtension):
name = "view"
# The default behavior of the `View` extension class.
class ExtensionClass(ComponentExtension.ExtensionClass):
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:
It will actually be implemented as if the View
class subclassed from base class ViewExtension.ExtensionClass
:
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 ComponentExtension.ExtensionClass
.
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:
Or by reference:
# 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.
from django_components.extension import (
ComponentExtension,
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
# 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:
Once registered, in any component, you can define a ColorLogger
attribute:
This will log the component name and color when the component is created, deleted, or rendered.
Utility functionsยค
django-components provides a few utility functions to help with writing extensions:
all_components()
- returns a list of all created component classes.all_registries()
- returns a list of all created registry instances.
Accessing the component class from within an extensionยค
When you are writing the extension class that will be nested inside a Component class, e.g.
You can access the owner Component class (MyTable
) from within methods of the extension class (MyExtension
) by using the component_class
attribute:
Here is how the component_class
attribute may be used with our ColorLogger
extension shown above:
class ColorLoggerExtensionClass(ComponentExtension.ExtensionClass):
color: str
def log(self, msg: str) -> None:
print(f"{self.component_class.name}: {msg}")
class ColorLoggerExtension(ComponentExtension):
name = "color_logger"
# All `Component.ColorLogger` classes will inherit from this class.
ExtensionClass = ColorLoggerExtensionClass
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.
For example, if you have an extension that defines a command that prints "Hello world", you can run the command with:
Where:
python manage.py components
- is the Django entrypointext run
- is the subcommand to run extension commandsmy_ext
- is the extension namehello
- is the command name
Defining Commandsยค
To define a command, subclass from ComponentCommand
. This subclass should define:
name
- the command's namehelp
- the command's help texthandle
- the logic to execute when the command is run
from django_components import ComponentCommand, ComponentExtension
class HelloCommand(ComponentCommand):
name = "hello"
help = "Say hello"
def handle(self, *args, **kwargs):
print("Hello, world!")
class MyExt(ComponentExtension):
name = "my_ext"
commands = [HelloCommand]
Defining Command Arguments and Optionsยค
Commands can accept positional arguments and options (e.g. --foo
), which are defined using the arguments
attribute of the ComponentCommand
class.
The arguments are parsed with argparse
into a dictionary of arguments and options. These are then available as keyword arguments to the handle
method of the command.
from django_components import CommandArg, ComponentCommand, ComponentExtension
class HelloCommand(ComponentCommand):
name = "hello"
help = "Say hello"
arguments = [
# Positional argument
CommandArg(
name_or_flags="name",
help="The name to say hello to",
),
# Optional argument
CommandArg(
name_or_flags=["--shout", "-s"],
action="store_true",
help="Shout the hello",
),
]
def handle(self, name: str, *args, **kwargs):
shout = kwargs.get("shout", False)
msg = f"Hello, {name}!"
if shout:
msg = msg.upper()
print(msg)
You can run the command with arguments and options:
Note
Command definitions are parsed with argparse
, so you can use all the features of argparse
to define your arguments and options.
See the argparse documentation for more information.
django-components defines types as CommandArg
, CommandArgGroup
, CommandSubcommand
, and CommandParserInput
to help with type checking.
Note
If a command doesn't have the handle
method defined, the command will print a help message and exit.
Grouping Argumentsยค
Arguments can be grouped using CommandArgGroup
to provide better organization and help messages.
Read more on argparse argument groups.
from django_components import CommandArg, CommandArgGroup, ComponentCommand, ComponentExtension
class HelloCommand(ComponentCommand):
name = "hello"
help = "Say hello"
# Argument parsing is managed by `argparse`.
arguments = [
# Positional argument
CommandArg(
name_or_flags="name",
help="The name to say hello to",
),
# Optional argument
CommandArg(
name_or_flags=["--shout", "-s"],
action="store_true",
help="Shout the hello",
),
# When printing the command help message, `--bar` and `--baz`
# will be grouped under "group bar".
CommandArgGroup(
title="group bar",
description="Group description.",
arguments=[
CommandArg(
name_or_flags="--bar",
help="Bar description.",
),
CommandArg(
name_or_flags="--baz",
help="Baz description.",
),
],
),
]
def handle(self, name: str, *args, **kwargs):
shout = kwargs.get("shout", False)
msg = f"Hello, {name}!"
if shout:
msg = msg.upper()
print(msg)
Subcommandsยค
Extensions can define subcommands, allowing for more complex command structures.
Subcommands are defined similarly to root commands, as subclasses of ComponentCommand
class.
However, instead of defining the subcommands in the commands
attribute of the extension, you define them in the subcommands
attribute of the parent command:
from django_components import CommandArg, CommandArgGroup, ComponentCommand, ComponentExtension
class ChildCommand(ComponentCommand):
name = "child"
help = "Child command"
def handle(self, *args, **kwargs):
print("Child command")
class ParentCommand(ComponentCommand):
name = "parent"
help = "Parent command"
subcommands = [
ChildCommand,
]
def handle(self, *args, **kwargs):
print("Parent command")
In this example, we can run two commands.
Either the parent command:
Or the child command:
Warning
Subcommands are independent of the parent command. When a subcommand runs, the parent command is NOT executed.
As such, if you want to pass arguments to both the parent and child commands, e.g.:
You should instead pass all the arguments to the subcommand:
Print command helpยค
By default, all commands will print their help message when run with the --help
/ -h
flag.
The help message prints out all the arguments and options available for the command, as well as any subcommands.
Testing Commandsยค
Commands can be tested using Django's call_command()
function, which allows you to simulate running the command in tests.
from django.core.management import call_command
call_command('components', 'ext', 'run', 'my_ext', 'hello', '--name', 'John')
To capture the output of the command, you can use the StringIO
module to redirect the output to a string:
from io import StringIO
out = StringIO()
with patch("sys.stdout", new=out):
call_command('components', 'ext', 'run', 'my_ext', 'hello', '--name', 'John')
output = out.getvalue()
And to temporarily set the extensions, you can use the @djc_test
decorator.
Thus, a full test example can then look like this:
from io import StringIO
from unittest.mock import patch
from django.core.management import call_command
from django_components.testing import djc_test
@djc_test(
components_settings={
"extensions": [
"my_app.extensions.MyExtension",
],
},
)
def test_hello_command(self):
out = StringIO()
with patch("sys.stdout", new=out):
call_command('components', 'ext', 'run', 'my_ext', 'hello', '--name', 'John')
output = out.getvalue()
assert output == "Hello, John!\n"
Extension URLsยค
Extensions can define custom views and endpoints that can be accessed through the Django application.
To define URLs for an extension, set them in the urls
attribute of your ComponentExtension
class. Each URL is defined using the URLRoute
class, which specifies the path, handler, and optional name for the route.
Here's an example of how to define URLs within an extension:
from django_components.extension import ComponentExtension, URLRoute
from django.http import HttpResponse
def my_view(request):
return HttpResponse("Hello from my extension!")
class MyExtension(ComponentExtension):
name = "my_extension"
urls = [
URLRoute(path="my-view/", handler=my_view, name="my_view"),
URLRoute(path="another-view/<int:id>/", handler=my_view, name="another_view"),
]
Warning
The URLRoute
objects are different from objects created with Django's django.urls.path()
. Do NOT use URLRoute
objects in Django's urlpatterns
and vice versa!
django-components uses a custom URLRoute
class to define framework-agnostic routing rules.
As of v0.131, URLRoute
objects are directly converted to Django's URLPattern
and URLResolver
objects.
Accessing Extension URLsยค
The URLs defined in an extension are available under the path
For example, if you have defined a URL with the path my-view/<str:name>/
in an extension named my_extension
, it can be accessed at:
Nested URLsยค
Extensions can also define nested URLs to allow for more complex routing structures.
To define nested URLs, set the children
attribute of the URLRoute
object to a list of child URLRoute
objects:
class MyExtension(ComponentExtension):
name = "my_extension"
urls = [
URLRoute(
path="parent/",
name="parent_view",
children=[
URLRoute(path="child/<str:name>/", handler=my_view, name="child_view"),
],
),
]
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ยค
The URLRoute
class is framework-agnostic, so that extensions could be used with non-Django frameworks in the future.
However, that means that there may be some extra fields that Django's django.urls.path()
accepts, but which are not defined on the URLRoute
object.
To address this, the URLRoute
object has an extra
attribute, which is a dictionary that can be used to pass any extra kwargs to django.urls.path()
:
URLRoute(
path="my-view/<str:name>/",
handler=my_view,
name="my_view",
extra={"kwargs": {"foo": "bar"} },
)
Is the same as:
because URLRoute
is converted to Django's route like so: