Skip to content

django_components ¤

Main package for Django Components.

app_settings ¤

ContextBehavior ¤

Bases: str, Enum

DJANGO class-attribute instance-attribute ¤

DJANGO = 'django'

With this setting, component fills behave as usual Django tags. That is, they enrich the context, and pass it along.

  1. Component fills use the context of the component they are within.
  2. Variables from get_context_data are available to the component fill.

Example:

Given this template

{% with cheese="feta" %}
  {% component 'my_comp' %}
    {{ my_var }}  # my_var
    {{ cheese }}  # cheese
  {% endcomponent %}
{% endwith %}

and this context returned from the get_context_data() method

{ "my_var": 123 }

Then if component "my_comp" defines context

{ "my_var": 456 }

Then this will render:

456   # my_var
feta  # cheese

Because "my_comp" overrides the variable "my_var", so {{ my_var }} equals 456.

And variable "cheese" will equal feta, because the fill CAN access the current context.

ISOLATED class-attribute instance-attribute ¤

ISOLATED = 'isolated'

This setting makes the component fills behave similar to Vue or React, where the fills use EXCLUSIVELY the context variables defined in get_context_data.

Example:

Given this template

{% with cheese="feta" %}
  {% component 'my_comp' %}
    {{ my_var }}  # my_var
    {{ cheese }}  # cheese
  {% endcomponent %}
{% endwith %}

and this context returned from the get_context_data() method

{ "my_var": 123 }

Then if component "my_comp" defines context

{ "my_var": 456 }

Then this will render:

123   # my_var
      # cheese

Because both variables "my_var" and "cheese" are taken from the root context. Since "cheese" is not defined in root context, it's empty.

attributes ¤

append_attributes ¤

append_attributes(*args: Tuple[str, Any]) -> Dict

Merges the key-value pairs and returns a new dictionary.

If a key is present multiple times, its values are concatenated with a space character as separator in the final dictionary.

Source code in src/django_components/attributes.py
def append_attributes(*args: Tuple[str, Any]) -> Dict:
    """
    Merges the key-value pairs and returns a new dictionary.

    If a key is present multiple times, its values are concatenated with a space
    character as separator in the final dictionary.
    """
    result: Dict = {}

    for key, value in args:
        if key in result:
            result[key] += " " + value
        else:
            result[key] = value

    return result

attributes_to_string ¤

attributes_to_string(attributes: Mapping[str, Any]) -> str

Convert a dict of attributes to a string.

Source code in src/django_components/attributes.py
def attributes_to_string(attributes: Mapping[str, Any]) -> str:
    """Convert a dict of attributes to a string."""
    attr_list = []

    for key, value in attributes.items():
        if value is None or value is False:
            continue
        if value is True:
            attr_list.append(conditional_escape(key))
        else:
            attr_list.append(format_html('{}="{}"', key, value))

    return mark_safe(SafeString(" ").join(attr_list))

autodiscover ¤

autodiscover ¤

autodiscover(map_module: Optional[Callable[[str], str]] = None) -> List[str]

Search for component files and import them. Returns a list of module paths of imported files.

Autodiscover searches in the locations as defined by Loader.get_dirs.

You can map the module paths with map_module function. This serves as an escape hatch for when you need to use this function in tests.

Source code in src/django_components/autodiscover.py
def autodiscover(
    map_module: Optional[Callable[[str], str]] = None,
) -> List[str]:
    """
    Search for component files and import them. Returns a list of module
    paths of imported files.

    Autodiscover searches in the locations as defined by `Loader.get_dirs`.

    You can map the module paths with `map_module` function. This serves
    as an escape hatch for when you need to use this function in tests.
    """
    dirs = get_dirs(include_apps=False)
    component_filepaths = search_dirs(dirs, "**/*.py")
    logger.debug(f"Autodiscover found {len(component_filepaths)} files in component directories.")

    if hasattr(settings, "BASE_DIR") and settings.BASE_DIR:
        project_root = str(settings.BASE_DIR)
    else:
        # Fallback for getting the root dir, see https://stackoverflow.com/a/16413955/9788634
        project_root = os.path.abspath(os.path.dirname(__name__))

    modules: List[str] = []

    # We handle dirs from `COMPONENTS.dirs` and from individual apps separately.
    #
    # Because for dirs in `COMPONENTS.dirs`, we assume they will be nested under `BASE_DIR`,
    # and that `BASE_DIR` is the current working dir (CWD). So the path relatively to `BASE_DIR`
    # is ALSO the python import path.
    for filepath in component_filepaths:
        module_path = _filepath_to_python_module(filepath, project_root, None)
        # Ignore files starting with dot `.` or files in dirs that start with dot.
        #
        # If any of the parts of the path start with a dot, e.g. the filesystem path
        # is `./abc/.def`, then this gets converted to python module as `abc..def`
        #
        # NOTE: This approach also ignores files:
        #   - with two dots in the middle (ab..cd.py)
        #   - an extra dot at the end (abcd..py)
        #   - files outside of the parent component (../abcd.py).
        # But all these are NOT valid python modules so that's fine.
        if ".." in module_path:
            continue

        modules.append(module_path)

    # For for apps, the directories may be outside of the project, e.g. in case of third party
    # apps. So we have to resolve the python import path relative to the package name / the root
    # import path for the app.
    # See https://github.com/EmilStenstrom/django-components/issues/669
    for conf in apps.get_app_configs():
        for app_dir in app_settings.APP_DIRS:
            comps_path = Path(conf.path).joinpath(app_dir)
            if not comps_path.exists():
                continue
            app_component_filepaths = search_dirs([comps_path], "**/*.py")
            for filepath in app_component_filepaths:
                app_component_module = _filepath_to_python_module(filepath, conf.path, conf.name)
                modules.append(app_component_module)

    return _import_modules(modules, map_module)

import_libraries ¤

import_libraries(map_module: Optional[Callable[[str], str]] = None) -> List[str]

Import modules set in COMPONENTS.libraries setting.

You can map the module paths with map_module function. This serves as an escape hatch for when you need to use this function in tests.

Source code in src/django_components/autodiscover.py
def import_libraries(
    map_module: Optional[Callable[[str], str]] = None,
) -> List[str]:
    """
    Import modules set in `COMPONENTS.libraries` setting.

    You can map the module paths with `map_module` function. This serves
    as an escape hatch for when you need to use this function in tests.
    """
    from django_components.app_settings import app_settings

    return _import_modules(app_settings.LIBRARIES, map_module)

search_dirs ¤

search_dirs(dirs: List[Path], search_glob: str) -> List[Path]

Search the directories for the given glob pattern. Glob search results are returned as a flattened list.

Source code in src/django_components/autodiscover.py
def search_dirs(dirs: List[Path], search_glob: str) -> List[Path]:
    """
    Search the directories for the given glob pattern. Glob search results are returned
    as a flattened list.
    """
    matched_files: List[Path] = []
    for directory in dirs:
        for path in glob.iglob(str(Path(directory) / search_glob), recursive=True):
            matched_files.append(Path(path))

    return matched_files

component ¤

Component ¤

Component(
    registered_name: Optional[str] = None,
    component_id: Optional[str] = None,
    outer_context: Optional[Context] = None,
    fill_content: Optional[Dict[str, FillContent]] = None,
    registry: Optional[ComponentRegistry] = None,
)

Bases: Generic[ArgsType, KwargsType, DataType, SlotsType]

Source code in src/django_components/component.py
def __init__(
    self,
    registered_name: Optional[str] = None,
    component_id: Optional[str] = None,
    outer_context: Optional[Context] = None,
    fill_content: Optional[Dict[str, FillContent]] = None,
    registry: Optional[ComponentRegistry] = None,  # noqa F811
):
    # When user first instantiates the component class before calling
    # `render` or `render_to_response`, then we want to allow the render
    # function to make use of the instantiated object.
    #
    # So while `MyComp.render()` creates a new instance of MyComp internally,
    # if we do `MyComp(registered_name="abc").render()`, then we use the
    # already-instantiated object.
    #
    # To achieve that, we want to re-assign the class methods as instance methods.
    # For that we have to "unwrap" the class methods via __func__.
    # See https://stackoverflow.com/a/76706399/9788634
    self.render_to_response = types.MethodType(self.__class__.render_to_response.__func__, self)  # type: ignore
    self.render = types.MethodType(self.__class__.render.__func__, self)  # type: ignore
    self.as_view = types.MethodType(self.__class__.as_view.__func__, self)  # type: ignore

    self.registered_name: Optional[str] = registered_name
    self.outer_context: Context = outer_context or Context()
    self.fill_content = fill_content or {}
    self.component_id = component_id or gen_id()
    self.registry = registry or registry_
    self._render_stack: Deque[RenderStackItem[ArgsType, KwargsType, SlotsType]] = deque()
    # None == uninitialized, False == No types, Tuple == types
    self._types: Optional[Union[Tuple[Any, Any, Any, Any], Literal[False]]] = None

Media class-attribute instance-attribute ¤

Defines JS and CSS media files associated with this component.

css class-attribute instance-attribute ¤

css: Optional[str] = None

Inlined CSS associated with this component.

input property ¤

input: RenderInput[ArgsType, KwargsType, SlotsType]

Input holds the data (like arg, kwargs, slots) that were passsed to the current execution of the render method.

is_filled property ¤

is_filled: Dict[str, bool]

Dictionary describing which slots have or have not been filled.

This attribute is available for use only within the template as {{ component_vars.is_filled.slot_name }}, and within on_render_before and on_render_after hooks.

js class-attribute instance-attribute ¤

js: Optional[str] = None

Inlined JS associated with this component.

media instance-attribute ¤

media: Media

Normalized definition of JS and CSS media files associated with this component.

NOTE: This field is generated from Component.Media class.

response_class class-attribute instance-attribute ¤

response_class = HttpResponse

This allows to configure what class is used to generate response from render_to_response

template class-attribute instance-attribute ¤

template: Optional[Union[str, Template]] = None

Inlined Django template associated with this component. Can be a plain string or a Template instance.

Only one of template_name, get_template_name, template or get_template must be defined.

template_name class-attribute instance-attribute ¤

template_name: Optional[str] = None

Filepath to the Django template associated with this component.

The filepath must be relative to either the file where the component class was defined, or one of the roots of STATIFILES_DIRS.

Only one of template_name, get_template_name, template or get_template must be defined.

as_view classmethod ¤

as_view(**initkwargs: Any) -> ViewFn

Shortcut for calling Component.View.as_view and passing component instance to it.

Source code in src/django_components/component.py
@classmethod
def as_view(cls, **initkwargs: Any) -> ViewFn:
    """
    Shortcut for calling `Component.View.as_view` and passing component instance to it.
    """
    # This method may be called as class method or as instance method.
    # If called as class method, create a new instance.
    if isinstance(cls, Component):
        comp: Component = cls
    else:
        comp = cls()

    # Allow the View class to access this component via `self.component`
    return comp.View.as_view(**initkwargs, component=comp)

get_template ¤

get_template(context: Context) -> Optional[Union[str, Template]]

Inlined Django template associated with this component. Can be a plain string or a Template instance.

Only one of template_name, get_template_name, template or get_template must be defined.

Source code in src/django_components/component.py
def get_template(self, context: Context) -> Optional[Union[str, Template]]:
    """
    Inlined Django template associated with this component. Can be a plain string or a Template instance.

    Only one of `template_name`, `get_template_name`, `template` or `get_template` must be defined.
    """
    return None

get_template_name ¤

get_template_name(context: Context) -> Optional[str]

Filepath to the Django template associated with this component.

The filepath must be relative to either the file where the component class was defined, or one of the roots of STATIFILES_DIRS.

Only one of template_name, get_template_name, template or get_template must be defined.

Source code in src/django_components/component.py
def get_template_name(self, context: Context) -> Optional[str]:
    """
    Filepath to the Django template associated with this component.

    The filepath must be relative to either the file where the component class was defined,
    or one of the roots of `STATIFILES_DIRS`.

    Only one of `template_name`, `get_template_name`, `template` or `get_template` must be defined.
    """
    return None

inject ¤

inject(key: str, default: Optional[Any] = None) -> Any

Use this method to retrieve the data that was passed to a {% provide %} tag with the corresponding key.

To retrieve the data, inject() must be called inside a component that's inside the {% provide %} tag.

You may also pass a default that will be used if the provide tag with given key was NOT found.

This method mut be used inside the get_context_data() method and raises an error if called elsewhere.

Example:

Given this template:

{% provide "provider" hello="world" %}
    {% component "my_comp" %}
    {% endcomponent %}
{% endprovide %}

And given this definition of "my_comp" component:

from django_components import Component, register

@register("my_comp")
class MyComp(Component):
    template = "hi {{ data.hello }}!"
    def get_context_data(self):
        data = self.inject("provider")
        return {"data": data}

This renders into:

hi world!

As the {{ data.hello }} is taken from the "provider".

Source code in src/django_components/component.py
def inject(self, key: str, default: Optional[Any] = None) -> Any:
    """
    Use this method to retrieve the data that was passed to a `{% provide %}` tag
    with the corresponding key.

    To retrieve the data, `inject()` must be called inside a component that's
    inside the `{% provide %}` tag.

    You may also pass a default that will be used if the `provide` tag with given
    key was NOT found.

    This method mut be used inside the `get_context_data()` method and raises
    an error if called elsewhere.

    Example:

    Given this template:
    ```django
    {% provide "provider" hello="world" %}
        {% component "my_comp" %}
        {% endcomponent %}
    {% endprovide %}
    ```

    And given this definition of "my_comp" component:
    ```py
    from django_components import Component, register

    @register("my_comp")
    class MyComp(Component):
        template = "hi {{ data.hello }}!"
        def get_context_data(self):
            data = self.inject("provider")
            return {"data": data}
    ```

    This renders into:
    ```
    hi world!
    ```

    As the `{{ data.hello }}` is taken from the "provider".
    """
    if self.input is None:
        raise RuntimeError(
            f"Method 'inject()' of component '{self.name}' was called outside of 'get_context_data()'"
        )

    return get_injected_context_var(self.name, self.input.context, key, default)

