A RetroSearch Logo

Home - News ( United States | United Kingdom | Italy | Germany ) - Football scores

Search Query:

Showing content from https://django-components.github.io/django-components/latest/concepts/advanced/extensions/ below:

Extensions - Django-Components

Extensions

New in version 0.131

Django-components functionality can be extended with "extensions". Extensions allow for powerful customization and integrations. They can:

Live examples¤ Install 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 ComponentConfig(ExtensionComponentConfig):
        ...

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:

See the full list in Extension Hooks Reference.

Per-component configuration¤

Each extension has a corresponding nested class within the Component class. These allow to configure the extensions on a per-component basis.

E.g.:

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.

class MyTable(Component):
    class MyExtension:
        def get(self, request):
            # `self.component` points to the instance of `MyTable` Component.
            return self.component.render_to_response(request=request)
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_class.render_to_response(request=request)

        def post(self, request):
            return self.component_class.render_to_response(request=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_cls.__name__

        def parameters(self) -> Parameters:
            return {
                "server": {
                    "id": self.component_cls.__name__,
                }
            }

        def stories(self) -> List[StoryAnnotations]:
            return []

        ...
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 setting.

The extensions_defaults setting is a dictionary where the key is the extension name and the value is a dictionary of config attributes:

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:

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:

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:

class MyTable(Component):
    def get_template_data(self, args, kwargs, slots, context):
        # `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_template_data(self, args, kwargs, slots, context):
        # `self.storybook` points to the instance of `Storybook` extension.
        return {
            "title": self.storybook.title(),
        }
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.

Extension class¤

To create an extension, define a class that inherits from ComponentExtension and implement the desired hooks.

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"

Warning

The name attribute MUST be unique across all extensions.

Moreover, the name attribute MUST NOT conflict with existing Component class API.

So if you name an extension render, it will conflict with the render() method of the Component class.

Component config¤

In previous sections we've seen the View and Storybook extensions classes that were nested within the Component class:

class MyComponent(Component):
    class View:
        ...

    class Storybook:
        ...

These can be understood as component-specific overrides or configuration.

Whether it's Component.View or Component.Storybook, their respective extensions defined how these nested classes will behave.

For example, the View extension defines the API that users may override in ViewExtension.ComponentConfig:

from django_components.extension import ComponentExtension, ExtensionComponentConfig

class ViewExtension(ComponentExtension):
    name = "view"

    # The default behavior of the `View` extension class.
    class ComponentConfig(ExtensionComponentConfig):
        def get(self, request):
            raise NotImplementedError("You must implement the `get` method.")

        def post(self, request):
            raise NotImplementedError("You must implement the `post` method.")

        ...

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:

class MyTable(Component):
    class View:
        def get(self, request):
            # Do something
            ...

Behind the scenes it is as if you defined the following:

class MyTable(Component):
    class View(ViewExtension.ComponentConfig):
        def get(self, request):
            # Do something
            ...

Warning

When writing an extension, the ComponentConfig MUST subclass the base class ExtensionComponentConfig.

This base class ensures that the extension class will have access to the component instance.

Install your extension¤

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:

# settings.py
COMPONENTS = {
    "extensions": [
        "my_app.extensions.MyExtension",
    ],
}

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:

from django_components.extension import (
    ComponentExtension,
    ExtensionComponentConfig,
    OnComponentClassCreatedContext,
    OnComponentClassDeletedContext,
    OnComponentInputContext,
)


class ColorLoggerExtension(ComponentExtension):
    name = "color_logger"

    # All `Component.ColorLogger` classes will inherit from this class.
    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):
        log.info(
            f"Component {ctx.component_cls} created.",
            color=ctx.component_cls.ColorLogger.color,
        )

    def on_component_class_deleted(self, ctx: OnComponentClassDeletedContext):
        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):
        log.info(
            f"Rendering component {ctx.component_cls}.",
            color=ctx.component.color_logger.color,
        )

To use the ColorLoggerExtension, add it to your settings:

# settings.py
COMPONENTS = {
    "extensions": [
        ColorLoggerExtension,
    ],
}

Once installed, in any component, you can define a ColorLogger attribute:

class MyComponent(Component):
    class ColorLogger:
        color = "red"

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:

Access component class¤

You can access the owner Component class (MyTable) from within methods of the extension class (MyExtension) by using the component_cls attribute:

class MyTable(Component):
    class MyExtension:
        def some_method(self):
            print(self.component_cls)

Here is how the component_cls attribute may be used with our ColorLogger extension shown above:

class ColorLoggerComponentConfig(ExtensionComponentConfig):
    color: str

    def log(self, msg: str) -> None:
        print(f"{self.component_cls.__name__}: {msg}")


class ColorLoggerExtension(ComponentExtension):
    name = "color_logger"

    # All `Component.ColorLogger` classes will inherit from this class.
    ComponentConfig = ColorLoggerComponentConfig
Pass slot metadata¤

When a slot is passed to a component, it is copied, so that the original slot is not modified with rendering metadata.

Therefore, don't use slot's identity to associate metadata with the slot:

# ❌ Don't do this:
slots_cache = {}

class LoggerExtension(ComponentExtension):
    name = "logger"

    def on_component_input(self, ctx: OnComponentInputContext):
        for slot in ctx.component.slots.values():
            slots_cache[id(slot)] = {"some": "metadata"}

Instead, use the Slot.extra attribute, which is copied from the original slot:

# ✅ Do this:
class LoggerExtension(ComponentExtension):
    name = "logger"

    # Save component-level logger settings for each slot when a component is rendered.
    def on_component_input(self, ctx: OnComponentInputContext):
        for slot in ctx.component.slots.values():
            slot.extra["logger"] = ctx.component.logger

    # Then, when a fill is rendered with `{% slot %}`, we can access the logger settings
    # from the slot's metadata.
    def on_slot_rendered(self, ctx: OnSlotRenderedContext):
        logger = ctx.slot.extra["logger"]
        logger.log(...)
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:

python manage.py components ext run my_ext hello

Where:

Define commands¤

To define a command, subclass from ComponentCommand. This subclass should define:

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]
Define 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:

python manage.py components ext run my_ext hello John --shout
>>> HELLO, JOHN!

Note

If a command doesn't have the handle method defined, the command will print a help message and exit.

Argument groups¤

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:

python manage.py components ext run parent
>>> Parent command

Or the child command:

python manage.py components ext run parent child
>>> 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.:

python manage.py components ext run parent --foo child --bar

You should instead pass all the arguments to the subcommand:

python manage.py components ext run parent child --foo --bar
Help message¤

By default, all commands will print their help message when run with the --help / -h flag.

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¤

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.

URL paths¤

The URLs defined in an extension are available under the path

/components/ext/<extension_name>/

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:

/components/ext/my_extension/my-view/john/
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

/components/ext/my_extension/parent/child/john/

would call the my_view handler with the parameter name set to "John".

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:

django.urls.path(
    "my-view/<str:name>/",
    view=my_view,
    name="my_view",
    kwargs={"foo": "bar"},
)

because URLRoute is converted to Django's route like so:

django.urls.path(
    route.path,
    view=route.handler,
    name=route.name,
    **route.extra,
)
2025-05-31 Juro Oravec

RetroSearch is an open source project built by @garambo | Open a GitHub Issue

Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo

HTML: 3.2 | Encoding: UTF-8 | Version: 0.7.4