on_render_after ¤

on_render_after(context: Context, template: Template, content: str) -> Optional[SlotResult]

Hook that runs just after the component's template was rendered. It receives the rendered output as the last argument.

You can use this hook to access the context or the template, but modifying them won't have any effect.

To override the content that gets rendered, you can return a string or SafeString from this hook.

Source code in src/django_components/component.py
def on_render_after(self, context: Context, template: Template, content: str) -> Optional[SlotResult]:
    """
    Hook that runs just after the component's template was rendered.
    It receives the rendered output as the last argument.

    You can use this hook to access the context or the template, but modifying
    them won't have any effect.

    To override the content that gets rendered, you can return a string or SafeString
    from this hook.
    """
    pass

on_render_before ¤

on_render_before(context: Context, template: Template) -> None

Hook that runs just before the component's template is rendered.

You can use this hook to access or modify the context or the template.

Source code in src/django_components/component.py
def on_render_before(self, context: Context, template: Template) -> None:
    """
    Hook that runs just before the component's template is rendered.

    You can use this hook to access or modify the context or the template.
    """
    pass

render classmethod ¤

render(
    context: Optional[Union[Dict[str, Any], Context]] = None,
    args: Optional[ArgsType] = None,
    kwargs: Optional[KwargsType] = None,
    slots: Optional[SlotsType] = None,
    escape_slots_content: bool = True,
) -> str

Render the component into a string.

Inputs: - args - Positional args for the component. This is the same as calling the component as {% component "my_comp" arg1 arg2 ... %} - kwargs - Kwargs for the component. This is the same as calling the component as {% component "my_comp" key1=val1 key2=val2 ... %} - slots - Component slot fills. This is the same as pasing {% fill %} tags to the component. Accepts a dictionary of { slot_name: slot_content } where slot_content can be a string or render function. - escape_slots_content - Whether the content from slots should be escaped. - context - A context (dictionary or Django's Context) within which the component is rendered. The keys on the context can be accessed from within the template. - NOTE: In "isolated" mode, context is NOT accessible, and data MUST be passed via component's args and kwargs.

Example:

MyComponent.render(
    args=[1, "two", {}],
    kwargs={
        "key": 123,
    },
    slots={
        "header": 'STATIC TEXT HERE',
        "footer": lambda ctx, slot_kwargs, slot_ref: f'CTX: {ctx['hello']} SLOT_DATA: {slot_kwargs['abc']}',
    },
    escape_slots_content=False,
)

Source code in src/django_components/component.py
@classmethod
def render(
    cls,
    context: Optional[Union[Dict[str, Any], Context]] = None,
    args: Optional[ArgsType] = None,
    kwargs: Optional[KwargsType] = None,
    slots: Optional[SlotsType] = None,
    escape_slots_content: bool = True,
) -> str:
    """
    Render the component into a string.

    Inputs:
    - `args` - Positional args for the component. This is the same as calling the component
      as `{% component "my_comp" arg1 arg2 ... %}`
    - `kwargs` - Kwargs for the component. This is the same as calling the component
      as `{% component "my_comp" key1=val1 key2=val2 ... %}`
    - `slots` - Component slot fills. This is the same as pasing `{% fill %}` tags to the component.
        Accepts a dictionary of `{ slot_name: slot_content }` where `slot_content` can be a string
        or render function.
    - `escape_slots_content` - Whether the content from `slots` should be escaped.
    - `context` - A context (dictionary or Django's Context) within which the component
      is rendered. The keys on the context can be accessed from within the template.
        - NOTE: In "isolated" mode, context is NOT accessible, and data MUST be passed via
          component's args and kwargs.

    Example:
    ```py
    MyComponent.render(
        args=[1, "two", {}],
        kwargs={
            "key": 123,
        },
        slots={
            "header": 'STATIC TEXT HERE',
            "footer": lambda ctx, slot_kwargs, slot_ref: f'CTX: {ctx['hello']} SLOT_DATA: {slot_kwargs['abc']}',
        },
        escape_slots_content=False,
    )
    ```
    """
    # This method may be called as class method or as instance method.
    # If called as class method, create a new instance.
    if isinstance(cls, Component):
        comp: Component = cls
    else:
        comp = cls()

    return comp._render(context, args, kwargs, slots, escape_slots_content)

render_css_dependencies ¤

render_css_dependencies() -> SafeString

Render only CSS dependencies available in the media class or provided as a string.

Source code in src/django_components/component.py
def render_css_dependencies(self) -> SafeString:
    """Render only CSS dependencies available in the media class or provided as a string."""
    if self.css is not None:
        return mark_safe(f"<style>{self.css}</style>")
    return mark_safe("\n".join(self.media.render_css()))

render_dependencies ¤

render_dependencies() -> SafeString

Helper function to render all dependencies for a component.

Source code in src/django_components/component.py
def render_dependencies(self) -> SafeString:
    """Helper function to render all dependencies for a component."""
    dependencies = []

    css_deps = self.render_css_dependencies()
    if css_deps:
        dependencies.append(css_deps)

    js_deps = self.render_js_dependencies()
    if js_deps:
        dependencies.append(js_deps)

    return mark_safe("\n".join(dependencies))

render_js_dependencies ¤

render_js_dependencies() -> SafeString

Render only JS dependencies available in the media class or provided as a string.

Source code in src/django_components/component.py
def render_js_dependencies(self) -> SafeString:
    """Render only JS dependencies available in the media class or provided as a string."""
    if self.js is not None:
        return mark_safe(f"<script>{self.js}</script>")
    return mark_safe("\n".join(self.media.render_js()))

render_to_response classmethod ¤

render_to_response(
    context: Optional[Union[Dict[str, Any], Context]] = None,
    slots: Optional[SlotsType] = None,
    escape_slots_content: bool = True,
    args: Optional[ArgsType] = None,
    kwargs: Optional[KwargsType] = None,
    *response_args: Any,
    **response_kwargs: Any
) -> HttpResponse

Render the component and wrap the content in the response class.

The response class is taken from Component.response_class. Defaults to django.http.HttpResponse.

This is the interface for the django.views.View class which allows us to use components as Django views with component.as_view().

Inputs: - args - Positional args for the component. This is the same as calling the component as {% component "my_comp" arg1 arg2 ... %} - kwargs - Kwargs for the component. This is the same as calling the component as {% component "my_comp" key1=val1 key2=val2 ... %} - slots - Component slot fills. This is the same as pasing {% fill %} tags to the component. Accepts a dictionary of { slot_name: slot_content } where slot_content can be a string or render function. - escape_slots_content - Whether the content from slots should be escaped. - context - A context (dictionary or Django's Context) within which the component is rendered. The keys on the context can be accessed from within the template. - NOTE: In "isolated" mode, context is NOT accessible, and data MUST be passed via component's args and kwargs.

Any additional args and kwargs are passed to the response_class.

Example:

MyComponent.render_to_response(
    args=[1, "two", {}],
    kwargs={
        "key": 123,
    },
    slots={
        "header": 'STATIC TEXT HERE',
        "footer": lambda ctx, slot_kwargs, slot_ref: f'CTX: {ctx['hello']} SLOT_DATA: {slot_kwargs['abc']}',
    },
    escape_slots_content=False,
    # HttpResponse input
    status=201,
    headers={...},
)
# HttpResponse(content=..., status=201, headers=...)

Source code in src/django_components/component.py
@classmethod
def render_to_response(
    cls,
    context: Optional[Union[Dict[str, Any], Context]] = None,
    slots: Optional[SlotsType] = None,
    escape_slots_content: bool = True,
    args: Optional[ArgsType] = None,
    kwargs: Optional[KwargsType] = None,
    *response_args: Any,
    **response_kwargs: Any,
) -> HttpResponse:
    """
    Render the component and wrap the content in the response class.

    The response class is taken from `Component.response_class`. Defaults to `django.http.HttpResponse`.

    This is the interface for the `django.views.View` class which allows us to
    use components as Django views with `component.as_view()`.

    Inputs:
    - `args` - Positional args for the component. This is the same as calling the component
      as `{% component "my_comp" arg1 arg2 ... %}`
    - `kwargs` - Kwargs for the component. This is the same as calling the component
      as `{% component "my_comp" key1=val1 key2=val2 ... %}`
    - `slots` - Component slot fills. This is the same as pasing `{% fill %}` tags to the component.
        Accepts a dictionary of `{ slot_name: slot_content }` where `slot_content` can be a string
        or render function.
    - `escape_slots_content` - Whether the content from `slots` should be escaped.
    - `context` - A context (dictionary or Django's Context) within which the component
      is rendered. The keys on the context can be accessed from within the template.
        - NOTE: In "isolated" mode, context is NOT accessible, and data MUST be passed via
          component's args and kwargs.

    Any additional args and kwargs are passed to the `response_class`.

    Example:
    ```py
    MyComponent.render_to_response(
        args=[1, "two", {}],
        kwargs={
            "key": 123,
        },
        slots={
            "header": 'STATIC TEXT HERE',
            "footer": lambda ctx, slot_kwargs, slot_ref: f'CTX: {ctx['hello']} SLOT_DATA: {slot_kwargs['abc']}',
        },
        escape_slots_content=False,
        # HttpResponse input
        status=201,
        headers={...},
    )
    # HttpResponse(content=..., status=201, headers=...)
    ```
    """
    content = cls.render(
        args=args,
        kwargs=kwargs,
        context=context,
        slots=slots,
        escape_slots_content=escape_slots_content,
    )
    return cls.response_class(content, *response_args, **response_kwargs)

ComponentNode ¤

ComponentNode(
    name: str,
    args: List[Expression],
    kwargs: RuntimeKwargs,
    registry: ComponentRegistry,
    isolated_context: bool = False,
    fill_nodes: Optional[List[FillNode]] = None,
    node_id: Optional[str] = None,
)

Bases: BaseNode

Django.template.Node subclass that renders a django-components component

Source code in src/django_components/component.py
def __init__(
    self,
    name: str,
    args: List[Expression],
    kwargs: RuntimeKwargs,
    registry: ComponentRegistry,  # noqa F811
    isolated_context: bool = False,
    fill_nodes: Optional[List[FillNode]] = None,
    node_id: Optional[str] = None,
) -> None:
    super().__init__(nodelist=NodeList(fill_nodes), args=args, kwargs=kwargs, node_id=node_id)

    self.name = name
    self.isolated_context = isolated_context
    self.fill_nodes = fill_nodes or []
    self.registry = registry

ComponentView ¤

ComponentView(component: Component, **kwargs: Any)

Bases: View

Subclass of django.views.View where the Component instance is available via self.component.

Source code in src/django_components/component.py
def __init__(self, component: "Component", **kwargs: Any) -> None:
    super().__init__(**kwargs)
    self.component = component

component_media ¤

ComponentMediaInput ¤

Defines JS and CSS media files associated with this component.

MediaMeta ¤

Bases: MediaDefiningClass

Metaclass for handling media files for components.

Similar to MediaDefiningClass, this class supports the use of Media attribute to define associated JS/CSS files, which are then available under media attribute as a instance of Media class.

This subclass has following changes:

1. Support for multiple interfaces of JS/CSS¤
  1. As plain strings

    class MyComponent(Component):
        class Media:
            js = "path/to/script.js"
            css = "path/to/style.css"
    

  2. As lists

    class MyComponent(Component):
        class Media:
            js = ["path/to/script1.js", "path/to/script2.js"]
            css = ["path/to/style1.css", "path/to/style2.css"]
    

  3. [CSS ONLY] Dicts of strings

    class MyComponent(Component):
        class Media:
            css = {
                "all": "path/to/style1.css",
                "print": "path/to/style2.css",
            }
    

  4. [CSS ONLY] Dicts of lists

    class MyComponent(Component):
        class Media:
            css = {
                "all": ["path/to/style1.css"],
                "print": ["path/to/style2.css"],
            }
    

2. Media are first resolved relative to class definition file¤

E.g. if in a directory my_comp you have script.js and my_comp.py, and my_comp.py looks like this:

class MyComponent(Component):
    class Media:
        js = "script.js"

Then script.js will be resolved as my_comp/script.js.

3. Media can be defined as str, bytes, PathLike, SafeString, or function of thereof¤

E.g.:

def lazy_eval_css():
    # do something
    return path

class MyComponent(Component):
    class Media:
        js = b"script.js"
        css = lazy_eval_css
4. Subclass Media class with media_class¤

Normal MediaDefiningClass creates an instance of Media class under the media attribute. This class allows to override which class will be instantiated with media_class attribute:

class MyMedia(Media):
    def render_js(self):
        ...

class MyComponent(Component):
    media_class = MyMedia
    def get_context_data(self):
        assert isinstance(self.media, MyMedia)

component_registry ¤

registry module-attribute ¤

The default and global component registry. Use this instance to directly register or remove components:

# Register components
registry.register("button", ButtonComponent)
registry.register("card", CardComponent)
# Get single
registry.get("button")
# Get all
registry.all()
# Unregister single
registry.unregister("button")
# Unregister all
registry.clear()

ComponentRegistry ¤

ComponentRegistry(
    library: Optional[Library] = None, settings: Optional[Union[RegistrySettings, Callable[[ComponentRegistry], RegistrySettings]]] = None
)

Manages which components can be used in the template tags.

Each ComponentRegistry instance is associated with an instance of Django's Library. So when you register or unregister a component to/from a component registry, behind the scenes the registry automatically adds/removes the component's template tag to/from the Library.

The Library instance can be set at instantiation. If omitted, then the default Library instance from django_components is used. The Library instance can be accessed under library attribute.

Example:

# Use with default Library
registry = ComponentRegistry()

# Or a custom one
my_lib = Library()
registry = ComponentRegistry(library=my_lib)

# Usage
registry.register("button", ButtonComponent)
registry.register("card", CardComponent)
registry.all()
registry.clear()
registry.get()
Source code in src/django_components/component_registry.py
def __init__(
    self,
    library: Optional[Library] = None,
    settings: Optional[Union[RegistrySettings, Callable[["ComponentRegistry"], RegistrySettings]]] = None,
) -> None:
    self._registry: Dict[str, ComponentRegistryEntry] = {}  # component name -> component_entry mapping
    self._tags: Dict[str, Set[str]] = {}  # tag -> list[component names]
    self._library = library
    self._settings_input = settings
    self._settings: Optional[Callable[[], InternalRegistrySettings]] = None

    all_registries.append(self)

library property ¤

library: Library

The template tag library with which the component registry is associated.

all ¤

all() -> Dict[str, Type[Component]]

Retrieve all registered component classes.

Example:

# First register components
registry.register("button", ButtonComponent)
registry.register("card", CardComponent)
# Then get all
registry.all()
# > {
# >   "button": ButtonComponent,
# >   "card": CardComponent,
# > }
Source code in src/django_components/component_registry.py
def all(self) -> Dict[str, Type["Component"]]:
    """
    Retrieve all registered component classes.

    Example:

    ```py
    # First register components
    registry.register("button", ButtonComponent)
    registry.register("card", CardComponent)
    # Then get all
    registry.all()
    # > {
    # >   "button": ButtonComponent,
    # >   "card": CardComponent,
    # > }
    ```
    """
    comps = {key: entry.cls for key, entry in self._registry.items()}
    return comps

clear ¤

clear() -> None

Clears the registry, unregistering all components.

Example:

# First register components
registry.register("button", ButtonComponent)
registry.register("card", CardComponent)
# Then clear
registry.clear()
# Then get all
registry.all()
# > {}
Source code in src/django_components/component_registry.py
def clear(self) -> None:
    """
    Clears the registry, unregistering all components.

    Example:

    ```py
    # First register components
    registry.register("button", ButtonComponent)
    registry.register("card", CardComponent)
    # Then clear
    registry.clear()
    # Then get all
    registry.all()
    # > {}
    ```
    """
    all_comp_names = list(self._registry.keys())
    for comp_name in all_comp_names:
        self.unregister(comp_name)

    self._registry = {}
    self._tags = {}

get ¤

get(name: str) -> Type[Component]

Retrieve a component class registered under the given name.

Raises NotRegistered if the given name is not registered.

Example:

# First register component
registry.register("button", ButtonComponent)
# Then get
registry.get("button")
# > ButtonComponent
Source code in src/django_components/component_registry.py
def get(self, name: str) -> Type["Component"]:
    """
    Retrieve a component class registered under the given name.

    Raises `NotRegistered` if the given name is not registered.

    Example:

    ```py
    # First register component
    registry.register("button", ButtonComponent)
    # Then get
    registry.get("button")
    # > ButtonComponent
    ```
    """
    if name not in self._registry:
        raise NotRegistered('The component "%s" is not registered' % name)

    return self._registry[name].cls

register ¤

register(name: str, component: Type[Component]) -> None

Register a component with this registry under the given name.

A component MUST be registered before it can be used in a template such as:

{% component "my_comp" %}{% endcomponent %}

Raises AlreadyRegistered if a different component was already registered under the same name.

Example:

registry.register("button", ButtonComponent)
Source code in src/django_components/component_registry.py
def register(self, name: str, component: Type["Component"]) -> None:
    """
    Register a component with this registry under the given name.

    A component MUST be registered before it can be used in a template such as:
    ```django
    {% component "my_comp" %}{% endcomponent %}
    ```

    Raises `AlreadyRegistered` if a different component was already registered
    under the same name.

    Example:

    ```py
    registry.register("button", ButtonComponent)
    ```
    """
    existing_component = self._registry.get(name)
    if existing_component and existing_component.cls._class_hash != component._class_hash:
        raise AlreadyRegistered('The component "%s" has already been registered' % name)

    entry = self._register_to_library(name, component)

    # Keep track of which components use which tags, because multiple components may
    # use the same tag.
    tag = entry.tag
    if tag not in self._tags:
        self._tags[tag] = set()
    self._tags[tag].add(name)

    self._registry[name] = entry

unregister ¤

unregister(name: str) -> None

Unlinks a previously-registered component from the registry under the given name.

Once a component is unregistered, it CANNOT be used in a template anymore. Following would raise an error:

{% component "my_comp" %}{% endcomponent %}

Raises NotRegistered if the given name is not registered.

Example:

# First register component
registry.register("button", ButtonComponent)
# Then unregister
registry.unregister("button")
Source code in src/django_components/component_registry.py
def unregister(self, name: str) -> None:
    """
    Unlinks a previously-registered component from the registry under the given name.

    Once a component is unregistered, it CANNOT be used in a template anymore.
    Following would raise an error:
    ```django
    {% component "my_comp" %}{% endcomponent %}
    ```

    Raises `NotRegistered` if the given name is not registered.

    Example:

    ```py
    # First register component
    registry.register("button", ButtonComponent)
    # Then unregister
    registry.unregister("button")
    ```
    """
    # Validate
    self.get(name)

    entry = self._registry[name]
    tag = entry.tag

    # Unregister the tag from library if this was the last component using this tag
    # Unlink component from tag
    self._tags[tag].remove(name)

    # Cleanup
    is_tag_empty = not len(self._tags[tag])
    if is_tag_empty:
        del self._tags[tag]

    # Only unregister a tag if it's NOT protected
    is_protected = is_tag_protected(self.library, tag)
    if not is_protected:
        # Unregister the tag from library if this was the last component using this tag
        if is_tag_empty and tag in self.library.tags:
            del self.library.tags[tag]

    del self._registry[name]

register ¤

register(name: str, registry: Optional[ComponentRegistry] = None) -> Callable[[_TComp], _TComp]

Class decorator to register a component.

Usage:

@register("my_component")
class MyComponent(Component):
    ...

Optionally specify which ComponentRegistry the component should be registered to by setting the registry kwarg:

my_lib = django.template.Library()
my_reg = ComponentRegistry(library=my_lib)

@register("my_component", registry=my_reg)
class MyComponent(Component):
    ...
Source code in src/django_components/component_registry.py
def register(name: str, registry: Optional[ComponentRegistry] = None) -> Callable[[_TComp], _TComp]:
    """
    Class decorator to register a component.

    Usage:

    ```py
    @register("my_component")
    class MyComponent(Component):
        ...
    ```

    Optionally specify which `ComponentRegistry` the component should be registered to by
    setting the `registry` kwarg:

    ```py
    my_lib = django.template.Library()
    my_reg = ComponentRegistry(library=my_lib)

    @register("my_component", registry=my_reg)
    class MyComponent(Component):
        ...
    ```
    """
    if registry is None:
        registry = _the_registry

    def decorator(component: _TComp) -> _TComp:
        registry.register(name=name, component=component)
        return component

    return decorator

components ¤

dynamic ¤

DynamicComponent ¤

DynamicComponent(
    registered_name: Optional[str] = None,
    component_id: Optional[str] = None,
    outer_context: Optional[Context] = None,
    fill_content: Optional[Dict[str, FillContent]] = None,
    registry: Optional[ComponentRegistry] = None,
)

Bases: Component

Dynamic component - This component takes inputs and renders the outputs depending on the is and registry arguments.

  • is - required - The component class or registered name of the component that will be rendered in this place.

  • registry - optional - Specify the registry to search for the registered name. If omitted, all registries are searched.

Source code in src/django_components/component.py
def __init__(
    self,
    registered_name: Optional[str] = None,
    component_id: Optional[str] = None,
    outer_context: Optional[Context] = None,
    fill_content: Optional[Dict[str, FillContent]] = None,
    registry: Optional[ComponentRegistry] = None,  # noqa F811
):
    # When user first instantiates the component class before calling
    # `render` or `render_to_response`, then we want to allow the render
    # function to make use of the instantiated object.
    #
    # So while `MyComp.render()` creates a new instance of MyComp internally,
    # if we do `MyComp(registered_name="abc").render()`, then we use the
    # already-instantiated object.
    #
    # To achieve that, we want to re-assign the class methods as instance methods.
    # For that we have to "unwrap" the class methods via __func__.
    # See https://stackoverflow.com/a/76706399/9788634
    self.render_to_response = types.MethodType(self.__class__.render_to_response.__func__, self)  # type: ignore
    self.render = types.MethodType(self.__class__.render.__func__, self)  # type: ignore
    self.as_view = types.MethodType(self.__class__.as_view.__func__, self)  # type: ignore

    self.registered_name: Optional[str] = registered_name
    self.outer_context: Context = outer_context or Context()
    self.fill_content = fill_content or {}
    self.component_id = component_id or gen_id()
    self.registry = registry or registry_
    self._render_stack: Deque[RenderStackItem[ArgsType, KwargsType, SlotsType]] = deque()
    # None == uninitialized, False == No types, Tuple == types
    self._types: Optional[Union[Tuple[Any, Any, Any, Any], Literal[False]]] = None
Media class-attribute instance-attribute ¤

Defines JS and CSS media files associated with this component.

css class-attribute instance-attribute ¤
css: Optional[str] = None

Inlined CSS associated with this component.

input property ¤
input: RenderInput[ArgsType, KwargsType, SlotsType]

Input holds the data (like arg, kwargs, slots) that were passsed to the current execution of the render method.

is_filled property ¤
is_filled: Dict[str, bool]

Dictionary describing which slots have or have not been filled.

This attribute is available for use only within the template as {{ component_vars.is_filled.slot_name }}, and within on_render_before and on_render_after hooks.

js class-attribute instance-attribute ¤
js: Optional[str] = None

Inlined JS associated with this component.

media instance-attribute ¤
media: Media

Normalized definition of JS and CSS media files associated with this component.

NOTE: This field is generated from Component.Media class.

response_class class-attribute instance-attribute ¤
response_class = HttpResponse

This allows to configure what class is used to generate response from render_to_response

template_name class-attribute instance-attribute ¤
template_name: Optional[str] = None

Filepath to the Django template associated with this component.

The filepath must be relative to either the file where the component class was defined, or one of the roots of STATIFILES_DIRS.

Only one of template_name, get_template_name, template or get_template must be defined.

as_view classmethod ¤
as_view(**initkwargs: Any) -> ViewFn

Shortcut for calling Component.View.as_view and passing component instance to it.

Source code in src/django_components/component.py
@classmethod
def as_view(cls, **initkwargs: Any) -> ViewFn:
    """
    Shortcut for calling `Component.View.as_view` and passing component instance to it.
    """
    # This method may be called as class method or as instance method.
    # If called as class method, create a new instance.
    if isinstance(cls, Component):
        comp: Component = cls
    else:
        comp = cls()

    # Allow the View class to access this component via `self.component`
    return comp.View.as_view(**initkwargs, component=comp)
get_template ¤
get_template(context: Context) -> Optional[Union[str, Template]]

Inlined Django template associated with this component. Can be a plain string or a Template instance.

Only one of template_name, get_template_name, template or get_template must be defined.

Source code in src/django_components/component.py
def get_template(self, context: Context) -> Optional[Union[str, Template]]:
    """
    Inlined Django template associated with this component. Can be a plain string or a Template instance.

    Only one of `template_name`, `get_template_name`, `template` or `get_template` must be defined.
    """
    return None
get_template_name ¤
get_template_name(context: Context) -> Optional[str]

Filepath to the Django template associated with this component.

The filepath must be relative to either the file where the component class was defined, or one of the roots of STATIFILES_DIRS.

Only one of template_name, get_template_name, template or get_template must be defined.

Source code in src/django_components/component.py
def get_template_name(self, context: Context) -> Optional[str]:
    """
    Filepath to the Django template associated with this component.

    The filepath must be relative to either the file where the component class was defined,
    or one of the roots of `STATIFILES_DIRS`.

    Only one of `template_name`, `get_template_name`, `template` or `get_template` must be defined.
    """
    return None
inject ¤
inject(key: str, default: Optional[Any] = None) -> Any

Use this method to retrieve the data that was passed to a {% provide %} tag with the corresponding key.

To retrieve the data, inject() must be called inside a component that's inside the {% provide %} tag.

You may also pass a default that will be used if the provide tag with given key was NOT found.

This method mut be used inside the get_context_data() method and raises an error if called elsewhere.

Example:

Given this template:

{% provide "provider" hello="world" %}
    {% component "my_comp" %}
    {% endcomponent %}
{% endprovide %}

And given this definition of "my_comp" component:

from django_components import Component, register

@register("my_comp")
class MyComp(Component):
    template = "hi {{ data.hello }}!"
    def get_context_data(self):
        data = self.inject("provider")
        return {"data": data}

This renders into:

hi world!

As the {{ data.hello }} is taken from the "provider".

Source code in src/django_components/component.py
def inject(self, key: str, default: Optional[Any] = None) -> Any:
    """
    Use this method to retrieve the data that was passed to a `{% provide %}` tag
    with the corresponding key.

    To retrieve the data, `inject()` must be called inside a component that's
    inside the `{% provide %}` tag.

    You may also pass a default that will be used if the `provide` tag with given
    key was NOT found.

    This method mut be used inside the `get_context_data()` method and raises
    an error if called elsewhere.

    Example:

    Given this template:
    ```django
    {% provide "provider" hello="world" %}
        {% component "my_comp" %}
        {% endcomponent %}
    {% endprovide %}
    ```

    And given this definition of "my_comp" component:
    ```py
    from django_components import Component, register

    @register("my_comp")
    class MyComp(Component):
        template = "hi {{ data.hello }}!"
        def get_context_data(self):
            data = self.inject("provider")
            return {"data": data}
    ```

    This renders into:
    ```
    hi world!
    ```

    As the `{{ data.hello }}` is taken from the "provider".
    """
    if self.input is None:
        raise RuntimeError(
            f"Method 'inject()' of component '{self.name}' was called outside of 'get_context_data()'"
        )

    return get_injected_context_var(self.name, self.input.context, key, default)
on_render_after ¤
on_render_after(context: Context, template: Template, content: str) -> Optional[SlotResult]

Hook that runs just after the component's template was rendered. It receives the rendered output as the last argument.

You can use this hook to access the context or the template, but modifying them won't have any effect.

To override the content that gets rendered, you can return a string or SafeString from this hook.

Source code in src/django_components/component.py
def on_render_after(self, context: Context, template: Template, content: str) -> Optional[SlotResult]:
    """
    Hook that runs just after the component's template was rendered.
    It receives the rendered output as the last argument.

    You can use this hook to access the context or the template, but modifying
    them won't have any effect.

    To override the content that gets rendered, you can return a string or SafeString
    from this hook.
    """
    pass
on_render_before ¤
on_render_before(context: Context, template: Template) -> None

Hook that runs just before the component's template is rendered.

You can use this hook to access or modify the context or the template.

Source code in src/django_components/component.py
def on_render_before(self, context: Context, template: Template) -> None:
    """
    Hook that runs just before the component's template is rendered.

    You can use this hook to access or modify the context or the template.
    """
    pass
render classmethod ¤
render(
    context: Optional[Union[Dict[str, Any], Context]] = None,
    args: Optional[ArgsType] = None,
    kwargs: Optional[KwargsType] = None,
    slots: Optional[SlotsType] = None,
    escape_slots_content: bool = True,
) -> str

Render the component into a string.

Inputs: - args - Positional args for the component. This is the same as calling the component as {% component "my_comp" arg1 arg2 ... %} - kwargs - Kwargs for the component. This is the same as calling the component as {% component "my_comp" key1=val1 key2=val2 ... %} - slots - Component slot fills. This is the same as pasing {% fill %} tags to the component. Accepts a dictionary of { slot_name: slot_content } where slot_content can be a string or render function. - escape_slots_content - Whether the content from slots should be escaped. - context - A context (dictionary or Django's Context) within which the component is rendered. The keys on the context can be accessed from within the template. - NOTE: In "isolated" mode, context is NOT accessible, and data MUST be passed via component's args and kwargs.

Example:

MyComponent.render(
    args=[1, "two", {}],
    kwargs={
        "key": 123,
    },
    slots={
        "header": 'STATIC TEXT HERE',
        "footer": lambda ctx, slot_kwargs, slot_ref: f'CTX: {ctx['hello']} SLOT_DATA: {slot_kwargs['abc']}',
    },
    escape_slots_content=False,
)

Source code in src/django_components/component.py
@classmethod
def render(
    cls,
    context: Optional[Union[Dict[str, Any], Context]] = None,
    args: Optional[ArgsType] = None,
    kwargs: Optional[KwargsType] = None,
    slots: Optional[SlotsType] = None,
    escape_slots_content: bool = True,
) -> str:
    """
    Render the component into a string.

    Inputs:
    - `args` - Positional args for the component. This is the same as calling the component
      as `{% component "my_comp" arg1 arg2 ... %}`
    - `kwargs` - Kwargs for the component. This is the same as calling the component
      as `{% component "my_comp" key1=val1 key2=val2 ... %}`
    - `slots` - Component slot fills. This is the same as pasing `{% fill %}` tags to the component.
        Accepts a dictionary of `{ slot_name: slot_content }` where `slot_content` can be a string
        or render function.
    - `escape_slots_content` - Whether the content from `slots` should be escaped.
    - `context` - A context (dictionary or Django's Context) within which the component
      is rendered. The keys on the context can be accessed from within the template.
        - NOTE: In "isolated" mode, context is NOT accessible, and data MUST be passed via
          component's args and kwargs.

    Example:
    ```py
    MyComponent.render(
        args=[1, "two", {}],
        kwargs={
            "key": 123,
        },
        slots={
            "header": 'STATIC TEXT HERE',
            "footer": lambda ctx, slot_kwargs, slot_ref: f'CTX: {ctx['hello']} SLOT_DATA: {slot_kwargs['abc']}',
        },
        escape_slots_content=False,
    )
    ```
    """
    # This method may be called as class method or as instance method.
    # If called as class method, create a new instance.
    if isinstance(cls, Component):
        comp: Component = cls
    else:
        comp = cls()

    return comp._render(context, args, kwargs, slots, escape_slots_content)
render_css_dependencies ¤
render_css_dependencies() -> SafeString

Render only CSS dependencies available in the media class or provided as a string.

Source code in src/django_components/component.py
def render_css_dependencies(self) -> SafeString:
    """Render only CSS dependencies available in the media class or provided as a string."""
    if self.css is not None:
        return mark_safe(f"<style>{self.css}</style>")
    return mark_safe("\n".join(self.media.render_css()))
render_dependencies ¤
render_dependencies() -> SafeString

Helper function to render all dependencies for a component.

Source code in src/django_components/component.py
def render_dependencies(self) -> SafeString:
    """Helper function to render all dependencies for a component."""
    dependencies = []

    css_deps = self.render_css_dependencies()
    if css_deps:
        dependencies.append(css_deps)

    js_deps = self.render_js_dependencies()
    if js_deps:
        dependencies.append(js_deps)

    return mark_safe("\n".join(dependencies))
render_js_dependencies ¤
render_js_dependencies() -> SafeString

Render only JS dependencies available in the media class or provided as a string.

Source code in src/django_components/component.py
def render_js_dependencies(self) -> SafeString:
    """Render only JS dependencies available in the media class or provided as a string."""
    if self.js is not None:
        return mark_safe(f"<script>{self.js}</script>")
    return mark_safe("\n".join(self.media.render_js()))
render_to_response classmethod ¤
render_to_response(
    context: Optional[Union[Dict[str, Any], Context]] = None,
    slots: Optional[SlotsType] = None,
    escape_slots_content: bool = True,
    args: Optional[ArgsType] = None,
    kwargs: Optional[KwargsType] = None,
    *response_args: Any,
    **response_kwargs: Any
) -> HttpResponse

Render the component and wrap the content in the response class.

The response class is taken from Component.response_class. Defaults to django.http.HttpResponse.

This is the interface for the django.views.View class which allows us to use components as Django views with component.as_view().

Inputs: - args - Positional args for the component. This is the same as calling the component as {% component "my_comp" arg1 arg2 ... %} - kwargs - Kwargs for the component. This is the same as calling the component as {% component "my_comp" key1=val1 key2=val2 ... %} - slots - Component slot fills. This is the same as pasing {% fill %} tags to the component. Accepts a dictionary of { slot_name: slot_content } where slot_content can be a string or render function. - escape_slots_content - Whether the content from slots should be escaped. - context - A context (dictionary or Django's Context) within which the component is rendered. The keys on the context can be accessed from within the template. - NOTE: In "isolated" mode, context is NOT accessible, and data MUST be passed via component's args and kwargs.

Any additional args and kwargs are passed to the response_class.

Example:

MyComponent.render_to_response(
    args=[1, "two", {}],
    kwargs={
        "key": 123,
    },
    slots={
        "header": 'STATIC TEXT HERE',
        "footer": lambda ctx, slot_kwargs, slot_ref: f'CTX: {ctx['hello']} SLOT_DATA: {slot_kwargs['abc']}',
    },
    escape_slots_content=False,
    # HttpResponse input
    status=201,
    headers={...},
)
# HttpResponse(content=..., status=201, headers=...)

Source code in src/django_components/component.py
@classmethod
def render_to_response(
    cls,
    context: Optional[Union[Dict[str, Any], Context]] = None,
    slots: Optional[SlotsType] = None,
    escape_slots_content: bool = True,
    args: Optional[ArgsType] = None,
    kwargs: Optional[KwargsType] = None,
    *response_args: Any,
    **response_kwargs: Any,
) -> HttpResponse:
    """
    Render the component and wrap the content in the response class.

    The response class is taken from `Component.response_class`. Defaults to `django.http.HttpResponse`.

    This is the interface for the `django.views.View` class which allows us to
    use components as Django views with `component.as_view()`.

    Inputs:
    - `args` - Positional args for the component. This is the same as calling the component
      as `{% component "my_comp" arg1 arg2 ... %}`
    - `kwargs` - Kwargs for the component. This is the same as calling the component
      as `{% component "my_comp" key1=val1 key2=val2 ... %}`
    - `slots` - Component slot fills. This is the same as pasing `{% fill %}` tags to the component.
        Accepts a dictionary of `{ slot_name: slot_content }` where `slot_content` can be a string
        or render function.
    - `escape_slots_content` - Whether the content from `slots` should be escaped.
    - `context` - A context (dictionary or Django's Context) within which the component
      is rendered. The keys on the context can be accessed from within the template.
        - NOTE: In "isolated" mode, context is NOT accessible, and data MUST be passed via
          component's args and kwargs.

    Any additional args and kwargs are passed to the `response_class`.

    Example:
    ```py
    MyComponent.render_to_response(
        args=[1, "two", {}],
        kwargs={
            "key": 123,
        },
        slots={
            "header": 'STATIC TEXT HERE',
            "footer": lambda ctx, slot_kwargs, slot_ref: f'CTX: {ctx['hello']} SLOT_DATA: {slot_kwargs['abc']}',
        },
        escape_slots_content=False,
        # HttpResponse input
        status=201,
        headers={...},
    )
    # HttpResponse(content=..., status=201, headers=...)
    ```
    """
    content = cls.render(
        args=args,
        kwargs=kwargs,
        context=context,
        slots=slots,
        escape_slots_content=escape_slots_content,
    )
    return cls.response_class(content, *response_args, **response_kwargs)

context ¤

This file centralizes various ways we use Django's Context class pass data across components, nodes, slots, and contexts.

You can think of the Context as our storage system.

copy_forloop_context ¤

copy_forloop_context(from_context: Context, to_context: Context) -> None

Forward the info about the current loop

Source code in src/django_components/context.py
def copy_forloop_context(from_context: Context, to_context: Context) -> None:
    """Forward the info about the current loop"""
    # Note that the ForNode (which implements for loop behavior) does not
    # only add the `forloop` key, but also keys corresponding to the loop elements
    # So if the loop syntax is `{% for my_val in my_lists %}`, then ForNode also
    # sets a `my_val` key.
    # For this reason, instead of copying individual keys, we copy the whole stack layer
    # set by ForNode.
    if "forloop" in from_context:
        forloop_dict_index = find_last_index(from_context.dicts, lambda d: "forloop" in d)
        to_context.update(from_context.dicts[forloop_dict_index])

get_injected_context_var ¤

get_injected_context_var(component_name: str, context: Context, key: str, default: Optional[Any] = None) -> Any

Retrieve a 'provided' field. The field MUST have been previously 'provided' by the component's ancestors using the {% provide %} template tag.

Source code in src/django_components/context.py
def get_injected_context_var(
    component_name: str,
    context: Context,
    key: str,
    default: Optional[Any] = None,
) -> Any:
    """
    Retrieve a 'provided' field. The field MUST have been previously 'provided'
    by the component's ancestors using the `{% provide %}` template tag.
    """
    # NOTE: For simplicity, we keep the provided values directly on the context.
    # This plays nicely with Django's Context, which behaves like a stack, so "newer"
    # values overshadow the "older" ones.
    internal_key = _INJECT_CONTEXT_KEY_PREFIX + key

    # Return provided value if found
    if internal_key in context:
        return context[internal_key]

    # If a default was given, return that
    if default is not None:
        return default

    # Otherwise raise error
    raise KeyError(
        f"Component '{component_name}' tried to inject a variable '{key}' before it was provided."
        f" To fix this, make sure that at least one ancestor of component '{component_name}' has"
        f" the variable '{key}' in their 'provide' attribute."
    )

prepare_context ¤

prepare_context(context: Context, component_id: str) -> None

Initialize the internal context state.

Source code in src/django_components/context.py
def prepare_context(
    context: Context,
    component_id: str,
) -> None:
    """Initialize the internal context state."""
    # Initialize mapping dicts within this rendering run.
    # This is shared across the whole render chain, thus we set it only once.
    if _FILLED_SLOTS_CONTENT_CONTEXT_KEY not in context:
        context[_FILLED_SLOTS_CONTENT_CONTEXT_KEY] = {}

    set_component_id(context, component_id)

set_component_id ¤

set_component_id(context: Context, component_id: str) -> None

We use the Context object to pass down info on inside of which component we are currently rendering.

Source code in src/django_components/context.py
def set_component_id(context: Context, component_id: str) -> None:
    """
    We use the Context object to pass down info on inside of which component
    we are currently rendering.
    """
    context[_CURRENT_COMP_CONTEXT_KEY] = component_id

set_provided_context_var ¤

set_provided_context_var(context: Context, key: str, provided_kwargs: Dict[str, Any]) -> None

'Provide' given data under given key. In other words, this data can be retrieved using self.inject(key) inside of get_context_data() method of components that are nested inside the {% provide %} tag.

Source code in src/django_components/context.py
def set_provided_context_var(
    context: Context,
    key: str,
    provided_kwargs: Dict[str, Any],
) -> None:
    """
    'Provide' given data under given key. In other words, this data can be retrieved
    using `self.inject(key)` inside of `get_context_data()` method of components that
    are nested inside the `{% provide %}` tag.
    """
    # NOTE: We raise TemplateSyntaxError since this func should be called only from
    # within template.
    if not key:
        raise TemplateSyntaxError(
            "Provide tag received an empty string. Key must be non-empty and a valid identifier."
        )
    if not key.isidentifier():
        raise TemplateSyntaxError(
            "Provide tag received a non-identifier string. Key must be non-empty and a valid identifier."
        )

    # We turn the kwargs into a NamedTuple so that the object that's "provided"
    # is immutable. This ensures that the data returned from `inject` will always
    # have all the keys that were passed to the `provide` tag.
    tpl_cls = namedtuple("DepInject", provided_kwargs.keys())  # type: ignore[misc]
    payload = tpl_cls(**provided_kwargs)

    internal_key = _INJECT_CONTEXT_KEY_PREFIX + key
    context[internal_key] = payload

expression ¤

Operator ¤

Bases: ABC

Operator describes something that somehow changes the inputs to template tags (the {% %}).

For example, a SpreadOperator inserts one or more kwargs at the specified location.

SpreadOperator ¤

SpreadOperator(expr: Expression)

Bases: Operator

Operator that inserts one or more kwargs at the specified location.

Source code in src/django_components/expression.py
def __init__(self, expr: Expression) -> None:
    self.expr = expr

process_aggregate_kwargs ¤

process_aggregate_kwargs(kwargs: Mapping[str, Any]) -> Dict[str, Any]

This function aggregates "prefixed" kwargs into dicts. "Prefixed" kwargs start with some prefix delimited with : (e.g. attrs:).

Example:

process_component_kwargs({"abc:one": 1, "abc:two": 2, "def:three": 3, "four": 4})
# {"abc": {"one": 1, "two": 2}, "def": {"three": 3}, "four": 4}


We want to support a use case similar to Vue's fallthrough attributes. In other words, where a component author can designate a prop (input) which is a dict and which will be rendered as HTML attributes.

This is useful for allowing component users to tweak styling or add event handling to the underlying HTML. E.g.:

class="pa-4 d-flex text-black" or @click.stop="alert('clicked!')"

So if the prop is attrs, and the component is called like so:

{% component "my_comp" attrs=attrs %}

then, if attrs is:

{"class": "text-red pa-4", "@click": "dispatch('my_event', 123)"}

and the component template is:

<div {% html_attrs attrs add:class="extra-class" %}></div>

Then this renders:

<div class="text-red pa-4 extra-class" @click="dispatch('my_event', 123)" ></div>

However, this way it is difficult for the component user to define the attrs variable, especially if they want to combine static and dynamic values. Because they will need to pre-process the attrs dict.

So, instead, we allow to "aggregate" props into a dict. So all props that start with attrs:, like attrs:class="text-red", will be collected into a dict at key attrs.

This provides sufficient flexiblity to make it easy for component users to provide "fallthrough attributes", and sufficiently easy for component authors to process that input while still being able to provide their own keys.

Source code in src/django_components/expression.py
def process_aggregate_kwargs(kwargs: Mapping[str, Any]) -> Dict[str, Any]:
    """
    This function aggregates "prefixed" kwargs into dicts. "Prefixed" kwargs
    start with some prefix delimited with `:` (e.g. `attrs:`).

    Example:
    ```py
    process_component_kwargs({"abc:one": 1, "abc:two": 2, "def:three": 3, "four": 4})
    # {"abc": {"one": 1, "two": 2}, "def": {"three": 3}, "four": 4}
    ```

    ---

    We want to support a use case similar to Vue's fallthrough attributes.
    In other words, where a component author can designate a prop (input)
    which is a dict and which will be rendered as HTML attributes.

    This is useful for allowing component users to tweak styling or add
    event handling to the underlying HTML. E.g.:

    `class="pa-4 d-flex text-black"` or `@click.stop="alert('clicked!')"`

    So if the prop is `attrs`, and the component is called like so:
    ```django
    {% component "my_comp" attrs=attrs %}
    ```

    then, if `attrs` is:
    ```py
    {"class": "text-red pa-4", "@click": "dispatch('my_event', 123)"}
    ```

    and the component template is:
    ```django
    <div {% html_attrs attrs add:class="extra-class" %}></div>
    ```

    Then this renders:
    ```html
    <div class="text-red pa-4 extra-class" @click="dispatch('my_event', 123)" ></div>
    ```

    However, this way it is difficult for the component user to define the `attrs`
    variable, especially if they want to combine static and dynamic values. Because
    they will need to pre-process the `attrs` dict.

    So, instead, we allow to "aggregate" props into a dict. So all props that start
    with `attrs:`, like `attrs:class="text-red"`, will be collected into a dict
    at key `attrs`.

    This provides sufficient flexiblity to make it easy for component users to provide
    "fallthrough attributes", and sufficiently easy for component authors to process
    that input while still being able to provide their own keys.
    """
    processed_kwargs = {}
    nested_kwargs: Dict[str, Dict[str, Any]] = {}
    for key, val in kwargs.items():
        if not is_aggregate_key(key):
            processed_kwargs[key] = val
            continue

        # NOTE: Trim off the prefix from keys
        prefix, sub_key = key.split(":", 1)
        if prefix not in nested_kwargs:
            nested_kwargs[prefix] = {}
        nested_kwargs[prefix][sub_key] = val

    # Assign aggregated values into normal input
    for key, val in nested_kwargs.items():
        if key in processed_kwargs:
            raise TemplateSyntaxError(
                f"Received argument '{key}' both as a regular input ({key}=...)"
                f" and as an aggregate dict ('{key}:key=...'). Must be only one of the two"
            )
        processed_kwargs[key] = val

    return processed_kwargs

finders ¤

ComponentsFileSystemFinder ¤

ComponentsFileSystemFinder(app_names: Any = None, *args: Any, **kwargs: Any)

Bases: BaseFinder

A static files finder based on FileSystemFinder.

Differences: - This finder uses COMPONENTS.dirs setting to locate files instead of STATICFILES_DIRS. - Whether a file within COMPONENTS.dirs is considered a STATIC file is configured by COMPONENTS.static_files_allowed and COMPONENTS.forbidden_static_files. - If COMPONENTS.dirs is not set, defaults to settings.BASE_DIR / "components"

Source code in src/django_components/finders.py
def __init__(self, app_names: Any = None, *args: Any, **kwargs: Any) -> None:
    component_dirs = [str(p) for p in get_dirs()]

    # NOTE: The rest of the __init__ is the same as `django.contrib.staticfiles.finders.FileSystemFinder`,
    # but using our locations instead of STATICFILES_DIRS.

    # List of locations with static files
    self.locations: List[Tuple[str, str]] = []

    # Maps dir paths to an appropriate storage instance
    self.storages: Dict[str, FileSystemStorage] = {}
    for root in component_dirs:
        if isinstance(root, (list, tuple)):
            prefix, root = root
        else:
            prefix = ""
        if (prefix, root) not in self.locations:
            self.locations.append((prefix, root))
    for prefix, root in self.locations:
        filesystem_storage = FileSystemStorage(location=root)
        filesystem_storage.prefix = prefix
        self.storages[root] = filesystem_storage

    super().__init__(*args, **kwargs)

find ¤

find(path: str, all: bool = False) -> Union[List[str], str]

Look for files in the extra locations as defined in COMPONENTS.dirs.

Source code in src/django_components/finders.py
def find(self, path: str, all: bool = False) -> Union[List[str], str]:
    """
    Look for files in the extra locations as defined in COMPONENTS.dirs.
    """
    matches: List[str] = []
    for prefix, root in self.locations:
        if root not in searched_locations:
            searched_locations.append(root)
        matched_path = self.find_location(root, path, prefix)
        if matched_path:
            if not all:
                return matched_path
            matches.append(matched_path)
    return matches

find_location ¤

find_location(root: str, path: str, prefix: Optional[str] = None) -> Optional[str]

Find a requested static file in a location and return the found absolute path (or None if no match).

Source code in src/django_components/finders.py
def find_location(self, root: str, path: str, prefix: Optional[str] = None) -> Optional[str]:
    """
    Find a requested static file in a location and return the found
    absolute path (or ``None`` if no match).
    """
    if prefix:
        prefix = "%s%s" % (prefix, os.sep)
        if not path.startswith(prefix):
            return None
        path = path.removeprefix(prefix)
    path = safe_join(root, path)

    if os.path.exists(path) and self._is_path_valid(path):
        return path
    return None

list ¤

list(ignore_patterns: List[str]) -> Iterable[Tuple[str, FileSystemStorage]]

List all files in all locations.

Source code in src/django_components/finders.py
def list(self, ignore_patterns: List[str]) -> Iterable[Tuple[str, FileSystemStorage]]:
    """
    List all files in all locations.
    """
    for prefix, root in self.locations:
        # Skip nonexistent directories.
        if os.path.isdir(root):
            storage = self.storages[root]
            for path in get_files(storage, ignore_patterns):
                if self._is_path_valid(path):
                    yield path, storage

library ¤

Module for interfacing with Django's Library (django.template.library)

PROTECTED_TAGS module-attribute ¤

PROTECTED_TAGS = [
    "component_dependencies",
    "component_css_dependencies",
    "component_js_dependencies",
    "fill",
    "html_attrs",
    "provide",
    "slot",
]

These are the names that users cannot choose for their components, as they would conflict with other tags in the Library.

logger ¤

trace ¤

trace(logger: Logger, message: str, *args: Any, **kwargs: Any) -> None

TRACE level logger.

To display TRACE logs, set the logging level to 5.

Example:

LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "stream": sys.stdout,
        },
    },
    "loggers": {
        "django_components": {
            "level": 5,
            "handlers": ["console"],
        },
    },
}

Source code in src/django_components/logger.py
def trace(logger: logging.Logger, message: str, *args: Any, **kwargs: Any) -> None:
    """
    TRACE level logger.

    To display TRACE logs, set the logging level to 5.

    Example:
    ```py
    LOGGING = {
        "version": 1,
        "disable_existing_loggers": False,
        "handlers": {
            "console": {
                "class": "logging.StreamHandler",
                "stream": sys.stdout,
            },
        },
        "loggers": {
            "django_components": {
                "level": 5,
                "handlers": ["console"],
            },
        },
    }
    ```
    """
    if actual_trace_level_num == -1:
        setup_logging()
    if logger.isEnabledFor(actual_trace_level_num):
        logger.log(actual_trace_level_num, message, *args, **kwargs)

trace_msg ¤

trace_msg(
    action: Literal["PARSE", "ASSOC", "RENDR", "GET", "SET"],
    node_type: Literal["COMP", "FILL", "SLOT", "PROVIDE", "N/A"],
    node_name: str,
    node_id: str,
    msg: str = "",
    component_id: Optional[str] = None,
) -> None

TRACE level logger with opinionated format for tracing interaction of components, nodes, and slots. Formats messages like so:

"ASSOC SLOT test_slot ID 0088 TO COMP 0087"

Source code in src/django_components/logger.py
def trace_msg(
    action: Literal["PARSE", "ASSOC", "RENDR", "GET", "SET"],
    node_type: Literal["COMP", "FILL", "SLOT", "PROVIDE", "N/A"],
    node_name: str,
    node_id: str,
    msg: str = "",
    component_id: Optional[str] = None,
) -> None:
    """
    TRACE level logger with opinionated format for tracing interaction of components,
    nodes, and slots. Formats messages like so:

    `"ASSOC SLOT test_slot ID 0088 TO COMP 0087"`
    """
    msg_prefix = ""
    if action == "ASSOC":
        if not component_id:
            raise ValueError("component_id must be set for the ASSOC action")
        msg_prefix = f"TO COMP {component_id}"
    elif action == "RENDR" and node_type == "FILL":
        if not component_id:
            raise ValueError("component_id must be set for the RENDER action")
        msg_prefix = f"FOR COMP {component_id}"

    msg_parts = [f"{action} {node_type} {node_name} ID {node_id}", *([msg_prefix] if msg_prefix else []), msg]
    full_msg = " ".join(msg_parts)

    # NOTE: When debugging tests during development, it may be easier to change
    # this to `print()`
    trace(logger, full_msg)

middleware ¤

ComponentDependencyMiddleware ¤

ComponentDependencyMiddleware(get_response: Callable[[HttpRequest], HttpResponse])

Middleware that inserts CSS/JS dependencies for all rendered components at points marked with template tags.

Source code in src/django_components/middleware.py
def __init__(self, get_response: "Callable[[HttpRequest], HttpResponse]") -> None:
    self.get_response = get_response

    if iscoroutinefunction(self.get_response):
        markcoroutinefunction(self)

DependencyReplacer ¤

DependencyReplacer(css_string: bytes, js_string: bytes)

Replacer for use in re.sub that replaces the first placeholder CSS and JS tags it encounters and removes any subsequent ones.

Source code in src/django_components/middleware.py
def __init__(self, css_string: bytes, js_string: bytes) -> None:
    self.js_string = js_string
    self.css_string = css_string

join_media ¤

join_media(components: Iterable[Component]) -> Media

Return combined media object for iterable of components.

Source code in src/django_components/middleware.py
def join_media(components: Iterable["Component"]) -> Media:
    """Return combined media object for iterable of components."""

    return sum([component.media for component in components], Media())

node ¤

BaseNode ¤

BaseNode(
    nodelist: Optional[NodeList] = None,
    node_id: Optional[str] = None,
    args: Optional[List[Expression]] = None,
    kwargs: Optional[RuntimeKwargs] = None,
)

Bases: Node

Shared behavior for our subclasses of Django's Node

Source code in src/django_components/node.py
def __init__(
    self,
    nodelist: Optional[NodeList] = None,
    node_id: Optional[str] = None,
    args: Optional[List[Expression]] = None,
    kwargs: Optional[RuntimeKwargs] = None,
):
    self.nodelist = nodelist or NodeList()
    self.node_id = node_id or gen_id()
    self.args = args or []
    self.kwargs = kwargs or RuntimeKwargs({})

get_node_children ¤

get_node_children(node: Node, context: Optional[Context] = None) -> NodeList

Get child Nodes from Node's nodelist atribute.

This function is taken from get_nodes_by_type method of django.template.base.Node.

Source code in src/django_components/node.py
def get_node_children(node: Node, context: Optional[Context] = None) -> NodeList:
    """
    Get child Nodes from Node's nodelist atribute.

    This function is taken from `get_nodes_by_type` method of `django.template.base.Node`.
    """
    # Special case - {% extends %} tag - Load the template and go deeper
    if isinstance(node, ExtendsNode):
        # NOTE: When {% extends %} node is being parsed, it collects all remaining template
        # under node.nodelist.
        # Hence, when we come across ExtendsNode in the template, we:
        # 1. Go over all nodes in the template using `node.nodelist`
        # 2. Go over all nodes in the "parent" template, via `node.get_parent`
        nodes = NodeList()
        nodes.extend(node.nodelist)
        template = node.get_parent(context)
        nodes.extend(template.nodelist)
        return nodes

    # Special case - {% include %} tag - Load the template and go deeper
    elif isinstance(node, IncludeNode):
        template = get_template_for_include_node(node, context)
        return template.nodelist

    nodes = NodeList()
    for attr in node.child_nodelists:
        nodelist = getattr(node, attr, [])
        if nodelist:
            nodes.extend(nodelist)
    return nodes

get_template_for_include_node ¤

get_template_for_include_node(include_node: IncludeNode, context: Context) -> Template

This snippet is taken directly from IncludeNode.render(). Unfortunately the render logic doesn't separate out template loading logic from rendering, so we have to copy the method.

Source code in src/django_components/node.py
def get_template_for_include_node(include_node: IncludeNode, context: Context) -> Template:
    """
    This snippet is taken directly from `IncludeNode.render()`. Unfortunately the
    render logic doesn't separate out template loading logic from rendering, so we
    have to copy the method.
    """
    template = include_node.template.resolve(context)
    # Does this quack like a Template?
    if not callable(getattr(template, "render", None)):
        # If not, try the cache and select_template().
        template_name = template or ()
        if isinstance(template_name, str):
            template_name = (
                construct_relative_path(
                    include_node.origin.template_name,
                    template_name,
                ),
            )
        else:
            template_name = tuple(template_name)
        cache = context.render_context.dicts[0].setdefault(include_node, {})
        template = cache.get(template_name)
        if template is None:
            template = context.template.engine.select_template(template_name)
            cache[template_name] = template
    # Use the base.Template of a backends.django.Template.
    elif hasattr(template, "template"):
        template = template.template
    return template

walk_nodelist ¤

walk_nodelist(nodes: NodeList, callback: Callable[[Node], Optional[str]], context: Optional[Context] = None) -> None

Recursively walk a NodeList, calling callback for each Node.

Source code in src/django_components/node.py
def walk_nodelist(
    nodes: NodeList,
    callback: Callable[[Node], Optional[str]],
    context: Optional[Context] = None,
) -> None:
    """Recursively walk a NodeList, calling `callback` for each Node."""
    node_queue: List[NodeTraverse] = [NodeTraverse(node=node, parent=None) for node in nodes]
    while len(node_queue):
        traverse = node_queue.pop()
        callback(traverse)
        child_nodes = get_node_children(traverse.node, context)
        child_traverses = [NodeTraverse(node=child_node, parent=traverse) for child_node in child_nodes]
        node_queue.extend(child_traverses)

provide ¤

ProvideNode ¤

ProvideNode(nodelist: NodeList, trace_id: str, node_id: Optional[str] = None, kwargs: Optional[RuntimeKwargs] = None)

Bases: BaseNode

Implementation of the {% provide %} tag. For more info see Component.inject.

Source code in src/django_components/provide.py
def __init__(
    self,
    nodelist: NodeList,
    trace_id: str,
    node_id: Optional[str] = None,
    kwargs: Optional[RuntimeKwargs] = None,
):
    super().__init__(nodelist=nodelist, args=None, kwargs=kwargs, node_id=node_id)

    self.nodelist = nodelist
    self.node_id = node_id or gen_id()
    self.trace_id = trace_id
    self.kwargs = kwargs or RuntimeKwargs({})

slots ¤

FillContent dataclass ¤

FillContent(content_func: SlotFunc[TSlotData], slot_default_var: Optional[SlotDefaultName], slot_data_var: Optional[SlotDataName])

Bases: Generic[TSlotData]

This represents content set with the {% fill %} tag, e.g.:

{% component "my_comp" %}
    {% fill "first_slot" %} <--- This
        hi
        {{ my_var }}
        hello
    {% endfill %}
{% endcomponent %}

FillNode ¤

FillNode(nodelist: NodeList, kwargs: RuntimeKwargs, trace_id: str, node_id: Optional[str] = None, is_implicit: bool = False)

Bases: BaseNode

Set when a component tag pair is passed template content that excludes fill tags. Nodes of this type contribute their nodelists to slots marked as 'default'.

Source code in src/django_components/slots.py
def __init__(
    self,
    nodelist: NodeList,
    kwargs: RuntimeKwargs,
    trace_id: str,
    node_id: Optional[str] = None,
    is_implicit: bool = False,
):
    super().__init__(nodelist=nodelist, args=None, kwargs=kwargs, node_id=node_id)

    self.is_implicit = is_implicit
    self.trace_id = trace_id
    self.component_id: Optional[str] = None

Slot ¤

Bases: NamedTuple

This represents content set with the {% slot %} tag, e.g.:

{% slot "my_comp" default %} <--- This
    hi
    {{ my_var }}
    hello
{% endslot %}

SlotFill dataclass ¤

SlotFill(
    name: str,
    escaped_name: str,
    is_filled: bool,
    content_func: SlotFunc[TSlotData],
    slot_default_var: Optional[SlotDefaultName],
    slot_data_var: Optional[SlotDataName],
)

Bases: Generic[TSlotData]

SlotFill describes what WILL be rendered.

It is a Slot that has been resolved against FillContents passed to a Component.

SlotNode ¤

SlotNode(
    nodelist: NodeList,
    trace_id: str,
    node_id: Optional[str] = None,
    kwargs: Optional[RuntimeKwargs] = None,
    is_required: bool = False,
    is_default: bool = False,
)

Bases: BaseNode

Source code in src/django_components/slots.py
def __init__(
    self,
    nodelist: NodeList,
    trace_id: str,
    node_id: Optional[str] = None,
    kwargs: Optional[RuntimeKwargs] = None,
    is_required: bool = False,
    is_default: bool = False,
):
    super().__init__(nodelist=nodelist, args=None, kwargs=kwargs, node_id=node_id)

    self.is_required = is_required
    self.is_default = is_default
    self.trace_id = trace_id

SlotRef ¤

SlotRef(slot: SlotNode, context: Context)

SlotRef allows to treat a slot as a variable. The slot is rendered only once the instance is coerced to string.

This is used to access slots as variables inside the templates. When a SlotRef is rendered in the template with {{ my_lazy_slot }}, it will output the contents of the slot.

Source code in src/django_components/slots.py
def __init__(self, slot: "SlotNode", context: Context):
    self._slot = slot
    self._context = context

parse_slot_fill_nodes_from_component_nodelist ¤

parse_slot_fill_nodes_from_component_nodelist(nodes: Tuple[Node, ...], ignored_nodes: Tuple[Type[Node]]) -> List[FillNode]

Given a component body (django.template.NodeList), find all slot fills, whether defined explicitly with {% fill %} or implicitly.

So if we have a component body:

{% component "mycomponent" %}
    {% fill "first_fill" %}
        Hello!
    {% endfill %}
    {% fill "second_fill" %}
        Hello too!
    {% endfill %}
{% endcomponent %}
Then this function returns the nodes (django.template.Node) for fill "first_fill" and fill "second_fill".

Source code in src/django_components/slots.py
@lazy_cache(lambda: lru_cache(maxsize=app_settings.TEMPLATE_CACHE_SIZE))
def parse_slot_fill_nodes_from_component_nodelist(
    nodes: Tuple[Node, ...],
    ignored_nodes: Tuple[Type[Node]],
) -> List[FillNode]:
    """
    Given a component body (`django.template.NodeList`), find all slot fills,
    whether defined explicitly with `{% fill %}` or implicitly.

    So if we have a component body:
    ```django
    {% component "mycomponent" %}
        {% fill "first_fill" %}
            Hello!
        {% endfill %}
        {% fill "second_fill" %}
            Hello too!
        {% endfill %}
    {% endcomponent %}
    ```
    Then this function returns the nodes (`django.template.Node`) for `fill "first_fill"`
    and `fill "second_fill"`.
    """
    fill_nodes: List[FillNode] = []
    if nodelist_has_content(nodes):
        for parse_fn in (
            _try_parse_as_default_fill,
            _try_parse_as_named_fill_tag_set,
        ):
            curr_fill_nodes = parse_fn(nodes, ignored_nodes)
            if curr_fill_nodes:
                fill_nodes = curr_fill_nodes
                break
        else:
            raise TemplateSyntaxError(
                "Illegal content passed to 'component' tag pair. "
                "Possible causes: 1) Explicit 'fill' tags cannot occur alongside other "
                "tags except comment tags; 2) Default (default slot-targeting) content "
                "is mixed with explict 'fill' tags."
            )
    return fill_nodes

resolve_slots ¤

resolve_slots(
    context: Context,
    template: Template,
    component_name: Optional[str],
    fill_content: Dict[SlotName, FillContent],
    is_dynamic_component: bool = False,
) -> Tuple[Dict[SlotId, Slot], Dict[SlotId, SlotFill]]

Search the template for all SlotNodes, and associate the slots with the given fills.

Returns tuple of: - Slots defined in the component's Template with {% slot %} tag - SlotFills (AKA slots matched with fills) describing what will be rendered for each slot.

Source code in src/django_components/slots.py
def resolve_slots(
    context: Context,
    template: Template,
    component_name: Optional[str],
    fill_content: Dict[SlotName, FillContent],
    is_dynamic_component: bool = False,
) -> Tuple[Dict[SlotId, Slot], Dict[SlotId, SlotFill]]:
    """
    Search the template for all SlotNodes, and associate the slots
    with the given fills.

    Returns tuple of:
    - Slots defined in the component's Template with `{% slot %}` tag
    - SlotFills (AKA slots matched with fills) describing what will be rendered for each slot.
    """
    slot_fills = {
        name: SlotFill(
            name=name,
            escaped_name=_escape_slot_name(name),
            is_filled=True,
            content_func=fill.content_func,
            slot_default_var=fill.slot_default_var,
            slot_data_var=fill.slot_data_var,
        )
        for name, fill in fill_content.items()
    }

    slots: Dict[SlotId, Slot] = {}
    # This holds info on which slot (key) has which slots nested in it (value list)
    slot_children: Dict[SlotId, List[SlotId]] = {}
    all_nested_slots: Set[SlotId] = set()

    def on_node(entry: NodeTraverse) -> None:
        node = entry.node
        if not isinstance(node, SlotNode):
            return

        slot_name, _ = node.resolve_kwargs(context, component_name)

        # 1. Collect slots
        # Basically we take all the important info form the SlotNode, so the logic is
        # less coupled to Django's Template/Node. Plain tuples should also help with
        # troubleshooting.
        slot = Slot(
            id=node.node_id,
            name=slot_name,
            nodelist=node.nodelist,
            is_default=node.is_default,
            is_required=node.is_required,
        )
        slots[node.node_id] = slot

        # 2. Figure out which Slots are nested in other Slots, so we can render
        # them from outside-inwards, so we can skip inner Slots if fills are provided.
        # We should end up with a graph-like data like:
        # - 0001: [0002]
        # - 0002: []
        # - 0003: [0004]
        # In other words, the data tells us that slot ID 0001 is PARENT of slot 0002.
        parent_slot_entry = entry.parent
        while parent_slot_entry is not None:
            if not isinstance(parent_slot_entry.node, SlotNode):
                parent_slot_entry = parent_slot_entry.parent
                continue

            parent_slot_id = parent_slot_entry.node.node_id
            if parent_slot_id not in slot_children:
                slot_children[parent_slot_id] = []
            slot_children[parent_slot_id].append(node.node_id)
            all_nested_slots.add(node.node_id)
            break

    walk_nodelist(template.nodelist, on_node, context)

    # 3. Figure out which slot the default/implicit fill belongs to
    slot_fills = _resolve_default_slot(
        template_name=template.name,
        component_name=component_name,
        slots=slots,
        slot_fills=slot_fills,
        is_dynamic_component=is_dynamic_component,
    )

    # 4. Detect any errors with slots/fills
    # NOTE: We ignore errors for the dynamic component, as the underlying component
    # will deal with it
    if not is_dynamic_component:
        _report_slot_errors(slots, slot_fills, component_name)

    # 5. Find roots of the slot relationships
    top_level_slot_ids: List[SlotId] = [node_id for node_id in slots.keys() if node_id not in all_nested_slots]

    # 6. Walk from out-most slots inwards, and decide whether and how
    # we will render each slot.
    resolved_slots: Dict[SlotId, SlotFill] = {}
    slot_ids_queue = deque([*top_level_slot_ids])
    while len(slot_ids_queue):
        slot_id = slot_ids_queue.pop()
        slot = slots[slot_id]

        # Check if there is a slot fill for given slot name
        if slot.name in slot_fills:
            # If yes, we remember which slot we want to replace with already-rendered fills
            resolved_slots[slot_id] = slot_fills[slot.name]
            # Since the fill cannot include other slots, we can leave this path
            continue
        else:
            # If no, then the slot is NOT filled, and we will render the slot's default (what's
            # between the slot tags)
            resolved_slots[slot_id] = SlotFill(
                name=slot.name,
                escaped_name=_escape_slot_name(slot.name),
                is_filled=False,
                content_func=_nodelist_to_slot_render_func(slot.nodelist),
                slot_default_var=None,
                slot_data_var=None,
            )
            # Since the slot's default CAN include other slots (because it's defined in
            # the same template), we need to enqueue the slot's children
            if slot_id in slot_children and slot_children[slot_id]:
                slot_ids_queue.extend(slot_children[slot_id])

    # By the time we get here, we should know, for each slot, how it will be rendered
    # -> Whether it will be replaced with a fill, or whether we render slot's defaults.
    return slots, resolved_slots

tag_formatter ¤

ComponentFormatter ¤

ComponentFormatter(tag: str)

Bases: TagFormatterABC

The original django_component's component tag formatter, it uses the component and endcomponent tags, and the component name is gives as the first positional arg.

Example as block:

{% component "mycomp" abc=123 %}
    {% fill "myfill" %}
        ...
    {% endfill %}
{% endcomponent %}

Example as inlined tag:

{% component "mycomp" abc=123 / %}

Source code in src/django_components/tag_formatter.py
def __init__(self, tag: str):
    self.tag = tag

InternalTagFormatter ¤

InternalTagFormatter(tag_formatter: TagFormatterABC)

Internal wrapper around user-provided TagFormatters, so that we validate the outputs.

Source code in src/django_components/tag_formatter.py
def __init__(self, tag_formatter: TagFormatterABC):
    self.tag_formatter = tag_formatter

ShorthandComponentFormatter ¤

Bases: TagFormatterABC

The component tag formatter that uses <name> / end<name> tags.

This is similar to django-web-components and django-slippers syntax.

Example as block:

{% mycomp abc=123 %}
    {% fill "myfill" %}
        ...
    {% endfill %}
{% endmycomp %}

Example as inlined tag:

{% mycomp abc=123 / %}

TagFormatterABC ¤

Bases: ABC

end_tag abstractmethod ¤

end_tag(name: str) -> str

Formats the end tag of a block component.

Source code in src/django_components/tag_formatter.py
@abc.abstractmethod
def end_tag(self, name: str) -> str:
    """Formats the end tag of a block component."""
    ...

parse abstractmethod ¤

parse(tokens: List[str]) -> TagResult

Given the tokens (words) of a component start tag, this function extracts the component name from the tokens list, and returns TagResult, which is a tuple of (component_name, remaining_tokens).

Example:

Given a component declarations:

{% component "my_comp" key=val key2=val2 %}

This function receives a list of tokens

['component', '"my_comp"', 'key=val', 'key2=val2']

component is the tag name, which we drop. "my_comp" is the component name, but we must remove the extra quotes. And we pass remaining tokens unmodified, as that's the input to the component.

So in the end, we return a tuple:

('my_comp', ['key=val', 'key2=val2'])

Source code in src/django_components/tag_formatter.py
@abc.abstractmethod
def parse(self, tokens: List[str]) -> TagResult:
    """
    Given the tokens (words) of a component start tag, this function extracts
    the component name from the tokens list, and returns `TagResult`, which
    is a tuple of `(component_name, remaining_tokens)`.

    Example:

    Given a component declarations:

    `{% component "my_comp" key=val key2=val2 %}`

    This function receives a list of tokens

    `['component', '"my_comp"', 'key=val', 'key2=val2']`

    `component` is the tag name, which we drop. `"my_comp"` is the component name,
    but we must remove the extra quotes. And we pass remaining tokens unmodified,
    as that's the input to the component.

    So in the end, we return a tuple:

    `('my_comp', ['key=val', 'key2=val2'])`
    """
    ...

start_tag abstractmethod ¤

start_tag(name: str) -> str

Formats the start tag of a component.

Source code in src/django_components/tag_formatter.py
@abc.abstractmethod
def start_tag(self, name: str) -> str:
    """Formats the start tag of a component."""
    ...

TagResult ¤

Bases: NamedTuple

The return value from TagFormatter.parse()

component_name instance-attribute ¤

component_name: str

Component name extracted from the template tag

tokens instance-attribute ¤

tokens: List[str]

Remaining tokens (words) that were passed to the tag, with component name removed

get_tag_formatter ¤

get_tag_formatter(registry: ComponentRegistry) -> InternalTagFormatter

Returns an instance of the currently configured component tag formatter.

Source code in src/django_components/tag_formatter.py
def get_tag_formatter(registry: "ComponentRegistry") -> InternalTagFormatter:
    """Returns an instance of the currently configured component tag formatter."""
    # Allow users to configure the component TagFormatter
    formatter_cls_or_str = registry.settings.TAG_FORMATTER

    if isinstance(formatter_cls_or_str, str):
        tag_formatter: TagFormatterABC = import_string(formatter_cls_or_str)
    else:
        tag_formatter = formatter_cls_or_str

    return InternalTagFormatter(tag_formatter)

template ¤

cached_template ¤

cached_template(
    template_string: str,
    template_cls: Optional[Type[Template]] = None,
    origin: Optional[Origin] = None,
    name: Optional[str] = None,
    engine: Optional[Any] = None,
) -> Template

Create a Template instance that will be cached as per the TEMPLATE_CACHE_SIZE setting.

Source code in src/django_components/template.py
def cached_template(
    template_string: str,
    template_cls: Optional[Type[Template]] = None,
    origin: Optional[Origin] = None,
    name: Optional[str] = None,
    engine: Optional[Any] = None,
) -> Template:
    """Create a Template instance that will be cached as per the `TEMPLATE_CACHE_SIZE` setting."""
    template = _create_template(template_cls or Template, template_string, engine)

    # Assign the origin and name separately, so the caching doesn't depend on them
    # Since we might be accessing a template from cache, we want to define these only once
    if not getattr(template, "_dc_cached", False):
        template.origin = origin or Origin(UNKNOWN_SOURCE)
        template.name = name
        template._dc_cached = True

    return template

template_loader ¤

Template loader that loads templates from each Django app's "components" directory.

Loader ¤

Bases: Loader

get_dirs ¤

get_dirs(include_apps: bool = True) -> List[Path]

Prepare directories that may contain component files:

Searches for dirs set in COMPONENTS.dirs settings. If none set, defaults to searching for a "components" app. The dirs in COMPONENTS.dirs must be absolute paths.

In addition to that, also all apps are checked for [app]/components dirs.

Paths are accepted only if they resolve to a directory. E.g. /path/to/django_project/my_app/components/.

BASE_DIR setting is required.

Source code in src/django_components/template_loader.py
def get_dirs(self, include_apps: bool = True) -> List[Path]:
    """
    Prepare directories that may contain component files:

    Searches for dirs set in `COMPONENTS.dirs` settings. If none set, defaults to searching
    for a "components" app. The dirs in `COMPONENTS.dirs` must be absolute paths.

    In addition to that, also all apps are checked for `[app]/components` dirs.

    Paths are accepted only if they resolve to a directory.
    E.g. `/path/to/django_project/my_app/components/`.

    `BASE_DIR` setting is required.
    """
    # Allow to configure from settings which dirs should be checked for components
    component_dirs = app_settings.DIRS

    # TODO_REMOVE_IN_V1
    is_legacy_paths = (
        # Use value of `STATICFILES_DIRS` ONLY if `COMPONENT.dirs` not set
        not getattr(settings, "COMPONENTS", {}).get("dirs", None) is not None
        and hasattr(settings, "STATICFILES_DIRS")
        and settings.STATICFILES_DIRS
    )
    if is_legacy_paths:
        # NOTE: For STATICFILES_DIRS, we use the defaults even for empty list.
        # We don't do this for COMPONENTS.dirs, so user can explicitly specify "NO dirs".
        component_dirs = settings.STATICFILES_DIRS or [settings.BASE_DIR / "components"]
    source = "STATICFILES_DIRS" if is_legacy_paths else "COMPONENTS.dirs"

    logger.debug(
        "Template loader will search for valid template dirs from following options:\n"
        + "\n".join([f" - {str(d)}" for d in component_dirs])
    )

    # Add `[app]/[APP_DIR]` to the directories. This is, by default `[app]/components`
    app_paths: List[Path] = []
    if include_apps:
        for conf in apps.get_app_configs():
            for app_dir in app_settings.APP_DIRS:
                comps_path = Path(conf.path).joinpath(app_dir)
                if comps_path.exists():
                    app_paths.append(comps_path)

    directories: Set[Path] = set(app_paths)

    # Validate and add other values from the config
    for component_dir in component_dirs:
        # Consider tuples for STATICFILES_DIRS (See #489)
        # See https://docs.djangoproject.com/en/5.0/ref/settings/#prefixes-optional
        if isinstance(component_dir, (tuple, list)):
            component_dir = component_dir[1]
        try:
            Path(component_dir)
        except TypeError:
            logger.warning(
                f"{source} expected str, bytes or os.PathLike object, or tuple/list of length 2. "
                f"See Django documentation for STATICFILES_DIRS. Got {type(component_dir)} : {component_dir}"
            )
            continue

        if not Path(component_dir).is_absolute():
            raise ValueError(f"{source} must contain absolute paths, got '{component_dir}'")
        else:
            directories.add(Path(component_dir).resolve())

    logger.debug(
        "Template loader matched following template dirs:\n" + "\n".join([f" - {str(d)}" for d in directories])
    )
    return list(directories)

get_dirs ¤

get_dirs(include_apps: bool = True, engine: Optional[Engine] = None) -> List[Path]

Helper for using django_component's FilesystemLoader class to obtain a list of directories where component python files may be defined.

Source code in src/django_components/template_loader.py
def get_dirs(include_apps: bool = True, engine: Optional[Engine] = None) -> List[Path]:
    """
    Helper for using django_component's FilesystemLoader class to obtain a list
    of directories where component python files may be defined.
    """
    current_engine = engine
    if current_engine is None:
        current_engine = Engine.get_default()

    loader = Loader(current_engine)
    return loader.get_dirs(include_apps)

template_parser ¤

Overrides for the Django Template system to allow finer control over template parsing.

Based on Django Slippers v0.6.2 - https://github.com/mixxorz/slippers/blob/main/slippers/template.py

parse_bits ¤

parse_bits(
    parser: Parser, bits: List[str], params: List[str], name: str
) -> Tuple[List[FilterExpression], List[Tuple[str, FilterExpression]]]

Parse bits for template tag helpers simple_tag and inclusion_tag, in particular by detecting syntax errors and by extracting positional and keyword arguments.

This is a simplified version of django.template.library.parse_bits where we use custom regex to handle special characters in keyword names.

Furthermore, our version allows duplicate keys, and instead of return kwargs as a dict, we return it as a list of key-value pairs. So it is up to the user of this function to decide whether they support duplicate keys or not.

Source code in src/django_components/template_parser.py
def parse_bits(
    parser: Parser,
    bits: List[str],
    params: List[str],
    name: str,
) -> Tuple[List[FilterExpression], List[Tuple[str, FilterExpression]]]:
    """
    Parse bits for template tag helpers simple_tag and inclusion_tag, in
    particular by detecting syntax errors and by extracting positional and
    keyword arguments.

    This is a simplified version of `django.template.library.parse_bits`
    where we use custom regex to handle special characters in keyword names.

    Furthermore, our version allows duplicate keys, and instead of return kwargs
    as a dict, we return it as a list of key-value pairs. So it is up to the
    user of this function to decide whether they support duplicate keys or not.
    """
    args: List[FilterExpression] = []
    kwargs: List[Tuple[str, FilterExpression]] = []
    unhandled_params = list(params)
    for bit in bits:
        # First we try to extract a potential kwarg from the bit
        kwarg = token_kwargs([bit], parser)
        if kwarg:
            # The kwarg was successfully extracted
            param, value = kwarg.popitem()
            # All good, record the keyword argument
            kwargs.append((str(param), value))
            if param in unhandled_params:
                # If using the keyword syntax for a positional arg, then
                # consume it.
                unhandled_params.remove(param)
        else:
            if kwargs:
                raise TemplateSyntaxError(
                    "'%s' received some positional argument(s) after some " "keyword argument(s)" % name
                )
            else:
                # Record the positional argument
                args.append(parser.compile_filter(bit))
                try:
                    # Consume from the list of expected positional arguments
                    unhandled_params.pop(0)
                except IndexError:
                    pass
    if unhandled_params:
        # Some positional arguments were not supplied
        raise TemplateSyntaxError(
            "'%s' did not receive value(s) for the argument(s): %s"
            % (name, ", ".join("'%s'" % p for p in unhandled_params))
        )
    return args, kwargs

token_kwargs ¤

token_kwargs(bits: List[str], parser: Parser) -> Dict[str, FilterExpression]

Parse token keyword arguments and return a dictionary of the arguments retrieved from the bits token list.

bits is a list containing the remainder of the token (split by spaces) that is to be checked for arguments. Valid arguments are removed from this list.

There is no requirement for all remaining token bits to be keyword arguments, so return the dictionary as soon as an invalid argument format is reached.

Source code in src/django_components/template_parser.py
def token_kwargs(bits: List[str], parser: Parser) -> Dict[str, FilterExpression]:
    """
    Parse token keyword arguments and return a dictionary of the arguments
    retrieved from the ``bits`` token list.

    `bits` is a list containing the remainder of the token (split by spaces)
    that is to be checked for arguments. Valid arguments are removed from this
    list.

    There is no requirement for all remaining token ``bits`` to be keyword
    arguments, so return the dictionary as soon as an invalid argument format
    is reached.
    """
    if not bits:
        return {}
    match = kwarg_re.match(bits[0])
    kwarg_format = match and match[1]
    if not kwarg_format:
        return {}

    kwargs: Dict[str, FilterExpression] = {}
    while bits:
        if kwarg_format:
            match = kwarg_re.match(bits[0])
            if not match or not match[1]:
                return kwargs
            key, value = match.groups()
            del bits[:1]
        else:
            if len(bits) < 3 or bits[1] != "as":
                return kwargs
            key, value = bits[2], bits[0]
            del bits[:3]

        # This is the only difference from the original token_kwargs. We use
        # the ComponentsFilterExpression instead of the original FilterExpression.
        kwargs[key] = ComponentsFilterExpression(value, parser)
        if bits and not kwarg_format:
            if bits[0] != "and":
                return kwargs
            del bits[:1]
    return kwargs

templatetags ¤

component_tags ¤

component ¤

component(parser: Parser, token: Token, registry: ComponentRegistry, tag_name: str) -> ComponentNode
To give the component access to the template context

{% component "name" positional_arg keyword_arg=value ... %}

To render the component in an isolated context

{% component "name" positional_arg keyword_arg=value ... only %}

Positional and keyword arguments can be literals or template variables. The component name must be a single- or double-quotes string and must be either the first positional argument or, if there are no positional arguments, passed as 'name'.

Source code in src/django_components/templatetags/component_tags.py
def component(parser: Parser, token: Token, registry: ComponentRegistry, tag_name: str) -> ComponentNode:
    """
    To give the component access to the template context:
        ```#!htmldjango {% component "name" positional_arg keyword_arg=value ... %}```

    To render the component in an isolated context:
        ```#!htmldjango {% component "name" positional_arg keyword_arg=value ... only %}```

    Positional and keyword arguments can be literals or template variables.
    The component name must be a single- or double-quotes string and must
    be either the first positional argument or, if there are no positional
    arguments, passed as 'name'.
    """
    _fix_nested_tags(parser, token)
    bits = token.split_contents()

    # Let the TagFormatter pre-process the tokens
    formatter = get_tag_formatter(registry)
    result = formatter.parse([*bits])
    end_tag = formatter.end_tag(result.component_name)

    # NOTE: The tokens returned from TagFormatter.parse do NOT include the tag itself
    bits = [bits[0], *result.tokens]
    token.contents = " ".join(bits)

    tag = _parse_tag(
        tag_name,
        parser,
        token,
        params=[],
        extra_params=True,  # Allow many args
        flags=[COMP_ONLY_FLAG],
        keywordonly_kwargs=True,
        repeatable_kwargs=False,
        end_tag=end_tag,
    )

    # Check for isolated context keyword
    isolated_context = tag.flags[COMP_ONLY_FLAG]

    trace_msg("PARSE", "COMP", result.component_name, tag.id)

    body = tag.parse_body()
    fill_nodes = parse_slot_fill_nodes_from_component_nodelist(tuple(body), ignored_nodes=(ComponentNode,))

    # Tag all fill nodes as children of this particular component instance
    for node in fill_nodes:
        trace_msg("ASSOC", "FILL", node.trace_id, node.node_id, component_id=tag.id)
        node.component_id = tag.id

    component_node = ComponentNode(
        name=result.component_name,
        args=tag.args,
        kwargs=tag.kwargs,
        isolated_context=isolated_context,
        fill_nodes=fill_nodes,
        node_id=tag.id,
        registry=registry,
    )

    trace_msg("PARSE", "COMP", result.component_name, tag.id, "...Done!")
    return component_node

component_css_dependencies ¤

component_css_dependencies(preload: str = '') -> SafeString

Marks location where CSS link tags should be rendered.

Source code in src/django_components/templatetags/component_tags.py
@register.simple_tag(name="component_css_dependencies")
def component_css_dependencies(preload: str = "") -> SafeString:
    """Marks location where CSS link tags should be rendered."""

    if is_dependency_middleware_active():
        preloaded_dependencies = []
        for component in _get_components_from_preload_str(preload):
            preloaded_dependencies.append(RENDERED_COMMENT_TEMPLATE.format(name=component.registered_name))
        return mark_safe("\n".join(preloaded_dependencies) + CSS_DEPENDENCY_PLACEHOLDER)
    else:
        rendered_dependencies = []
        for component in _get_components_from_registry(component_registry):
            rendered_dependencies.append(component.render_css_dependencies())

        return mark_safe("\n".join(rendered_dependencies))

component_dependencies ¤

component_dependencies(preload: str = '') -> SafeString

Marks location where CSS link and JS script tags should be rendered.

Source code in src/django_components/templatetags/component_tags.py
@register.simple_tag(name="component_dependencies")
def component_dependencies(preload: str = "") -> SafeString:
    """Marks location where CSS link and JS script tags should be rendered."""

    if is_dependency_middleware_active():
        preloaded_dependencies = []
        for component in _get_components_from_preload_str(preload):
            preloaded_dependencies.append(RENDERED_COMMENT_TEMPLATE.format(name=component.registered_name))
        return mark_safe("\n".join(preloaded_dependencies) + CSS_DEPENDENCY_PLACEHOLDER + JS_DEPENDENCY_PLACEHOLDER)
    else:
        rendered_dependencies = []
        for component in _get_components_from_registry(component_registry):
            rendered_dependencies.append(component.render_dependencies())

        return mark_safe("\n".join(rendered_dependencies))

component_js_dependencies ¤

component_js_dependencies(preload: str = '') -> SafeString

Marks location where JS script tags should be rendered.

Source code in src/django_components/templatetags/component_tags.py
@register.simple_tag(name="component_js_dependencies")
def component_js_dependencies(preload: str = "") -> SafeString:
    """Marks location where JS script tags should be rendered."""

    if is_dependency_middleware_active():
        preloaded_dependencies = []
        for component in _get_components_from_preload_str(preload):
            preloaded_dependencies.append(RENDERED_COMMENT_TEMPLATE.format(name=component.registered_name))
        return mark_safe("\n".join(preloaded_dependencies) + JS_DEPENDENCY_PLACEHOLDER)
    else:
        rendered_dependencies = []
        for component in _get_components_from_registry(component_registry):
            rendered_dependencies.append(component.render_js_dependencies())

        return mark_safe("\n".join(rendered_dependencies))

fill ¤

fill(parser: Parser, token: Token) -> FillNode

Block tag whose contents 'fill' (are inserted into) an identically named 'slot'-block in the component template referred to by a parent component. It exists to make component nesting easier.

This tag is available only within a {% component %}..{% endcomponent %} block. Runtime checks should prohibit other usages.

Source code in src/django_components/templatetags/component_tags.py
@register.tag("fill")
def fill(parser: Parser, token: Token) -> FillNode:
    """
    Block tag whose contents 'fill' (are inserted into) an identically named
    'slot'-block in the component template referred to by a parent component.
    It exists to make component nesting easier.

    This tag is available only within a {% component %}..{% endcomponent %} block.
    Runtime checks should prohibit other usages.
    """
    tag = _parse_tag(
        "fill",
        parser,
        token,
        params=[SLOT_NAME_KWARG],
        optional_params=[SLOT_NAME_KWARG],
        keywordonly_kwargs=[SLOT_DATA_KWARG, SLOT_DEFAULT_KWARG],
        repeatable_kwargs=False,
        end_tag="endfill",
    )

    fill_name_kwarg = tag.kwargs.kwargs.get(SLOT_NAME_KWARG, None)
    trace_id = f"fill-id-{tag.id} ({fill_name_kwarg})" if fill_name_kwarg else f"fill-id-{tag.id}"

    trace_msg("PARSE", "FILL", trace_id, tag.id)

    body = tag.parse_body()
    fill_node = FillNode(
        nodelist=body,
        node_id=tag.id,
        kwargs=tag.kwargs,
        trace_id=trace_id,
    )

    trace_msg("PARSE", "FILL", trace_id, tag.id, "...Done!")
    return fill_node

html_attrs ¤

html_attrs(parser: Parser, token: Token) -> HtmlAttrsNode

This tag takes: - Optional dictionary of attributes (attrs) - Optional dictionary of defaults (defaults) - Additional kwargs that are appended to the former two

The inputs are merged and resulting dict is rendered as HTML attributes (key="value").

Rules: 1. Both attrs and defaults can be passed as positional args or as kwargs 2. Both attrs and defaults are optional (can be omitted) 3. Both attrs and defaults are dictionaries, and we can define them the same way we define dictionaries for the component tag. So either as attrs=attrs or attrs:key=value. 4. All other kwargs (key=value) are appended and can be repeated.

Normal kwargs (key=value) are concatenated to existing keys. So if e.g. key "class" is supplied with value "my-class", then adding class="extra-class" will result in `class="my-class extra-class".

Example:

{% html_attrs attrs defaults:class="default-class" class="extra-class" data-id="123" %}

Source code in src/django_components/templatetags/component_tags.py
@register.tag("html_attrs")
def html_attrs(parser: Parser, token: Token) -> HtmlAttrsNode:
    """
    This tag takes:
    - Optional dictionary of attributes (`attrs`)
    - Optional dictionary of defaults (`defaults`)
    - Additional kwargs that are appended to the former two

    The inputs are merged and resulting dict is rendered as HTML attributes
    (`key="value"`).

    Rules:
    1. Both `attrs` and `defaults` can be passed as positional args or as kwargs
    2. Both `attrs` and `defaults` are optional (can be omitted)
    3. Both `attrs` and `defaults` are dictionaries, and we can define them the same way
       we define dictionaries for the `component` tag. So either as `attrs=attrs` or
       `attrs:key=value`.
    4. All other kwargs (`key=value`) are appended and can be repeated.

    Normal kwargs (`key=value`) are concatenated to existing keys. So if e.g. key
    "class" is supplied with value "my-class", then adding `class="extra-class"`
    will result in `class="my-class extra-class".

    Example:
    ```htmldjango
    {% html_attrs attrs defaults:class="default-class" class="extra-class" data-id="123" %}
    ```
    """
    tag = _parse_tag(
        "html_attrs",
        parser,
        token,
        params=[HTML_ATTRS_ATTRS_KEY, HTML_ATTRS_DEFAULTS_KEY],
        optional_params=[HTML_ATTRS_ATTRS_KEY, HTML_ATTRS_DEFAULTS_KEY],
        flags=[],
        keywordonly_kwargs=True,
        repeatable_kwargs=True,
    )

    return HtmlAttrsNode(
        kwargs=tag.kwargs,
        kwarg_pairs=tag.kwarg_pairs,
    )

types ¤

Helper types for IDEs.

utils ¤

gen_id ¤

gen_id(length: int = 5) -> str

Generate a unique ID that can be associated with a Node

Source code in src/django_components/utils.py
def gen_id(length: int = 5) -> str:
    """Generate a unique ID that can be associated with a Node"""
    # Global counter to avoid conflicts
    global _id
    _id += 1

    # Pad the ID with `0`s up to 4 digits, e.g. `0007`
    return f"{_id:04}"

lazy_cache ¤

lazy_cache(make_cache: Callable[[], Callable[[Callable], Callable]]) -> Callable[[TFunc], TFunc]

Decorator that caches the given function similarly to functools.lru_cache. But the cache is instantiated only at first invocation.

cache argument is a function that generates the cache function, e.g. functools.lru_cache().

Source code in src/django_components/utils.py
def lazy_cache(
    make_cache: Callable[[], Callable[[Callable], Callable]],
) -> Callable[[TFunc], TFunc]:
    """
    Decorator that caches the given function similarly to `functools.lru_cache`.
    But the cache is instantiated only at first invocation.

    `cache` argument is a function that generates the cache function,
    e.g. `functools.lru_cache()`.
    """
    _cached_fn = None

    def decorator(fn: TFunc) -> TFunc:
        @functools.wraps(fn)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            # Lazily initialize the cache
            nonlocal _cached_fn
            if not _cached_fn:
                # E.g. `lambda: functools.lru_cache(maxsize=app_settings.TEMPLATE_CACHE_SIZE)`
                cache = make_cache()
                _cached_fn = cache(fn)

            return _cached_fn(*args, **kwargs)

        # Allow to access the LRU cache methods
        # See https://stackoverflow.com/a/37654201/9788634
        wrapper.cache_info = lambda: _cached_fn.cache_info()  # type: ignore
        wrapper.cache_clear = lambda: _cached_fn.cache_clear()  # type: ignore

        # And allow to remove the cache instance (mostly for tests)
        def cache_remove() -> None:
            nonlocal _cached_fn
            _cached_fn = None

        wrapper.cache_remove = cache_remove  # type: ignore

        return cast(TFunc, wrapper)

    return decorator