+ {% endif %}
+ ```
+
+ NOTE: `component_vars.is_filled` automatically escaped slot names, so that even slot names that are
+ not valid python identifiers could be set as slot names. `component_vars.slots` no longer does that.
+
+- Component attribute `Component.is_filled` is now deprecated. Will be removed in v1. Use `Component.slots` instead.
+
+ Before:
+
+ ```py
+ class MyComponent(Component):
+ def get_template_data(self, args, kwargs, slots, context):
+ if self.is_filled.footer:
+ color = "red"
+ else:
+ color = "blue"
+
+ return {
+ "color": color,
+ }
+ ```
+
+ After:
+
+ ```py
+ class MyComponent(Component):
+ def get_template_data(self, args, kwargs, slots, context):
+ if "footer" in slots:
+ color = "red"
+ else:
+ color = "blue"
+
+ return {
+ "color": color,
+ }
+ ```
+
+ NOTE: `Component.is_filled` automatically escaped slot names, so that even slot names that are
+ not valid python identifiers could be set as slot names. `Component.slots` no longer does that.
+
+**Miscellaneous**
+
+- Template caching with `cached_template()` helper and `template_cache_size` setting is deprecated.
+ These will be removed in v1.
+
+ This feature made sense if you were dynamically generating templates for components using
+ `Component.get_template_string()` and `Component.get_template()`.
+
+ However, in v1, each Component will have at most one static template. This static template
+ is cached internally per component class, and reused across renders.
+
+ This makes the template caching feature obsolete.
+
+ If you relied on `cached_template()`, you should either:
+
+ 1. Wrap the templates as Components.
+ 2. Manage the cache of Templates yourself.
+
+- The `debug_highlight_components` and `debug_highlight_slots` settings are deprecated.
+ These will be removed in v1.
+
+ The debug highlighting feature was re-implemented as an extension.
+ As such, the recommended way for enabling it has changed:
+
+ Before:
+
+ ```python
+ COMPONENTS = ComponentsSettings(
+ debug_highlight_components=True,
+ debug_highlight_slots=True,
+ )
+ ```
+
+ After:
+
+ Set `extensions_defaults` in your `settings.py` file.
+
+ ```python
+ COMPONENTS = ComponentsSettings(
+ extensions_defaults={
+ "debug_highlight": {
+ "highlight_components": True,
+ "highlight_slots": True,
+ },
+ },
+ )
+ ```
+
+ Alternatively, you can enable highlighting for specific components by setting `Component.DebugHighlight.highlight_components` to `True`:
+
+ ```python
+ class MyComponent(Component):
+ class DebugHighlight:
+ highlight_components = True
+ highlight_slots = True
+ ```
+
+#### Feat
+
+- New method to render template variables - `get_template_data()`
+
+ `get_template_data()` behaves the same way as `get_context_data()`, but has
+ a different function signature to accept also slots and context.
+
+ ```py
+ class Button(Component):
+ def get_template_data(self, args, kwargs, slots, context):
+ return {
+ "val1": args[0],
+ "val2": kwargs["field"],
+ }
+ ```
+
+ If you define `Component.Args`, `Component.Kwargs`, `Component.Slots`, then
+ the `args`, `kwargs`, `slots` arguments will be instances of these classes:
+
+ ```py
+ class Button(Component):
+ class Args(NamedTuple):
+ field1: str
+
+ class Kwargs(NamedTuple):
+ field2: int
+
+ def get_template_data(self, args: Args, kwargs: Kwargs, slots, context):
+ return {
+ "val1": args.field1,
+ "val2": kwargs.field2,
+ }
+ ```
+
+- Input validation is now part of the render process.
+
+ When you specify the input types (such as `Component.Args`, `Component.Kwargs`, etc),
+ the actual inputs to data methods (`Component.get_template_data()`, etc) will be instances of the types you specified.
+
+ This practically brings back input validation, because the instantiation of the types
+ will raise an error if the inputs are not valid.
+
+ Read more on [Typing and validation](https://django-components.github.io/django-components/latest/concepts/fundamentals/typing_and_validation/)
+
+- Render emails or other non-browser HTML with new "dependencies strategies"
+
+ When rendering a component with `Component.render()` or `Component.render_to_response()`,
+ the `deps_strategy` kwarg (previously `type`) now accepts additional options:
+
+ - `"simple"`
+ - `"prepend"`
+ - `"append"`
+ - `"ignore"`
+
+ ```py
+ Calendar.render_to_response(
+ request=request,
+ kwargs={
+ "date": request.GET.get("date", ""),
+ },
+ deps_strategy="append",
+ )
+ ```
+
+ Comparison of dependencies render strategies:
+
+ - `"document"`
+ - Smartly inserts JS / CSS into placeholders or into `` and `` tags.
+ - Inserts extra script to allow `fragment` strategy to work.
+ - Assumes the HTML will be rendered in a JS-enabled browser.
+ - `"fragment"`
+ - A lightweight HTML fragment to be inserted into a document with AJAX.
+ - Ignores placeholders and any `` / `` tags.
+ - No JS / CSS included.
+ - `"simple"`
+ - Smartly insert JS / CSS into placeholders or into `` and `` tags.
+ - No extra script loaded.
+ - `"prepend"`
+ - Insert JS / CSS before the rendered HTML.
+ - Ignores placeholders and any `` / `` tags.
+ - No extra script loaded.
+ - `"append"`
+ - Insert JS / CSS after the rendered HTML.
+ - Ignores placeholders and any `` / `` tags.
+ - No extra script loaded.
+ - `"ignore"`
+ - Rendered HTML is left as-is. You can still process it with a different strategy later with `render_dependencies()`.
+ - Used for inserting rendered HTML into other components.
+
+ See [Dependencies rendering](https://django-components.github.io/django-components/0.140.1/concepts/advanced/rendering_js_css/) for more info.
+
+- New `Component.args`, `Component.kwargs`, `Component.slots` attributes available on the component class itself.
+
+ These attributes are the same as the ones available in `Component.get_template_data()`.
+
+ You can use these in other methods like `Component.on_render_before()` or `Component.on_render_after()`.
+
+ ```py
+ from django_components import Component, SlotInput
+
+ class Table(Component):
+ class Args(NamedTuple):
+ page: int
+
+ class Kwargs(NamedTuple):
+ per_page: int
+
+ class Slots(NamedTuple):
+ content: SlotInput
+
+ def on_render_before(self, context: Context, template: Optional[Template]) -> None:
+ assert self.args.page == 123
+ assert self.kwargs.per_page == 10
+ content_html = self.slots.content()
+ ```
+
+ Same as with the parameters in `Component.get_template_data()`, they will be instances of the `Args`, `Kwargs`, `Slots` classes
+ if defined, or plain lists / dictionaries otherwise.
+
+- 4 attributes that were previously available only under the `Component.input` attribute
+ are now available directly on the Component instance:
+
+ - `Component.raw_args`
+ - `Component.raw_kwargs`
+ - `Component.raw_slots`
+ - `Component.deps_strategy`
+
+ The first 3 attributes are the same as the deprecated `Component.input.args`, `Component.input.kwargs`, `Component.input.slots` properties.
+
+ Compared to the `Component.args` / `Component.kwargs` / `Component.slots` attributes,
+ these "raw" attributes are not typed and will remain as plain lists / dictionaries
+ even if you define the `Args`, `Kwargs`, `Slots` classes.
+
+ The `Component.deps_strategy` attribute is the same as the deprecated `Component.input.deps_strategy` property.
+
+- New template variables `{{ component_vars.args }}`, `{{ component_vars.kwargs }}`, `{{ component_vars.slots }}`
+
+ These attributes are the same as the ones available in `Component.get_template_data()`.
+
+ ```django
+ {# Typed #}
+ {% if component_vars.args.page == 123 %}
+
+ {% endif %}
+ ```
+
+ Same as with the parameters in `Component.get_template_data()`, they will be instances of the `Args`, `Kwargs`, `Slots` classes
+ if defined, or plain lists / dictionaries otherwise.
+
+- New component lifecycle hook `Component.on_render()`.
+
+ This hook is called when the component is being rendered.
+
+ You can override this method to:
+
+ - Change what template gets rendered
+ - Modify the context
+ - Modify the rendered output after it has been rendered
+ - Handle errors
+
+ See [on_render](https://django-components.github.io/django-components/0.140.1/concepts/advanced/hooks/#on_render) for more info.
+
+- `get_component_url()` now optionally accepts `query` and `fragment` arguments.
+
+ ```py
+ from django_components import get_component_url
+
+ url = get_component_url(
+ MyComponent,
+ query={"foo": "bar"},
+ fragment="baz",
+ )
+ # /components/ext/view/components/c1ab2c3?foo=bar#baz
+ ```
+
+- The `BaseNode` class has a new `contents` attribute, which contains the raw contents (string) of the tag body.
+
+ This is relevant when you define custom template tags with `@template_tag` decorator or `BaseNode` class.
+
+ When you define a custom template tag like so:
+
+ ```py
+ from django_components import BaseNode, template_tag
+
+ @template_tag(
+ library,
+ tag="mytag",
+ end_tag="endmytag",
+ allowed_flags=["required"]
+ )
+ def mytag(node: BaseNode, context: Context, name: str, **kwargs) -> str:
+ print(node.contents)
+ return f"Hello, {name}!"
+ ```
+
+ And render it like so:
+
+ ```django
+ {% mytag name="John" %}
+ Hello, world!
+ {% endmytag %}
+ ```
+
+ Then, the `contents` attribute of the `BaseNode` instance will contain the string `"Hello, world!"`.
+
+- The `BaseNode` class also has two new metadata attributes:
+
+ - `template_name` - the name of the template that rendered the node.
+ - `template_component` - the component class that the template belongs to.
+
+ This is useful for debugging purposes.
+
+- `Slot` class now has 3 new metadata fields:
+
+ 1. `Slot.contents` attribute contains the original contents:
+
+ - If `Slot` was created from `{% fill %}` tag, `Slot.contents` will contain the body of the `{% fill %}` tag.
+ - If `Slot` was created from string via `Slot("...")`, `Slot.contents` will contain that string.
+ - If `Slot` was created from a function, `Slot.contents` will contain that function.
+
+ 2. `Slot.extra` attribute where you can put arbitrary metadata about the slot.
+
+ 3. `Slot.fill_node` attribute tells where the slot comes from:
+
+ - `FillNode` instance if the slot was created from `{% fill %}` tag.
+ - `ComponentNode` instance if the slot was created as a default slot from a `{% component %}` tag.
+ - `None` if the slot was created from a string, function, or `Slot` instance.
+
+ See [Slot metadata](https://django-components.github.io/django-components/0.140.1/concepts/fundamentals/slots/#slot-metadata).
+
+- `{% fill %}` tag now accepts `body` kwarg to pass a Slot instance to fill.
+
+ First pass a `Slot` instance to the template
+ with the `get_template_data()` method:
+
+ ```python
+ from django_components import component, Slot
+
+ class Table(Component):
+ def get_template_data(self, args, kwargs, slots, context):
+ return {
+ "my_slot": Slot(lambda ctx: "Hello, world!"),
+ }
+ ```
+
+ Then pass the slot to the `{% fill %}` tag:
+
+ ```django
+ {% component "table" %}
+ {% fill "pagination" body=my_slot / %}
+ {% endcomponent %}
+ ```
+
+- You can now access the `{% component %}` tag (`ComponentNode` instance) from which a Component
+ was created. Use `Component.node` to access it.
+
+ This is mostly useful for extensions, which can use this to detect if the given Component
+ comes from a `{% component %}` tag or from a different source (such as `Component.render()`).
+
+ `Component.node` is `None` if the component is created by `Component.render()` (but you
+ can pass in the `node` kwarg yourself).
+
+ ```py
+ class MyComponent(Component):
+ def get_template_data(self, context, template):
+ if self.node is not None:
+ assert self.node.name == "my_component"
+ ```
+
+- Node classes `ComponentNode`, `FillNode`, `ProvideNode`, and `SlotNode` are part of the public API.
+
+ These classes are what is instantiated when you use `{% component %}`, `{% fill %}`, `{% provide %}`, and `{% slot %}` tags.
+
+ You can for example use these for type hints:
+
+ ```py
+ from django_components import Component, ComponentNode
+
+ class MyTable(Component):
+ def get_template_data(self, args, kwargs, slots, context):
+ if kwargs.get("show_owner"):
+ node: Optional[ComponentNode] = self.node
+ owner: Optional[Component] = self.node.template_component
+ else:
+ node = None
+ owner = None
+
+ return {
+ "owner": owner,
+ "node": node,
+ }
+ ```
+
+- Component caching can now take slots into account, by setting `Component.Cache.include_slots` to `True`.
+
+ ```py
+ class MyComponent(Component):
+ class Cache:
+ enabled = True
+ include_slots = True
+ ```
+
+ In which case the following two calls will generate separate cache entries:
+
+ ```django
+ {% component "my_component" position="left" %}
+ Hello, Alice
+ {% endcomponent %}
+
+ {% component "my_component" position="left" %}
+ Hello, Bob
+ {% endcomponent %}
+ ```
+
+ Same applies to `Component.render()` with string slots:
+
+ ```py
+ MyComponent.render(
+ kwargs={"position": "left"},
+ slots={"content": "Hello, Alice"}
+ )
+ MyComponent.render(
+ kwargs={"position": "left"},
+ slots={"content": "Hello, Bob"}
+ )
+ ```
+
+ Read more on [Component caching](https://django-components.github.io/django-components/0.140.1/concepts/advanced/component_caching/).
+
+- New extension hook `on_slot_rendered()`
+
+ This hook is called when a slot is rendered, and allows you to access and/or modify the rendered result.
+
+ This is used by the ["debug highlight" feature](https://django-components.github.io/django-components/0.140.1/guides/other/troubleshooting/#component-and-slot-highlighting).
+
+ To modify the rendered result, return the new value:
+
+ ```py
+ class MyExtension(ComponentExtension):
+ def on_slot_rendered(self, ctx: OnSlotRenderedContext) -> Optional[str]:
+ return ctx.result + ""
+ ```
+
+ If you don't want to modify the rendered result, return `None`.
+
+ See all [Extension hooks](https://django-components.github.io/django-components/0.140.1/reference/extension_hooks/).
+
+- When creating extensions, the previous syntax with `ComponentExtension.ExtensionClass` was causing
+ Mypy errors, because Mypy doesn't allow using class attributes as bases:
+
+ Before:
+
+ ```py
+ from django_components import ComponentExtension
+
+ class MyExtension(ComponentExtension):
+ class ExtensionClass(ComponentExtension.ExtensionClass): # Error!
+ pass
+ ```
+
+ Instead, you can import `ExtensionComponentConfig` directly:
+
+ After:
+
+ ```py
+ from django_components import ComponentExtension, ExtensionComponentConfig
+
+ class MyExtension(ComponentExtension):
+ class ComponentConfig(ExtensionComponentConfig):
+ pass
+ ```
+
+#### Refactor
+
+- When a component is being rendered, a proper `Component` instance is now created.
+
+ Previously, the `Component` state was managed as half-instance, half-stack.
+
+- Component's "Render API" (args, kwargs, slots, context, inputs, request, context data, etc)
+ can now be accessed also outside of the render call. So now its possible to take the component
+ instance out of `get_template_data()` (although this is not recommended).
+
+- Components can now be defined without a template.
+
+ Previously, the following would raise an error:
+
+ ```py
+ class MyComponent(Component):
+ pass
+ ```
+
+ "Template-less" components can be used together with `Component.on_render()` to dynamically
+ pick what to render:
+
+ ```py
+ class TableNew(Component):
+ template_file = "table_new.html"
+
+ class TableOld(Component):
+ template_file = "table_old.html"
+
+ class Table(Component):
+ def on_render(self, context, template):
+ if self.kwargs.get("feat_table_new_ui"):
+ return TableNew.render(args=self.args, kwargs=self.kwargs, slots=self.slots)
+ else:
+ return TableOld.render(args=self.args, kwargs=self.kwargs, slots=self.slots)
+ ```
+
+ "Template-less" components can be also used as a base class for other components, or as mixins.
+
+- Passing `Slot` instance to `Slot` constructor raises an error.
+
+- Extension hook `on_component_rendered` now receives `error` field.
+
+ `on_component_rendered` now behaves similar to `Component.on_render_after`:
+
+ - Raising error in this hook overrides what error will be returned from `Component.render()`.
+ - Returning new string overrides what will be returned from `Component.render()`.
+
+ Before:
+
+ ```py
+ class OnComponentRenderedContext(NamedTuple):
+ component: "Component"
+ component_cls: Type["Component"]
+ component_id: str
+ result: str
+ ```
+
+ After:
+
+ ```py
+ class OnComponentRenderedContext(NamedTuple):
+ component: "Component"
+ component_cls: Type["Component"]
+ component_id: str
+ result: Optional[str]
+ error: Optional[Exception]
+ ```
+
+#### Fix
+
+- Fix bug: Context processors data was being generated anew for each component. Now the data is correctly created once and reused across components with the same request ([#1165](https://github.com/django-components/django-components/issues/1165)).
+
+- Fix KeyError on `component_context_cache` when slots are rendered outside of the component's render context. ([#1189](https://github.com/django-components/django-components/issues/1189))
+
+- Component classes now have `do_not_call_in_templates=True` to prevent them from being called as functions in templates.
+
+## v0.139.1
+
+#### Fix
+
+- Fix compatibility of component caching with `{% extend %}` block ([#1135](https://github.com/django-components/django-components/issues/1135))
+
+#### Refactor
+
+- Component ID is now prefixed with `c`, e.g. `c123456`.
+
+- When typing a Component, you can now specify as few or as many parameters as you want.
+
+ ```py
+ Component[Args]
+ Component[Args, Kwargs]
+ Component[Args, Kwargs, Slots]
+ Component[Args, Kwargs, Slots, Data]
+ Component[Args, Kwargs, Slots, Data, JsData]
+ Component[Args, Kwargs, Slots, Data, JsData, CssData]
+ ```
+
+ All omitted parameters will default to `Any`.
+
+- Added `typing_extensions` to the project as a dependency
+
+- Multiple extensions with the same name (case-insensitive) now raise an error
+
+- Extension names (case-insensitive) also MUST NOT conflict with existing Component class API.
+
+ So if you name an extension `render`, it will conflict with the `render()` method of the `Component` class,
+ and thus raise an error.
+
+## v0.139.0
+
+#### Fix
+
+- Fix bug: Fix compatibility with `Finder.find()` in Django 5.2 ([#1119](https://github.com/django-components/django-components/issues/1119))
+
+## v0.138
+
+#### Fix
+
+- Fix bug: Allow components with `Url.public = True` to be defined before `django.setup()`
+
+## v0.137
+
+#### Feat
+
+- Each Component class now has a `class_id` attribute, which is unique to the component subclass.
+
+ NOTE: This is different from `Component.id`, which is unique to each rendered instance.
+
+ To look up a component class by its `class_id`, use `get_component_by_class_id()`.
+
+- It's now easier to create URLs for component views.
+
+ Before, you had to call `Component.as_view()` and pass that to `urlpatterns`.
+
+ Now this can be done for you if you set `Component.Url.public` to `True`:
+
+ ```py
+ class MyComponent(Component):
+ class Url:
+ public = True
+ ...
+ ```
+
+ Then, to get the URL for the component, use `get_component_url()`:
+
+ ```py
+ from django_components import get_component_url
+
+ url = get_component_url(MyComponent)
+ ```
+
+ This way you don't have to mix your app URLs with component URLs.
+
+ Read more on [Component views and URLs](https://django-components.github.io/django-components/0.137/concepts/fundamentals/component_views_urls/).
+
+- Per-component caching - Set `Component.Cache.enabled` to `True` to enable caching for a component.
+
+ Component caching allows you to store the rendered output of a component. Next time the component is rendered
+ with the same input, the cached output is returned instead of re-rendering the component.
+
+ ```py
+ class TestComponent(Component):
+ template = "Hello"
+
+ class Cache:
+ enabled = True
+ ttl = 0.1 # .1 seconds TTL
+ cache_name = "custom_cache"
+
+ # Custom hash method for args and kwargs
+ # NOTE: The default implementation simply serializes the input into a string.
+ # As such, it might not be suitable for complex objects like Models.
+ def hash(self, *args, **kwargs):
+ return f"{json.dumps(args)}:{json.dumps(kwargs)}"
+
+ ```
+
+ Read more on [Component caching](https://django-components.github.io/django-components/0.137/concepts/advanced/component_caching/).
+
+- `@djc_test` can now be called without first calling `django.setup()`, in which case it does it for you.
+
+- Expose `ComponentInput` class, which is a typing for `Component.input`.
+
+#### Deprecation
+
+- Currently, view request handlers such as `get()` and `post()` methods can be defined
+ directly on the `Component` class:
+
+ ```py
+ class MyComponent(Component):
+ def get(self, request):
+ return self.render_to_response()
+ ```
+
+ Or, nested within the `Component.View` class:
+
+ ```py
+ class MyComponent(Component):
+ class View:
+ def get(self, request):
+ return self.render_to_response()
+ ```
+
+ In v1, these methods should be defined only on the `Component.View` class instead.
+
+#### Refactor
+
+- `Component.get_context_data()` can now omit a return statement or return `None`.
+
+## 🚨📢 v0.136
+
+#### 🚨📢 BREAKING CHANGES
+
+- Component input validation was moved to a separate extension [`djc-ext-pydantic`](https://github.com/django-components/djc-ext-pydantic).
+
+ If you relied on components raising errors when inputs were invalid, you need to install `djc-ext-pydantic` and add it to extensions:
+
+ ```python
+ # settings.py
+ COMPONENTS = {
+ "extensions": [
+ "djc_pydantic.PydanticExtension",
+ ],
+ }
+ ```
+
+#### Fix
+
+- Make it possible to resolve URLs added by extensions by their names
+
## v0.135
#### Feat
@@ -56,6 +1628,8 @@ where each class name or style property can be managed separately.
%}
```
+ Read more on [HTML attributes](https://django-components.github.io/django-components/0.135/concepts/fundamentals/html_attributes/).
+
#### Fix
- Fix compat with Windows when reading component files ([#1074](https://github.com/django-components/django-components/issues/1074))
@@ -122,7 +1696,7 @@ where each class name or style property can be managed separately.
- If using `pytest`, the decorator allows you to parametrize Django or Components settings.
- The decorator also serves as a stand-in for Django's `@override_settings`.
- See the API reference for [`@djc_test`](https://django-components.github.io/django-components/0.131/reference/testing_api/#djc_test) for more details.
+ See the API reference for [`@djc_test`](https://django-components.github.io/django-components/0.131/reference/testing_api/#django_components.testing.djc_test) for more details.
- `ComponentRegistry` now has a `has()` method to check if a component is registered
without raising an error.
@@ -324,12 +1898,12 @@ If you see any broken links or other issues, please report them in [#922](https:
- Component inheritance:
- When you subclass a component, the JS and CSS defined on parent's `Media` class is now inherited by the child component.
- - You can disable or customize Media inheritance by setting `extend` attribute on the `Component.Media` nested class. This work similarly to Django's [`Media.extend`](https://docs.djangoproject.com/en/5.1/topics/forms/media/#extend).
+ - You can disable or customize Media inheritance by setting `extend` attribute on the `Component.Media` nested class. This work similarly to Django's [`Media.extend`](https://docs.djangoproject.com/en/5.2/topics/forms/media/#extend).
- When child component defines either `template` or `template_file`, both of parent's `template` and `template_file` are ignored. The same applies to `js_file` and `css_file`.
- Autodiscovery now ignores files and directories that start with an underscore (`_`), except `__init__.py`
-- The [Signals](https://docs.djangoproject.com/en/5.1/topics/signals/) emitted by or during the use of django-components are now documented, together the `template_rendered` signal.
+- The [Signals](https://docs.djangoproject.com/en/5.2/topics/signals/) emitted by or during the use of django-components are now documented, together the `template_rendered` signal.
## v0.123
@@ -341,7 +1915,7 @@ If you see any broken links or other issues, please report them in [#922](https:
#### Feat
-- Add support for HTML fragments. HTML fragments can be rendered by passing `type="fragment"` to `Component.render()` or `Component.render_to_response()`. Read more on how to [use HTML fragments with HTMX, AlpineJS, or vanillaJS](https://django-components.github.io/django-components/latest/concepts/advanced/html_tragments).
+- Add support for HTML fragments. HTML fragments can be rendered by passing `type="fragment"` to `Component.render()` or `Component.render_to_response()`. Read more on how to [use HTML fragments with HTMX, AlpineJS, or vanillaJS](https://django-components.github.io/django-components/latest/concepts/advanced/html_fragments).
## v0.121
@@ -959,7 +2533,7 @@ importing them.
- `SETTINGS_MODULE` - Define component dirs using `STATICFILES_DIRS`
- - Previously, autodiscovery handled relative files in `STATICFILES_DIRS`. To align with Django, `STATICFILES_DIRS` now must be full paths ([Django docs](https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-STATICFILES_DIRS)).
+ - Previously, autodiscovery handled relative files in `STATICFILES_DIRS`. To align with Django, `STATICFILES_DIRS` now must be full paths ([Django docs](https://docs.djangoproject.com/en/5.2/ref/settings/#std-setting-STATICFILES_DIRS)).
## 🚨📢 v0.81
diff --git a/README.md b/README.md
index 35ee93ad..8df3262e 100644
--- a/README.md
+++ b/README.md
@@ -23,9 +23,10 @@ A component in django-components can be as simple as a Django template and Pytho
```
```py
-# components/calendar/calendar.html
-from django_components import Component
+# components/calendar/calendar.py
+from django_components import Component, register
+@register("calendar")
class Calendar(Component):
template_file = "calendar.html"
```
@@ -56,15 +57,16 @@ document.querySelector(".calendar").onclick = () => {
```py
# components/calendar/calendar.py
-from django_components import Component
+from django_components import Component, register
+@register("calendar")
class Calendar(Component):
template_file = "calendar.html"
js_file = "calendar.js"
css_file = "calendar.css"
- def get_context_data(self, date):
- return {"date": date}
+ def get_template_data(self, args, kwargs, slots, context):
+ return {"date": kwargs["date"]}
```
Use the component like this:
@@ -121,21 +123,23 @@ class Calendar(Component):
# Additional JS and CSS
class Media:
- js = ["https://cdn.jsdelivr.net/npm/htmx.org@2.1.1/dist/htmx.min.js"]
+ js = ["https://cdn.jsdelivr.net/npm/htmx.org@2/dist/htmx.min.js"]
css = ["bootstrap/dist/css/bootstrap.min.css"]
# Variables available in the template
- def get_context_data(self, date):
+ def get_template_data(self, args, kwargs, slots, context):
return {
- "date": date
+ "date": kwargs["date"]
}
```
### Composition with slots
-- Render components inside templates with `{% component %}` tag.
-- Compose them with `{% slot %}` and `{% fill %}` tags.
-- Vue-like slot system, including scoped slots.
+- Render components inside templates with
+ [`{% component %}`](https://django-components.github.io/django-components/latest/reference/template_tags#component) tag.
+- Compose them with [`{% slot %}`](https://django-components.github.io/django-components/latest/reference/template_tags#slot)
+ and [`{% fill %}`](https://django-components.github.io/django-components/latest/reference/template_tags#fill) tags.
+- Vue-like slot system, including [scoped slots](https://django-components.github.io/django-components/latest/concepts/fundamentals/slots/#slot-data).
```django
{% component "Layout"
@@ -169,14 +173,17 @@ class Calendar(Component):
### Extended template tags
-`django-components` extends Django's template tags syntax with:
+`django-components` is designed for flexibility, making working with templates a breeze.
-- Literal lists and dictionaries in template tags
-- Self-closing tags `{% mytag / %}`
-- Multi-line template tags
-- Spread operator `...` to dynamically pass args or kwargs into the template tag
-- Nested template tags like `"{{ first_name }} {{ last_name }}"`
-- Flat definition of dictionary keys `attr:key=val`
+It extends Django's template tags syntax with:
+
+
+- Literal lists and dictionaries in the template
+- [Self-closing tags](https://django-components.github.io/django-components/latest/concepts/fundamentals/template_tag_syntax#self-closing-tags) `{% mytag / %}`
+- [Multi-line template tags](https://django-components.github.io/django-components/latest/concepts/fundamentals/template_tag_syntax#multiline-tags)
+- [Spread operator](https://django-components.github.io/django-components/latest/concepts/fundamentals/template_tag_syntax#spread-operator) `...` to dynamically pass args or kwargs into the template tag
+- [Template tags inside literal strings](https://django-components.github.io/django-components/latest/concepts/fundamentals/template_tag_syntax#template-tags-inside-literal-strings) like `"{{ first_name }} {{ last_name }}"`
+- [Pass dictonaries by their key-value pairs](https://django-components.github.io/django-components/latest/concepts/fundamentals/template_tag_syntax#pass-dictonary-by-its-key-value-pairs) `attr:key=val`
```django
{% component "table"
@@ -201,13 +208,70 @@ class Calendar(Component):
/ %}
```
+You too can define template tags with these features by using
+[`@template_tag()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.template_tag)
+or [`BaseNode`](https://django-components.github.io/django-components/latest/reference/api/#django_components.BaseNode).
+
+Read more on [Custom template tags](https://django-components.github.io/django-components/latest/concepts/advanced/template_tags/).
+
+### Full programmatic access
+
+When you render a component, you can access everything about the component:
+
+- Component input: [args, kwargs, slots and context](https://django-components.github.io/django-components/latest/concepts/fundamentals/render_api/#component-inputs)
+- Component's template, CSS and JS
+- Django's [context processors](https://django-components.github.io/django-components/latest/concepts/fundamentals/render_api/#request-and-context-processors)
+- Unique [render ID](https://django-components.github.io/django-components/latest/concepts/fundamentals/render_api/#component-id)
+
+```python
+class Table(Component):
+ js_file = "table.js"
+ css_file = "table.css"
+
+ template = """
+
+ {{ variable }}
+
+ """
+
+ def get_template_data(self, args, kwargs, slots, context):
+ # Access component's ID
+ assert self.id == "djc1A2b3c"
+
+ # Access component's inputs and slots
+ assert self.args == [123, "str"]
+ assert self.kwargs == {"variable": "test", "another": 1}
+ footer_slot = self.slots["footer"]
+ some_var = self.context["some_var"]
+
+ # Access the request object and Django's context processors, if available
+ assert self.request.GET == {"query": "something"}
+ assert self.context_processors_data['user'].username == "admin"
+
+ return {
+ "variable": kwargs["variable"],
+ }
+
+# Access component's HTML / JS / CSS
+Table.template
+Table.js
+Table.css
+
+# Render the component
+rendered = Table.render(
+ kwargs={"variable": "test", "another": 1},
+ args=(123, "str"),
+ slots={"footer": "MY_FOOTER"},
+)
+```
+
### Granular HTML attributes
Use the [`{% html_attrs %}`](https://django-components.github.io/django-components/latest/concepts/fundamentals/html_attributes/) template tag to render HTML attributes.
+
It supports:
-- Defining attributes as dictionaries
-- Defining attributes as keyword arguments
+- Defining attributes as whole dictionaries or keyword arguments
- Merging attributes from multiple sources
- Boolean attributes
- Appending attributes
@@ -224,13 +288,19 @@ It supports:
>
```
-`{% html_attrs %}` offers a Vue-like granular control over `class` and `style` HTML attributes,
+[`{% html_attrs %}`](https://django-components.github.io/django-components/latest/concepts/fundamentals/html_attributes/) offers a Vue-like granular control for
+[`class`](https://django-components.github.io/django-components/latest/concepts/fundamentals/html_attributes/#merging-class-attributes)
+and [`style`](https://django-components.github.io/django-components/latest/concepts/fundamentals/html_attributes/#merging-style-attributes)
+HTML attributes,
where you can use a dictionary to manage each class name or style property separately.
```django
{% html_attrs
class="foo bar"
- class={"baz": True, "foo": False}
+ class={
+ "baz": True,
+ "foo": False,
+ }
class="extra"
%}
```
@@ -238,7 +308,11 @@ where you can use a dictionary to manage each class name or style property separ
```django
{% html_attrs
style="text-align: center; background-color: blue;"
- style={"background-color": "green", "color": None, "width": False}
+ style={
+ "background-color": "green",
+ "color": None,
+ "width": False,
+ }
style="position: absolute; height: 12px;"
%}
```
@@ -247,11 +321,13 @@ Read more about [HTML attributes](https://django-components.github.io/django-com
### HTML fragment support
-`django-components` makes integration with HTMX, AlpineJS or jQuery easy by allowing components to be rendered as HTML fragments:
+`django-components` makes integration with HTMX, AlpineJS or jQuery easy by allowing components to be rendered as [HTML fragments](https://django-components.github.io/django-components/latest/concepts/advanced/html_fragments/):
-- Components's JS and CSS is loaded automatically when the fragment is inserted into the DOM.
+- Components's JS and CSS files are loaded automatically when the fragment is inserted into the DOM.
-- Expose components as views with `get`, `post`, `put`, `patch`, `delete` methods
+- Components can be [exposed as Django Views](https://django-components.github.io/django-components/latest/concepts/fundamentals/component_views_urls/) with `get()`, `post()`, `put()`, `patch()`, `delete()` methods
+
+- Automatically create an endpoint for a component with [`Component.View.public`](https://django-components.github.io/django-components/latest/concepts/fundamentals/component_views_urls/#register-urls-automatically)
```py
# components/calendar/calendar.py
@@ -259,94 +335,163 @@ Read more about [HTML attributes](https://django-components.github.io/django-com
class Calendar(Component):
template_file = "calendar.html"
- def get(self, request, *args, **kwargs):
- page = request.GET.get("page", 1)
- return self.render_to_response(
- kwargs={
- "page": page,
- }
- )
+ class View:
+ # Register Component with `urlpatterns`
+ public = True
- def get_context_data(self, page):
+ # Define handlers
+ def get(self, request, *args, **kwargs):
+ page = request.GET.get("page", 1)
+ return self.component.render_to_response(
+ request=request,
+ kwargs={
+ "page": page,
+ },
+ )
+
+ def get_template_data(self, args, kwargs, slots, context):
return {
- "page": page,
+ "page": kwargs["page"],
}
-# urls.py
-path("calendar/", Calendar.as_view()),
+# Get auto-generated URL for the component
+url = get_component_url(Calendar)
+
+# Or define explicit URL in urls.py
+path("calendar/", Calendar.as_view())
```
-### Type hints
+### Provide / Inject
-Opt-in to type hints by defining types for component's args, kwargs, slots, and more:
+`django-components` supports the provide / inject pattern, similarly to React's [Context Providers](https://react.dev/learn/passing-data-deeply-with-context) or Vue's [provide / inject](https://vuejs.org/guide/components/provide-inject):
+
+- Use the [`{% provide %}`](https://django-components.github.io/django-components/latest/reference/template_tags/#provide) tag to provide data to the component tree
+- Use the [`Component.inject()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.Component.inject) method to inject data into the component
+
+Read more about [Provide / Inject](https://django-components.github.io/django-components/latest/concepts/advanced/provide_inject).
+
+```django
+
+ {% provide "theme" variant="light" %}
+ {% component "header" / %}
+ {% endprovide %}
+
+```
+
+```djc_py
+@register("header")
+class Header(Component):
+ template = "..."
+
+ def get_template_data(self, args, kwargs, slots, context):
+ theme = self.inject("theme").variant
+ return {
+ "theme": theme,
+ }
+```
+
+### Input validation and static type hints
+
+Avoid needless errors with [type hints and runtime input validation](https://django-components.github.io/django-components/latest/concepts/fundamentals/typing_and_validation/).
+
+To opt-in to input validation, define types for component's args, kwargs, slots, and more:
```py
-from typing import NotRequired, Tuple, TypedDict, SlotContent, SlotFunc
+from typing import NamedTuple, Optional
+from django.template import Context
+from django_components import Component, Slot, SlotInput
-ButtonArgs = Tuple[int, str]
+class Button(Component):
+ class Args(NamedTuple):
+ size: int
+ text: str
-class ButtonKwargs(TypedDict):
- variable: str
- another: int
- maybe_var: NotRequired[int] # May be omitted
+ class Kwargs(NamedTuple):
+ variable: str
+ another: int
+ maybe_var: Optional[int] = None # May be omitted
-class ButtonData(TypedDict):
- variable: str
+ class Slots(NamedTuple):
+ my_slot: Optional[SlotInput] = None
+ another_slot: SlotInput
-class ButtonSlots(TypedDict):
- my_slot: NotRequired[SlotFunc]
- another_slot: SlotContent
-
-ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots, ButtonData, JsData, CssData]
-
-class Button(ButtonType):
- def get_context_data(self, *args, **kwargs):
- self.input.args[0] # int
- self.input.kwargs["variable"] # str
- self.input.slots["my_slot"] # SlotFunc[MySlotData]
-
- return {} # Error: Key "variable" is missing
+ def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
+ args.size # int
+ kwargs.variable # str
+ slots.my_slot # Slot[MySlotData]
```
-When you then call `Button.render()` or `Button.render_to_response()`, you will get type hints:
+To have type hints when calling
+[`Button.render()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.Component.render) or
+[`Button.render_to_response()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.Component.render_to_response),
+wrap the inputs in their respective `Args`, `Kwargs`, and `Slots` classes:
```py
Button.render(
# Error: First arg must be `int`, got `float`
- args=(1.25, "abc"),
+ args=Button.Args(
+ size=1.25,
+ text="abc",
+ ),
# Error: Key "another" is missing
- kwargs={
- "variable": "text",
- },
+ kwargs=Button.Kwargs(
+ variable="text",
+ ),
)
```
### Extensions
-Django-components functionality can be extended with "extensions". Extensions allow for powerful customization and integrations. They can:
+Django-components functionality can be extended with [Extensions](https://django-components.github.io/django-components/latest/concepts/advanced/extensions/).
+Extensions allow for powerful customization and integrations. They can:
-- Tap into lifecycle events, such as when a component is created, deleted, or registered.
-- Add new attributes and methods to the components under an extension-specific nested class.
+- Tap into lifecycle events, such as when a component is created, deleted, or registered
+- Add new attributes and methods to the components
+- Add custom CLI commands
+- Add custom URLs
+
+Some of the extensions include:
+
+- [Component caching](https://github.com/django-components/django-components/blob/master/src/django_components/extensions/cache.py)
+- [Django View integration](https://github.com/django-components/django-components/blob/master/src/django_components/extensions/view.py)
+- [Component defaults](https://github.com/django-components/django-components/blob/master/src/django_components/extensions/defaults.py)
+- [Pydantic integration (input validation)](https://github.com/django-components/djc-ext-pydantic)
Some of the planned extensions include:
-- Caching
- AlpineJS integration
- Storybook integration
-- Pydantic validation
- Component-level benchmarking with asv
+### Caching
+
+- [Components can be cached](https://django-components.github.io/django-components/latest/concepts/advanced/component_caching/) using Django's cache framework.
+- Caching rules can be configured on a per-component basis.
+- Components are cached based on their input. Or you can write custom caching logic.
+
+```py
+from django_components import Component
+
+class MyComponent(Component):
+ class Cache:
+ enabled = True
+ ttl = 60 * 60 * 24 # 1 day
+
+ def hash(self, *args, **kwargs):
+ return hash(f"{json.dumps(args)}:{json.dumps(kwargs)}")
+```
+
### Simple testing
-- Write tests for components with `@djc_test` decorator.
+- Write tests for components with [`@djc_test`](https://django-components.github.io/django-components/latest/concepts/advanced/testing/) decorator.
- The decorator manages global state, ensuring that tests don't leak.
- If using `pytest`, the decorator allows you to parametrize Django or Components settings.
-- The decorator also serves as a stand-in for Django's `@override_settings`.
+- The decorator also serves as a stand-in for Django's [`@override_settings`](https://docs.djangoproject.com/en/5.2/topics/testing/tools/#django.test.override_settings).
```python
-from djc_test import djc_test
+from django_components.testing import djc_test
-from components.my_component import MyTable
+from components.my_table import MyTable
@djc_test
def test_my_table():
@@ -358,11 +503,6 @@ def test_my_table():
assert rendered == "
My table
"
```
-### Handle large projects with ease
-
-- Components can be infinitely nested.
-- (Soon) Optimize performance with component-level caching
-
### Debugging features
- **Visual component inspection**: Highlight components and slots directly in your browser.
@@ -386,10 +526,6 @@ def test_my_table():
{% endcalendar %}
```
-### Other features
-
-- Vue-like provide / inject system
-
## Documentation
[Read the full documentation here](https://django-components.github.io/django-components/latest/).
@@ -418,7 +554,7 @@ to see the latest features and fixes.
One of our goals with `django-components` is to make it easy to share components between projects. If you have a set of components that you think would be useful to others, please open a pull request to add them to the list below.
-- [django-htmx-components](https://github.com/iwanalabs/django-htmx-components): A set of components for use with [htmx](https://htmx.org/). Try out the [live demo](https://dhc.iwanalabs.com/).
+- [django-htmx-components](https://github.com/iwanalabs/django-htmx-components): A set of components for use with [htmx](https://htmx.org/).
- [djc-heroicons](https://pypi.org/project/djc-heroicons/): A component that renders icons from [Heroicons.com](https://heroicons.com/).
diff --git a/benchmarks/README.md b/benchmarks/README.md
index 4ef8dc8d..f5f5d524 100644
--- a/benchmarks/README.md
+++ b/benchmarks/README.md
@@ -26,7 +26,7 @@ django-components uses `asv` for these use cases:
1. When a git tag is created and pushed, we also update the documentation website (see `docs.yml`).
2. Before we publish the docs website, we generate the HTML report for the benchmark results.
3. The generated report is placed in the `docs/benchmarks/` directory, and is thus
- published with the rest of the docs website and available under [`/benchmarks/`](https://django-components.github.io/django-components/benchmarks).
+ published with the rest of the docs website and available under [`/benchmarks/`](https://django-components.github.io/django-components/latest/benchmarks).
- NOTE: The location where the report is placed is defined in `asv.conf.json`.
- Compare performance between commits on pull requests:
diff --git a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.peakmem_render_lg_first.json b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.peakmem_render_lg_first.json
index 9d9fcae0..97e6d62b 100644
--- a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.peakmem_render_lg_first.json
+++ b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.peakmem_render_lg_first.json
@@ -1 +1 @@
-[[1662, [52920320.0, 54566912.0]], [1672, [52350976.0, 54599680.0]], [1687, [52109312.0, 54779904.0]], [1691, [52899840.0, 54730752.0]]]
\ No newline at end of file
+[[1662, [52920320.0, 54566912.0]], [1672, [52350976.0, 54599680.0]], [1687, [52109312.0, 54779904.0]], [1691, [52899840.0, 54730752.0]], [1709, [52936704.0, 55009280.0]], [1726, [52379648.0, 54992896.0]], [1766, [53084160.0, 55382016.0]], [1770, [53047296.0, 55373824.0]], [1776, [52490240.0, 55361536.0]], [1801, [53153792.0, 55410688.0]], [1937, [52957184.0, 55177216.0]], [1960, [52932608.0, 55693312.0]], [1996, [53096448.0, 55484416.0]], [2029, [52715520.0, 56090624.0]]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.peakmem_render_lg_subsequent.json b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.peakmem_render_lg_subsequent.json
index c363f137..a16e0075 100644
--- a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.peakmem_render_lg_subsequent.json
+++ b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.peakmem_render_lg_subsequent.json
@@ -1 +1 @@
-[[1662, [53800960.0, 54734848.0]], [1672, [52289536.0, 55099392.0]], [1687, [52142080.0, 55255040.0]], [1691, [53796864.0, 55238656.0]]]
\ No newline at end of file
+[[1662, [53800960.0, 54734848.0]], [1672, [52289536.0, 55099392.0]], [1687, [52142080.0, 55255040.0]], [1691, [53796864.0, 55238656.0]], [1709, [53768192.0, 55455744.0]], [1726, [51998720.0, 55451648.0]], [1766, [53739520.0, 55812096.0]], [1770, [53948416.0, 55824384.0]], [1776, [52097024.0, 55791616.0]], [1801, [53919744.0, 55799808.0]], [1937, [52822016.0, 56242176.0]], [1960, [53063680.0, 56180736.0]], [1996, [53018624.0, 56389632.0]], [2029, [52736000.0, 56791040.0]]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.peakmem_render_sm_first.json b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.peakmem_render_sm_first.json
index d4bbd38b..6f16f68b 100644
--- a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.peakmem_render_sm_first.json
+++ b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.peakmem_render_sm_first.json
@@ -1 +1 @@
-[[1662, [44191744.0, 44191744.0]], [1672, [44056576.0, 44048384.0]], [1687, [44191744.0, 44310528.0]], [1691, [44183552.0, 44175360.0]]]
\ No newline at end of file
+[[1662, [44191744.0, 44191744.0]], [1672, [44056576.0, 44048384.0]], [1687, [44191744.0, 44310528.0]], [1691, [44183552.0, 44175360.0]], [1709, [44191744.0, 44314624.0]], [1726, [44195840.0, 44314624.0]], [1766, [44322816.0, 44314624.0]], [1770, [44326912.0, 44322816.0]], [1776, [44183552.0, 44306432.0]], [1801, [44195840.0, 44453888.0]], [1937, [44756992.0, 44744704.0]], [1960, [44716032.0, 44834816.0]], [1996, [44716032.0, 44969984.0]], [2029, [44871680.0, 44912640.0]]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.peakmem_render_sm_subsequent.json b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.peakmem_render_sm_subsequent.json
index 3af18864..76bd2928 100644
--- a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.peakmem_render_sm_subsequent.json
+++ b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.peakmem_render_sm_subsequent.json
@@ -1 +1 @@
-[[1662, [44195840.0, 44187648.0]], [1672, [44060672.0, 43917312.0]], [1687, [44105728.0, 44310528.0]], [1691, [44187648.0, 44183552.0]]]
\ No newline at end of file
+[[1662, [44195840.0, 44187648.0]], [1672, [44060672.0, 43917312.0]], [1687, [44105728.0, 44310528.0]], [1691, [44187648.0, 44183552.0]], [1709, [44191744.0, 44437504.0]], [1726, [44322816.0, 44314624.0]], [1766, [44322816.0, 44310528.0]], [1770, [44101632.0, 44310528.0]], [1776, [44314624.0, 44437504.0]], [1801, [44191744.0, 44453888.0]], [1937, [44527616.0, 44744704.0]], [1960, [44716032.0, 44838912.0]], [1996, [44724224.0, 44969984.0]], [2029, [44617728.0, 44986368.0]]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.timeraw_render_lg_first.json b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.timeraw_render_lg_first.json
index 6f67f7da..e53588c7 100644
--- a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.timeraw_render_lg_first.json
+++ b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.timeraw_render_lg_first.json
@@ -1 +1 @@
-[[1662, [0.06960565700001098, 0.25608221199996706]], [1672, [0.07114163800000028, 0.26389872900000455]], [1687, [0.06910802600003763, 0.25746033199999374]], [1691, [0.07048037500001669, 0.2598985070000026]]]
\ No newline at end of file
+[[1662, [0.06960565700001098, 0.25608221199996706]], [1672, [0.07114163800000028, 0.26389872900000455]], [1687, [0.06910802600003763, 0.25746033199999374]], [1691, [0.07048037500001669, 0.2598985070000026]], [1709, [0.07402671400001282, 0.26584690599997884]], [1726, [0.07297276199997782, 0.2569234329999972]], [1766, [0.07308550800001967, 0.26274096600002395]], [1770, [0.0749189080000292, 0.26436952000000247]], [1776, [0.07303507899999317, 0.2628890319999755]], [1801, [0.07360306399999672, 0.2678246009999725]], [1937, [0.07941284200001064, 0.26779402600004687]], [1960, [0.08026317200000221, 0.26819844099998136]], [1996, [0.0814841690000776, 0.28364495499999975]], [2029, [0.08105427499998541, 0.29477426600001877]]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.timeraw_render_lg_subsequent.json b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.timeraw_render_lg_subsequent.json
index 11c930ae..72d384fe 100644
--- a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.timeraw_render_lg_subsequent.json
+++ b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.timeraw_render_lg_subsequent.json
@@ -1 +1 @@
-[[1662, [0.03327357099999517, 0.1421111020000012]], [1672, [0.033918617999972867, 0.14395761299999776]], [1687, [0.03317536700001256, 0.14245594600001255]], [1691, [0.034316510999985894, 0.1444248799999741]]]
\ No newline at end of file
+[[1662, [0.03327357099999517, 0.1421111020000012]], [1672, [0.033918617999972867, 0.14395761299999776]], [1687, [0.03317536700001256, 0.14245594600001255]], [1691, [0.034316510999985894, 0.1444248799999741]], [1709, [0.03742426899998463, 0.14901454800002512]], [1726, [0.03658580800001232, 0.1459621130000528]], [1766, [0.03723830100000214, 0.15196534300002895]], [1770, [0.03752758399997447, 0.15356457899997622]], [1776, [0.03678920999999491, 0.14955294699998944]], [1801, [0.037022983000014165, 0.15138703899998518]], [1937, [0.043317416999911984, 0.15457556900003055]], [1960, [0.04349111400000538, 0.15453611999998884]], [1996, [0.04362213900003553, 0.16551773399999092]], [2029, [0.043648402000002307, 0.17461173199995983]]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.timeraw_render_sm_first.json b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.timeraw_render_sm_first.json
index 89f6168b..ffd21cc0 100644
--- a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.timeraw_render_sm_first.json
+++ b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.timeraw_render_sm_first.json
@@ -1 +1 @@
-[[1662, [0.0035443229999998493, 0.00467639600003622]], [1672, [0.0036137869999777195, 0.004807943000002979]], [1687, [0.0035223549999727766, 0.004706463999980315]], [1691, [0.00364059099999281, 0.004926952999994683]]]
\ No newline at end of file
+[[1662, [0.0035443229999998493, 0.00467639600003622]], [1672, [0.0036137869999777195, 0.004807943000002979]], [1687, [0.0035223549999727766, 0.004706463999980315]], [1691, [0.00364059099999281, 0.004926952999994683]], [1709, [0.003602947999979733, 0.004853936999950292]], [1726, [0.0035008030000085455, 0.004695608999981005]], [1766, [0.003566315000000486, 0.004791812000007667]], [1770, [0.0036766670000361046, 0.004929383999979109]], [1776, [0.0035613420000117912, 0.004760385999986738]], [1801, [0.003639607999986083, 0.004848561000017071]], [1937, [0.0036632869999948525, 0.00493345400002454]], [1960, [0.0036145729999930154, 0.004811176000004025]], [1996, [0.00375721499995052, 0.0049729269999261305]], [2029, [0.0037106409999978496, 0.004899473999955717]]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.timeraw_render_sm_subsequent.json b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.timeraw_render_sm_subsequent.json
index b11fd117..7e2dcfd6 100644
--- a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.timeraw_render_sm_subsequent.json
+++ b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.timeraw_render_sm_subsequent.json
@@ -1 +1 @@
-[[1662, [0.00010400499999718704, 0.0005328339999977061]], [1672, [0.00010086800000408402, 0.0005549249999887707]], [1687, [9.818199998790078e-05, 0.0005511469999817109]], [1691, [0.0001005780000014056, 0.0005555879999974422]]]
\ No newline at end of file
+[[1662, [0.00010400499999718704, 0.0005328339999977061]], [1672, [0.00010086800000408402, 0.0005549249999887707]], [1687, [9.818199998790078e-05, 0.0005511469999817109]], [1691, [0.0001005780000014056, 0.0005555879999974422]], [1709, [0.00012266099997759738, 0.0005711430000019391]], [1726, [0.00011641800000461444, 0.0005489540000098714]], [1766, [0.00011609900002440554, 0.0005779780000239043]], [1770, [0.0001176700000087294, 0.0005864990000077341]], [1776, [0.00011622699999236374, 0.0005842630000074678]], [1801, [0.00011665800002447213, 0.000582710000003317]], [1937, [0.00012153600005149201, 0.0005999570000199128]], [1960, [0.00012332000000014887, 0.0005915369999911491]], [1996, [0.00012686900004155177, 0.0006182140000419167]], [2029, [0.00012706900002967814, 0.0006100459999629493]]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.timeraw_startup_lg.json b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.timeraw_startup_lg.json
index 7851ca5d..028b02cc 100644
--- a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.timeraw_startup_lg.json
+++ b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.timeraw_startup_lg.json
@@ -1 +1 @@
-[[1662, [0.21775109000003567, 0.21398552899995593]], [1672, [0.22476057199997967, 0.22048105400000395]], [1687, [0.21809406599999193, 0.2131839880000257]], [1691, [0.22356123500000535, 0.22167734499998915]]]
\ No newline at end of file
+[[1662, [0.21775109000003567, 0.21398552899995593]], [1672, [0.22476057199997967, 0.22048105400000395]], [1687, [0.21809406599999193, 0.2131839880000257]], [1691, [0.22356123500000535, 0.22167734499998915]], [1709, [0.22133603999998286, 0.21805855799999563]], [1726, [0.2166100470000174, 0.21420494400001644]], [1766, [0.22339861599999722, 0.22020213500002228]], [1770, [0.22985272800002576, 0.22544496099999378]], [1776, [0.22073260000001937, 0.2182690520000392]], [1801, [0.224061646999985, 0.2246476189999953]], [1937, [0.22743783699991127, 0.226070988999993]], [1960, [0.2252378419999843, 0.2247263650000093]], [1996, [0.23076480500003527, 0.23163660399995933]], [2029, [0.22799248500001568, 0.22723498599998493]]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Other.timeraw_import_time.json b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Other.timeraw_import_time.json
index eb6262fb..eb01068a 100644
--- a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Other.timeraw_import_time.json
+++ b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Other.timeraw_import_time.json
@@ -1 +1 @@
-[[1662, 0.19832900800003017], [1672, 0.20217585500000723], [1687, 0.19726691500000015], [1691, 0.20350580199999513]]
\ No newline at end of file
+[[1662, 0.19832900800003017], [1672, 0.20217585500000723], [1687, 0.19726691500000015], [1691, 0.20350580199999513], [1709, 0.19950735400001918], [1726, 0.19625152499997967], [1766, 0.20073733000003813], [1770, 0.20376683500001036], [1776, 0.19919827600000417], [1801, 0.2053688209999791], [1937, 0.2063091950000171], [1960, 0.20468290799999522], [1996, 0.21042045099989082], [2029, 0.2056691309999792]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.peakmem_render_lg_first.json b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.peakmem_render_lg_first.json
index 4fcb10a1..e5e24d49 100644
--- a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.peakmem_render_lg_first.json
+++ b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.peakmem_render_lg_first.json
@@ -1 +1 @@
-[[1662, [54439936.0, 53968896.0]], [1672, [54616064.0, 54140928.0]], [1687, [54767616.0, 54296576.0]], [1691, [54743040.0, 54087680.0]]]
\ No newline at end of file
+[[1662, [54439936.0, 53968896.0]], [1672, [54616064.0, 54140928.0]], [1687, [54767616.0, 54296576.0]], [1691, [54743040.0, 54087680.0]], [1709, [55001088.0, 54312960.0]], [1726, [54992896.0, 54345728.0]], [1766, [55373824.0, 54894592.0]], [1770, [55246848.0, 54898688.0]], [1776, [55357440.0, 54874112.0]], [1801, [55382016.0, 54882304.0]], [1937, [55222272.0, 54722560.0]], [1960, [55263232.0, 54693888.0]], [1996, [55476224.0, 54968320.0]], [2029, [56090624.0, 55582720.0]]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.peakmem_render_lg_subsequent.json b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.peakmem_render_lg_subsequent.json
index b61972cd..0606c7d9 100644
--- a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.peakmem_render_lg_subsequent.json
+++ b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.peakmem_render_lg_subsequent.json
@@ -1 +1 @@
-[[1662, [54968320.0, 54792192.0]], [1672, [54849536.0, 54841344.0]], [1687, [55271424.0, 55304192.0]], [1691, [54984704.0, 54964224.0]]]
\ No newline at end of file
+[[1662, [54968320.0, 54792192.0]], [1672, [54849536.0, 54841344.0]], [1687, [55271424.0, 55304192.0]], [1691, [54984704.0, 54964224.0]], [1709, [55439360.0, 55369728.0]], [1726, [55455744.0, 55177216.0]], [1766, [55545856.0, 55631872.0]], [1770, [55812096.0, 55611392.0]], [1776, [55640064.0, 55631872.0]], [1801, [55812096.0, 55902208.0]], [1937, [56008704.0, 56143872.0]], [1960, [55783424.0, 56160256.0]], [1996, [56352768.0, 56516608.0]], [2029, [56786944.0, 56778752.0]]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.peakmem_render_sm_first.json b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.peakmem_render_sm_first.json
index 1b23a9a5..bdcfccea 100644
--- a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.peakmem_render_sm_first.json
+++ b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.peakmem_render_sm_first.json
@@ -1 +1 @@
-[[1662, [44187648.0, 44183552.0]], [1672, [44048384.0, 44048384.0]], [1687, [44314624.0, 44310528.0]], [1691, [44179456.0, 44175360.0]]]
\ No newline at end of file
+[[1662, [44187648.0, 44183552.0]], [1672, [44048384.0, 44048384.0]], [1687, [44314624.0, 44310528.0]], [1691, [44179456.0, 44175360.0]], [1709, [44314624.0, 44310528.0]], [1726, [44314624.0, 44314624.0]], [1766, [44318720.0, 44314624.0]], [1770, [44322816.0, 44314624.0]], [1776, [44306432.0, 44240896.0]], [1801, [44453888.0, 44453888.0]], [1937, [44744704.0, 44744704.0]], [1960, [44838912.0, 44838912.0]], [1996, [44969984.0, 44969984.0]], [2029, [44843008.0, 44851200.0]]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.peakmem_render_sm_subsequent.json b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.peakmem_render_sm_subsequent.json
index 375f6a72..e93df054 100644
--- a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.peakmem_render_sm_subsequent.json
+++ b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.peakmem_render_sm_subsequent.json
@@ -1 +1 @@
-[[1662, [44187648.0, 44187648.0]], [1672, [44052480.0, 44052480.0]], [1687, [44314624.0, 44310528.0]], [1691, [44179456.0, 44179456.0]]]
\ No newline at end of file
+[[1662, [44187648.0, 44187648.0]], [1672, [44052480.0, 44052480.0]], [1687, [44314624.0, 44310528.0]], [1691, [44179456.0, 44179456.0]], [1709, [44310528.0, 44314624.0]], [1726, [44314624.0, 44314624.0]], [1766, [44310528.0, 44314624.0]], [1770, [44314624.0, 44318720.0]], [1776, [44437504.0, 44437504.0]], [1801, [44449792.0, 44449792.0]], [1937, [44744704.0, 44744704.0]], [1960, [44965888.0, 44834816.0]], [1996, [44974080.0, 44974080.0]], [2029, [44982272.0, 44986368.0]]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.timeraw_render_lg_first.json b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.timeraw_render_lg_first.json
index 17399f1d..47b453a9 100644
--- a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.timeraw_render_lg_first.json
+++ b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.timeraw_render_lg_first.json
@@ -1 +1 @@
-[[1662, [0.2574955810000006, 0.2591010970000127]], [1672, [0.2600247560000071, 0.26185358800000813]], [1687, [0.2567828300000201, 0.2602957870000182]], [1691, [0.259077934000004, 0.2619792840000059]]]
\ No newline at end of file
+[[1662, [0.2574955810000006, 0.2591010970000127]], [1672, [0.2600247560000071, 0.26185358800000813]], [1687, [0.2567828300000201, 0.2602957870000182]], [1691, [0.259077934000004, 0.2619792840000059]], [1709, [0.2646600410000133, 0.2676605120000204]], [1726, [0.2570519909999689, 0.2606809000000112]], [1766, [0.262679922000018, 0.2686107789999994]], [1770, [0.265977821000007, 0.26914772099999595]], [1776, [0.2626667089999728, 0.2663110299999971]], [1801, [0.2658582709999848, 0.2712929850000023]], [1937, [0.2675778039999841, 0.2724974679999832]], [1960, [0.26819597400000816, 0.2740507329999957]], [1996, [0.2794132599999557, 0.28440619299999526]], [2029, [0.2920349000000044, 0.2976166970000236]]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.timeraw_render_lg_subsequent.json b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.timeraw_render_lg_subsequent.json
index 6b8c77a0..2c7b894a 100644
--- a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.timeraw_render_lg_subsequent.json
+++ b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.timeraw_render_lg_subsequent.json
@@ -1 +1 @@
-[[1662, [0.14273938200000202, 0.1464969190000147]], [1672, [0.14515931700000806, 0.14909453600000688]], [1687, [0.1423055980000072, 0.14642362500001127]], [1691, [0.1436571560000175, 0.14915657599999577]]]
\ No newline at end of file
+[[1662, [0.14273938200000202, 0.1464969190000147]], [1672, [0.14515931700000806, 0.14909453600000688]], [1687, [0.1423055980000072, 0.14642362500001127]], [1691, [0.1436571560000175, 0.14915657599999577]], [1709, [0.14860135300000366, 0.15305296299999327]], [1726, [0.14520097999997006, 0.14991973799999414]], [1766, [0.15071133700001837, 0.15540660900001058]], [1770, [0.15150350199996865, 0.1558047899999906]], [1776, [0.14876902899999322, 0.15549233400000162]], [1801, [0.15248822700002052, 0.15465820200000735]], [1937, [0.15459265900005903, 0.15926110200007315]], [1960, [0.15396625699997912, 0.16023626799997714]], [1996, [0.16650312799993117, 0.17177308600003016]], [2029, [0.17414895399997476, 0.178393189000019]]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.timeraw_render_sm_first.json b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.timeraw_render_sm_first.json
index 0fa139ce..36aecfd8 100644
--- a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.timeraw_render_sm_first.json
+++ b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.timeraw_render_sm_first.json
@@ -1 +1 @@
-[[1662, [0.004720848000005162, 0.004705489000002672]], [1672, [0.004856270999994194, 0.00490694800001279]], [1687, [0.00473016699999107, 0.004734037999980956]], [1691, [0.004871503999993365, 0.0048899079999955575]]]
\ No newline at end of file
+[[1662, [0.004720848000005162, 0.004705489000002672]], [1672, [0.004856270999994194, 0.00490694800001279]], [1687, [0.00473016699999107, 0.004734037999980956]], [1691, [0.004871503999993365, 0.0048899079999955575]], [1709, [0.0048215560000244295, 0.004858458999990489]], [1726, [0.004671787999996013, 0.004672599999992144]], [1766, [0.00478528000002143, 0.0047485900000197034]], [1770, [0.004901490999998259, 0.004895917999988342]], [1776, [0.00480728600001612, 0.00472804499997892]], [1801, [0.004847185000016907, 0.004857667999999649]], [1937, [0.004923484000073586, 0.004925836999973399]], [1960, [0.004825538000005736, 0.0047952310000027865]], [1996, [0.005049280000093859, 0.004947880000145233]], [2029, [0.004897051999989799, 0.004863266000029398]]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.timeraw_render_sm_subsequent.json b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.timeraw_render_sm_subsequent.json
index a23d2b71..a12a8dd8 100644
--- a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.timeraw_render_sm_subsequent.json
+++ b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.timeraw_render_sm_subsequent.json
@@ -1 +1 @@
-[[1662, [0.0005377129999999397, 0.0005395769999836375]], [1672, [0.000547750000009728, 0.0005677989999810507]], [1687, [0.0005471899999918151, 0.0005447550000212686]], [1691, [0.0005559489999882317, 0.0005480739999939033]]]
\ No newline at end of file
+[[1662, [0.0005377129999999397, 0.0005395769999836375]], [1672, [0.000547750000009728, 0.0005677989999810507]], [1687, [0.0005471899999918151, 0.0005447550000212686]], [1691, [0.0005559489999882317, 0.0005480739999939033]], [1709, [0.0005736080000247057, 0.0005720849999875099]], [1726, [0.000542692999999872, 0.0005430530000012368]], [1766, [0.0005853119999983392, 0.000582014999963576]], [1770, [0.0005929909999622396, 0.000583071999983531]], [1776, [0.0005810670000130358, 0.000576186999978745]], [1801, [0.0005717709999828458, 0.0005785939999896073]], [1937, [0.0005969709999362749, 0.0005864510000037626]], [1960, [0.0005953940000154034, 0.0005933700000468889]], [1996, [0.0006160310000495883, 0.0006166809999967882]], [2029, [0.0006159270000125616, 0.0006080119999865019]]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.timeraw_startup_lg.json b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.timeraw_startup_lg.json
index f33e1155..520c7cbf 100644
--- a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.timeraw_startup_lg.json
+++ b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/isolated vs django modes.timeraw_startup_lg.json
@@ -1 +1 @@
-[[1662, [0.21402431699999624, 0.21364062999998623]], [1672, [0.2221746719999942, 0.2222580240000127]], [1687, [0.2142312400000037, 0.21397752699999728]], [1691, [0.22129613300000983, 0.21942976399998315]]]
\ No newline at end of file
+[[1662, [0.21402431699999624, 0.21364062999998623]], [1672, [0.2221746719999942, 0.2222580240000127]], [1687, [0.2142312400000037, 0.21397752699999728]], [1691, [0.22129613300000983, 0.21942976399998315]], [1709, [0.2199001029999863, 0.22046102699999892]], [1726, [0.2147675530000015, 0.21506381099999317]], [1766, [0.22056839900000114, 0.21916191200000412]], [1770, [0.22394285699999728, 0.22330144500000415]], [1776, [0.21867883100003382, 0.21859779499999377]], [1801, [0.22378945699995256, 0.22211803700002974]], [1937, [0.22545313400001987, 0.22602228000005198]], [1960, [0.22564571399999522, 0.22598634599995648]], [1996, [0.2295973340000046, 0.23030742100002044]], [2029, [0.22777395400004252, 0.2292747939999913]]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/summary.json b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/summary.json
index 56306a82..5cfe9e2e 100644
--- a/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/summary.json
+++ b/docs/benchmarks/graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/summary.json
@@ -1 +1 @@
-[{"name": "Components vs Django.peakmem_render_lg_first", "idx": 0, "pretty_name": "render - large - first render (mem)('django')", "last_rev": 1691, "last_value": 52625408.0, "last_err": 339968.0, "prev_value": null, "change_rev": null}, {"name": "Components vs Django.peakmem_render_lg_first", "idx": 1, "pretty_name": "render - large - first render (mem)('django-components')", "last_rev": 1691, "last_value": 54730752.0, "last_err": 0.0, "prev_value": 54779904.0, "change_rev": [1687, 1691]}, {"name": "Components vs Django.peakmem_render_lg_subsequent", "idx": 0, "pretty_name": "render - large - second render (mem)('django')", "last_rev": 1691, "last_value": 53796864.0, "last_err": 0.0, "prev_value": 52142080.0, "change_rev": [1687, 1691]}, {"name": "Components vs Django.peakmem_render_lg_subsequent", "idx": 1, "pretty_name": "render - large - second render (mem)('django-components')", "last_rev": 1691, "last_value": 55238656.0, "last_err": 0.0, "prev_value": 55255040.0, "change_rev": [1687, 1691]}, {"name": "Components vs Django.peakmem_render_sm_first", "idx": 0, "pretty_name": "render - small - first render (mem)('django')", "last_rev": 1691, "last_value": 44183552.0, "last_err": 0.0, "prev_value": 44191744.0, "change_rev": [1687, 1691]}, {"name": "Components vs Django.peakmem_render_sm_first", "idx": 1, "pretty_name": "render - small - first render (mem)('django-components')", "last_rev": 1691, "last_value": 44183552.0, "last_err": 69632.0, "prev_value": null, "change_rev": null}, {"name": "Components vs Django.peakmem_render_sm_subsequent", "idx": 0, "pretty_name": "render - small - second render (mem)('django')", "last_rev": 1691, "last_value": 44146688.0, "last_err": 54272.0, "prev_value": null, "change_rev": null}, {"name": "Components vs Django.peakmem_render_sm_subsequent", "idx": 1, "pretty_name": "render - small - second render (mem)('django-components')", "last_rev": 1691, "last_value": 44185600.0, "last_err": 99328.0, "prev_value": null, "change_rev": null}, {"name": "Components vs Django.timeraw_render_lg_first", "idx": 0, "pretty_name": "render - large - first render('django')", "last_rev": 1691, "last_value": 0.06960565700001098, "last_err": 0.0006334964007728495, "prev_value": null, "change_rev": null}, {"name": "Components vs Django.timeraw_render_lg_first", "idx": 1, "pretty_name": "render - large - first render('django-components')", "last_rev": 1691, "last_value": 0.25746033199999374, "last_err": 0.0016121547240513887, "prev_value": null, "change_rev": null}, {"name": "Components vs Django.timeraw_render_lg_subsequent", "idx": 0, "pretty_name": "render - large - second render('django')", "last_rev": 1691, "last_value": 0.034316510999985894, "last_err": 0.0, "prev_value": 0.03327357099999517, "change_rev": [1687, 1691]}, {"name": "Components vs Django.timeraw_render_lg_subsequent", "idx": 1, "pretty_name": "render - large - second render('django-components')", "last_rev": 1691, "last_value": 0.14245594600001255, "last_err": 0.0007892086137296424, "prev_value": null, "change_rev": null}, {"name": "Components vs Django.timeraw_render_sm_first", "idx": 0, "pretty_name": "render - small - first render('django')", "last_rev": 1691, "last_value": 0.0035443229999998493, "last_err": 3.9775229923656616e-05, "prev_value": null, "change_rev": null}, {"name": "Components vs Django.timeraw_render_sm_first", "idx": 1, "pretty_name": "render - small - first render('django-components')", "last_rev": 1691, "last_value": 0.004706463999980315, "last_err": 6.320807031907739e-05, "prev_value": null, "change_rev": null}, {"name": "Components vs Django.timeraw_render_sm_subsequent", "idx": 0, "pretty_name": "render - small - second render('django')", "last_rev": 1691, "last_value": 0.0001005780000014056, "last_err": 1.8215993261458849e-06, "prev_value": null, "change_rev": null}, {"name": "Components vs Django.timeraw_render_sm_subsequent", "idx": 1, "pretty_name": "render - small - second render('django-components')", "last_rev": 1691, "last_value": 0.0005555879999974422, "last_err": 0.0, "prev_value": 0.0005511469999817109, "change_rev": [1687, 1691]}, {"name": "Components vs Django.timeraw_startup_lg", "idx": 0, "pretty_name": "startup - large('django')", "last_rev": 1691, "last_value": 0.21809406599999193, "last_err": 0.002824520187900999, "prev_value": null, "change_rev": null}, {"name": "Components vs Django.timeraw_startup_lg", "idx": 1, "pretty_name": "startup - large('django-components')", "last_rev": 1691, "last_value": 0.21398552899995593, "last_err": 0.002874371370933512, "prev_value": null, "change_rev": null}, {"name": "Other.timeraw_import_time", "idx": null, "pretty_name": "import time", "last_rev": 1691, "last_value": 0.19726691500000015, "last_err": 0.002767772305529618, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.peakmem_render_lg_first", "idx": 0, "pretty_name": "render - large - first render (mem)('isolated')", "last_rev": 1691, "last_value": 54743040.0, "last_err": 0.0, "prev_value": 54767616.0, "change_rev": [1687, 1691]}, {"name": "isolated vs django modes.peakmem_render_lg_first", "idx": 1, "pretty_name": "render - large - first render (mem)('django')", "last_rev": 1691, "last_value": 54114304.0, "last_err": 95232.0, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.peakmem_render_lg_subsequent", "idx": 0, "pretty_name": "render - large - second render (mem)('isolated')", "last_rev": 1691, "last_value": 54976512.0, "last_err": 109568.0, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.peakmem_render_lg_subsequent", "idx": 1, "pretty_name": "render - large - second render (mem)('django')", "last_rev": 1691, "last_value": 54964224.0, "last_err": 0.0, "prev_value": 55304192.0, "change_rev": [1687, 1691]}, {"name": "isolated vs django modes.peakmem_render_sm_first", "idx": 0, "pretty_name": "render - small - first render (mem)('isolated')", "last_rev": 1691, "last_value": 44183552.0, "last_err": 68608.0, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.peakmem_render_sm_first", "idx": 1, "pretty_name": "render - small - first render (mem)('django')", "last_rev": 1691, "last_value": 44179456.0, "last_err": 67584.0, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.peakmem_render_sm_subsequent", "idx": 0, "pretty_name": "render - small - second render (mem)('isolated')", "last_rev": 1691, "last_value": 44183552.0, "last_err": 67584.0, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.peakmem_render_sm_subsequent", "idx": 1, "pretty_name": "render - small - second render (mem)('django')", "last_rev": 1691, "last_value": 44183552.0, "last_err": 66560.0, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.timeraw_render_lg_first", "idx": 0, "pretty_name": "render - large - first render('isolated')", "last_rev": 1691, "last_value": 0.2574955810000006, "last_err": 0.0010127240760288319, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.timeraw_render_lg_first", "idx": 1, "pretty_name": "render - large - first render('django')", "last_rev": 1691, "last_value": 0.2602957870000182, "last_err": 0.001054658307736009, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.timeraw_render_lg_subsequent", "idx": 0, "pretty_name": "render - large - second render('isolated')", "last_rev": 1691, "last_value": 0.14273938200000202, "last_err": 0.0007838296553272511, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.timeraw_render_lg_subsequent", "idx": 1, "pretty_name": "render - large - second render('django')", "last_rev": 1691, "last_value": 0.1464969190000147, "last_err": 0.0008140335540209737, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.timeraw_render_sm_first", "idx": 0, "pretty_name": "render - small - first render('isolated')", "last_rev": 1691, "last_value": 0.00473016699999107, "last_err": 4.6852849337824035e-05, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.timeraw_render_sm_first", "idx": 1, "pretty_name": "render - small - first render('django')", "last_rev": 1691, "last_value": 0.004734037999980956, "last_err": 6.293299290849468e-05, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.timeraw_render_sm_subsequent", "idx": 0, "pretty_name": "render - small - second render('isolated')", "last_rev": 1691, "last_value": 0.000547750000009728, "last_err": 5.144407928178802e-06, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.timeraw_render_sm_subsequent", "idx": 1, "pretty_name": "render - small - second render('django')", "last_rev": 1691, "last_value": 0.0005480739999939033, "last_err": 0.0, "prev_value": 0.0005447550000212686, "change_rev": [1687, 1691]}, {"name": "isolated vs django modes.timeraw_startup_lg", "idx": 0, "pretty_name": "startup - large('isolated')", "last_rev": 1691, "last_value": 0.2142312400000037, "last_err": 0.002749125332585417, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.timeraw_startup_lg", "idx": 1, "pretty_name": "startup - large('django')", "last_rev": 1691, "last_value": 0.21397752699999728, "last_err": 0.0023596495974945756, "prev_value": null, "change_rev": null}]
\ No newline at end of file
+[{"name": "Components vs Django.peakmem_render_lg_first", "idx": 0, "pretty_name": "render - large - first render (mem)('django')", "last_rev": 2029, "last_value": 52926464.0, "last_err": 238738.2857142857, "prev_value": null, "change_rev": null}, {"name": "Components vs Django.peakmem_render_lg_first", "idx": 1, "pretty_name": "render - large - first render (mem)('django-components')", "last_rev": 2029, "last_value": 55269376.0, "last_err": 352841.14285714284, "prev_value": null, "change_rev": null}, {"name": "Components vs Django.peakmem_render_lg_subsequent", "idx": 0, "pretty_name": "render - large - second render (mem)('django')", "last_rev": 2029, "last_value": 53041152.0, "last_err": 638098.2857142857, "prev_value": null, "change_rev": null}, {"name": "Components vs Django.peakmem_render_lg_subsequent", "idx": 1, "pretty_name": "render - large - second render (mem)('django-components')", "last_rev": 2029, "last_value": 55795712.0, "last_err": 429494.85714285716, "prev_value": null, "change_rev": null}, {"name": "Components vs Django.peakmem_render_sm_first", "idx": 0, "pretty_name": "render - small - first render (mem)('django')", "last_rev": 2029, "last_value": 44195840.0, "last_err": 193682.2857142857, "prev_value": null, "change_rev": null}, {"name": "Components vs Django.peakmem_render_sm_first", "idx": 1, "pretty_name": "render - small - first render (mem)('django-components')", "last_rev": 2029, "last_value": 44314624.0, "last_err": 206555.42857142858, "prev_value": null, "change_rev": null}, {"name": "Components vs Django.peakmem_render_sm_subsequent", "idx": 0, "pretty_name": "render - small - second render (mem)('django')", "last_rev": 2029, "last_value": 44255232.0, "last_err": 179346.2857142857, "prev_value": null, "change_rev": null}, {"name": "Components vs Django.peakmem_render_sm_subsequent", "idx": 1, "pretty_name": "render - small - second render (mem)('django-components')", "last_rev": 2029, "last_value": 44376064.0, "last_err": 238153.14285714287, "prev_value": null, "change_rev": null}, {"name": "Components vs Django.timeraw_render_lg_first", "idx": 0, "pretty_name": "render - large - first render('django')", "last_rev": 2029, "last_value": 0.07308550800001967, "last_err": 0.002970056988688463, "prev_value": null, "change_rev": null}, {"name": "Components vs Django.timeraw_render_lg_first", "idx": 1, "pretty_name": "render - large - first render('django-components')", "last_rev": 2029, "last_value": 0.26436952000000247, "last_err": 0.005644097774403133, "prev_value": null, "change_rev": null}, {"name": "Components vs Django.timeraw_render_lg_subsequent", "idx": 0, "pretty_name": "render - large - second render('django')", "last_rev": 2029, "last_value": 0.04362213900003553, "last_err": 0.00010141391999156615, "prev_value": 0.03723830100000214, "change_rev": [1801, 1937]}, {"name": "Components vs Django.timeraw_render_lg_subsequent", "idx": 1, "pretty_name": "render - large - second render('django-components')", "last_rev": 2029, "last_value": 0.15138703899998518, "last_err": 0.006705386360797588, "prev_value": null, "change_rev": null}, {"name": "Components vs Django.timeraw_render_sm_first", "idx": 0, "pretty_name": "render - small - first render('django')", "last_rev": 2029, "last_value": 0.003602947999979733, "last_err": 5.419141048740966e-05, "prev_value": null, "change_rev": null}, {"name": "Components vs Django.timeraw_render_sm_first", "idx": 1, "pretty_name": "render - small - first render('django-components')", "last_rev": 2029, "last_value": 0.004811176000004025, "last_err": 7.476107336392598e-05, "prev_value": null, "change_rev": null}, {"name": "Components vs Django.timeraw_render_sm_subsequent", "idx": 0, "pretty_name": "render - small - second render('django')", "last_rev": 2029, "last_value": 0.00011641800000461444, "last_err": 6.71568788300834e-06, "prev_value": null, "change_rev": null}, {"name": "Components vs Django.timeraw_render_sm_subsequent", "idx": 1, "pretty_name": "render - small - second render('django-components')", "last_rev": 2029, "last_value": 0.0005711430000019391, "last_err": 2.3656907602254418e-05, "prev_value": null, "change_rev": null}, {"name": "Components vs Django.timeraw_startup_lg", "idx": 0, "pretty_name": "startup - large('django')", "last_rev": 2029, "last_value": 0.22356123500000535, "last_err": 0.003663370721264278, "prev_value": null, "change_rev": null}, {"name": "Components vs Django.timeraw_startup_lg", "idx": 1, "pretty_name": "startup - large('django-components')", "last_rev": 2029, "last_value": 0.22048105400000395, "last_err": 0.005292030830577897, "prev_value": null, "change_rev": null}, {"name": "Other.timeraw_import_time", "idx": null, "pretty_name": "import time", "last_rev": 2029, "last_value": 0.19919827600000417, "last_err": 0.0040859881894921846, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.peakmem_render_lg_first", "idx": 0, "pretty_name": "render - large - first render (mem)('isolated')", "last_rev": 2029, "last_value": 55234560.0, "last_err": 314806.85714285716, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.peakmem_render_lg_first", "idx": 1, "pretty_name": "render - large - first render (mem)('django')", "last_rev": 2029, "last_value": 54708224.0, "last_err": 355474.28571428574, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.peakmem_render_lg_subsequent", "idx": 0, "pretty_name": "render - large - second render (mem)('isolated')", "last_rev": 2029, "last_value": 55592960.0, "last_err": 405796.5714285714, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.peakmem_render_lg_subsequent", "idx": 1, "pretty_name": "render - large - second render (mem)('django')", "last_rev": 2029, "last_value": 55621632.0, "last_err": 478939.4285714286, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.peakmem_render_sm_first", "idx": 0, "pretty_name": "render - small - first render (mem)('isolated')", "last_rev": 2029, "last_value": 44316672.0, "last_err": 201874.2857142857, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.peakmem_render_sm_first", "idx": 1, "pretty_name": "render - small - first render (mem)('django')", "last_rev": 2029, "last_value": 44314624.0, "last_err": 207433.14285714287, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.peakmem_render_sm_subsequent", "idx": 0, "pretty_name": "render - small - second render (mem)('isolated')", "last_rev": 2029, "last_value": 44314624.0, "last_err": 228498.2857142857, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.peakmem_render_sm_subsequent", "idx": 1, "pretty_name": "render - small - second render (mem)('django')", "last_rev": 2029, "last_value": 44316672.0, "last_err": 219428.57142857142, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.timeraw_render_lg_first", "idx": 0, "pretty_name": "render - large - first render('isolated')", "last_rev": 2029, "last_value": 0.2646600410000133, "last_err": 0.007677844017113229, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.timeraw_render_lg_first", "idx": 1, "pretty_name": "render - large - first render('django')", "last_rev": 2029, "last_value": 0.2676605120000204, "last_err": 0.00784939843762128, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.timeraw_render_lg_subsequent", "idx": 0, "pretty_name": "render - large - second render('isolated')", "last_rev": 2029, "last_value": 0.15071133700001837, "last_err": 0.006184059019873227, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.timeraw_render_lg_subsequent", "idx": 1, "pretty_name": "render - large - second render('django')", "last_rev": 2029, "last_value": 0.15540660900001058, "last_err": 0.009898742429098032, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.timeraw_render_sm_first", "idx": 0, "pretty_name": "render - small - first render('isolated')", "last_rev": 2029, "last_value": 0.004825538000005736, "last_err": 5.909934210844408e-05, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.timeraw_render_sm_first", "idx": 1, "pretty_name": "render - small - first render('django')", "last_rev": 2029, "last_value": 0.0047952310000027865, "last_err": 7.102011245001072e-05, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.timeraw_render_sm_subsequent", "idx": 0, "pretty_name": "render - small - second render('isolated')", "last_rev": 2029, "last_value": 0.0005736080000247057, "last_err": 2.103986485491359e-05, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.timeraw_render_sm_subsequent", "idx": 1, "pretty_name": "render - small - second render('django')", "last_rev": 2029, "last_value": 0.000582014999963576, "last_err": 1.7671096085730627e-05, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.timeraw_startup_lg", "idx": 0, "pretty_name": "startup - large('isolated')", "last_rev": 2029, "last_value": 0.22129613300000983, "last_err": 0.003939909589791106, "prev_value": null, "change_rev": null}, {"name": "isolated vs django modes.timeraw_startup_lg", "idx": 1, "pretty_name": "startup - large('django')", "last_rev": 2029, "last_value": 0.21916191200000412, "last_err": 0.00459183551388232, "prev_value": null, "change_rev": null}]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/summary/Components vs Django.peakmem_render_lg_first.json b/docs/benchmarks/graphs/summary/Components vs Django.peakmem_render_lg_first.json
index 0492ab87..9b9d166a 100644
--- a/docs/benchmarks/graphs/summary/Components vs Django.peakmem_render_lg_first.json
+++ b/docs/benchmarks/graphs/summary/Components vs Django.peakmem_render_lg_first.json
@@ -1 +1 @@
-[[1662, 53737309.613078326], [1672, 53463506.59363525], [1687, 53427924.42970294], [1691, 53807508.99158667]]
\ No newline at end of file
+[[1662, 53737309.613078326], [1672, 53463506.59363525], [1687, 53427924.42970294], [1691, 53807508.99158667], [1709, 53963042.655257314], [1726, 53670369.245800875], [1766, 54220916.6140389], [1770, 54198077.75539557], [1776, 53906774.26269022], [1801, 54270509.344660625], [1937, 54055804.31664803], [1960, 54295416.494559616], [1996, 54277301.04707094], [2029, 54376892.25474807]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/summary/Components vs Django.peakmem_render_lg_subsequent.json b/docs/benchmarks/graphs/summary/Components vs Django.peakmem_render_lg_subsequent.json
index 27b56f59..16c667e1 100644
--- a/docs/benchmarks/graphs/summary/Components vs Django.peakmem_render_lg_subsequent.json
+++ b/docs/benchmarks/graphs/summary/Components vs Django.peakmem_render_lg_subsequent.json
@@ -1 +1 @@
-[[1662, 54265895.0709751], [1672, 53676080.7209516], [1687, 53675997.57883592], [1691, 54512993.537089705]]
\ No newline at end of file
+[[1662, 54265895.0709751], [1672, 53676080.7209516], [1687, 53675997.57883592], [1691, 54512993.537089705], [1709, 54605449.27839023], [1726, 53697436.790693834], [1766, 54766004.5031032], [1770, 54878384.55144014], [1776, 53912680.86221259], [1801, 54851721.60114168], [1937, 54505276.07990639], [1960, 54599968.83944605], [1996, 54678155.56971878], [2029, 54725974.50425164]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/summary/Components vs Django.peakmem_render_sm_first.json b/docs/benchmarks/graphs/summary/Components vs Django.peakmem_render_sm_first.json
index 57927060..b5b77f9d 100644
--- a/docs/benchmarks/graphs/summary/Components vs Django.peakmem_render_sm_first.json
+++ b/docs/benchmarks/graphs/summary/Components vs Django.peakmem_render_sm_first.json
@@ -1 +1 @@
-[[1662, 44191743.99999999], [1672, 44052479.80957694], [1687, 44251096.14326895], [1691, 44179455.81012423]]
\ No newline at end of file
+[[1662, 44191743.99999999], [1672, 44052479.80957694], [1687, 44251096.14326895], [1691, 44179455.81012423], [1709, 44253141.3491094], [1726, 44255192.14695785], [1766, 44318719.81072088], [1770, 44324863.95268679], [1776, 44244949.34121254], [1801, 44324676.21343578], [1937, 44750847.578234404], [1960, 44775384.609963], [1996, 44842828.229087956], [2029, 44892155.328466915]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/summary/Components vs Django.peakmem_render_sm_subsequent.json b/docs/benchmarks/graphs/summary/Components vs Django.peakmem_render_sm_subsequent.json
index 0afca6b5..764451a5 100644
--- a/docs/benchmarks/graphs/summary/Components vs Django.peakmem_render_sm_subsequent.json
+++ b/docs/benchmarks/graphs/summary/Components vs Django.peakmem_render_sm_subsequent.json
@@ -1 +1 @@
-[[1662, 44191743.81017703], [1672, 43988933.59873213], [1687, 44208009.40445502], [1691, 44185599.95253766]]
\ No newline at end of file
+[[1662, 44191743.81017703], [1672, 43988933.59873213], [1687, 44208009.40445502], [1691, 44185599.95253766], [1709, 44314453.63272547], [1726, 44318719.81072088], [1766, 44316671.57410231], [1770, 44205956.60747199], [1776, 44376021.4672124], [1801, 44322622.19567646], [1937, 44636028.0238471], [1960, 44777429.84849827], [1996, 44846935.655543014], [2029, 44801668.84315699]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/summary/Components vs Django.timeraw_render_lg_first.json b/docs/benchmarks/graphs/summary/Components vs Django.timeraw_render_lg_first.json
index c60851e1..d7a7b9c6 100644
--- a/docs/benchmarks/graphs/summary/Components vs Django.timeraw_render_lg_first.json
+++ b/docs/benchmarks/graphs/summary/Components vs Django.timeraw_render_lg_first.json
@@ -1 +1 @@
-[[1662, 0.13350944016163727], [1672, 0.1370189324406613], [1687, 0.13338881256624893], [1691, 0.13534306127506]]
\ No newline at end of file
+[[1662, 0.13350944016163727], [1672, 0.1370189324406613], [1687, 0.13338881256624893], [1691, 0.13534306127506], [1709, 0.14028461383291016], [1726, 0.1369248426273554], [1766, 0.13857329097819557], [1770, 0.14073477092350728], [1776, 0.1385645020210802], [1801, 0.14040196312080028], [1937, 0.14582964264952603], [1960, 0.14671897491501892], [1996, 0.15202819951982394], [2029, 0.15457268328939747]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/summary/Components vs Django.timeraw_render_lg_subsequent.json b/docs/benchmarks/graphs/summary/Components vs Django.timeraw_render_lg_subsequent.json
index 5f09b806..807386ec 100644
--- a/docs/benchmarks/graphs/summary/Components vs Django.timeraw_render_lg_subsequent.json
+++ b/docs/benchmarks/graphs/summary/Components vs Django.timeraw_render_lg_subsequent.json
@@ -1 +1 @@
-[[1662, 0.0687644082522681], [1672, 0.06987734456556612], [1687, 0.06874611472573841], [1691, 0.07039998567606925]]
\ No newline at end of file
+[[1662, 0.0687644082522681], [1672, 0.06987734456556612], [1687, 0.06874611472573841], [1691, 0.07039998567606925], [1709, 0.07467771106069107], [1726, 0.07307627413528986], [1766, 0.0752258677863117], [1770, 0.07591381717343901], [1776, 0.0741750279629251], [1801, 0.07486521068773488], [1937, 0.08182795598310513], [1960, 0.08198138820511656], [1996, 0.08497198126158123], [2029, 0.08730133488241124]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/summary/Components vs Django.timeraw_render_sm_first.json b/docs/benchmarks/graphs/summary/Components vs Django.timeraw_render_sm_first.json
index 5ec5a6ea..bd943ad3 100644
--- a/docs/benchmarks/graphs/summary/Components vs Django.timeraw_render_sm_first.json
+++ b/docs/benchmarks/graphs/summary/Components vs Django.timeraw_render_sm_first.json
@@ -1 +1 @@
-[[1662, 0.004071198582731586], [1672, 0.004168318834979474], [1687, 0.004071589002161507], [1691, 0.004235212007582172]]
\ No newline at end of file
+[[1662, 0.004071198582731586], [1672, 0.004168318834979474], [1687, 0.004071589002161507], [1691, 0.004235212007582172], [1709, 0.004181923314217816], [1726, 0.004054429932062044], [1766, 0.004133897799028137], [1770, 0.004257194320585938], [1776, 0.004117446125697445], [1801, 0.004200816754404154], [1937, 0.004251194879485355], [1960, 0.0041701734817425696], [1996, 0.004322540447211732], [2029, 0.004263823296369016]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/summary/Components vs Django.timeraw_render_sm_subsequent.json b/docs/benchmarks/graphs/summary/Components vs Django.timeraw_render_sm_subsequent.json
index 86539d99..8d882c01 100644
--- a/docs/benchmarks/graphs/summary/Components vs Django.timeraw_render_sm_subsequent.json
+++ b/docs/benchmarks/graphs/summary/Components vs Django.timeraw_render_sm_subsequent.json
@@ -1 +1 @@
-[[1662, 0.00023540900613243872], [1672, 0.0002365886195511814], [1687, 0.0002326213978668684], [1691, 0.0002363893607261623]]
\ No newline at end of file
+[[1662, 0.00023540900613243872], [1672, 0.0002365886195511814], [1687, 0.0002326213978668684], [1691, 0.0002363893607261623], [1709, 0.0002646827752432008], [1726, 0.00025280056719810247], [1766, 0.00025904182642747317], [1770, 0.0002627038966898471], [1776, 0.00026058997620285855], [1801, 0.000260725493948419], [1937, 0.0002700303204925571], [1960, 0.00027008950893915996], [1996, 0.0002800574798090668], [2029, 0.000278420428825539]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/summary/Components vs Django.timeraw_startup_lg.json b/docs/benchmarks/graphs/summary/Components vs Django.timeraw_startup_lg.json
index a8607bf8..a58dd726 100644
--- a/docs/benchmarks/graphs/summary/Components vs Django.timeraw_startup_lg.json
+++ b/docs/benchmarks/graphs/summary/Components vs Django.timeraw_startup_lg.json
@@ -1 +1 @@
-[[1662, 0.21586009863792485], [1672, 0.22261052942796597], [1687, 0.21562505130206716], [1691, 0.2226172972159168]]
\ No newline at end of file
+[[1662, 0.21586009863792485], [1672, 0.22261052942796597], [1687, 0.21562505130206716], [1691, 0.2226172972159168], [1709, 0.21969118716012626], [1726, 0.21540413874268913], [1766, 0.2217946171557135], [1770, 0.22763817627917332], [1776, 0.21949736979633283], [1801, 0.22435444169386096], [1937, 0.22675338309844276], [1960, 0.22498195815021013], [1996, 0.23120029358312028], [2029, 0.22761342037999505]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/summary/Other.timeraw_import_time.json b/docs/benchmarks/graphs/summary/Other.timeraw_import_time.json
index eb6262fb..eb01068a 100644
--- a/docs/benchmarks/graphs/summary/Other.timeraw_import_time.json
+++ b/docs/benchmarks/graphs/summary/Other.timeraw_import_time.json
@@ -1 +1 @@
-[[1662, 0.19832900800003017], [1672, 0.20217585500000723], [1687, 0.19726691500000015], [1691, 0.20350580199999513]]
\ No newline at end of file
+[[1662, 0.19832900800003017], [1672, 0.20217585500000723], [1687, 0.19726691500000015], [1691, 0.20350580199999513], [1709, 0.19950735400001918], [1726, 0.19625152499997967], [1766, 0.20073733000003813], [1770, 0.20376683500001036], [1776, 0.19919827600000417], [1801, 0.2053688209999791], [1937, 0.2063091950000171], [1960, 0.20468290799999522], [1996, 0.21042045099989082], [2029, 0.2056691309999792]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/summary/isolated vs django modes.peakmem_render_lg_first.json b/docs/benchmarks/graphs/summary/isolated vs django modes.peakmem_render_lg_first.json
index 4efea462..4429f303 100644
--- a/docs/benchmarks/graphs/summary/isolated vs django modes.peakmem_render_lg_first.json
+++ b/docs/benchmarks/graphs/summary/isolated vs django modes.peakmem_render_lg_first.json
@@ -1 +1 @@
-[[1662, 54203904.32644733], [1672, 54377977.05567385], [1687, 54531587.401090905], [1691, 54414373.37457081]]
\ No newline at end of file
+[[1662, 54203904.32644733], [1672, 54377977.05567385], [1687, 54531587.401090905], [1691, 54414373.37457081], [1709, 54655941.05401974], [1726, 54668354.35558938], [1766, 55133687.30603648], [1770, 55072492.873806104], [1776, 55115246.19008138], [1801, 55131593.83007953], [1937, 54971848.18483294], [1960, 54977822.99733244], [1996, 55221688.06930552], [2029, 55836094.494666085]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/summary/isolated vs django modes.peakmem_render_lg_subsequent.json b/docs/benchmarks/graphs/summary/isolated vs django modes.peakmem_render_lg_subsequent.json
index 60732732..bed4ea23 100644
--- a/docs/benchmarks/graphs/summary/isolated vs django modes.peakmem_render_lg_subsequent.json
+++ b/docs/benchmarks/graphs/summary/isolated vs django modes.peakmem_render_lg_subsequent.json
@@ -1 +1 @@
-[[1662, 54880185.34368702], [1672, 54845439.84705003], [1687, 55287805.57238104], [1691, 54974463.04630629]]
\ No newline at end of file
+[[1662, 54880185.34368702], [1672, 54845439.84705003], [1687, 55287805.57238104], [1691, 54974463.04630629], [1709, 55404533.06087942], [1726, 55316304.695168346], [1766, 55588847.36277981], [1770, 55711653.6193069], [1776, 55635967.849223286], [1801, 55857133.82825839], [1937, 56076247.273349956], [1960, 55971522.87008585], [1996, 56434628.542863145], [2029, 56782847.85226863]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/summary/isolated vs django modes.peakmem_render_sm_first.json b/docs/benchmarks/graphs/summary/isolated vs django modes.peakmem_render_sm_first.json
index 0aaaf14f..d6903d84 100644
--- a/docs/benchmarks/graphs/summary/isolated vs django modes.peakmem_render_sm_first.json
+++ b/docs/benchmarks/graphs/summary/isolated vs django modes.peakmem_render_sm_first.json
@@ -1 +1 @@
-[[1662, 44185599.95253766], [1672, 44048383.99999999], [1687, 44312575.95267366], [1691, 44177407.95252886]]
\ No newline at end of file
+[[1662, 44185599.95253766], [1672, 44048383.99999999], [1687, 44312575.95267366], [1691, 44177407.95252886], [1709, 44312575.95267366], [1726, 44314624.0], [1766, 44316671.95267803], [1770, 44318719.81072088], [1776, 44273651.87380721], [1801, 44453888.0], [1937, 44744704.0], [1960, 44838912.00000001], [1996, 44969983.99999999], [2029, 44847103.812950954]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/summary/isolated vs django modes.peakmem_render_sm_subsequent.json b/docs/benchmarks/graphs/summary/isolated vs django modes.peakmem_render_sm_subsequent.json
index e99dc278..1f44ef9f 100644
--- a/docs/benchmarks/graphs/summary/isolated vs django modes.peakmem_render_sm_subsequent.json
+++ b/docs/benchmarks/graphs/summary/isolated vs django modes.peakmem_render_sm_subsequent.json
@@ -1 +1 @@
-[[1662, 44187648.0], [1672, 44052479.99999999], [1687, 44312575.95267366], [1691, 44179455.99999999]]
\ No newline at end of file
+[[1662, 44187648.0], [1672, 44052479.99999999], [1687, 44312575.95267366], [1691, 44179455.99999999], [1709, 44312575.95267366], [1726, 44314624.0], [1766, 44312575.95267366], [1770, 44316671.95267803], [1776, 44437504.0], [1801, 44449792.0], [1937, 44744704.0], [1960, 44900304.17220587], [1996, 44974080.0], [2029, 44984319.953380376]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/summary/isolated vs django modes.timeraw_render_lg_first.json b/docs/benchmarks/graphs/summary/isolated vs django modes.timeraw_render_lg_first.json
index 22f901e9..58c7bd0b 100644
--- a/docs/benchmarks/graphs/summary/isolated vs django modes.timeraw_render_lg_first.json
+++ b/docs/benchmarks/graphs/summary/isolated vs django modes.timeraw_render_lg_first.json
@@ -1 +1 @@
-[[1662, 0.2582970915627115], [1672, 0.2609375697890752], [1687, 0.2585333418012986], [1691, 0.2605245701455466]]
\ No newline at end of file
+[[1662, 0.2582970915627115], [1672, 0.2609375697890752], [1687, 0.2585333418012986], [1691, 0.2605245701455466], [1709, 0.26615604836262874], [1726, 0.25886008645727265], [1766, 0.2656287982807661], [1770, 0.2675580766089799], [1776, 0.2644825926606367], [1801, 0.26856188100049755], [1937, 0.2700264321932048], [1960, 0.2711075492537049], [1996, 0.28189867248766043], [2029, 0.29481258851469266]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/summary/isolated vs django modes.timeraw_render_lg_subsequent.json b/docs/benchmarks/graphs/summary/isolated vs django modes.timeraw_render_lg_subsequent.json
index 5f10114d..56f4bd5d 100644
--- a/docs/benchmarks/graphs/summary/isolated vs django modes.timeraw_render_lg_subsequent.json
+++ b/docs/benchmarks/graphs/summary/isolated vs django modes.timeraw_render_lg_subsequent.json
@@ -1 +1 @@
-[[1662, 0.144605946222714], [1672, 0.14711376894836906], [1687, 0.14434992731884352], [1691, 0.14638104217028877]]
\ No newline at end of file
+[[1662, 0.144605946222714], [1672, 0.14711376894836906], [1687, 0.14434992731884352], [1691, 0.14638104217028877], [1709, 0.1508107336447194], [1726, 0.14754149544768042], [1766, 0.15304096778650703], [1770, 0.1536390943522132], [1776, 0.15209353551720362], [1801, 0.15356938175949056], [1937, 0.15690951925702573], [1960, 0.15706997937098616], [1996, 0.16911758076913888], [2029, 0.17625829701058932]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/summary/isolated vs django modes.timeraw_render_sm_first.json b/docs/benchmarks/graphs/summary/isolated vs django modes.timeraw_render_sm_first.json
index e5fa8dde..5ba7c936 100644
--- a/docs/benchmarks/graphs/summary/isolated vs django modes.timeraw_render_sm_first.json
+++ b/docs/benchmarks/graphs/summary/isolated vs django modes.timeraw_render_sm_first.json
@@ -1 +1 @@
-[[1662, 0.004713162243622524], [1672, 0.004881543738505435], [1687, 0.004732102104161917], [1691, 0.00488069732533968]]
\ No newline at end of file
+[[1662, 0.004713162243622524], [1672, 0.004881543738505435], [1687, 0.004732102104161917], [1691, 0.00488069732533968], [1709, 0.004839972328668506], [1726, 0.004672193982353972], [1766, 0.004766899700580667], [1770, 0.004898703707479391], [1776, 0.004767500868992566], [1801, 0.004852423669122516], [1937, 0.004924660359490744], [1960, 0.004810360631940079], [1996, 0.004998322871483767], [2029, 0.004880129761791827]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/summary/isolated vs django modes.timeraw_render_sm_subsequent.json b/docs/benchmarks/graphs/summary/isolated vs django modes.timeraw_render_sm_subsequent.json
index 86b696b1..dec783d0 100644
--- a/docs/benchmarks/graphs/summary/isolated vs django modes.timeraw_render_sm_subsequent.json
+++ b/docs/benchmarks/graphs/summary/isolated vs django modes.timeraw_render_sm_subsequent.json
@@ -1 +1 @@
-[[1662, 0.0005386441936864901], [1672, 0.0005576844109755481], [1687, 0.0005459711425132094], [1691, 0.0005519974567116778]]
\ No newline at end of file
+[[1662, 0.0005386441936864901], [1672, 0.0005576844109755481], [1687, 0.0005459711425132094], [1691, 0.0005519974567116778], [1709, 0.0005728459938648165], [1726, 0.0005428729701593198], [1766, 0.0005836611719634209], [1770, 0.0005880105852110292], [1776, 0.0005786218553806627], [1801, 0.0005751723828193878], [1937, 0.0005916876201898046], [1960, 0.000594381138510516], [1996, 0.0006163559143381376], [2029, 0.0006119567036345985]]
\ No newline at end of file
diff --git a/docs/benchmarks/graphs/summary/isolated vs django modes.timeraw_startup_lg.json b/docs/benchmarks/graphs/summary/isolated vs django modes.timeraw_startup_lg.json
index 4abb7293..2b8c756c 100644
--- a/docs/benchmarks/graphs/summary/isolated vs django modes.timeraw_startup_lg.json
+++ b/docs/benchmarks/graphs/summary/isolated vs django modes.timeraw_startup_lg.json
@@ -1 +1 @@
-[[1662, 0.21383238744211777], [1672, 0.22221634409189991], [1687, 0.21410434591886193], [1691, 0.2203609725843055]]
\ No newline at end of file
+[[1662, 0.21383238744211777], [1672, 0.22221634409189991], [1687, 0.21410434591886193], [1691, 0.2203609725843055], [1709, 0.22018038637622225], [1726, 0.2149156309515977], [1766, 0.2198640308272821], [1770, 0.22362192103085216], [1776, 0.21863830924562072], [1801, 0.22295218072522197], [1937, 0.22573752762853083], [1960, 0.22581596577171012], [1996, 0.22995210340856065], [2029, 0.2285231418957897]]
\ No newline at end of file
diff --git a/docs/benchmarks/index.json b/docs/benchmarks/index.json
index 98a7a9f3..d98e1855 100644
--- a/docs/benchmarks/index.json
+++ b/docs/benchmarks/index.json
@@ -1 +1 @@
-{"project": "django-components", "project_url": "/django-components/", "show_commit_url": "#", "hash_length": 8, "revision_to_hash": {"250": "45a18626d74b280aa05498aa380eba687010cc0f", "268": "c9f0068741871b24756045805aaab8fbf368601a", "343": "ed2cd57bdcc12bf390f42058061a6ed077e558b0", "359": "501a055fe21abb4c7ad9af1a3a1b42eea24afacf", "369": "1937d594100d0651b151c8edbe1189b5a88b358f", "380": "1d091f50999cca0fa8c7852da2ea71684eadda6d", "388": "f5c3f64c866d06dfa2435d1d18b936dddb9eb67c", "427": "24d9db39c3c55f5eb91c9ce2112ad22507ccdd60", "457": "9103eda2f4ed80f46ff03d215491009c5476b410", "487": "384ff79e0875ac3a64a2755010ef86ec09a090b5", "500": "c102019811586f40cb11952f283797ddee96de3c", "503": "a350ad1a869bb46c6bac093f54ac40ea85d8cd8f", "508": "16022429da16e459b8a1e6e9cd8ab41d95dcb59d", "536": "ea7beb518c435389d9daff7ea557eec9ddc2464e", "550": "2cfc7285e12677a54247e9998967081c0e031efc", "552": "fcbfae3c5f0611519636290ef99b25bc0a319825", "564": "316310e842f276fd43436b432781430bd255410b", "575": "e0a5c2a4bcf5e8f577e19333e3b719440c37b10b", "577": "c174aa9802cc73b5d8fef4b0c704de3fb68b1cbb", "598": "979781012532f18c147a570ff4441c65c7503b6f", "612": "9d7d0b40b9d3bbd8e5b754e3b1b20df982cdd184", "622": "24032ac2ea80627f0fdde88058a2c3fbcae0d3fe", "646": "4f1a8184465fc472995bc226bebd66c250d501f6", "651": "8ce649498ff63a64761c83cf713983b7e2f24b81", "663": "93facba53e050619a477fa214da89fccf118ec13", "675": "ea33e0db6c54486b67e2fa496ec1c3ec3de06ec7", "681": "6874b1531df0e8836000bbc76fcca5a3de42724a", "691": "9f4243232018708fdf330f0e922a09317c60234c", "704": "188a9dc7eef4c45ee4f87efa8a0d6728e94f210d", "724": "76a0cde3af7b3061127d149c138fbc78462a07fe", "729": "c932f28cb4840dc45d2d26b4d927c1480459272f", "742": "7058f05e0415965ddd162d0ba663c5106e7ffb66", "748": "95d6eacb5c03a611f92cd9a6e254cea7e0ce57eb", "752": "d512cfb0fec8a42ab58d2f106047fc4c0ec65427", "757": "04160f4e0b2ec4d901d6724c34ed8df883363064", "773": "20847b978b3ddb5979489fbb4bd8e971b9b16fbf", "775": "ae5cda9f727454a077ab16c51a237fec076bd442", "779": "cd791caa572c9acd6d56445c04d6c9b55a50c602", "787": "274be104789e31955f88e26f14c89ed648f25448", "806": "c3a80b729049b5ea1b4799e742e4100a7640efab", "845": "0abb5aa63ff0396328ef4dfdc2e78e872671f135", "855": "e4e787b29dc9cd2cf35ad02f22bad6fa15a7211c", "867": "e346c07298b268d9212fcf93d132968e69824730", "884": "2316f79dff66260b7a9fe91effb55c74524deca0", "887": "085c60a8c96442d9fd4f72d66a7eb295e7cfb2d9", "889": "ba86cee578490df944ad167674db32182b8bbf7b", "891": "dd292b03508b7c03b2ad4f9fb9c17dec747845ba", "898": "badffdda3538a8082137453c757509ed4f035a3e", "902": "c07f0e634121ced0119bb665ed544ca4447c416b", "904": "8bbe81d7171ec3512f250927653d8454c7ff005e", "908": "881c36219a45581086da35a17d3715ee0698ca88", "910": "9bfb50b8f27f7ff521226109ab30426633f6b514", "912": "3a7d5355cfa5d37641a4c80ee9fd2423cea10c1b", "930": "09a720009767f45bebcfa48f3587219c03082601", "935": "a4b4905bee778a51f9a99a042ce14ed2424cc9fb", "940": "31257a475dcfdacaaeb408f59ea92b736a668815", "942": "d47927054c820ecf3f9f97a62f03dfbb5496a11b", "956": "fbbbf6c694b8213832fc127ee8a3e31872308aff", "958": "d819f3ff491866abaeb9a7dbba6a6ca3c21da9f8", "960": "c202c5a901106392ccdde49100211182c986eca5", "965": "7bbfcf7565159d92de41bb0d738b7878d78f4533", "978": "b89c09aa5f13903b1d056c70afbfd327f0ed6362", "980": "03af25aad6841b6edb56c925d2599817e08ceb44", "982": "5cb649fae62a42e525c1ea9fb74d7838886cc1a8", "986": "f97717cdb35eaadb78b1f468728b1bd386e742d8", "1056": "b26a2011380c959bfc98043821f6b4aa337a281d", "1058": "8c5b088c31b7166932e18739f882c2eef632f3a4", "1060": "682bfc42397c0bdbeb1a2b6ccabb8aca89686d4f", "1089": "8f13a641ac096e93a0464f048a4fa53e591bb8db", "1098": "30d04fe1b053e6c5de0b6f34a7758a627517be8c", "1125": "f7846b9c0ae7a8fd0b7f6edf664316e485455e76", "1132": "a3d66586b19b7e2aacbf793edfba20bcd9858f4a", "1179": "0064de9c78db5eccc7c498e2a6d4c3c5ffa745ec", "1252": "d093bfb05212555e14c5cd46c94b0ba057cbeceb", "1270": "c0a4fd5f685d694218b583b2d0a3e8417925d53a", "1277": "2a4b0f52894d5d0bb8d325d6096a3950286db541", "1282": "5a23101038d7085959087b2352e71590eafe9529", "1287": "ce3305a5fff095e990707de851f85bc27120596c", "1312": "cdc830fca3bdd98669e0510841536be14d1bd728", "1328": "468a593a472c35379fe7567918e8a534b2d53748", "1337": "2f14e8e0908657459909c6338106e561edc5d0f4", "1352": "a5659691d0f947643ce6542b7f09075e3f931646", "1361": "aaeba99f54b206baed072ffd0572035ddf5118a7", "1366": "6813c9d7aa4acfb04df9665651a2864d04bbe8ba", "1381": "6681fc0085fdb4b76cb74c1bbf20d44fa22f40fe", "1387": "6bb73bd8afd350db84889efc09b74255cea08979", "1392": "c76f8198dd497b34735fc17a0f7a678bdd811a3f", "1447": "3bbd4326e6c1bfb4a02039e16021a3f720910308", "1488": "914576e68133bc6ec356f3918016c45691ed3534", "1505": "3d187d7abad80f8464bd70dfba847d27a8e173db", "1514": "e105500350a320dfccf36fc19cad7421d38c3736", "1546": "61515b34540d27311fc3286b50212c1e314ce39e", "1571": "691443a0a821eb1a02f9b42ad0e32bcd71d94943", "1591": "dcd4203eeadafc5d500b4b72c5cf04f1fe7317e7", "1662": "d0a42a2698f2ba21e7ab2dec750c5dbadeda0db5", "1672": "2037ed20b7252cc52008e69bdd3d8e095ad6ea08", "1687": "2472c2ad338a23fba015d4d9816cb62d1325455f", "1691": "42818ad6ffb47bd650d8a379b84c3d48394f9f77"}, "revision_to_date": {"250": 1630912817000, "268": 1631273531000, "343": 1654029104000, "359": 1657786741000, "369": 1657798498000, "380": 1658057264000, "388": 1658128984000, "427": 1670931380000, "457": 1673095465000, "487": 1675977478000, "500": 1678833725000, "503": 1678834812000, "508": 1679329468000, "536": 1680808865000, "550": 1681214445000, "552": 1681388770000, "564": 1682351069000, "575": 1684433482000, "577": 1684760359000, "598": 1693734426000, "612": 1695996512000, "622": 1696883504000, "646": 1702127213000, "651": 1702852887000, "663": 1705268832000, "675": 1705701480000, "681": 1706343740000, "691": 1706393538000, "704": 1707467055000, "724": 1708588716000, "729": 1708984309000, "742": 1709796107000, "748": 1710542375000, "752": 1711216985000, "757": 1711278671000, "773": 1711752109000, "775": 1711786713000, "779": 1712212506000, "787": 1712872453000, "806": 1713128940000, "845": 1713390260000, "855": 1713901297000, "867": 1714590581000, "884": 1714912319000, "887": 1715087715000, "889": 1715111097000, "891": 1715505986000, "898": 1716105516000, "902": 1716441018000, "904": 1716488913000, "908": 1717051130000, "910": 1717227227000, "912": 1717232115000, "930": 1718176374000, "935": 1718991590000, "940": 1720427258000, "942": 1720429478000, "956": 1722279535000, "958": 1722325182000, "960": 1722666822000, "965": 1722890429000, "978": 1723843975000, "980": 1723993321000, "982": 1724243667000, "986": 1724363119000, "1056": 1724732902000, "1058": 1724824521000, "1060": 1724924003000, "1089": 1725480929000, "1098": 1725655525000, "1125": 1726084755000, "1132": 1726347108000, "1179": 1728564427000, "1252": 1732525062000, "1270": 1732629701000, "1277": 1732644681000, "1282": 1732658332000, "1287": 1732726640000, "1312": 1733164405000, "1328": 1733471329000, "1337": 1733644953000, "1352": 1733834654000, "1361": 1734080521000, "1366": 1734245122000, "1381": 1734464187000, "1387": 1734600366000, "1392": 1734955786000, "1447": 1736287315000, "1488": 1737558545000, "1505": 1738157680000, "1514": 1738404847000, "1546": 1738662755000, "1571": 1739736734000, "1591": 1740050674000, "1662": 1742502414000, "1672": 1742645505000, "1687": 1742720064000, "1691": 1742765538000}, "params": {"machine": ["ci-linux"], "python": ["3.13"], "django": ["5.1"], "djc-core-html-parser": [""], "branch": ["master"]}, "graph_param_list": [{"machine": "ci-linux", "python": "3.13", "django": "5.1", "djc-core-html-parser": "", "branch": "master"}], "benchmarks": {"Components vs Django.peakmem_render_lg_first": {"code": "class DjangoComponentsVsDjangoTests:\n @benchmark(\n pretty_name=\"render - large - first render (mem)\",\n group_name=DJC_VS_DJ_GROUP,\n number=1,\n rounds=5,\n params={\n \"renderer\": [\"django\", \"django-components\"],\n },\n setup=lambda renderer: setup_templating_memory_benchmark(renderer, \"lg\", \"first\", \"isolated\"),\n )\n def peakmem_render_lg_first(self, renderer: TemplatingRenderer):\n do_render()\n\nsetup=lambda renderer: setup_templating_memory_benchmark(renderer, \"lg\", \"first\", \"isolated\"),", "name": "Components vs Django.peakmem_render_lg_first", "param_names": ["renderer"], "params": [["'django'", "'django-components'"]], "pretty_name": "render - large - first render (mem)", "type": "peakmemory", "unit": "bytes", "version": "301c396f017f45a5b3f71e85df58d15f54153fcfd951af7ef424641d4b31b528"}, "Components vs Django.peakmem_render_lg_subsequent": {"code": "class DjangoComponentsVsDjangoTests:\n @benchmark(\n pretty_name=\"render - large - second render (mem)\",\n group_name=DJC_VS_DJ_GROUP,\n number=1,\n rounds=5,\n params={\n \"renderer\": [\"django\", \"django-components\"],\n },\n setup=lambda renderer: setup_templating_memory_benchmark(renderer, \"lg\", \"subsequent\", \"isolated\"),\n )\n def peakmem_render_lg_subsequent(self, renderer: TemplatingRenderer):\n do_render()\n\nsetup=lambda renderer: setup_templating_memory_benchmark(renderer, \"lg\", \"subsequent\", \"isolated\"),", "name": "Components vs Django.peakmem_render_lg_subsequent", "param_names": ["renderer"], "params": [["'django'", "'django-components'"]], "pretty_name": "render - large - second render (mem)", "type": "peakmemory", "unit": "bytes", "version": "9a44e9999ef3ef42ea7e01323727490244febb43d66a87a4d8f88c6b8a133b8b"}, "Components vs Django.peakmem_render_sm_first": {"code": "class DjangoComponentsVsDjangoTests:\n @benchmark(\n pretty_name=\"render - small - first render (mem)\",\n group_name=DJC_VS_DJ_GROUP,\n number=1,\n rounds=5,\n params={\n \"renderer\": [\"django\", \"django-components\"],\n },\n setup=lambda renderer: setup_templating_memory_benchmark(renderer, \"sm\", \"first\", \"isolated\"),\n )\n def peakmem_render_sm_first(self, renderer: TemplatingRenderer):\n do_render()\n\nsetup=lambda renderer: setup_templating_memory_benchmark(renderer, \"sm\", \"first\", \"isolated\"),", "name": "Components vs Django.peakmem_render_sm_first", "param_names": ["renderer"], "params": [["'django'", "'django-components'"]], "pretty_name": "render - small - first render (mem)", "type": "peakmemory", "unit": "bytes", "version": "e93b7a5193681c883edf85bdb30b1bc0821263bf51033fdcee215b155085e036"}, "Components vs Django.peakmem_render_sm_subsequent": {"code": "class DjangoComponentsVsDjangoTests:\n @benchmark(\n pretty_name=\"render - small - second render (mem)\",\n group_name=DJC_VS_DJ_GROUP,\n number=1,\n rounds=5,\n params={\n \"renderer\": [\"django\", \"django-components\"],\n },\n setup=lambda renderer: setup_templating_memory_benchmark(renderer, \"sm\", \"subsequent\", \"isolated\"),\n )\n def peakmem_render_sm_subsequent(self, renderer: TemplatingRenderer):\n do_render()\n\nsetup=lambda renderer: setup_templating_memory_benchmark(renderer, \"sm\", \"subsequent\", \"isolated\"),", "name": "Components vs Django.peakmem_render_sm_subsequent", "param_names": ["renderer"], "params": [["'django'", "'django-components'"]], "pretty_name": "render - small - second render (mem)", "type": "peakmemory", "unit": "bytes", "version": "b46e0820b18950aa7cc5e61306ff3425b76b4da9dca42d64fae5b1d25c6c9026"}, "Components vs Django.timeraw_render_lg_first": {"code": "class DjangoComponentsVsDjangoTests:\n @benchmark(\n pretty_name=\"render - large - first render\",\n group_name=DJC_VS_DJ_GROUP,\n number=1,\n rounds=5,\n params={\n \"renderer\": [\"django\", \"django-components\"],\n },\n include_in_quick_benchmark=True,\n )\n def timeraw_render_lg_first(self, renderer: TemplatingRenderer):\n return prepare_templating_benchmark(renderer, \"lg\", \"first\", \"isolated\")", "min_run_count": 2, "name": "Components vs Django.timeraw_render_lg_first", "number": 1, "param_names": ["renderer"], "params": [["'django'", "'django-components'"]], "pretty_name": "render - large - first render", "repeat": 0, "rounds": 5, "sample_time": 0.01, "type": "time", "unit": "seconds", "version": "be3bf6236960046a028b6ea007aad28b2337fc2b906b8ce317a09a5d4f1a6193", "warmup_time": -1}, "Components vs Django.timeraw_render_lg_subsequent": {"code": "class DjangoComponentsVsDjangoTests:\n @benchmark(\n pretty_name=\"render - large - second render\",\n group_name=DJC_VS_DJ_GROUP,\n number=1,\n rounds=5,\n params={\n \"renderer\": [\"django\", \"django-components\"],\n },\n )\n def timeraw_render_lg_subsequent(self, renderer: TemplatingRenderer):\n return prepare_templating_benchmark(renderer, \"lg\", \"subsequent\", \"isolated\")", "min_run_count": 2, "name": "Components vs Django.timeraw_render_lg_subsequent", "number": 1, "param_names": ["renderer"], "params": [["'django'", "'django-components'"]], "pretty_name": "render - large - second render", "repeat": 0, "rounds": 5, "sample_time": 0.01, "type": "time", "unit": "seconds", "version": "b98221c11a0ee6e9de0778d416d31b9dd514a674d9017a2bb9b2fc1cd0f01920", "warmup_time": -1}, "Components vs Django.timeraw_render_sm_first": {"code": "class DjangoComponentsVsDjangoTests:\n @benchmark(\n pretty_name=\"render - small - first render\",\n group_name=DJC_VS_DJ_GROUP,\n number=1,\n rounds=5,\n params={\n \"renderer\": [\"django\", \"django-components\"],\n },\n )\n def timeraw_render_sm_first(self, renderer: TemplatingRenderer):\n return prepare_templating_benchmark(renderer, \"sm\", \"first\", \"isolated\")", "min_run_count": 2, "name": "Components vs Django.timeraw_render_sm_first", "number": 1, "param_names": ["renderer"], "params": [["'django'", "'django-components'"]], "pretty_name": "render - small - first render", "repeat": 0, "rounds": 5, "sample_time": 0.01, "type": "time", "unit": "seconds", "version": "f1fc17e4a31c71f4d9265f1122da52e7cf57addb4dfa02606e303b33d6431b9b", "warmup_time": -1}, "Components vs Django.timeraw_render_sm_subsequent": {"code": "class DjangoComponentsVsDjangoTests:\n @benchmark(\n pretty_name=\"render - small - second render\",\n group_name=DJC_VS_DJ_GROUP,\n number=1,\n rounds=5,\n params={\n \"renderer\": [\"django\", \"django-components\"],\n },\n )\n def timeraw_render_sm_subsequent(self, renderer: TemplatingRenderer):\n return prepare_templating_benchmark(renderer, \"sm\", \"subsequent\", \"isolated\")", "min_run_count": 2, "name": "Components vs Django.timeraw_render_sm_subsequent", "number": 1, "param_names": ["renderer"], "params": [["'django'", "'django-components'"]], "pretty_name": "render - small - second render", "repeat": 0, "rounds": 5, "sample_time": 0.01, "type": "time", "unit": "seconds", "version": "6fce1cd85a9344fee383b40a22f27862120b9488a628420625592dc14e0307d3", "warmup_time": -1}, "Components vs Django.timeraw_startup_lg": {"code": "class DjangoComponentsVsDjangoTests:\n @benchmark(\n pretty_name=\"startup - large\",\n group_name=DJC_VS_DJ_GROUP,\n number=1,\n rounds=5,\n params={\n \"renderer\": [\"django\", \"django-components\"],\n },\n )\n def timeraw_startup_lg(self, renderer: TemplatingRenderer):\n return prepare_templating_benchmark(renderer, \"lg\", \"startup\", \"isolated\")", "min_run_count": 2, "name": "Components vs Django.timeraw_startup_lg", "number": 1, "param_names": ["renderer"], "params": [["'django'", "'django-components'"]], "pretty_name": "startup - large", "repeat": 0, "rounds": 5, "sample_time": 0.01, "type": "time", "unit": "seconds", "version": "53151821c128ad0ecfb0707fff3146e1abd8d0bcfa301aa056b5d3fae3d793e2", "warmup_time": -1}, "Other.timeraw_import_time": {"code": "class OtherTests:\n @benchmark(\n pretty_name=\"import time\",\n group_name=OTHER_GROUP,\n number=1,\n rounds=5,\n )\n def timeraw_import_time(self):\n return prepare_templating_benchmark(\"django-components\", \"lg\", \"startup\", \"isolated\", imports_only=True)", "min_run_count": 2, "name": "Other.timeraw_import_time", "number": 1, "param_names": [], "params": [], "pretty_name": "import time", "repeat": 0, "rounds": 5, "sample_time": 0.01, "type": "time", "unit": "seconds", "version": "a0a1c1c0db22509410b946d0d4384b52ea4a09b47b6048d7d1cfb89b0c7fe5c3", "warmup_time": -1}, "isolated vs django modes.peakmem_render_lg_first": {"code": "class IsolatedVsDjangoContextModesTests:\n @benchmark(\n pretty_name=\"render - large - first render (mem)\",\n group_name=DJC_ISOLATED_VS_NON_GROUP,\n number=1,\n rounds=5,\n params={\n \"context_mode\": [\"isolated\", \"django\"],\n },\n setup=lambda context_mode: setup_templating_memory_benchmark(\n \"django-components\",\n \"lg\",\n \"first\",\n context_mode,\n ),\n )\n def peakmem_render_lg_first(self, context_mode: DjcContextMode):\n do_render()\n\nsetup=lambda context_mode: setup_templating_memory_benchmark(\n \"django-components\",\n \"lg\",\n \"first\",\n context_mode,\n),", "name": "isolated vs django modes.peakmem_render_lg_first", "param_names": ["context_mode"], "params": [["'isolated'", "'django'"]], "pretty_name": "render - large - first render (mem)", "type": "peakmemory", "unit": "bytes", "version": "c4bf0016d48d210f08b8db733b57c7dcba1cebbf548c458b93b86ace387067e9"}, "isolated vs django modes.peakmem_render_lg_subsequent": {"code": "class IsolatedVsDjangoContextModesTests:\n @benchmark(\n pretty_name=\"render - large - second render (mem)\",\n group_name=DJC_ISOLATED_VS_NON_GROUP,\n number=1,\n rounds=5,\n params={\n \"context_mode\": [\"isolated\", \"django\"],\n },\n setup=lambda context_mode: setup_templating_memory_benchmark(\n \"django-components\",\n \"lg\",\n \"subsequent\",\n context_mode,\n ),\n )\n def peakmem_render_lg_subsequent(self, context_mode: DjcContextMode):\n do_render()\n\nsetup=lambda context_mode: setup_templating_memory_benchmark(\n \"django-components\",\n \"lg\",\n \"subsequent\",\n context_mode,\n),", "name": "isolated vs django modes.peakmem_render_lg_subsequent", "param_names": ["context_mode"], "params": [["'isolated'", "'django'"]], "pretty_name": "render - large - second render (mem)", "type": "peakmemory", "unit": "bytes", "version": "65bb1b8586487197a79bb6073e4c71642877b845b6eb42d1bd32398299daffbf"}, "isolated vs django modes.peakmem_render_sm_first": {"code": "class IsolatedVsDjangoContextModesTests:\n @benchmark(\n pretty_name=\"render - small - first render (mem)\",\n group_name=DJC_ISOLATED_VS_NON_GROUP,\n number=1,\n rounds=5,\n params={\n \"context_mode\": [\"isolated\", \"django\"],\n },\n setup=lambda context_mode: setup_templating_memory_benchmark(\"django-components\", \"sm\", \"first\", context_mode),\n )\n def peakmem_render_sm_first(self, context_mode: DjcContextMode):\n do_render()\n\nsetup=lambda context_mode: setup_templating_memory_benchmark(\"django-components\", \"sm\", \"first\", context_mode),", "name": "isolated vs django modes.peakmem_render_sm_first", "param_names": ["context_mode"], "params": [["'isolated'", "'django'"]], "pretty_name": "render - small - first render (mem)", "type": "peakmemory", "unit": "bytes", "version": "c51b91fc583295776062822225e720b5ed71aef9c9288217c401c54283c62840"}, "isolated vs django modes.peakmem_render_sm_subsequent": {"code": "class IsolatedVsDjangoContextModesTests:\n @benchmark(\n pretty_name=\"render - small - second render (mem)\",\n group_name=DJC_ISOLATED_VS_NON_GROUP,\n number=1,\n rounds=5,\n params={\n \"context_mode\": [\"isolated\", \"django\"],\n },\n setup=lambda context_mode: setup_templating_memory_benchmark(\n \"django-components\",\n \"sm\",\n \"subsequent\",\n context_mode,\n ),\n )\n def peakmem_render_sm_subsequent(self, context_mode: DjcContextMode):\n do_render()\n\nsetup=lambda context_mode: setup_templating_memory_benchmark(\n \"django-components\",\n \"sm\",\n \"subsequent\",\n context_mode,\n),", "name": "isolated vs django modes.peakmem_render_sm_subsequent", "param_names": ["context_mode"], "params": [["'isolated'", "'django'"]], "pretty_name": "render - small - second render (mem)", "type": "peakmemory", "unit": "bytes", "version": "54d747fb8f40179b7ff3d2fc49eb195909ad1c880b5ef7b82f82742b27b67260"}, "isolated vs django modes.timeraw_render_lg_first": {"code": "class IsolatedVsDjangoContextModesTests:\n @benchmark(\n pretty_name=\"render - large - first render\",\n group_name=DJC_ISOLATED_VS_NON_GROUP,\n number=1,\n rounds=5,\n params={\n \"context_mode\": [\"isolated\", \"django\"],\n },\n )\n def timeraw_render_lg_first(self, context_mode: DjcContextMode):\n return prepare_templating_benchmark(\"django-components\", \"lg\", \"first\", context_mode)", "min_run_count": 2, "name": "isolated vs django modes.timeraw_render_lg_first", "number": 1, "param_names": ["context_mode"], "params": [["'isolated'", "'django'"]], "pretty_name": "render - large - first render", "repeat": 0, "rounds": 5, "sample_time": 0.01, "type": "time", "unit": "seconds", "version": "f94af83427c6346f88f8785a3cd2fc42415ac5a9fbbdb7de71d27e22e6a81699", "warmup_time": -1}, "isolated vs django modes.timeraw_render_lg_subsequent": {"code": "class IsolatedVsDjangoContextModesTests:\n @benchmark(\n pretty_name=\"render - large - second render\",\n group_name=DJC_ISOLATED_VS_NON_GROUP,\n number=1,\n rounds=5,\n params={\n \"context_mode\": [\"isolated\", \"django\"],\n },\n )\n def timeraw_render_lg_subsequent(self, context_mode: DjcContextMode):\n return prepare_templating_benchmark(\"django-components\", \"lg\", \"subsequent\", context_mode)", "min_run_count": 2, "name": "isolated vs django modes.timeraw_render_lg_subsequent", "number": 1, "param_names": ["context_mode"], "params": [["'isolated'", "'django'"]], "pretty_name": "render - large - second render", "repeat": 0, "rounds": 5, "sample_time": 0.01, "type": "time", "unit": "seconds", "version": "9f7c2fde6b33f0451a1794ed903c48d96cd7822f67da502cec36fe8e977c2414", "warmup_time": -1}, "isolated vs django modes.timeraw_render_sm_first": {"code": "class IsolatedVsDjangoContextModesTests:\n @benchmark(\n pretty_name=\"render - small - first render\",\n group_name=DJC_ISOLATED_VS_NON_GROUP,\n number=1,\n rounds=5,\n params={\n \"context_mode\": [\"isolated\", \"django\"],\n },\n )\n def timeraw_render_sm_first(self, context_mode: DjcContextMode):\n return prepare_templating_benchmark(\"django-components\", \"sm\", \"first\", context_mode)", "min_run_count": 2, "name": "isolated vs django modes.timeraw_render_sm_first", "number": 1, "param_names": ["context_mode"], "params": [["'isolated'", "'django'"]], "pretty_name": "render - small - first render", "repeat": 0, "rounds": 5, "sample_time": 0.01, "type": "time", "unit": "seconds", "version": "d15ca68909d7f1f43ff16863befb6f42681f17461417fc0069eefd6db3569296", "warmup_time": -1}, "isolated vs django modes.timeraw_render_sm_subsequent": {"code": "class IsolatedVsDjangoContextModesTests:\n @benchmark(\n pretty_name=\"render - small - second render\",\n group_name=DJC_ISOLATED_VS_NON_GROUP,\n number=1,\n rounds=5,\n params={\n \"context_mode\": [\"isolated\", \"django\"],\n },\n )\n def timeraw_render_sm_subsequent(self, context_mode: DjcContextMode):\n return prepare_templating_benchmark(\"django-components\", \"sm\", \"subsequent\", context_mode)", "min_run_count": 2, "name": "isolated vs django modes.timeraw_render_sm_subsequent", "number": 1, "param_names": ["context_mode"], "params": [["'isolated'", "'django'"]], "pretty_name": "render - small - second render", "repeat": 0, "rounds": 5, "sample_time": 0.01, "type": "time", "unit": "seconds", "version": "7444bc9516dd087e3f420349345eae991ad6941bbd22fce45265b18034b7cf77", "warmup_time": -1}, "isolated vs django modes.timeraw_startup_lg": {"code": "class IsolatedVsDjangoContextModesTests:\n @benchmark(\n pretty_name=\"startup - large\",\n group_name=DJC_ISOLATED_VS_NON_GROUP,\n number=1,\n rounds=5,\n params={\n \"context_mode\": [\"isolated\", \"django\"],\n },\n )\n def timeraw_startup_lg(self, context_mode: DjcContextMode):\n return prepare_templating_benchmark(\"django-components\", \"lg\", \"startup\", context_mode)", "min_run_count": 2, "name": "isolated vs django modes.timeraw_startup_lg", "number": 1, "param_names": ["context_mode"], "params": [["'isolated'", "'django'"]], "pretty_name": "startup - large", "repeat": 0, "rounds": 5, "sample_time": 0.01, "type": "time", "unit": "seconds", "version": "eabe311ebee4a15c5816617be12f00ec30376f7506bd668219e1c50bc897c134", "warmup_time": -1}}, "machines": {"ci-linux": {"machine": "ci-linux", "version": 1}}, "tags": {"0.100": 1125, "0.101": 1132, "0.102": 1179, "0.110": 1252, "0.111": 1270, "0.112": 1277, "0.113": 1282, "0.114": 1287, "0.115": 1312, "0.116": 1328, "0.117": 1337, "0.118": 1352, "0.119": 1361, "0.120": 1366, "0.121": 1381, "0.122": 1387, "0.123": 1392, "0.124": 1447, "0.125": 1488, "0.126": 1505, "0.127": 1514, "0.128": 1546, "0.129": 1571, "0.130": 1591, "0.131": 1662, "0.132": 1672, "0.133": 1687, "0.134": 1691, "0.16": 250, "0.17": 268, "0.26.2": 508, "0.27": 550, "0.27.1": 552, "0.27.2": 564, "0.28.0": 575, "0.28.1": 577, "0.67": 845, "0.68": 855, "0.70": 867, "0.71": 884, "0.72": 887, "0.73": 889, "0.74": 891, "0.75": 898, "0.76": 902, "0.77": 904, "0.78": 908, "0.79": 910, "0.80": 912, "0.81": 930, "0.82": 935, "0.83": 940, "0.84": 942, "0.85": 956, "0.86": 958, "0.87": 960, "0.88": 965, "0.89": 978, "0.90": 980, "0.91": 982, "0.92": 986, "0.93": 1056, "0.94": 1058, "0.95": 1060, "0.96": 1089, "0.97": 1098, "0.18": 343, "0.22": 388, "0.21": 380, "0.20": 369, "0.19": 359, "0.24": 457, "0.23": 427, "0.25": 487, "0.26": 500, "0.26.1": 503, "0.26.3": 536, "0.28.2": 598, "0.28.3": 612, "0.29": 622, "0.30": 646, "0.31": 651, "0.32": 663, "0.33": 675, "0.34": 681, "0.34.1": 691, "0.35": 704, "0.37": 724, "0.50": 729, "0.51": 742, "0.52": 748, "0.60": 752, "0.61": 757, "0.62": 773, "0.63": 775, "0.64": 779, "0.65": 787, "0.66": 806}, "pages": [["", "Grid view", "Display as a agrid"], ["summarylist", "List view", "Display as a list"], ["regressions", "Show regressions", "Display information about recent regressions"]]}
\ No newline at end of file
+{"project": "django-components", "project_url": "/django-components/", "show_commit_url": "#", "hash_length": 8, "revision_to_hash": {"250": "45a18626d74b280aa05498aa380eba687010cc0f", "268": "c9f0068741871b24756045805aaab8fbf368601a", "343": "ed2cd57bdcc12bf390f42058061a6ed077e558b0", "359": "501a055fe21abb4c7ad9af1a3a1b42eea24afacf", "369": "1937d594100d0651b151c8edbe1189b5a88b358f", "380": "1d091f50999cca0fa8c7852da2ea71684eadda6d", "388": "f5c3f64c866d06dfa2435d1d18b936dddb9eb67c", "427": "24d9db39c3c55f5eb91c9ce2112ad22507ccdd60", "457": "9103eda2f4ed80f46ff03d215491009c5476b410", "487": "384ff79e0875ac3a64a2755010ef86ec09a090b5", "500": "c102019811586f40cb11952f283797ddee96de3c", "503": "a350ad1a869bb46c6bac093f54ac40ea85d8cd8f", "508": "16022429da16e459b8a1e6e9cd8ab41d95dcb59d", "536": "ea7beb518c435389d9daff7ea557eec9ddc2464e", "550": "2cfc7285e12677a54247e9998967081c0e031efc", "552": "fcbfae3c5f0611519636290ef99b25bc0a319825", "564": "316310e842f276fd43436b432781430bd255410b", "575": "e0a5c2a4bcf5e8f577e19333e3b719440c37b10b", "577": "c174aa9802cc73b5d8fef4b0c704de3fb68b1cbb", "598": "979781012532f18c147a570ff4441c65c7503b6f", "612": "9d7d0b40b9d3bbd8e5b754e3b1b20df982cdd184", "622": "24032ac2ea80627f0fdde88058a2c3fbcae0d3fe", "646": "4f1a8184465fc472995bc226bebd66c250d501f6", "651": "8ce649498ff63a64761c83cf713983b7e2f24b81", "663": "93facba53e050619a477fa214da89fccf118ec13", "675": "ea33e0db6c54486b67e2fa496ec1c3ec3de06ec7", "681": "6874b1531df0e8836000bbc76fcca5a3de42724a", "691": "9f4243232018708fdf330f0e922a09317c60234c", "704": "188a9dc7eef4c45ee4f87efa8a0d6728e94f210d", "724": "76a0cde3af7b3061127d149c138fbc78462a07fe", "729": "c932f28cb4840dc45d2d26b4d927c1480459272f", "742": "7058f05e0415965ddd162d0ba663c5106e7ffb66", "748": "95d6eacb5c03a611f92cd9a6e254cea7e0ce57eb", "752": "d512cfb0fec8a42ab58d2f106047fc4c0ec65427", "757": "04160f4e0b2ec4d901d6724c34ed8df883363064", "773": "20847b978b3ddb5979489fbb4bd8e971b9b16fbf", "775": "ae5cda9f727454a077ab16c51a237fec076bd442", "779": "cd791caa572c9acd6d56445c04d6c9b55a50c602", "787": "274be104789e31955f88e26f14c89ed648f25448", "806": "c3a80b729049b5ea1b4799e742e4100a7640efab", "845": "0abb5aa63ff0396328ef4dfdc2e78e872671f135", "855": "e4e787b29dc9cd2cf35ad02f22bad6fa15a7211c", "867": "e346c07298b268d9212fcf93d132968e69824730", "884": "2316f79dff66260b7a9fe91effb55c74524deca0", "887": "085c60a8c96442d9fd4f72d66a7eb295e7cfb2d9", "889": "ba86cee578490df944ad167674db32182b8bbf7b", "891": "dd292b03508b7c03b2ad4f9fb9c17dec747845ba", "898": "badffdda3538a8082137453c757509ed4f035a3e", "902": "c07f0e634121ced0119bb665ed544ca4447c416b", "904": "8bbe81d7171ec3512f250927653d8454c7ff005e", "908": "881c36219a45581086da35a17d3715ee0698ca88", "910": "9bfb50b8f27f7ff521226109ab30426633f6b514", "912": "3a7d5355cfa5d37641a4c80ee9fd2423cea10c1b", "930": "09a720009767f45bebcfa48f3587219c03082601", "935": "a4b4905bee778a51f9a99a042ce14ed2424cc9fb", "940": "31257a475dcfdacaaeb408f59ea92b736a668815", "942": "d47927054c820ecf3f9f97a62f03dfbb5496a11b", "956": "fbbbf6c694b8213832fc127ee8a3e31872308aff", "958": "d819f3ff491866abaeb9a7dbba6a6ca3c21da9f8", "960": "c202c5a901106392ccdde49100211182c986eca5", "965": "7bbfcf7565159d92de41bb0d738b7878d78f4533", "978": "b89c09aa5f13903b1d056c70afbfd327f0ed6362", "980": "03af25aad6841b6edb56c925d2599817e08ceb44", "982": "5cb649fae62a42e525c1ea9fb74d7838886cc1a8", "986": "f97717cdb35eaadb78b1f468728b1bd386e742d8", "1056": "b26a2011380c959bfc98043821f6b4aa337a281d", "1058": "8c5b088c31b7166932e18739f882c2eef632f3a4", "1060": "682bfc42397c0bdbeb1a2b6ccabb8aca89686d4f", "1089": "8f13a641ac096e93a0464f048a4fa53e591bb8db", "1098": "30d04fe1b053e6c5de0b6f34a7758a627517be8c", "1125": "f7846b9c0ae7a8fd0b7f6edf664316e485455e76", "1132": "a3d66586b19b7e2aacbf793edfba20bcd9858f4a", "1179": "0064de9c78db5eccc7c498e2a6d4c3c5ffa745ec", "1252": "d093bfb05212555e14c5cd46c94b0ba057cbeceb", "1270": "c0a4fd5f685d694218b583b2d0a3e8417925d53a", "1277": "2a4b0f52894d5d0bb8d325d6096a3950286db541", "1282": "5a23101038d7085959087b2352e71590eafe9529", "1287": "ce3305a5fff095e990707de851f85bc27120596c", "1312": "cdc830fca3bdd98669e0510841536be14d1bd728", "1328": "468a593a472c35379fe7567918e8a534b2d53748", "1337": "2f14e8e0908657459909c6338106e561edc5d0f4", "1352": "a5659691d0f947643ce6542b7f09075e3f931646", "1361": "aaeba99f54b206baed072ffd0572035ddf5118a7", "1366": "6813c9d7aa4acfb04df9665651a2864d04bbe8ba", "1381": "6681fc0085fdb4b76cb74c1bbf20d44fa22f40fe", "1387": "6bb73bd8afd350db84889efc09b74255cea08979", "1392": "c76f8198dd497b34735fc17a0f7a678bdd811a3f", "1447": "3bbd4326e6c1bfb4a02039e16021a3f720910308", "1488": "914576e68133bc6ec356f3918016c45691ed3534", "1505": "3d187d7abad80f8464bd70dfba847d27a8e173db", "1514": "e105500350a320dfccf36fc19cad7421d38c3736", "1546": "61515b34540d27311fc3286b50212c1e314ce39e", "1571": "691443a0a821eb1a02f9b42ad0e32bcd71d94943", "1591": "dcd4203eeadafc5d500b4b72c5cf04f1fe7317e7", "1662": "d0a42a2698f2ba21e7ab2dec750c5dbadeda0db5", "1672": "2037ed20b7252cc52008e69bdd3d8e095ad6ea08", "1687": "2472c2ad338a23fba015d4d9816cb62d1325455f", "1691": "42818ad6ffb47bd650d8a379b84c3d48394f9f77", "1709": "a6455d70f6c28ddbd4be8e58902f6cbc101e5ff3", "1726": "fdd29baa65e9ef78eb24a0ad2ca0b5d7c624dad3", "1766": "1319a95627493fc0745b5af0600af2dc8c5117f9", "1770": "07f747d70500bbe3e135725b0e1b102815ab9416", "1776": "ad402fc619922b6d2edf1e99b7082d2a58632076", "1801": "4c909486069f3c3c8ee7915239174f820f081da4", "1933": "593c66db7fce8f7d0e45768f516d1920e53d0967", "1937": "7b24b86f4a836c697acba926d9d6602afa45418d", "1960": "06c89cf9e89a432197a4cabb5a1d6864dd6089ac", "1996": "c692b7a3105c65414d2c23c357ffed9debdbf6e9", "2029": "5d7e235725449181f6b65b7bf97e43ea0e0f8552"}, "revision_to_date": {"250": 1630912817000, "268": 1631273531000, "343": 1654029104000, "359": 1657786741000, "369": 1657798498000, "380": 1658057264000, "388": 1658128984000, "427": 1670931380000, "457": 1673095465000, "487": 1675977478000, "500": 1678833725000, "503": 1678834812000, "508": 1679329468000, "536": 1680808865000, "550": 1681214445000, "552": 1681388770000, "564": 1682351069000, "575": 1684433482000, "577": 1684760359000, "598": 1693734426000, "612": 1695996512000, "622": 1696883504000, "646": 1702127213000, "651": 1702852887000, "663": 1705268832000, "675": 1705701480000, "681": 1706343740000, "691": 1706393538000, "704": 1707467055000, "724": 1708588716000, "729": 1708984309000, "742": 1709796107000, "748": 1710542375000, "752": 1711216985000, "757": 1711278671000, "773": 1711752109000, "775": 1711786713000, "779": 1712212506000, "787": 1712872453000, "806": 1713128940000, "845": 1713390260000, "855": 1713901297000, "867": 1714590581000, "884": 1714912319000, "887": 1715087715000, "889": 1715111097000, "891": 1715505986000, "898": 1716105516000, "902": 1716441018000, "904": 1716488913000, "908": 1717051130000, "910": 1717227227000, "912": 1717232115000, "930": 1718176374000, "935": 1718991590000, "940": 1720427258000, "942": 1720429478000, "956": 1722279535000, "958": 1722325182000, "960": 1722666822000, "965": 1722890429000, "978": 1723843975000, "980": 1723993321000, "982": 1724243667000, "986": 1724363119000, "1056": 1724732902000, "1058": 1724824521000, "1060": 1724924003000, "1089": 1725480929000, "1098": 1725655525000, "1125": 1726084755000, "1132": 1726347108000, "1179": 1728564427000, "1252": 1732525062000, "1270": 1732629701000, "1277": 1732644681000, "1282": 1732658332000, "1287": 1732726640000, "1312": 1733164405000, "1328": 1733471329000, "1337": 1733644953000, "1352": 1733834654000, "1361": 1734080521000, "1366": 1734245122000, "1381": 1734464187000, "1387": 1734600366000, "1392": 1734955786000, "1447": 1736287315000, "1488": 1737558545000, "1505": 1738157680000, "1514": 1738404847000, "1546": 1738662755000, "1571": 1739736734000, "1591": 1740050674000, "1662": 1742502414000, "1672": 1742645505000, "1687": 1742720064000, "1691": 1742765538000, "1709": 1743430242000, "1726": 1743837055000, "1766": 1744204166000, "1770": 1744216267000, "1776": 1744443333000, "1801": 1745143092000, "1933": 1749073294000, "1937": 1749076521000, "1960": 1749546769000, "1996": 1751538441000, "2029": 1753049108000}, "params": {"machine": ["ci-linux"], "python": ["3.13"], "django": ["5.1"], "djc-core-html-parser": [""], "branch": ["master"]}, "graph_param_list": [{"machine": "ci-linux", "python": "3.13", "django": "5.1", "djc-core-html-parser": "", "branch": "master"}], "benchmarks": {"Components vs Django.peakmem_render_lg_first": {"code": "class DjangoComponentsVsDjangoTests:\n @benchmark(\n pretty_name=\"render - large - first render (mem)\",\n group_name=DJC_VS_DJ_GROUP,\n number=1,\n rounds=5,\n params={\n \"renderer\": [\"django\", \"django-components\"],\n },\n setup=lambda renderer: setup_templating_memory_benchmark(renderer, \"lg\", \"first\", \"isolated\"),\n )\n def peakmem_render_lg_first(self, renderer: TemplatingRenderer):\n do_render()\n\nsetup=lambda renderer: setup_templating_memory_benchmark(renderer, \"lg\", \"first\", \"isolated\"),", "name": "Components vs Django.peakmem_render_lg_first", "param_names": ["renderer"], "params": [["'django'", "'django-components'"]], "pretty_name": "render - large - first render (mem)", "type": "peakmemory", "unit": "bytes", "version": "301c396f017f45a5b3f71e85df58d15f54153fcfd951af7ef424641d4b31b528"}, "Components vs Django.peakmem_render_lg_subsequent": {"code": "class DjangoComponentsVsDjangoTests:\n @benchmark(\n pretty_name=\"render - large - second render (mem)\",\n group_name=DJC_VS_DJ_GROUP,\n number=1,\n rounds=5,\n params={\n \"renderer\": [\"django\", \"django-components\"],\n },\n setup=lambda renderer: setup_templating_memory_benchmark(renderer, \"lg\", \"subsequent\", \"isolated\"),\n )\n def peakmem_render_lg_subsequent(self, renderer: TemplatingRenderer):\n do_render()\n\nsetup=lambda renderer: setup_templating_memory_benchmark(renderer, \"lg\", \"subsequent\", \"isolated\"),", "name": "Components vs Django.peakmem_render_lg_subsequent", "param_names": ["renderer"], "params": [["'django'", "'django-components'"]], "pretty_name": "render - large - second render (mem)", "type": "peakmemory", "unit": "bytes", "version": "9a44e9999ef3ef42ea7e01323727490244febb43d66a87a4d8f88c6b8a133b8b"}, "Components vs Django.peakmem_render_sm_first": {"code": "class DjangoComponentsVsDjangoTests:\n @benchmark(\n pretty_name=\"render - small - first render (mem)\",\n group_name=DJC_VS_DJ_GROUP,\n number=1,\n rounds=5,\n params={\n \"renderer\": [\"django\", \"django-components\"],\n },\n setup=lambda renderer: setup_templating_memory_benchmark(renderer, \"sm\", \"first\", \"isolated\"),\n )\n def peakmem_render_sm_first(self, renderer: TemplatingRenderer):\n do_render()\n\nsetup=lambda renderer: setup_templating_memory_benchmark(renderer, \"sm\", \"first\", \"isolated\"),", "name": "Components vs Django.peakmem_render_sm_first", "param_names": ["renderer"], "params": [["'django'", "'django-components'"]], "pretty_name": "render - small - first render (mem)", "type": "peakmemory", "unit": "bytes", "version": "e93b7a5193681c883edf85bdb30b1bc0821263bf51033fdcee215b155085e036"}, "Components vs Django.peakmem_render_sm_subsequent": {"code": "class DjangoComponentsVsDjangoTests:\n @benchmark(\n pretty_name=\"render - small - second render (mem)\",\n group_name=DJC_VS_DJ_GROUP,\n number=1,\n rounds=5,\n params={\n \"renderer\": [\"django\", \"django-components\"],\n },\n setup=lambda renderer: setup_templating_memory_benchmark(renderer, \"sm\", \"subsequent\", \"isolated\"),\n )\n def peakmem_render_sm_subsequent(self, renderer: TemplatingRenderer):\n do_render()\n\nsetup=lambda renderer: setup_templating_memory_benchmark(renderer, \"sm\", \"subsequent\", \"isolated\"),", "name": "Components vs Django.peakmem_render_sm_subsequent", "param_names": ["renderer"], "params": [["'django'", "'django-components'"]], "pretty_name": "render - small - second render (mem)", "type": "peakmemory", "unit": "bytes", "version": "b46e0820b18950aa7cc5e61306ff3425b76b4da9dca42d64fae5b1d25c6c9026"}, "Components vs Django.timeraw_render_lg_first": {"code": "class DjangoComponentsVsDjangoTests:\n @benchmark(\n pretty_name=\"render - large - first render\",\n group_name=DJC_VS_DJ_GROUP,\n number=1,\n rounds=5,\n params={\n \"renderer\": [\"django\", \"django-components\"],\n },\n include_in_quick_benchmark=True,\n )\n def timeraw_render_lg_first(self, renderer: TemplatingRenderer):\n return prepare_templating_benchmark(renderer, \"lg\", \"first\", \"isolated\")", "min_run_count": 2, "name": "Components vs Django.timeraw_render_lg_first", "number": 1, "param_names": ["renderer"], "params": [["'django'", "'django-components'"]], "pretty_name": "render - large - first render", "repeat": 0, "rounds": 5, "sample_time": 0.01, "type": "time", "unit": "seconds", "version": "be3bf6236960046a028b6ea007aad28b2337fc2b906b8ce317a09a5d4f1a6193", "warmup_time": -1}, "Components vs Django.timeraw_render_lg_subsequent": {"code": "class DjangoComponentsVsDjangoTests:\n @benchmark(\n pretty_name=\"render - large - second render\",\n group_name=DJC_VS_DJ_GROUP,\n number=1,\n rounds=5,\n params={\n \"renderer\": [\"django\", \"django-components\"],\n },\n )\n def timeraw_render_lg_subsequent(self, renderer: TemplatingRenderer):\n return prepare_templating_benchmark(renderer, \"lg\", \"subsequent\", \"isolated\")", "min_run_count": 2, "name": "Components vs Django.timeraw_render_lg_subsequent", "number": 1, "param_names": ["renderer"], "params": [["'django'", "'django-components'"]], "pretty_name": "render - large - second render", "repeat": 0, "rounds": 5, "sample_time": 0.01, "type": "time", "unit": "seconds", "version": "b98221c11a0ee6e9de0778d416d31b9dd514a674d9017a2bb9b2fc1cd0f01920", "warmup_time": -1}, "Components vs Django.timeraw_render_sm_first": {"code": "class DjangoComponentsVsDjangoTests:\n @benchmark(\n pretty_name=\"render - small - first render\",\n group_name=DJC_VS_DJ_GROUP,\n number=1,\n rounds=5,\n params={\n \"renderer\": [\"django\", \"django-components\"],\n },\n )\n def timeraw_render_sm_first(self, renderer: TemplatingRenderer):\n return prepare_templating_benchmark(renderer, \"sm\", \"first\", \"isolated\")", "min_run_count": 2, "name": "Components vs Django.timeraw_render_sm_first", "number": 1, "param_names": ["renderer"], "params": [["'django'", "'django-components'"]], "pretty_name": "render - small - first render", "repeat": 0, "rounds": 5, "sample_time": 0.01, "type": "time", "unit": "seconds", "version": "f1fc17e4a31c71f4d9265f1122da52e7cf57addb4dfa02606e303b33d6431b9b", "warmup_time": -1}, "Components vs Django.timeraw_render_sm_subsequent": {"code": "class DjangoComponentsVsDjangoTests:\n @benchmark(\n pretty_name=\"render - small - second render\",\n group_name=DJC_VS_DJ_GROUP,\n number=1,\n rounds=5,\n params={\n \"renderer\": [\"django\", \"django-components\"],\n },\n )\n def timeraw_render_sm_subsequent(self, renderer: TemplatingRenderer):\n return prepare_templating_benchmark(renderer, \"sm\", \"subsequent\", \"isolated\")", "min_run_count": 2, "name": "Components vs Django.timeraw_render_sm_subsequent", "number": 1, "param_names": ["renderer"], "params": [["'django'", "'django-components'"]], "pretty_name": "render - small - second render", "repeat": 0, "rounds": 5, "sample_time": 0.01, "type": "time", "unit": "seconds", "version": "6fce1cd85a9344fee383b40a22f27862120b9488a628420625592dc14e0307d3", "warmup_time": -1}, "Components vs Django.timeraw_startup_lg": {"code": "class DjangoComponentsVsDjangoTests:\n @benchmark(\n pretty_name=\"startup - large\",\n group_name=DJC_VS_DJ_GROUP,\n number=1,\n rounds=5,\n params={\n \"renderer\": [\"django\", \"django-components\"],\n },\n )\n def timeraw_startup_lg(self, renderer: TemplatingRenderer):\n return prepare_templating_benchmark(renderer, \"lg\", \"startup\", \"isolated\")", "min_run_count": 2, "name": "Components vs Django.timeraw_startup_lg", "number": 1, "param_names": ["renderer"], "params": [["'django'", "'django-components'"]], "pretty_name": "startup - large", "repeat": 0, "rounds": 5, "sample_time": 0.01, "type": "time", "unit": "seconds", "version": "53151821c128ad0ecfb0707fff3146e1abd8d0bcfa301aa056b5d3fae3d793e2", "warmup_time": -1}, "Other.timeraw_import_time": {"code": "class OtherTests:\n @benchmark(\n pretty_name=\"import time\",\n group_name=OTHER_GROUP,\n number=1,\n rounds=5,\n )\n def timeraw_import_time(self):\n return prepare_templating_benchmark(\"django-components\", \"lg\", \"startup\", \"isolated\", imports_only=True)", "min_run_count": 2, "name": "Other.timeraw_import_time", "number": 1, "param_names": [], "params": [], "pretty_name": "import time", "repeat": 0, "rounds": 5, "sample_time": 0.01, "type": "time", "unit": "seconds", "version": "a0a1c1c0db22509410b946d0d4384b52ea4a09b47b6048d7d1cfb89b0c7fe5c3", "warmup_time": -1}, "isolated vs django modes.peakmem_render_lg_first": {"code": "class IsolatedVsDjangoContextModesTests:\n @benchmark(\n pretty_name=\"render - large - first render (mem)\",\n group_name=DJC_ISOLATED_VS_NON_GROUP,\n number=1,\n rounds=5,\n params={\n \"context_mode\": [\"isolated\", \"django\"],\n },\n setup=lambda context_mode: setup_templating_memory_benchmark(\n \"django-components\",\n \"lg\",\n \"first\",\n context_mode,\n ),\n )\n def peakmem_render_lg_first(self, context_mode: DjcContextMode):\n do_render()\n\nsetup=lambda context_mode: setup_templating_memory_benchmark(\n \"django-components\",\n \"lg\",\n \"first\",\n context_mode,\n),", "name": "isolated vs django modes.peakmem_render_lg_first", "param_names": ["context_mode"], "params": [["'isolated'", "'django'"]], "pretty_name": "render - large - first render (mem)", "type": "peakmemory", "unit": "bytes", "version": "c4bf0016d48d210f08b8db733b57c7dcba1cebbf548c458b93b86ace387067e9"}, "isolated vs django modes.peakmem_render_lg_subsequent": {"code": "class IsolatedVsDjangoContextModesTests:\n @benchmark(\n pretty_name=\"render - large - second render (mem)\",\n group_name=DJC_ISOLATED_VS_NON_GROUP,\n number=1,\n rounds=5,\n params={\n \"context_mode\": [\"isolated\", \"django\"],\n },\n setup=lambda context_mode: setup_templating_memory_benchmark(\n \"django-components\",\n \"lg\",\n \"subsequent\",\n context_mode,\n ),\n )\n def peakmem_render_lg_subsequent(self, context_mode: DjcContextMode):\n do_render()\n\nsetup=lambda context_mode: setup_templating_memory_benchmark(\n \"django-components\",\n \"lg\",\n \"subsequent\",\n context_mode,\n),", "name": "isolated vs django modes.peakmem_render_lg_subsequent", "param_names": ["context_mode"], "params": [["'isolated'", "'django'"]], "pretty_name": "render - large - second render (mem)", "type": "peakmemory", "unit": "bytes", "version": "65bb1b8586487197a79bb6073e4c71642877b845b6eb42d1bd32398299daffbf"}, "isolated vs django modes.peakmem_render_sm_first": {"code": "class IsolatedVsDjangoContextModesTests:\n @benchmark(\n pretty_name=\"render - small - first render (mem)\",\n group_name=DJC_ISOLATED_VS_NON_GROUP,\n number=1,\n rounds=5,\n params={\n \"context_mode\": [\"isolated\", \"django\"],\n },\n setup=lambda context_mode: setup_templating_memory_benchmark(\"django-components\", \"sm\", \"first\", context_mode),\n )\n def peakmem_render_sm_first(self, context_mode: DjcContextMode):\n do_render()\n\nsetup=lambda context_mode: setup_templating_memory_benchmark(\"django-components\", \"sm\", \"first\", context_mode),", "name": "isolated vs django modes.peakmem_render_sm_first", "param_names": ["context_mode"], "params": [["'isolated'", "'django'"]], "pretty_name": "render - small - first render (mem)", "type": "peakmemory", "unit": "bytes", "version": "c51b91fc583295776062822225e720b5ed71aef9c9288217c401c54283c62840"}, "isolated vs django modes.peakmem_render_sm_subsequent": {"code": "class IsolatedVsDjangoContextModesTests:\n @benchmark(\n pretty_name=\"render - small - second render (mem)\",\n group_name=DJC_ISOLATED_VS_NON_GROUP,\n number=1,\n rounds=5,\n params={\n \"context_mode\": [\"isolated\", \"django\"],\n },\n setup=lambda context_mode: setup_templating_memory_benchmark(\n \"django-components\",\n \"sm\",\n \"subsequent\",\n context_mode,\n ),\n )\n def peakmem_render_sm_subsequent(self, context_mode: DjcContextMode):\n do_render()\n\nsetup=lambda context_mode: setup_templating_memory_benchmark(\n \"django-components\",\n \"sm\",\n \"subsequent\",\n context_mode,\n),", "name": "isolated vs django modes.peakmem_render_sm_subsequent", "param_names": ["context_mode"], "params": [["'isolated'", "'django'"]], "pretty_name": "render - small - second render (mem)", "type": "peakmemory", "unit": "bytes", "version": "54d747fb8f40179b7ff3d2fc49eb195909ad1c880b5ef7b82f82742b27b67260"}, "isolated vs django modes.timeraw_render_lg_first": {"code": "class IsolatedVsDjangoContextModesTests:\n @benchmark(\n pretty_name=\"render - large - first render\",\n group_name=DJC_ISOLATED_VS_NON_GROUP,\n number=1,\n rounds=5,\n params={\n \"context_mode\": [\"isolated\", \"django\"],\n },\n )\n def timeraw_render_lg_first(self, context_mode: DjcContextMode):\n return prepare_templating_benchmark(\"django-components\", \"lg\", \"first\", context_mode)", "min_run_count": 2, "name": "isolated vs django modes.timeraw_render_lg_first", "number": 1, "param_names": ["context_mode"], "params": [["'isolated'", "'django'"]], "pretty_name": "render - large - first render", "repeat": 0, "rounds": 5, "sample_time": 0.01, "type": "time", "unit": "seconds", "version": "f94af83427c6346f88f8785a3cd2fc42415ac5a9fbbdb7de71d27e22e6a81699", "warmup_time": -1}, "isolated vs django modes.timeraw_render_lg_subsequent": {"code": "class IsolatedVsDjangoContextModesTests:\n @benchmark(\n pretty_name=\"render - large - second render\",\n group_name=DJC_ISOLATED_VS_NON_GROUP,\n number=1,\n rounds=5,\n params={\n \"context_mode\": [\"isolated\", \"django\"],\n },\n )\n def timeraw_render_lg_subsequent(self, context_mode: DjcContextMode):\n return prepare_templating_benchmark(\"django-components\", \"lg\", \"subsequent\", context_mode)", "min_run_count": 2, "name": "isolated vs django modes.timeraw_render_lg_subsequent", "number": 1, "param_names": ["context_mode"], "params": [["'isolated'", "'django'"]], "pretty_name": "render - large - second render", "repeat": 0, "rounds": 5, "sample_time": 0.01, "type": "time", "unit": "seconds", "version": "9f7c2fde6b33f0451a1794ed903c48d96cd7822f67da502cec36fe8e977c2414", "warmup_time": -1}, "isolated vs django modes.timeraw_render_sm_first": {"code": "class IsolatedVsDjangoContextModesTests:\n @benchmark(\n pretty_name=\"render - small - first render\",\n group_name=DJC_ISOLATED_VS_NON_GROUP,\n number=1,\n rounds=5,\n params={\n \"context_mode\": [\"isolated\", \"django\"],\n },\n )\n def timeraw_render_sm_first(self, context_mode: DjcContextMode):\n return prepare_templating_benchmark(\"django-components\", \"sm\", \"first\", context_mode)", "min_run_count": 2, "name": "isolated vs django modes.timeraw_render_sm_first", "number": 1, "param_names": ["context_mode"], "params": [["'isolated'", "'django'"]], "pretty_name": "render - small - first render", "repeat": 0, "rounds": 5, "sample_time": 0.01, "type": "time", "unit": "seconds", "version": "d15ca68909d7f1f43ff16863befb6f42681f17461417fc0069eefd6db3569296", "warmup_time": -1}, "isolated vs django modes.timeraw_render_sm_subsequent": {"code": "class IsolatedVsDjangoContextModesTests:\n @benchmark(\n pretty_name=\"render - small - second render\",\n group_name=DJC_ISOLATED_VS_NON_GROUP,\n number=1,\n rounds=5,\n params={\n \"context_mode\": [\"isolated\", \"django\"],\n },\n )\n def timeraw_render_sm_subsequent(self, context_mode: DjcContextMode):\n return prepare_templating_benchmark(\"django-components\", \"sm\", \"subsequent\", context_mode)", "min_run_count": 2, "name": "isolated vs django modes.timeraw_render_sm_subsequent", "number": 1, "param_names": ["context_mode"], "params": [["'isolated'", "'django'"]], "pretty_name": "render - small - second render", "repeat": 0, "rounds": 5, "sample_time": 0.01, "type": "time", "unit": "seconds", "version": "7444bc9516dd087e3f420349345eae991ad6941bbd22fce45265b18034b7cf77", "warmup_time": -1}, "isolated vs django modes.timeraw_startup_lg": {"code": "class IsolatedVsDjangoContextModesTests:\n @benchmark(\n pretty_name=\"startup - large\",\n group_name=DJC_ISOLATED_VS_NON_GROUP,\n number=1,\n rounds=5,\n params={\n \"context_mode\": [\"isolated\", \"django\"],\n },\n )\n def timeraw_startup_lg(self, context_mode: DjcContextMode):\n return prepare_templating_benchmark(\"django-components\", \"lg\", \"startup\", context_mode)", "min_run_count": 2, "name": "isolated vs django modes.timeraw_startup_lg", "number": 1, "param_names": ["context_mode"], "params": [["'isolated'", "'django'"]], "pretty_name": "startup - large", "repeat": 0, "rounds": 5, "sample_time": 0.01, "type": "time", "unit": "seconds", "version": "eabe311ebee4a15c5816617be12f00ec30376f7506bd668219e1c50bc897c134", "warmup_time": -1}}, "machines": {"ci-linux": {"machine": "ci-linux", "version": 1}}, "tags": {"0.100": 1125, "0.101": 1132, "0.102": 1179, "0.110": 1252, "0.111": 1270, "0.112": 1277, "0.113": 1282, "0.114": 1287, "0.115": 1312, "0.116": 1328, "0.117": 1337, "0.118": 1352, "0.119": 1361, "0.120": 1366, "0.121": 1381, "0.122": 1387, "0.123": 1392, "0.124": 1447, "0.125": 1488, "0.126": 1505, "0.127": 1514, "0.128": 1546, "0.129": 1571, "0.130": 1591, "0.131": 1662, "0.132": 1672, "0.133": 1687, "0.134": 1691, "0.135": 1709, "0.136": 1726, "0.137": 1766, "0.138": 1770, "0.139": 1776, "0.139.1": 1801, "0.140.0": 1933, "0.140.1": 1937, "0.141.0": 1960, "0.141.1": 1996, "0.141.2": 2029, "0.16": 250, "0.17": 268, "0.26.2": 508, "0.27": 550, "0.27.1": 552, "0.27.2": 564, "0.28.0": 575, "0.28.1": 577, "0.67": 845, "0.68": 855, "0.70": 867, "0.71": 884, "0.72": 887, "0.73": 889, "0.74": 891, "0.75": 898, "0.76": 902, "0.77": 904, "0.78": 908, "0.79": 910, "0.80": 912, "0.81": 930, "0.82": 935, "0.83": 940, "0.84": 942, "0.85": 956, "0.86": 958, "0.87": 960, "0.88": 965, "0.89": 978, "0.90": 980, "0.91": 982, "0.92": 986, "0.93": 1056, "0.94": 1058, "0.95": 1060, "0.96": 1089, "0.97": 1098, "0.18": 343, "0.22": 388, "0.21": 380, "0.20": 369, "0.19": 359, "0.24": 457, "0.23": 427, "0.25": 487, "0.26": 500, "0.26.1": 503, "0.26.3": 536, "0.28.2": 598, "0.28.3": 612, "0.29": 622, "0.30": 646, "0.31": 651, "0.32": 663, "0.33": 675, "0.34": 681, "0.34.1": 691, "0.35": 704, "0.37": 724, "0.50": 729, "0.51": 742, "0.52": 748, "0.60": 752, "0.61": 757, "0.62": 773, "0.63": 775, "0.64": 779, "0.65": 787, "0.66": 806}, "pages": [["", "Grid view", "Display as a agrid"], ["summarylist", "List view", "Display as a list"], ["regressions", "Show regressions", "Display information about recent regressions"]]}
\ No newline at end of file
diff --git a/docs/benchmarks/info.json b/docs/benchmarks/info.json
index af848f82..032015bd 100644
--- a/docs/benchmarks/info.json
+++ b/docs/benchmarks/info.json
@@ -1,4 +1,4 @@
{
"asv-version": "0.6.4",
- "timestamp": 1742766051964
+ "timestamp": 1753049912703
}
\ No newline at end of file
diff --git a/docs/benchmarks/regressions.json b/docs/benchmarks/regressions.json
index 37090c9e..0203f18b 100644
--- a/docs/benchmarks/regressions.json
+++ b/docs/benchmarks/regressions.json
@@ -1 +1 @@
-{"regressions": []}
\ No newline at end of file
+{"regressions": [["Components vs Django.timeraw_render_lg_subsequent('django')", "graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.timeraw_render_lg_subsequent.json", {}, 0, 0.04362213900003553, 0.03327357099999517, [[1691, 1709, 0.03327357099999517, 0.03723830100000214], [1801, 1937, 0.03723830100000214, 0.04362213900003553]]]]}
\ No newline at end of file
diff --git a/docs/benchmarks/regressions.xml b/docs/benchmarks/regressions.xml
index 13c43fa1..3e718e7e 100644
--- a/docs/benchmarks/regressions.xml
+++ b/docs/benchmarks/regressions.xml
@@ -1,2 +1,8 @@
-tag:django-components.asv,1970-01-01:/cddbdcca8b398afd301fbfc73cc4d51103d4e3059c0e6b938d4c467ad3d1aa25Airspeed Velocitydjango-components performance regressions2025-03-23T21:40:51Z
\ No newline at end of file
+tag:django-components.asv,1970-01-01:/cddbdcca8b398afd301fbfc73cc4d51103d4e3059c0e6b938d4c467ad3d1aa25Airspeed Velocitydjango-components performance regressions2025-06-04T22:41:21Ztag:django-components.asv,2025-06-04:/38b868a38890eb5bccfd51983abf3a15cedf2846ef6cf2452e941122dbde2bde17.14% Components vs Django.timeraw_render_lg_subsequent('django')2025-06-04T22:41:21Z<a href="index.html#Components vs Django.timeraw_render_lg_subsequent?p-renderer=%27django%27&commits=4c909486069f3c3c8ee7915239174f820f081da4-7b24b86f4a836c697acba926d9d6602afa45418d">17.14% regression</a> on 2025-06-04 22:35:21 in commits <a href="#4c909486069f3c3c8ee7915239174f820f081da4">4c909486...7b24b86f</a>.<br>
+ New value: 43.6ms, old value: 37.2ms.<br>
+ Latest value: 43.6ms (31.10% worse
+ than best value 33.3ms).tag:django-components.asv,2025-03-31:/7a13128cbc4d175ca09ebda40e8a303789275bd84b00a5d496cfa08a26ad2f8b11.92% Components vs Django.timeraw_render_lg_subsequent('django')2025-03-31T14:16:53Z<a href="index.html#Components vs Django.timeraw_render_lg_subsequent?p-renderer=%27django%27&commits=42818ad6ffb47bd650d8a379b84c3d48394f9f77-a6455d70f6c28ddbd4be8e58902f6cbc101e5ff3">11.92% regression</a> on 2025-03-31 14:10:42 in commits <a href="#42818ad6ffb47bd650d8a379b84c3d48394f9f77">42818ad6...a6455d70</a>.<br>
+ New value: 37.2ms, old value: 33.3ms.<br>
+ Latest value: 43.6ms (31.10% worse
+ than best value 33.3ms).
\ No newline at end of file
diff --git a/docs/concepts/advanced/.nav.yml b/docs/concepts/advanced/.nav.yml
index 33e17dbc..3480a1e3 100644
--- a/docs/concepts/advanced/.nav.yml
+++ b/docs/concepts/advanced/.nav.yml
@@ -5,9 +5,10 @@ nav:
- Prop drilling and provide / inject: provide_inject.md
- Lifecycle hooks: hooks.md
- Registering components: component_registry.md
- - Typing and validation: typing_and_validation.md
+ - Component caching: component_caching.md
+ - Component context and scope: component_context_scope.md
- Custom template tags: template_tags.md
- - Tag formatters: tag_formatter.md
+ - Tag formatters: tag_formatters.md
- Extensions: extensions.md
- Testing: testing.md
- - Authoring component libraries: authoring_component_libraries.md
+ - Component libraries: component_libraries.md
diff --git a/docs/concepts/advanced/component_caching.md b/docs/concepts/advanced/component_caching.md
new file mode 100644
index 00000000..6eb3c9bf
--- /dev/null
+++ b/docs/concepts/advanced/component_caching.md
@@ -0,0 +1,181 @@
+Component caching allows you to store the rendered output of a component. Next time the component is rendered
+with the same input, the cached output is returned instead of re-rendering the component.
+
+This is particularly useful for components that are expensive to render or do not change frequently.
+
+!!! info
+
+ Component caching uses [Django's cache framework](https://docs.djangoproject.com/en/5.2/topics/cache/),
+ so you can use any cache backend that is supported by Django.
+
+### Enabling caching
+
+Caching is disabled by default.
+
+To enable caching for a component, set [`Component.Cache.enabled`](../../reference/api.md#django_components.ComponentCache.enabled) to `True`:
+
+```python
+from django_components import Component
+
+class MyComponent(Component):
+ class Cache:
+ enabled = True
+```
+
+### Time-to-live (TTL)
+
+You can specify a time-to-live (TTL) for the cache entry with [`Component.Cache.ttl`](../../reference/api.md#django_components.ComponentCache.ttl), which determines how long the entry remains valid. The TTL is specified in seconds.
+
+```python
+class MyComponent(Component):
+ class Cache:
+ enabled = True
+ ttl = 60 * 60 * 24 # 1 day
+```
+
+- If `ttl > 0`, entries are cached for the specified number of seconds.
+- If `ttl = -1`, entries are cached indefinitely.
+- If `ttl = 0`, entries are not cached.
+- If `ttl = None`, the default TTL is used.
+
+### Custom cache name
+
+Since component caching uses Django's cache framework, you can specify a custom cache name with [`Component.Cache.cache_name`](../../reference/api.md#django_components.ComponentCache.cache_name) to use a different cache backend:
+
+```python
+class MyComponent(Component):
+ class Cache:
+ enabled = True
+ cache_name = "my_cache"
+```
+
+### Cache key generation
+
+By default, the cache key is generated based on the component's input (args and kwargs). So the following two calls would generate separate entries in the cache:
+
+```py
+MyComponent.render(name="Alice")
+MyComponent.render(name="Bob")
+```
+
+However, you have full control over the cache key generation. As such, you can:
+
+- Cache the component on all inputs (default)
+- Cache the component on particular inputs
+- Cache the component irrespective of the inputs
+
+To achieve that, you can override
+the [`Component.Cache.hash()`](../../reference/api.md#django_components.ComponentCache.hash)
+method to customize how arguments are hashed into the cache key.
+
+```python
+class MyComponent(Component):
+ class Cache:
+ enabled = True
+
+ def hash(self, *args, **kwargs):
+ return f"{json.dumps(args)}:{json.dumps(kwargs)}"
+```
+
+For even more control, you can override other methods available on the [`ComponentCache`](../../reference/api.md#django_components.ComponentCache) class.
+
+!!! warning
+
+ The default implementation of `Cache.hash()` simply serializes the input into a string.
+ As such, it might not be suitable if you need to hash complex objects like Models.
+
+### Caching slots
+
+By default, the cache key is generated based ONLY on the args and kwargs.
+
+To cache the component based on the slots, set [`Component.Cache.include_slots`](../../reference/api.md#django_components.ComponentCache.include_slots) to `True`:
+
+```python
+class MyComponent(Component):
+ class Cache:
+ enabled = True
+ include_slots = True
+```
+
+with `include_slots = True`, the cache key will be generated also based on the given slots.
+
+As such, the following two calls would generate separate entries in the cache:
+
+```django
+{% component "my_component" position="left" %}
+ Hello, Alice
+{% endcomponent %}
+
+{% component "my_component" position="left" %}
+ Hello, Bob
+{% endcomponent %}
+```
+
+Same when using [`Component.render()`](../../reference/api.md#django_components.Component.render) with string slots:
+
+```py
+MyComponent.render(
+ kwargs={"position": "left"},
+ slots={"content": "Hello, Alice"}
+)
+MyComponent.render(
+ kwargs={"position": "left"},
+ slots={"content": "Hello, Bob"}
+)
+```
+
+!!! warning
+
+ Passing slots as functions to cached components with `include_slots=True` will raise an error.
+
+ ```py
+ MyComponent.render(
+ kwargs={"position": "left"},
+ slots={"content": lambda ctx: "Hello, Alice"}
+ )
+ ```
+
+!!! warning
+
+ Slot caching DOES NOT account for context variables within
+ the [`{% fill %}`](../../reference/template_tags.md#fill) tag.
+
+ For example, the following two cases will be treated as the same entry:
+
+ ```django
+ {% with my_var="foo" %}
+ {% component "mycomponent" name="foo" %}
+ {{ my_var }}
+ {% endcomponent %}
+ {% endwith %}
+
+ {% with my_var="bar" %}
+ {% component "mycomponent" name="bar" %}
+ {{ my_var }}
+ {% endcomponent %}
+ {% endwith %}
+ ```
+
+ Currently it's impossible to capture used variables. This will be addressed in v2.
+ Read more about it in [django-components/#1164](https://github.com/django-components/django-components/issues/1164).
+
+### Example
+
+Here's a complete example of a component with caching enabled:
+
+```python
+from django_components import Component
+
+class MyComponent(Component):
+ template = "Hello, {{ name }}"
+
+ class Cache:
+ enabled = True
+ ttl = 300 # Cache for 5 minutes
+ cache_name = "my_cache"
+
+ def get_template_data(self, args, kwargs, slots, context):
+ return {"name": kwargs["name"]}
+```
+
+In this example, the component's rendered output is cached for 5 minutes using the `my_cache` backend.
diff --git a/docs/concepts/fundamentals/component_context_scope.md b/docs/concepts/advanced/component_context_scope.md
similarity index 81%
rename from docs/concepts/fundamentals/component_context_scope.md
rename to docs/concepts/advanced/component_context_scope.md
index 119b5c43..8d4ad4ca 100644
--- a/docs/concepts/fundamentals/component_context_scope.md
+++ b/docs/concepts/advanced/component_context_scope.md
@@ -12,7 +12,7 @@ NOTE: `{% csrf_token %}` tags need access to the top-level context, and they wil
If you find yourself using the `only` modifier often, you can set the [context_behavior](#context-behavior) option to `"isolated"`, which automatically applies the `only` modifier. This is useful if you want to make sure that components don't accidentally access the outer context.
-Components can also access the outer context in their context methods like `get_context_data` by accessing the property `self.outer_context`.
+Components can also access the outer context in their context methods like `get_template_data` by accessing the property `self.outer_context`.
## Example of Accessing Outer Context
@@ -22,14 +22,14 @@ Components can also access the outer context in their context methods like `get_
```
-Assuming that the rendering context has variables such as `date`, you can use `self.outer_context` to access them from within `get_context_data`. Here's how you might implement it:
+Assuming that the rendering context has variables such as `date`, you can use `self.outer_context` to access them from within `get_template_data`. Here's how you might implement it:
```python
class Calender(Component):
...
- def get_context_data(self):
+ def get_template_data(self, args, kwargs, slots, context):
outer_field = self.outer_context["date"]
return {
"date": outer_fields,
@@ -53,10 +53,10 @@ This has two modes:
you can access are a union of:
- All the variables that were OUTSIDE the fill tag, including any\
- [`{% with %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#with) tags.
- - Any loops ([`{% for ... %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#cycle))
+ [`{% with %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#with) tags.
+ - Any loops ([`{% for ... %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#cycle))
that the `{% fill %}` tag is part of.
- - Data returned from [`Component.get_context_data()`](../../../reference/api#django_components.Component.get_context_data)
+ - Data returned from [`Component.get_template_data()`](../../../reference/api#django_components.Component.get_template_data)
of the component that owns the fill tag.
- `"isolated"`
@@ -67,14 +67,14 @@ This has two modes:
Inside the [`{% fill %}`](../../../reference/template_tags#fill) tag, you can ONLY access variables from 2 places:
- - Any loops ([`{% for ... %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#cycle))
+ - Any loops ([`{% for ... %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#cycle))
that the `{% fill %}` tag is part of.
- - [`Component.get_context_data()`](../../../reference/api#django_components.Component.get_context_data)
+ - [`Component.get_template_data()`](../../../reference/api#django_components.Component.get_template_data)
of the component which defined the template (AKA the "root" component).
!!! warning
- Notice that the component whose `get_context_data()` we use inside
+ Notice that the component whose `get_template_data()` we use inside
[`{% fill %}`](../../../reference/template_tags#fill)
is NOT the same across the two modes!
@@ -93,10 +93,10 @@ This has two modes:
"""
```
- - `"django"` - `my_var` has access to data from `get_context_data()` of both `Inner` and `Outer`.
+ - `"django"` - `my_var` has access to data from `get_template_data()` of both `Inner` and `Outer`.
If there are variables defined in both, then `Inner` overshadows `Outer`.
- - `"isolated"` - `my_var` has access to data from `get_context_data()` of ONLY `Outer`.
+ - `"isolated"` - `my_var` has access to data from `get_template_data()` of ONLY `Outer`.
### Example "django"
@@ -115,11 +115,11 @@ class RootComp(Component):
{% endwith %}
"""
- def get_context_data(self):
+ def get_template_data(self, args, kwargs, slots, context):
return { "my_var": 123 }
```
-Then if [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data)
+Then if [`get_template_data()`](../../../reference/api#django_components.Component.get_template_data)
of the component `"my_comp"` returns following data:
```py
@@ -154,11 +154,11 @@ class RootComp(Component):
{% endwith %}
"""
- def get_context_data(self):
+ def get_template_data(self, args, kwargs, slots, context):
return { "my_var": 123 }
```
-Then if [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data)
+Then if [`get_template_data()`](../../../reference/api#django_components.Component.get_template_data)
of the component `"my_comp"` returns following data:
```py
@@ -172,10 +172,10 @@ Then the template will be rendered as:
# cheese
```
-Because variables `"my_var"` and `"cheese"` are searched only inside `RootComponent.get_context_data()`.
+Because variables `"my_var"` and `"cheese"` are searched only inside `RootComponent.get_template_data()`.
But since `"cheese"` is not defined there, it's empty.
!!! info
- Notice that the variables defined with the [`{% with %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#with)
+ Notice that the variables defined with the [`{% with %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#with)
tag are ignored inside the [`{% fill %}`](../../../reference/template_tags#fill) tag with the `"isolated"` mode.
diff --git a/docs/concepts/advanced/authoring_component_libraries.md b/docs/concepts/advanced/component_libraries.md
similarity index 87%
rename from docs/concepts/advanced/authoring_component_libraries.md
rename to docs/concepts/advanced/component_libraries.md
index b1c41e1d..44aa4577 100644
--- a/docs/concepts/advanced/authoring_component_libraries.md
+++ b/docs/concepts/advanced/component_libraries.md
@@ -23,13 +23,13 @@ For live examples, see the [Community examples](../../overview/community.md#comm
|-- mytags.py
```
-2. Create custom [`Library`](https://docs.djangoproject.com/en/5.1/howto/custom-template-tags/#how-to-create-custom-template-tags-and-filters)
+2. Create custom [`Library`](https://docs.djangoproject.com/en/5.2/howto/custom-template-tags/#how-to-create-custom-template-tags-and-filters)
and [`ComponentRegistry`](django_components.component_registry.ComponentRegistry) instances in `mytags.py`
This will be the entrypoint for using the components inside Django templates.
- Remember that Django requires the [`Library`](https://docs.djangoproject.com/en/5.1/howto/custom-template-tags/#how-to-create-custom-template-tags-and-filters)
- instance to be accessible under the `register` variable ([See Django docs](https://docs.djangoproject.com/en/dev/howto/custom-template-tags)):
+ Remember that Django requires the [`Library`](https://docs.djangoproject.com/en/5.2/howto/custom-template-tags/#how-to-create-custom-template-tags-and-filters)
+ instance to be accessible under the `register` variable ([See Django docs](https://docs.djangoproject.com/en/5.2/howto/custom-template-tags)):
```py
from django.template import Library
@@ -101,35 +101,30 @@ For live examples, see the [Community examples](../../overview/community.md#comm
It's also a good idea to have a common prefix for your components, so they can be easily distinguished from users' components. In the example below, we use the prefix `my_` / `My`.
```djc_py
- from typing import Dict, NotRequired, Optional, Tuple, TypedDict
-
- from django_components import Component, SlotFunc, register, types
+ from typing import NamedTuple, Optional
+ from django_components import Component, SlotInput, register, types
from myapp.templatetags.mytags import comp_registry
- # Define the types
- class EmptyDict(TypedDict):
- pass
-
- type MyMenuArgs = Tuple[int, str]
-
- class MyMenuSlots(TypedDict):
- default: NotRequired[Optional[SlotFunc[EmptyDict]]]
-
- class MyMenuProps(TypedDict):
- vertical: NotRequired[bool]
- klass: NotRequired[str]
- style: NotRequired[str]
-
# Define the component
# NOTE: Don't forget to set the `registry`!
@register("my_menu", registry=comp_registry)
- class MyMenu(Component[MyMenuArgs, MyMenuProps, MyMenuSlots, Any, Any, Any]):
- def get_context_data(
- self,
- *args,
- attrs: Optional[Dict] = None,
- ):
+ class MyMenu(Component):
+ # Define the types
+ class Args(NamedTuple):
+ size: int
+ text: str
+
+ class Kwargs(NamedTuple):
+ vertical: Optional[bool] = None
+ klass: Optional[str] = None
+ style: Optional[str] = None
+
+ class Slots(NamedTuple):
+ default: Optional[SlotInput] = None
+
+ def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
+ attrs = ...
return {
"attrs": attrs,
}
@@ -153,7 +148,7 @@ For live examples, see the [Community examples](../../overview/community.md#comm
Since you, as the library author, are not in control of the file system, it is recommended to load the components manually.
- We recommend doing this in the [`AppConfig.ready()`](https://docs.djangoproject.com/en/5.1/ref/applications/#django.apps.AppConfig.ready)
+ We recommend doing this in the [`AppConfig.ready()`](https://docs.djangoproject.com/en/5.2/ref/applications/#django.apps.AppConfig.ready)
hook of your `apps.py`:
```py
@@ -175,7 +170,7 @@ For live examples, see the [Community examples](../../overview/community.md#comm
```
Note that you can also include any other startup logic within
- [`AppConfig.ready()`](https://docs.djangoproject.com/en/5.1/ref/applications/#django.apps.AppConfig.ready).
+ [`AppConfig.ready()`](https://docs.djangoproject.com/en/5.2/ref/applications/#django.apps.AppConfig.ready).
And that's it! The next step is to publish it.
@@ -190,7 +185,7 @@ django_components uses the [`build`](https://build.pypa.io/en/stable/) utility t
python -m build --sdist --wheel --outdir dist/ .
```
-And to publish to PyPI, you can use [`twine`](https://docs.djangoproject.com/en/5.1/ref/applications/#django.apps.AppConfig.ready)
+And to publish to PyPI, you can use [`twine`](https://docs.djangoproject.com/en/5.2/ref/applications/#django.apps.AppConfig.ready)
([See Python user guide](https://packaging.python.org/en/latest/tutorials/packaging-projects/#uploading-the-distribution-archives))
```bash
@@ -224,7 +219,7 @@ After the package has been published, all that remains is to install it in other
]
```
-3. Optionally add the template tags to the [`builtins`](https://docs.djangoproject.com/en/5.1/topics/templates/#django.template.backends.django.DjangoTemplates),
+3. Optionally add the template tags to the [`builtins`](https://docs.djangoproject.com/en/5.2/topics/templates/#django.template.backends.django.DjangoTemplates),
so you don't have to call `{% load mytags %}` in every template:
```python
diff --git a/docs/concepts/advanced/component_registry.md b/docs/concepts/advanced/component_registry.md
index 57e2e328..f5865420 100644
--- a/docs/concepts/advanced/component_registry.md
+++ b/docs/concepts/advanced/component_registry.md
@@ -12,9 +12,9 @@ class Calendar(Component):
template_file = "template.html"
# This component takes one parameter, a date string to show in the template
- def get_context_data(self, date):
+ def get_template_data(self, args, kwargs, slots, context):
return {
- "date": date,
+ "date": kwargs["date"],
}
```
diff --git a/docs/concepts/advanced/extensions.md b/docs/concepts/advanced/extensions.md
index f8188237..50d76cd4 100644
--- a/docs/concepts/advanced/extensions.md
+++ b/docs/concepts/advanced/extensions.md
@@ -6,7 +6,11 @@ Django-components functionality can be extended with "extensions". Extensions al
- Add new attributes and methods to the components under an extension-specific nested class.
- Define custom commands that can be executed via the Django management command interface.
-## Setting up extensions
+## Live examples
+
+- [djc-ext-pydantic](https://github.com/django-components/djc-ext-pydantic)
+
+## Install extensions
Extensions are configured in the Django settings under [`COMPONENTS.extensions`](../../../reference/settings#django_components.app_settings.ComponentsSettings.extensions).
@@ -18,7 +22,7 @@ Extensions can be set by either as an import string or by passing in a class:
class MyExtension(ComponentExtension):
name = "my_extension"
- class ExtensionClass(ComponentExtension.ExtensionClass):
+ class ComponentConfig(ExtensionComponentConfig):
...
COMPONENTS = ComponentsSettings(
@@ -38,16 +42,22 @@ Extensions can define methods to hook into lifecycle events, such as:
- Un/registering a component
- Creating or deleting a registry
- Pre-processing data passed to a component on render
-- Post-processing data returned from [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data)
+- Post-processing data returned from [`get_template_data()`](../../../reference/api#django_components.Component.get_template_data)
and others.
See the full list in [Extension Hooks Reference](../../../reference/extension_hooks).
-## Configuring extensions per component
+## Per-component configuration
Each extension has a corresponding nested class within the [`Component`](../../../reference/api#django_components.Component) class. These allow
to configure the extensions on a per-component basis.
+E.g.:
+
+- `"view"` extension -> [`Component.View`](../../../reference/api#django_components.Component.View)
+- `"cache"` extension -> [`Component.Cache`](../../../reference/api#django_components.Component.Cache)
+- `"defaults"` extension -> [`Component.Defaults`](../../../reference/api#django_components.Component.Defaults)
+
!!! note
**Accessing the component instance from inside the nested classes:**
@@ -57,31 +67,32 @@ to configure the extensions on a per-component basis.
```python
class MyTable(Component):
- class View:
+ class MyExtension:
def get(self, request):
# `self.component` points to the instance of `MyTable` Component.
- return self.component.get(request)
+ return self.component.render_to_response(request=request)
```
### Example: Component as View
-The [Components as Views](../../fundamentals/components_as_views) feature is actually implemented as an extension
+The [Components as Views](../../fundamentals/component_views_urls) feature is actually implemented as an extension
that is configured by a `View` nested class.
-You can override the `get`, `post`, etc methods to customize the behavior of the component as a view:
+You can override the `get()`, `post()`, etc methods to customize the behavior of the component as a view:
```python
class MyTable(Component):
class View:
def get(self, request):
- return self.component.get(request)
+ return self.component_class.render_to_response(request=request)
def post(self, request):
- return self.component.post(request)
+ return self.component_class.render_to_response(request=request)
...
```
+
### Example: Storybook integration
The Storybook integration (work in progress) is an extension that is configured by a `Storybook` nested class.
@@ -93,12 +104,12 @@ JSON file from the component.
class MyTable(Component):
class Storybook:
def title(self):
- return self.component.__class__.__name__
+ return self.component_cls.__name__
def parameters(self) -> Parameters:
return {
"server": {
- "id": self.component.__class__.__name__,
+ "id": self.component_cls.__name__,
}
}
@@ -108,17 +119,84 @@ class MyTable(Component):
...
```
-## Accessing extensions in components
+### Extension defaults
+
+Extensions are incredibly flexible, but configuring the same extension for every component can be a pain.
+
+For this reason, django-components allows for extension defaults. This is like setting the extension config for every component.
+
+To set extension defaults, use the [`COMPONENTS.extensions_defaults`](../../../reference/settings#django_components.app_settings.ComponentsSettings.extensions_defaults) setting.
+
+The `extensions_defaults` setting is a dictionary where the key is the extension name and the value is a dictionary of config attributes:
+
+```python
+COMPONENTS = ComponentsSettings(
+ extensions=[
+ "my_extension.MyExtension",
+ "storybook.StorybookExtension",
+ ],
+ extensions_defaults={
+ "my_extension": {
+ "key": "value",
+ },
+ "view": {
+ "public": True,
+ },
+ "cache": {
+ "ttl": 60,
+ },
+ "storybook": {
+ "title": lambda self: self.component_cls.__name__,
+ },
+ },
+)
+```
+
+Which is equivalent to setting the following for every component:
+
+```python
+class MyTable(Component):
+ class MyExtension:
+ key = "value"
+
+ class View:
+ public = True
+
+ class Cache:
+ ttl = 60
+
+ class Storybook:
+ def title(self):
+ return self.component_cls.__name__
+```
+
+!!! info
+
+ If you define an attribute as a function, it is like defining a method on the extension class.
+
+ E.g. in the example above, `title` is a method on the `Storybook` extension class.
+
+As the name suggests, these are defaults, and so you can still selectively override them on a per-component basis:
+
+```python
+class MyTable(Component):
+ class View:
+ public = False
+```
+
+### Extensions in component instances
Above, we've configured extensions `View` and `Storybook` for the `MyTable` component.
You can access the instances of these extension classes in the component instance.
+Extensions are available under their names (e.g. `self.view`, `self.storybook`).
+
For example, the View extension is available as `self.view`:
```python
class MyTable(Component):
- def get_context_data(self, request):
+ def get_template_data(self, args, kwargs, slots, context):
# `self.view` points to the instance of `View` extension.
return {
"view": self.view,
@@ -129,30 +207,30 @@ And the Storybook extension is available as `self.storybook`:
```python
class MyTable(Component):
- def get_context_data(self, request):
+ def get_template_data(self, args, kwargs, slots, context):
# `self.storybook` points to the instance of `Storybook` extension.
return {
"title": self.storybook.title(),
}
```
-Thus, you can use extensions to add methods or attributes that will be available to all components
-in their component context.
-
## Writing extensions
Creating extensions in django-components involves defining a class that inherits from
[`ComponentExtension`](../../../reference/api/#django_components.ComponentExtension).
This class can implement various lifecycle hooks and define new attributes or methods to be added to components.
-### Defining an extension
+### Extension class
To create an extension, define a class that inherits from [`ComponentExtension`](../../../reference/api/#django_components.ComponentExtension)
and implement the desired hooks.
- Each extension MUST have a `name` attribute. The name MUST be a valid Python identifier.
-- The extension MAY implement any of the [hook methods](../../../reference/extension_hooks).
-- Each hook method receives a context object with relevant data.
+- The extension may implement any of the [hook methods](../../../reference/extension_hooks).
+
+ Each hook method receives a context object with relevant data.
+
+- Extension may own [URLs](#extension-urls) or [CLI commands](#extension-commands).
```python
from django_components.extension import ComponentExtension, OnComponentClassCreatedContext
@@ -165,9 +243,18 @@ class MyExtension(ComponentExtension):
ctx.component_cls.my_attr = "my_value"
```
-### Defining the extension class
+!!! warning
-In previous sections we've seen the `View` and `Storybook` extensions classes that were nested within the `Component` class:
+ The `name` attribute MUST be unique across all extensions.
+
+ Moreover, the `name` attribute MUST NOT conflict with existing Component class API.
+
+ So if you name an extension `render`, it will conflict with the [`render()`](../../../reference/api/#django_components.Component.render) method of the `Component` class.
+
+### Component config
+
+In previous sections we've seen the `View` and `Storybook` extensions classes that were nested
+within the [`Component`](../../../reference/api/#django_components.Component) class:
```python
class MyComponent(Component):
@@ -180,32 +267,30 @@ class MyComponent(Component):
These can be understood as component-specific overrides or configuration.
-The nested extension classes like `View` or `Storybook` will actually subclass from a base extension
-class as defined on the [`ComponentExtension.ExtensionClass`](../../../reference/api/#django_components.ComponentExtension.ExtensionClass).
+Whether it's `Component.View` or `Component.Storybook`, their respective extensions
+defined how these nested classes will behave.
-This is how extensions define the "default" behavior of their nested extension classes.
-
-For example, the `View` base extension class defines the handlers for GET, POST, etc:
+For example, the View extension defines the API that users may override in `ViewExtension.ComponentConfig`:
```python
-from django_components.extension import ComponentExtension
+from django_components.extension import ComponentExtension, ExtensionComponentConfig
class ViewExtension(ComponentExtension):
name = "view"
# The default behavior of the `View` extension class.
- class ExtensionClass(ComponentExtension.ExtensionClass):
+ class ComponentConfig(ExtensionComponentConfig):
def get(self, request):
- return self.component.get(request)
+ raise NotImplementedError("You must implement the `get` method.")
def post(self, request):
- return self.component.post(request)
+ raise NotImplementedError("You must implement the `post` method.")
...
```
-In any component that then defines a nested `View` extension class, the `View` extension class will actually
-subclass from the `ViewExtension.ExtensionClass` class.
+In any component that then defines a nested `Component.View` extension class, the resulting `View` class
+will actually subclass from the `ViewExtension.ComponentConfig` class.
In other words, when you define a component like this:
@@ -217,11 +302,11 @@ class MyTable(Component):
...
```
-It will actually be implemented as if the `View` class subclassed from base class `ViewExtension.ExtensionClass`:
+Behind the scenes it is as if you defined the following:
```python
class MyTable(Component):
- class View(ViewExtension.ExtensionClass):
+ class View(ViewExtension.ComponentConfig):
def get(self, request):
# Do something
...
@@ -229,13 +314,13 @@ class MyTable(Component):
!!! warning
- When writing an extension, the `ExtensionClass` MUST subclass the base class [`ComponentExtension.ExtensionClass`](../../../reference/api/#django_components.ComponentExtension.ExtensionClass).
+ When writing an extension, the `ComponentConfig` MUST subclass the base class [`ExtensionComponentConfig`](../../../reference/api/#django_components.ExtensionComponentConfig).
This base class ensures that the extension class will have access to the component instance.
-### Registering extensions
+### Install your extension
-Once the extension is defined, it needs to be registered in the Django settings to be used by the application.
+Once the extension is defined, it needs to be installed in the Django settings to be used by the application.
Extensions can be given either as an extension class, or its import string:
@@ -271,30 +356,30 @@ To tie it all together, here's an example of a custom logging extension that log
```python
from django_components.extension import (
ComponentExtension,
+ ExtensionComponentConfig,
OnComponentClassCreatedContext,
OnComponentClassDeletedContext,
OnComponentInputContext,
)
-class ColorLoggerExtensionClass(ComponentExtension.ExtensionClass):
- color: str
-
class ColorLoggerExtension(ComponentExtension):
name = "color_logger"
# All `Component.ColorLogger` classes will inherit from this class.
- ExtensionClass = ColorLoggerExtensionClass
+ class ComponentConfig(ExtensionComponentConfig):
+ color: str
- # These hooks don't have access to the Component instance, only to the Component class,
- # so we access the color as `Component.ColorLogger.color`.
- def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
+ # These hooks don't have access to the Component instance,
+ # only to the Component class, so we access the color
+ # as `Component.ColorLogger.color`.
+ def on_component_class_created(self, ctx: OnComponentClassCreatedContext):
log.info(
f"Component {ctx.component_cls} created.",
color=ctx.component_cls.ColorLogger.color,
)
- def on_component_class_deleted(self, ctx: OnComponentClassDeletedContext) -> None:
+ def on_component_class_deleted(self, ctx: OnComponentClassDeletedContext):
log.info(
f"Component {ctx.component_cls} deleted.",
color=ctx.component_cls.ColorLogger.color,
@@ -302,7 +387,7 @@ class ColorLoggerExtension(ComponentExtension):
# This hook has access to the Component instance, so we access the color
# as `self.component.color_logger.color`.
- def on_component_input(self, ctx: OnComponentInputContext) -> None:
+ def on_component_input(self, ctx: OnComponentInputContext):
log.info(
f"Rendering component {ctx.component_cls}.",
color=ctx.component.color_logger.color,
@@ -320,7 +405,7 @@ COMPONENTS = {
}
```
-Once registered, in any component, you can define a `ColorLogger` attribute:
+Once installed, in any component, you can define a `ColorLogger` attribute:
```python
class MyComponent(Component):
@@ -337,45 +422,77 @@ django-components provides a few utility functions to help with writing extensio
- [`all_components()`](../../../reference/api#django_components.all_components) - returns a list of all created component classes.
- [`all_registries()`](../../../reference/api#django_components.all_registries) - returns a list of all created registry instances.
-### Accessing the component class from within an extension
+### Access component class
-When you are writing the extension class that will be nested inside a Component class, e.g.
+You can access the owner [`Component`](../../../reference/api/#django_components.Component) class (`MyTable`) from within methods
+of the extension class (`MyExtension`) by using
+the [`component_cls`](../../../reference/api/#django_components.ExtensionComponentConfig.component_cls) attribute:
```py
class MyTable(Component):
class MyExtension:
def some_method(self):
- ...
+ print(self.component_cls)
```
-You can access the owner Component class (`MyTable`) from within methods of the extension class (`MyExtension`) by using the `component_class` attribute:
-
-```py
-class MyTable(Component):
- class MyExtension:
- def some_method(self):
- print(self.component_class)
-```
-
-Here is how the `component_class` attribute may be used with our `ColorLogger`
+Here is how the `component_cls` attribute may be used with our `ColorLogger`
extension shown above:
```python
-class ColorLoggerExtensionClass(ComponentExtension.ExtensionClass):
+class ColorLoggerComponentConfig(ExtensionComponentConfig):
color: str
def log(self, msg: str) -> None:
- print(f"{self.component_class.name}: {msg}")
+ print(f"{self.component_cls.__name__}: {msg}")
class ColorLoggerExtension(ComponentExtension):
name = "color_logger"
# All `Component.ColorLogger` classes will inherit from this class.
- ExtensionClass = ColorLoggerExtensionClass
+ ComponentConfig = ColorLoggerComponentConfig
```
-## Extension Commands
+### Pass slot metadata
+
+When a slot is passed to a component, it is copied, so that the original slot is not modified
+with rendering metadata.
+
+Therefore, don't use slot's identity to associate metadata with the slot:
+
+```py
+# ❌ Don't do this:
+slots_cache = {}
+
+class LoggerExtension(ComponentExtension):
+ name = "logger"
+
+ def on_component_input(self, ctx: OnComponentInputContext):
+ for slot in ctx.component.slots.values():
+ slots_cache[id(slot)] = {"some": "metadata"}
+```
+
+Instead, use the [`Slot.extra`](../../../reference/api#django_components.Slot.extra) attribute,
+which is copied from the original slot:
+
+```python
+# ✅ Do this:
+class LoggerExtension(ComponentExtension):
+ name = "logger"
+
+ # Save component-level logger settings for each slot when a component is rendered.
+ def on_component_input(self, ctx: OnComponentInputContext):
+ for slot in ctx.component.slots.values():
+ slot.extra["logger"] = ctx.component.logger
+
+ # Then, when a fill is rendered with `{% slot %}`, we can access the logger settings
+ # from the slot's metadata.
+ def on_slot_rendered(self, ctx: OnSlotRenderedContext):
+ logger = ctx.slot.extra["logger"]
+ logger.log(...)
+```
+
+## Extension commands
Extensions in django-components can define custom commands that can be executed via the Django management command interface. This allows for powerful automation and customization capabilities.
@@ -392,9 +509,9 @@ Where:
- `my_ext` - is the extension name
- `hello` - is the command name
-### Defining Commands
+### Define commands
-To define a command, subclass from [`ComponentCommand`](../../../reference/api#django_components.ComponentCommand).
+To define a command, subclass from [`ComponentCommand`](../../../reference/extension_commands#django_components.ComponentCommand).
This subclass should define:
- `name` - the command's name
@@ -416,15 +533,15 @@ class MyExt(ComponentExtension):
commands = [HelloCommand]
```
-### Defining Command Arguments and Options
+### Define arguments and options
Commands can accept positional arguments and options (e.g. `--foo`), which are defined using the
-[`arguments`](../../../reference/api#django_components.ComponentCommand.arguments)
-attribute of the [`ComponentCommand`](../../../reference/api#django_components.ComponentCommand) class.
+[`arguments`](../../../reference/extension_commands#django_components.ComponentCommand.arguments)
+attribute of the [`ComponentCommand`](../../../reference/extension_commands#django_components.ComponentCommand) class.
The arguments are parsed with [`argparse`](https://docs.python.org/3/library/argparse.html)
into a dictionary of arguments and options. These are then available
-as keyword arguments to the [`handle`](../../../reference/api#django_components.ComponentCommand.handle)
+as keyword arguments to the [`handle`](../../../reference/extension_commands#django_components.ComponentCommand.handle)
method of the command.
```python
@@ -470,20 +587,20 @@ python manage.py components ext run my_ext hello John --shout
See the [argparse documentation](https://docs.python.org/3/library/argparse.html) for more information.
django-components defines types as
- [`CommandArg`](../../../reference/api#django_components.CommandArg),
- [`CommandArgGroup`](../../../reference/api#django_components.CommandArgGroup),
- [`CommandSubcommand`](../../../reference/api#django_components.CommandSubcommand),
- and [`CommandParserInput`](../../../reference/api#django_components.CommandParserInput)
+ [`CommandArg`](../../../reference/extension_commands#django_components.CommandArg),
+ [`CommandArgGroup`](../../../reference/extension_commands#django_components.CommandArgGroup),
+ [`CommandSubcommand`](../../../reference/extension_commands#django_components.CommandSubcommand),
+ and [`CommandParserInput`](../../../reference/extension_commands#django_components.CommandParserInput)
to help with type checking.
!!! note
- If a command doesn't have the [`handle`](../../../reference/api#django_components.ComponentCommand.handle)
+ If a command doesn't have the [`handle`](../../../reference/extension_commands#django_components.ComponentCommand.handle)
method defined, the command will print a help message and exit.
-### Grouping Arguments
+### Argument groups
-Arguments can be grouped using [`CommandArgGroup`](../../../reference/api#django_components.CommandArgGroup)
+Arguments can be grouped using [`CommandArgGroup`](../../../reference/extension_commands#django_components.CommandArgGroup)
to provide better organization and help messages.
Read more on [argparse argument groups](https://docs.python.org/3/library/argparse.html#argument-groups).
@@ -539,12 +656,12 @@ class HelloCommand(ComponentCommand):
Extensions can define subcommands, allowing for more complex command structures.
Subcommands are defined similarly to root commands, as subclasses of
-[`ComponentCommand`](../../../reference/api#django_components.ComponentCommand) class.
+[`ComponentCommand`](../../../reference/extension_commands#django_components.ComponentCommand) class.
However, instead of defining the subcommands in the
-[`commands`](../../../reference/api#django_components.ComponentExtension.commands)
+[`commands`](../../../reference/extension_commands#django_components.ComponentExtension.commands)
attribute of the extension, you define them in the
-[`subcommands`](../../../reference/api#django_components.ComponentCommand.subcommands)
+[`subcommands`](../../../reference/extension_commands#django_components.ComponentCommand.subcommands)
attribute of the parent command:
```python
@@ -600,7 +717,7 @@ python manage.py components ext run parent child
python manage.py components ext run parent child --foo --bar
```
-### Print command help
+### Help message
By default, all commands will print their help message when run with the `--help` / `-h` flag.
@@ -610,9 +727,9 @@ python manage.py components ext run my_ext --help
The help message prints out all the arguments and options available for the command, as well as any subcommands.
-### Testing Commands
+### Testing commands
-Commands can be tested using Django's [`call_command()`](https://docs.djangoproject.com/en/5.1/ref/django-admin/#running-management-commands-from-your-code)
+Commands can be tested using Django's [`call_command()`](https://docs.djangoproject.com/en/5.2/ref/django-admin/#running-management-commands-from-your-code)
function, which allows you to simulate running the command in tests.
```python
@@ -663,7 +780,7 @@ def test_hello_command(self):
Extensions can define custom views and endpoints that can be accessed through the Django application.
-To define URLs for an extension, set them in the [`urls`](../../../reference/api#django_components.ComponentExtension.urls) attribute of your [`ComponentExtension`](../../../reference/api#django_components.ComponentExtension) class. Each URL is defined using the [`URLRoute`](../../../reference/api#django_components.URLRoute) class, which specifies the path, handler, and optional name for the route.
+To define URLs for an extension, set them in the [`urls`](../../../reference/api#django_components.ComponentExtension.urls) attribute of your [`ComponentExtension`](../../../reference/api#django_components.ComponentExtension) class. Each URL is defined using the [`URLRoute`](../../../reference/extension_urls#django_components.URLRoute) class, which specifies the path, handler, and optional name for the route.
Here's an example of how to define URLs within an extension:
@@ -685,17 +802,17 @@ class MyExtension(ComponentExtension):
!!! warning
- The [`URLRoute`](../../../reference/api#django_components.URLRoute) objects
+ The [`URLRoute`](../../../reference/extension_urls#django_components.URLRoute) objects
are different from objects created with Django's
- [`django.urls.path()`](https://docs.djangoproject.com/en/5.1/ref/urls/#path).
- Do NOT use `URLRoute` objects in Django's [`urlpatterns`](https://docs.djangoproject.com/en/5.1/topics/http/urls/#example)
+ [`django.urls.path()`](https://docs.djangoproject.com/en/5.2/ref/urls/#path).
+ Do NOT use `URLRoute` objects in Django's [`urlpatterns`](https://docs.djangoproject.com/en/5.2/topics/http/urls/#example)
and vice versa!
- django-components uses a custom [`URLRoute`](../../../reference/api#django_components.URLRoute) class to define framework-agnostic routing rules.
+ django-components uses a custom [`URLRoute`](../../../reference/extension_urls#django_components.URLRoute) class to define framework-agnostic routing rules.
As of v0.131, `URLRoute` objects are directly converted to Django's `URLPattern` and `URLResolver` objects.
-### Accessing Extension URLs
+### URL paths
The URLs defined in an extension are available under the path
@@ -713,9 +830,9 @@ For example, if you have defined a URL with the path `my-view//` in an
Extensions can also define nested URLs to allow for more complex routing structures.
-To define nested URLs, set the [`children`](../../../reference/api#django_components.URLRoute.children)
-attribute of the [`URLRoute`](../../../reference/api#django_components.URLRoute) object to
-a list of child [`URLRoute`](../../../reference/api#django_components.URLRoute) objects:
+To define nested URLs, set the [`children`](../../../reference/extension_urls#django_components.URLRoute.children)
+attribute of the [`URLRoute`](../../../reference/extension_urls#django_components.URLRoute) object to
+a list of child [`URLRoute`](../../../reference/extension_urls#django_components.URLRoute) objects:
```python
class MyExtension(ComponentExtension):
@@ -740,17 +857,17 @@ In this example, the URL
would call the `my_view` handler with the parameter `name` set to `"John"`.
-### Passing kwargs and other extra fields to URL routes
+### Extra URL data
-The [`URLRoute`](../../../reference/api#django_components.URLRoute) class is framework-agnostic,
+The [`URLRoute`](../../../reference/extension_urls#django_components.URLRoute) class is framework-agnostic,
so that extensions could be used with non-Django frameworks in the future.
However, that means that there may be some extra fields that Django's
-[`django.urls.path()`](https://docs.djangoproject.com/en/5.1/ref/urls/#path)
+[`django.urls.path()`](https://docs.djangoproject.com/en/5.2/ref/urls/#path)
accepts, but which are not defined on the `URLRoute` object.
-To address this, the [`URLRoute`](../../../reference/api#django_components.URLRoute) object has
-an [`extra`](../../../reference/api#django_components.URLRoute.extra) attribute,
+To address this, the [`URLRoute`](../../../reference/extension_urls#django_components.URLRoute) object has
+an [`extra`](../../../reference/extension_urls#django_components.URLRoute.extra) attribute,
which is a dictionary that can be used to pass any extra kwargs to `django.urls.path()`:
```python
diff --git a/docs/concepts/advanced/hooks.md b/docs/concepts/advanced/hooks.md
index 5bfd2aa3..269134cb 100644
--- a/docs/concepts/advanced/hooks.md
+++ b/docs/concepts/advanced/hooks.md
@@ -1,58 +1,333 @@
_New in version 0.96_
-Component hooks are functions that allow you to intercept the rendering process at specific positions.
+Intercept the rendering lifecycle with Component hooks.
+
+Unlike the [extension hooks](../../../reference/extension_hooks/), these are defined directly
+on the [`Component`](../../../reference/api#django_components.Component) class.
## Available hooks
-- `on_render_before`
+### `on_render_before`
- ```py
- def on_render_before(
- self: Component,
- context: Context,
- template: Template
- ) -> None:
- ```
+```py
+def on_render_before(
+ self: Component,
+ context: Context,
+ template: Optional[Template],
+) -> None:
+```
- Hook that runs just before the component's template is rendered.
+[`Component.on_render_before`](../../../reference/api#django_components.Component.on_render_before) runs just before the component's template is rendered.
- You can use this hook to access or modify the context or the template:
+It is called for every component, including nested ones, as part of
+the component render lifecycle.
- ```py
- def on_render_before(self, context, template) -> None:
- # Insert value into the Context
- context["from_on_before"] = ":)"
+It receives the [Context](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context)
+and the [Template](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template)
+as arguments.
- # Append text into the Template
- template.nodelist.append(TextNode("FROM_ON_BEFORE"))
- ```
+The `template` argument is `None` if the component has no template.
-- `on_render_after`
+**Example:**
- ```py
- def on_render_after(
- self: Component,
- context: Context,
- template: Template,
- content: str
- ) -> None | str | SafeString:
- ```
+You can use this hook to access the context or the template:
- Hook that runs just after the component's template was rendered.
- It receives the rendered output as the last argument.
+```py
+from django.template import Context, Template
+from django_components import Component
- You can use this hook to access the context or the template, but modifying
- them won't have any effect.
+class MyTable(Component):
+ def on_render_before(self, context: Context, template: Optional[Template]) -> None:
+ # Insert value into the Context
+ context["from_on_before"] = ":)"
- To override the content that gets rendered, you can return a string or SafeString from this hook:
+ assert isinstance(template, Template)
+```
- ```py
- def on_render_after(self, context, template, content):
- # Prepend text to the rendered content
- return "Chocolate cookie recipe: " + content
- ```
+!!! warning
-## Component hooks example
+ If you want to pass data to the template, prefer using
+ [`get_template_data()`](../../../reference/api#django_components.Component.get_template_data)
+ instead of this hook.
+
+!!! warning
+
+ Do NOT modify the template in this hook. The template is reused across renders.
+
+ Since this hook is called for every component, this means that the template would be modified
+ every time a component is rendered.
+
+### `on_render`
+
+_New in version 0.140_
+
+```py
+def on_render(
+ self: Component,
+ context: Context,
+ template: Optional[Template],
+) -> Union[str, SafeString, OnRenderGenerator, None]:
+```
+
+[`Component.on_render`](../../../reference/api#django_components.Component.on_render) does the actual rendering.
+
+You can override this method to:
+
+- Change what template gets rendered
+- Modify the context
+- Modify the rendered output after it has been rendered
+- Handle errors
+
+The default implementation renders the component's
+[Template](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template)
+with the given
+[Context](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context).
+
+```py
+class MyTable(Component):
+ def on_render(self, context, template):
+ if template is None:
+ return None
+ else:
+ return template.render(context)
+```
+
+The `template` argument is `None` if the component has no template.
+
+#### Modifying rendered template
+
+To change what gets rendered, you can:
+
+- Render a different template
+- Render a component
+- Return a different string or SafeString
+
+```py
+class MyTable(Component):
+ def on_render(self, context, template):
+ return "Hello"
+```
+
+You can also use [`on_render()`](../../../reference/api#django_components.Component.on_render) as a router,
+rendering other components based on the parent component's arguments:
+
+```py
+class MyTable(Component):
+ def on_render(self, context, template):
+ # Select different component based on `feature_new_table` kwarg
+ if self.kwargs.get("feature_new_table"):
+ comp_cls = NewTable
+ else:
+ comp_cls = OldTable
+
+ # Render the selected component
+ return comp_cls.render(
+ args=self.args,
+ kwargs=self.kwargs,
+ slots=self.slots,
+ context=context,
+ )
+```
+
+#### Post-processing rendered template
+
+When you render the original template in [`on_render()`](../../../reference/api#django_components.Component.on_render) as:
+
+```py
+template.render(context)
+```
+
+The result is NOT the final output, but an intermediate result. Nested components
+are not rendered yet.
+
+Instead, django-components needs to take this result and process it
+to actually render the child components.
+
+To access the final output, you can `yield` the result instead of returning it.
+
+This will return a tuple of (rendered HTML, error). The error is `None` if the rendering succeeded.
+
+```py
+class MyTable(Component):
+ def on_render(self, context, template):
+ html, error = yield template.render(context)
+
+ if error is None:
+ # The rendering succeeded
+ return html
+ else:
+ # The rendering failed
+ print(f"Error: {error}")
+```
+
+At this point you can do 3 things:
+
+1. Return a new HTML
+
+ The new HTML will be used as the final output.
+
+ If the original template raised an error, it will be ignored.
+
+ ```py
+ class MyTable(Component):
+ def on_render(self, context, template):
+ html, error = yield template.render(context)
+
+ return "NEW HTML"
+ ```
+
+2. Raise a new exception
+
+ The new exception is what will bubble up from the component.
+
+ The original HTML and original error will be ignored.
+
+ ```py
+ class MyTable(Component):
+ def on_render(self, context, template):
+ html, error = yield template.render(context)
+
+ raise Exception("Error message")
+ ```
+
+3. Return nothing (or `None`) to handle the result as usual
+
+ If you don't raise an exception, and neither return a new HTML,
+ then original HTML / error will be used:
+
+ - If rendering succeeded, the original HTML will be used as the final output.
+ - If rendering failed, the original error will be propagated.
+
+ ```py
+ class MyTable(Component):
+ def on_render(self, context, template):
+ html, error = yield template.render(context)
+
+ if error is not None:
+ # The rendering failed
+ print(f"Error: {error}")
+ ```
+
+#### Example: ErrorBoundary
+
+[`on_render()`](../../../reference/api#django_components.Component.on_render) can be used to
+implement React's [ErrorBoundary](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary).
+
+That is, a component that catches errors in nested components and displays a fallback UI instead:
+
+```django
+{% component "error_boundary" %}
+ {% fill "content" %}
+ {% component "nested_component" %}
+ {% endfill %}
+ {% fill "fallback" %}
+ Sorry, something went wrong.
+ {% endfill %}
+{% endcomponent %}
+```
+
+To implement this, we render the fallback slot in [`on_render()`](../../../reference/api#django_components.Component.on_render)
+and return it if an error occured:
+
+```djc_py
+class ErrorFallback(Component):
+ template = """
+ {% slot "content" default / %}
+ """
+
+ def on_render(self, context, template):
+ fallback = self.slots.fallback
+
+ if fallback is None:
+ raise ValueError("fallback slot is required")
+
+ html, error = yield template.render(context)
+
+ if error is not None:
+ return fallback()
+ else:
+ return html
+```
+
+### `on_render_after`
+
+```py
+def on_render_after(
+ self: Component,
+ context: Context,
+ template: Optional[Template],
+ result: Optional[str | SafeString],
+ error: Optional[Exception],
+) -> Union[str, SafeString, None]:
+```
+
+[`on_render_after()`](../../../reference/api#django_components.Component.on_render_after) runs when the component was fully rendered,
+including all its children.
+
+It receives the same arguments as [`on_render_before()`](#on_render_before),
+plus the outcome of the rendering:
+
+- `result`: The rendered output of the component. `None` if the rendering failed.
+- `error`: The error that occurred during the rendering, or `None` if the rendering succeeded.
+
+[`on_render_after()`](../../../reference/api#django_components.Component.on_render_after) behaves the same way
+as the second part of [`on_render()`](#on_render) (after the `yield`).
+
+```py
+class MyTable(Component):
+ def on_render_after(self, context, template, result, error):
+ if error is None:
+ # The rendering succeeded
+ return result
+ else:
+ # The rendering failed
+ print(f"Error: {error}")
+```
+
+Same as [`on_render()`](#on_render),
+you can return a new HTML, raise a new exception, or return nothing:
+
+1. Return a new HTML
+
+ The new HTML will be used as the final output.
+
+ If the original template raised an error, it will be ignored.
+
+ ```py
+ class MyTable(Component):
+ def on_render_after(self, context, template, result, error):
+ return "NEW HTML"
+ ```
+
+2. Raise a new exception
+
+ The new exception is what will bubble up from the component.
+
+ The original HTML and original error will be ignored.
+
+ ```py
+ class MyTable(Component):
+ def on_render_after(self, context, template, result, error):
+ raise Exception("Error message")
+ ```
+
+3. Return nothing (or `None`) to handle the result as usual
+
+ If you don't raise an exception, and neither return a new HTML,
+ then original HTML / error will be used:
+
+ - If rendering succeeded, the original HTML will be used as the final output.
+ - If rendering failed, the original error will be propagated.
+
+ ```py
+ class MyTable(Component):
+ def on_render_after(self, context, template, result, error):
+ if error is not None:
+ # The rendering failed
+ print(f"Error: {error}")
+ ```
+
+## Example
You can use hooks together with [provide / inject](#how-to-use-provide--inject) to create components
that accept a list of items via a slot.
diff --git a/docs/concepts/advanced/html_fragments.md b/docs/concepts/advanced/html_fragments.md
index 75d0f6b3..3ac8ea4b 100644
--- a/docs/concepts/advanced/html_fragments.md
+++ b/docs/concepts/advanced/html_fragments.md
@@ -1,8 +1,7 @@
-Django-components provides a seamless integration with HTML fragments ([HTML over the wire](https://hotwired.dev/)),
-whether you're using HTMX, AlpineJS, or vanilla JavaScript.
+Django-components provides a seamless integration with HTML fragments with AJAX ([HTML over the wire](https://hotwired.dev/)),
+whether you're using jQuery, HTMX, AlpineJS, vanilla JavaScript, or other.
-When you define a component that has extra JS or CSS, and you use django-components
-to render the fragment, django-components will:
+If the fragment component has any JS or CSS, django-components will:
- Automatically load the associated JS and CSS
- Ensure that JS is loaded and executed only once even if the fragment is inserted multiple times
@@ -22,19 +21,23 @@ to render the fragment, django-components will:
4. A library like HTMX, AlpineJS, or custom function inserts the new HTML into
the correct place.
-## Document and fragment types
+## Document and fragment strategies
-Components support two modes of rendering - As a "document" or as a "fragment".
+Components support different ["strategies"](../../advanced/rendering_js_css#dependencies-strategies)
+for rendering JS and CSS.
+
+Two of them are used to enable HTML fragments - ["document"](../../advanced/rendering_js_css#document)
+and ["fragment"](../../advanced/rendering_js_css#fragment).
What's the difference?
-### Document mode
+### Document strategy
-Document mode assumes that the rendered components will be embedded into the HTML
+Document strategy assumes that the rendered components will be embedded into the HTML
of the initial page load. This means that:
- The JS and CSS is embedded into the HTML as `
+
+ ```
+
+- Components' secondary JS and CSS scripts
+ ([`Component.Media`](../../../reference/api/#django_components.Component.Media)) - inserted as links:
+
+ ```html
+
+
+ ```
+
+- A JS script is injected to manage component dependencies, enabling lazy loading of JS and CSS
+ for HTML fragments.
+
+!!! info
+
+ This strategy is required for fragments to work properly, as it sets up the dependency manager that fragments rely on.
+
+!!! note "How the dependency manager works"
+
+ The dependency manager is a JS script that keeps track of all the JS and CSS dependencies that have already been loaded.
+
+ When a fragment is inserted into the page, it will also insert a JSON `
+
+ ```
+
+- Components' secondary JS and CSS scripts
+ ([`Component.Media`](../../../reference/api/#django_components.Component.Media)) - inserted as links:
+
+ ```html
+
+
+ ```
+
+- No extra scripts are inserted.
+
+### `prepend`
+
+This is the same as [`"simple"`](#simple), but placeholders like [`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies) and HTML tags `` and `` are all ignored. The JS and CSS are **always** inserted **before** the rendered content.
+
+```python
+html = MyComponent.render(deps_strategy="prepend")
+```
+
+**Location:**
+
+JS and CSS is **always** inserted before the rendered content.
+
+**Included scripts:**
+
+Same as for the [`"simple"`](#simple) strategy.
+
+### `append`
+
+This is the same as [`"simple"`](#simple), but placeholders like [`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies) and HTML tags `` and `` are all ignored. The JS and CSS are **always** inserted **after** the rendered content.
+
+```python
+html = MyComponent.render(deps_strategy="append")
+```
+
+**Location:**
+
+JS and CSS is **always** inserted after the rendered content.
+
+**Included scripts:**
+
+Same as for the [`"simple"`](#simple) strategy.
+
+### `ignore`
+
+`deps_strategy="ignore"` is used when you do NOT want to process JS and CSS of the rendered HTML.
+
+```python
+html = MyComponent.render(deps_strategy="ignore")
+```
+
+The rendered HTML is left as-is. You can still process it with a different strategy later with `render_dependencies()`.
+
+This is useful when you want to insert rendered HTML into another component.
+
+```python
+html = MyComponent.render(deps_strategy="ignore")
+html = AnotherComponent.render(slots={"content": html})
+```
+
+## Manually rendering JS / CSS
+
+When rendering templates or components, django-components covers all the traditional ways how components
+or templates can be rendered:
+
+- [`Component.render()`](../../../reference/api/#django_components.Component.render)
+- [`Component.render_to_response()`](../../../reference/api/#django_components.Component.render_to_response)
+- [`Template.render()`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template.render)
+- [`django.shortcuts.render()`](https://docs.djangoproject.com/en/5.2/topics/http/shortcuts/#render)
+
+This way you don't need to manually handle rendering of JS / CSS.
+
+However, for advanced or low-level use cases, you may need to control when to render JS / CSS.
+
+In such case you can directly pass rendered HTML to [`render_dependencies()`](../../../reference/api/#django_components.render_dependencies).
+
+This function will extract all used components in the HTML string, and insert the components' JS and CSS
+based on given strategy.
+
+!!! info
+
+ The truth is that all the methods listed above call [`render_dependencies()`](../../../reference/api/#django_components.render_dependencies)
+ internally.
+
+**Example:**
+
+To see how [`render_dependencies()`](../../../reference/api/#django_components.render_dependencies) works,
+let's render a template with a component.
+
+We will render it twice:
+
+- First time, we let `template.render()` handle the rendering.
+- Second time, we prevent `template.render()` from inserting the component's JS and CSS with `deps_strategy="ignore"`.
+
+ Instead, we pass the "unprocessed" HTML to `render_dependencies()` ourselves to insert the component's JS and CSS.
```python
from django.template.base import Template
from django.template.context import Context
-from django_component import render_dependencies
+from django_components import render_dependencies
template = Template("""
{% load component_tags %}
@@ -138,85 +402,30 @@ template = Template("""
""")
-rendered = template.render(Context())
-rendered = render_dependencies(rendered)
+rendered = template.render(Context({}))
+
+rendered2_raw = template.render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
+rendered2 = render_dependencies(rendered2_raw)
+
+assert rendered == rendered2
```
-Same applies if you render a template using Django's [`django.shortcuts.render`](https://docs.djangoproject.com/en/5.1/topics/http/shortcuts/#render):
+Same applies to other strategies and other methods of rendering:
```python
-from django.shortcuts import render
+raw_html = MyComponent.render(deps_strategy="ignore")
+html = render_dependencies(raw_html, deps_strategy="document")
-def my_view(request):
- rendered = render(request, "pages/home.html")
- rendered = render_dependencies(rendered)
- return rendered
+html2 = MyComponent.render(deps_strategy="document")
+
+assert html == html2
```
-Alternatively, when you render HTML with [`Component.render()`](#TODO)
-or [`Component.render_to_response()`](#TODO),
-these, by default, call [`render_dependencies()`](#TODO) for you, so you don't have to:
+## HTML fragments
-```python
-from django_components import Component
+Django-components provides a seamless integration with HTML fragments with AJAX ([HTML over the wire](https://hotwired.dev/)),
+whether you're using jQuery, HTMX, AlpineJS, vanilla JavaScript, or other.
-class MyButton(Component):
- ...
+This is achieved by the combination of the [`"document"`](#document) and [`"fragment"`](#fragment) strategies.
-# No need to call `render_dependencies()`
-rendered = MyButton.render()
-```
-
-#### Inserting pre-rendered HTML into another component
-
-In previous section we've shown that [`render_dependencies()`](#TODO) does NOT need to be called
-when you render a component via [`Component.render()`](#TODO).
-
-API of django_components makes it possible to compose components in a "React-like" way,
-where we pre-render a piece of HTML and then insert it into a larger structure.
-
-To do this, you must add [`render_dependencies=False`](#TODO) to the nested components:
-
-```python
-card_actions = CardActions.render(
- kwargs={"editable": editable},
- render_dependencies=False,
-)
-
-card = Card.render(
- slots={"actions": card_actions},
- render_dependencies=False,
-)
-
-page = MyPage.render(
- slots={"card": card},
-)
-```
-
-Why is `render_dependencies=False` required?
-
-This is a technical limitation of the current implementation.
-
-As mentioned earlier, each time we call [`Component.render()`](#TODO),
-we also call [`render_dependencies()`](#TODO).
-
-However, there is a problem here - When we call [`render_dependencies()`](#TODO)
-inside [`CardActions.render()`](#TODO),
-we extract and REMOVE the info on components' JS and CSS from the HTML. But the template
-of `CardActions` contains no `{% component_depedencies %}` tags, and nor `` nor `` HTML tags.
-So the component's JS and CSS will NOT be inserted, and will be lost.
-
-To work around this, you must set [`render_dependencies=False`](#TODO) when rendering pieces of HTML
-with [`Component.render()`](#TODO) and inserting them into larger structures.
-
-#### Summary
-
-1. Every time you render HTML that contained components, you have to call [`render_dependencies()`](#TODO)
- on the rendered output.
-2. There are several ways to call [`render_dependencies()`](#TODO):
- - Using the [`ComponentDependencyMiddleware`](#TODO) middleware
- - Rendering the HTML by calling [`Component.render()`](#TODO) with `render_dependencies=True` (default)
- - Rendering the HTML by calling [`Component.render_to_response()`](#TODO) (always renders dependencies)
- - Directly passing rendered HTML to [`render_dependencies()`](#TODO)
-3. If you pre-render one component to pass it into another, the pre-rendered component must be rendered with
- [`render_dependencies=False`](#TODO).
+Read more about [HTML fragments](../../advanced/html_fragments).
diff --git a/docs/concepts/advanced/tag_formatter.md b/docs/concepts/advanced/tag_formatters.md
similarity index 100%
rename from docs/concepts/advanced/tag_formatter.md
rename to docs/concepts/advanced/tag_formatters.md
diff --git a/docs/concepts/advanced/template_tags.md b/docs/concepts/advanced/template_tags.md
index cc2426f9..ff40181c 100644
--- a/docs/concepts/advanced/template_tags.md
+++ b/docs/concepts/advanced/template_tags.md
@@ -52,7 +52,7 @@ This will allow you to use the tag in your templates like this:
### Parameters
-The `@template_tag` decorator accepts the following parameters:
+The [`@template_tag`](../../../reference/api#django_components.template_tag) decorator accepts the following parameters:
- `library`: The Django template library to register the tag with
- `tag`: The name of the template tag (e.g. `"mytag"` for `{% mytag %}`)
@@ -61,7 +61,8 @@ The `@template_tag` decorator accepts the following parameters:
### Function signature
-The function decorated with `@template_tag` must accept at least two arguments:
+The function decorated with [`@template_tag`](../../../reference/api#django_components.template_tag)
+must accept at least two arguments:
1. `node`: The node instance (we'll explain this in detail in the next section)
2. `context`: The Django template context
@@ -150,15 +151,18 @@ GreetNode.register(library)
### Node properties
-When using `BaseNode`, you have access to several useful properties:
+When using [`BaseNode`](../../../reference/api#django_components.BaseNode), you have access to several useful properties:
-- `node_id`: A unique identifier for this node instance
-- `flags`: Dictionary of flag values (e.g. `{"required": True}`)
-- `params`: List of raw parameters passed to the tag
-- `nodelist`: The template nodes between the start and end tags
-- `active_flags`: List of flags that are currently set to True
+- [`node_id`](../../../reference/api#django_components.BaseNode.node_id): A unique identifier for this node instance
+- [`flags`](../../../reference/api#django_components.BaseNode.flags): Dictionary of flag values (e.g. `{"required": True}`)
+- [`params`](../../../reference/api#django_components.BaseNode.params): List of raw parameters passed to the tag
+- [`nodelist`](../../../reference/api#django_components.BaseNode.nodelist): The template nodes between the start and end tags
+- [`contents`](../../../reference/api#django_components.BaseNode.contents): The raw contents between the start and end tags
+- [`active_flags`](../../../reference/api#django_components.BaseNode.active_flags): List of flags that are currently set to True
+- [`template_name`](../../../reference/api#django_components.BaseNode.template_name): The name of the `Template` instance inside which the node was defined
+- [`template_component`](../../../reference/api#django_components.BaseNode.template_component): The component class that the `Template` belongs to
-This is what the `node` parameter in the `@template_tag` decorator gives you access to - it's the instance of the node class that was automatically created for your template tag.
+This is what the `node` parameter in the [`@template_tag`](../../../reference/api#django_components.template_tag) decorator gives you access to - it's the instance of the node class that was automatically created for your template tag.
### Rendering content between tags
diff --git a/docs/concepts/advanced/testing.md b/docs/concepts/advanced/testing.md
index 83ee3ee5..5fc15665 100644
--- a/docs/concepts/advanced/testing.md
+++ b/docs/concepts/advanced/testing.md
@@ -4,7 +4,10 @@ The [`@djc_test`](../../../reference/testing_api#djc_test) decorator is a powerf
## Usage
-The [`@djc_test`](../../../reference/testing_api#djc_test) decorator can be applied to functions, methods, or classes. When applied to a class, it recursively decorates all methods starting with `test_`, including those in nested classes. This allows for comprehensive testing of component behavior.
+The [`@djc_test`](../../../reference/testing_api#djc_test) decorator can be applied to functions, methods, or classes.
+
+When applied to a class, it decorates all methods starting with `test_`, and all nested classes starting with `Test`,
+recursively.
### Applying to a Function
@@ -15,8 +18,6 @@ simply decorate the function as shown below:
import django
from django_components.testing import djc_test
-django.setup()
-
@djc_test
def test_my_component():
@register("my_component")
@@ -27,38 +28,34 @@ def test_my_component():
### Applying to a Class
-When applied to a class, `djc_test` decorates each `test_` method individually:
+When applied to a class, `djc_test` decorates each `test_` method, as well as all nested classes starting with `Test`.
```python
import django
from django_components.testing import djc_test
-django.setup()
-
@djc_test
class TestMyComponent:
def test_something(self):
...
- class Nested:
+ class TestNested:
def test_something_else(self):
...
```
-This is equivalent to applying the decorator to each method individually:
+This is equivalent to applying the decorator to both of the methods individually:
```python
import django
from django_components.testing import djc_test
-django.setup()
-
class TestMyComponent:
@djc_test
def test_something(self):
...
- class Nested:
+ class TestNested:
@djc_test
def test_something_else(self):
...
@@ -70,19 +67,26 @@ See the API reference for [`@djc_test`](../../../reference/testing_api#djc_test)
### Setting Up Django
-Before using [`djc_test`](../../../reference/testing_api#djc_test), ensure Django is set up:
+If you want to define a common Django settings that would be the baseline for all tests,
+you can call [`django.setup()`](https://docs.djangoproject.com/en/5.2/ref/applications/#django.setup)
+before the `@djc_test` decorator:
```python
import django
from django_components.testing import djc_test
-django.setup()
+django.setup(...)
@djc_test
def test_my_component():
...
```
+!!! info
+
+ If you omit [`django.setup()`](https://docs.djangoproject.com/en/5.2/ref/applications/#django.setup)
+ in the example above, `@djc_test` will call it for you, so you don't need to do it manually.
+
## Example: Parametrizing Context Behavior
You can parametrize the [context behavior](../../../reference/settings#django_components.app_settings.ComponentsSettings.context_behavior) using [`djc_test`](../../../reference/testing_api#djc_test):
@@ -106,6 +110,6 @@ from django_components.testing import djc_test
)
)
def test_context_behavior(components_settings):
- rendered = MyComponent().render()
+ rendered = MyComponent.render()
...
```
diff --git a/docs/concepts/advanced/typing_and_validation.md b/docs/concepts/advanced/typing_and_validation.md
deleted file mode 100644
index 83252790..00000000
--- a/docs/concepts/advanced/typing_and_validation.md
+++ /dev/null
@@ -1,173 +0,0 @@
-## Adding type hints with Generics
-
-_New in version 0.92_
-
-The `Component` class optionally accepts type parameters
-that allow you to specify the types of args, kwargs, slots, and
-data:
-
-```py
-class Button(Component[Args, Kwargs, Slots, Data, JsData, CssData]):
- ...
-```
-
-- `Args` - Must be a `Tuple` or `Any`
-- `Kwargs` - Must be a `TypedDict` or `Any`
-- `Data` - Must be a `TypedDict` or `Any`
-- `Slots` - Must be a `TypedDict` or `Any`
-
-Here's a full example:
-
-```py
-from typing import NotRequired, Tuple, TypedDict, SlotContent, SlotFunc
-
-# Positional inputs
-Args = Tuple[int, str]
-
-# Kwargs inputs
-class Kwargs(TypedDict):
- variable: str
- another: int
- maybe_var: NotRequired[int] # May be ommited
-
-# Data returned from `get_context_data`
-class Data(TypedDict):
- variable: str
-
-# The data available to the `my_slot` scoped slot
-class MySlotData(TypedDict):
- value: int
-
-# Slots
-class Slots(TypedDict):
- # Use SlotFunc for slot functions.
- # The generic specifies the `data` dictionary
- my_slot: NotRequired[SlotFunc[MySlotData]]
- # SlotContent == Union[str, SafeString]
- another_slot: SlotContent
-
-class Button(Component[Args, Kwargs, Slots, Data, JsData, CssData]):
- def get_context_data(self, variable, another):
- return {
- "variable": variable,
- }
-```
-
-When you then call `Component.render` or `Component.render_to_response`, you will get type hints:
-
-```py
-Button.render(
- # Error: First arg must be `int`, got `float`
- args=(1.25, "abc"),
- # Error: Key "another" is missing
- kwargs={
- "variable": "text",
- },
-)
-```
-
-### Usage for Python <3.11
-
-On Python 3.8-3.10, use `typing_extensions`
-
-```py
-from typing_extensions import TypedDict, NotRequired
-```
-
-Additionally on Python 3.8-3.9, also import `annotations`:
-
-```py
-from __future__ import annotations
-```
-
-Moreover, on 3.10 and less, you may not be able to use `NotRequired`, and instead you will need to mark either all keys are required, or all keys as optional, using TypeDict's `total` kwarg.
-
-[See PEP-655](https://peps.python.org/pep-0655) for more info.
-
-## Passing additional args or kwargs
-
-You may have a function that supports any number of args or kwargs:
-
-```py
-def get_context_data(self, *args, **kwargs):
- ...
-```
-
-This is not supported with the typed components.
-
-As a workaround:
-
-- For `*args`, set a positional argument that accepts a list of values:
-
- ```py
- # Tuple of one member of list of strings
- Args = Tuple[List[str]]
- ```
-
-- For `*kwargs`, set a keyword argument that accepts a dictionary of values:
-
- ```py
- class Kwargs(TypedDict):
- variable: str
- another: int
- # Pass any extra keys under `extra`
- extra: Dict[str, any]
- ```
-
-## Handling no args or no kwargs
-
-To declare that a component accepts no Args, Kwargs, etc, you can use `EmptyTuple` and `EmptyDict` types:
-
-```py
-from django_components import Component, EmptyDict, EmptyTuple
-
-Args = EmptyTuple
-Kwargs = Data = Slots = EmptyDict
-
-class Button(Component[Args, Kwargs, Slots, Data, JsData, CssData]):
- ...
-```
-
-## Runtime input validation with types
-
-_New in version 0.96_
-
-> NOTE: Kwargs, slots, and data validation is supported only for Python >=3.11
-
-In Python 3.11 and later, when you specify the component types, you will get also runtime validation of the inputs you pass to `Component.render` or `Component.render_to_response`.
-
-So, using the example from before, if you ignored the type errors and still ran the following code:
-
-```py
-Button.render(
- # Error: First arg must be `int`, got `float`
- args=(1.25, "abc"),
- # Error: Key "another" is missing
- kwargs={
- "variable": "text",
- },
-)
-```
-
-This would raise a `TypeError`:
-
-```txt
-Component 'Button' expected positional argument at index 0 to be , got 1.25 of type
-```
-
-In case you need to skip these errors, you can either set the faulty member to `Any`, e.g.:
-
-```py
-# Changed `int` to `Any`
-Args = Tuple[Any, str]
-```
-
-Or you can replace `Args` with `Any` altogether, to skip the validation of args:
-
-```py
-# Replaced `Args` with `Any`
-class Button(Component[Any, Kwargs, Slots, Data, JsData, CssData]):
- ...
-```
-
-Same applies to kwargs, data, and slots.
diff --git a/docs/concepts/fundamentals/.nav.yml b/docs/concepts/fundamentals/.nav.yml
index 8f23baf6..d88500a4 100644
--- a/docs/concepts/fundamentals/.nav.yml
+++ b/docs/concepts/fundamentals/.nav.yml
@@ -1,15 +1,17 @@
# `.nav.yml` is provided by https://lukasgeiter.github.io/mkdocs-awesome-nav
nav:
- Single-file components: single_file_components.md
- - Components in Python: components_in_python.md
- - Accessing component inputs: access_component_input.md
+ - HTML / JS / CSS files: html_js_css_files.md
+ - HTML / JS / CSS variables: html_js_css_variables.md
+ - Secondary JS / CSS files: secondary_js_css_files.md
- Component defaults: component_defaults.md
- - Component context and scope: component_context_scope.md
- - Template tag syntax: template_tag_syntax.md
+ - Render API: render_api.md
+ - Rendering components: rendering_components.md
- Slots: slots.md
+ - Template tag syntax: template_tag_syntax.md
- HTML attributes: html_attributes.md
- - Defining HTML / JS / CSS files: defining_js_css_html_files.md
- - Autodiscovery: autodiscovery.md
- - Components as views: components_as_views.md
+ - Component views and URLs: component_views_urls.md
- HTTP Request: http_request.md
+ - Typing and validation: typing_and_validation.md
- Subclassing components: subclassing_components.md
+ - Autodiscovery: autodiscovery.md
diff --git a/docs/concepts/fundamentals/access_component_input.md b/docs/concepts/fundamentals/access_component_input.md
deleted file mode 100644
index e6d32c7d..00000000
--- a/docs/concepts/fundamentals/access_component_input.md
+++ /dev/null
@@ -1,34 +0,0 @@
-When you call `Component.render` or `Component.render_to_response`, the inputs to these methods can be accessed from within the instance under `self.input`.
-
-This means that you can use `self.input` inside:
-
-- `get_context_data`
-- `get_template_name`
-- `get_template`
-- `on_render_before`
-- `on_render_after`
-
-`self.input` is only defined during the execution of `Component.render`, and raises a `RuntimeError` when called outside of this context.
-
-`self.input` has the same fields as the input to `Component.render`:
-
-```python
-class TestComponent(Component):
- def get_context_data(self, var1, var2, variable, another, **attrs):
- assert self.input.args == (123, "str")
- assert self.input.kwargs == {"variable": "test", "another": 1}
- assert self.input.slots == {"my_slot": "MY_SLOT"}
- assert isinstance(self.input.context, Context)
-
- return {
- "variable": variable,
- }
-
-rendered = TestComponent.render(
- kwargs={"variable": "test", "another": 1},
- args=(123, "str"),
- slots={"my_slot": "MY_SLOT"},
-)
-```
-
-NOTE: The slots in `self.input.slots` are normalized to slot functions.
diff --git a/docs/concepts/fundamentals/autodiscovery.md b/docs/concepts/fundamentals/autodiscovery.md
index 30c083f1..452db11a 100644
--- a/docs/concepts/fundamentals/autodiscovery.md
+++ b/docs/concepts/fundamentals/autodiscovery.md
@@ -1,6 +1,15 @@
-Every component that you want to use in the template with the [`{% component %}`](django_components.templateags.component_tags)
-tag needs to be registered with the [`ComponentRegistry`](django_components.component_registry.ComponentRegistry).
-Normally, we use the [`@register`](django_components.component_registry.register) decorator for that:
+django-components automatically searches for files containing components in the
+[`COMPONENTS.dirs`](../../../reference/settings#django_components.app_settings.ComponentsSettings.dirs) and
+[`COMPONENTS.app_dirs`](../../../reference/settings#django_components.app_settings.ComponentsSettings.app_dirs)
+directories.
+
+### Manually register components
+
+Every component that you want to use in the template with the
+[`{% component %}`](../../../reference/template_tags#component)
+tag needs to be registered with the [`ComponentRegistry`](../../../reference/api#django_components.ComponentRegistry).
+
+We use the [`@register`](../../../reference/api#django_components.register) decorator for that:
```python
from django_components import Component, register
@@ -12,6 +21,8 @@ class Calendar(Component):
But for the component to be registered, the code needs to be executed - and for that, the file needs to be imported as a module.
+This is the "discovery" part of the process.
+
One way to do that is by importing all your components in `apps.py`:
```python
@@ -30,23 +41,28 @@ class MyAppConfig(AppConfig):
However, there's a simpler way!
-By default, the Python files in the [`COMPONENTS.dirs`](django_components.app_settings.ComponentsSettings.dirs) directories (and app-level [`[app]/components/`](django_components.app_settings.ComponentsSettings.app_dirs)) are auto-imported in order to auto-register the components.
+### Autodiscovery
-Autodiscovery occurs when Django is loaded, during the [`AppConfig.ready`](https://docs.djangoproject.com/en/5.1/ref/applications/#django.apps.AppConfig.ready)
+By default, the Python files found in the
+[`COMPONENTS.dirs`](../../../reference/settings#django_components.app_settings.ComponentsSettings.dirs) and
+[`COMPONENTS.app_dirs`](../../../reference/settings#django_components.app_settings.ComponentsSettings.app_dirs)
+are auto-imported in order to execute the code that registers the components.
+
+Autodiscovery occurs when Django is loaded, during the [`AppConfig.ready()`](https://docs.djangoproject.com/en/5.2/ref/applications/#django.apps.AppConfig.ready)
hook of the `apps.py` file.
If you are using autodiscovery, keep a few points in mind:
-- Avoid defining any logic on the module-level inside the `components` dir, that you would not want to run anyway.
-- Components inside the auto-imported files still need to be registered with [`@register`](django_components.component_registry.register)
+- Avoid defining any logic on the module-level inside the components directories, that you would not want to run anyway.
+- Components inside the auto-imported files still need to be registered with [`@register`](../../../reference/api#django_components.register)
- Auto-imported component files must be valid Python modules, they must use suffix `.py`, and module name should follow [PEP-8](https://peps.python.org/pep-0008/#package-and-module-names).
- Subdirectories and files starting with an underscore `_` (except `__init__.py`) are ignored.
-Autodiscovery can be disabled in the [settings](django_components.app_settings.ComponentsSettings.autodiscovery).
+Autodiscovery can be disabled in the settings with [`autodiscover=False`](../../../reference/settings#django_components.app_settings.ComponentsSettings.autodiscover).
### Manually trigger autodiscovery
-Autodiscovery can be also triggered manually, using the [`autodiscover`](django_components.autodiscovery.autodiscover) function. This is useful if you want to run autodiscovery at a custom point of the lifecycle:
+Autodiscovery can be also triggered manually, using the [`autodiscover()`](../../../reference/api#django_components.autodiscover) function. This is useful if you want to run autodiscovery at a custom point of the lifecycle:
```python
from django_components import autodiscover
diff --git a/docs/concepts/fundamentals/component_defaults.md b/docs/concepts/fundamentals/component_defaults.md
index 90580b5f..a57daf45 100644
--- a/docs/concepts/fundamentals/component_defaults.md
+++ b/docs/concepts/fundamentals/component_defaults.md
@@ -1,5 +1,5 @@
When a component is being rendered, the component inputs are passed to various methods like
-[`get_context_data()`](../../../reference/api#django_components.Component.get_context_data),
+[`get_template_data()`](../../../reference/api#django_components.Component.get_template_data),
[`get_js_data()`](../../../reference/api#django_components.Component.get_js_data),
or [`get_css_data()`](../../../reference/api#django_components.Component.get_css_data).
@@ -10,8 +10,8 @@ no value is provided, or when the value is set to `None` for a particular input.
### Defining defaults
-To define defaults for a component, you create a nested `Defaults` class within your
-[`Component`](../../../reference/api#django_components.Component) class.
+To define defaults for a component, you create a nested [`Defaults`](../../../reference/api#django_components.Component.Defaults)
+class within your [`Component`](../../../reference/api#django_components.Component) class.
Each attribute in the `Defaults` class represents a default value for a corresponding input.
```py
@@ -24,16 +24,16 @@ class MyTable(Component):
position = "left"
selected_items = Default(lambda: [1, 2, 3])
- def get_context_data(self, position, selected_items):
+ def get_template_data(self, args, kwargs, slots, context):
return {
- "position": position,
- "selected_items": selected_items,
+ "position": kwargs["position"],
+ "selected_items": kwargs["selected_items"],
}
...
```
-In this example, `position` is a simple default value, while `selected_items` uses a factory function wrapped in `Default` to ensure a new list is created each time the default is used.
+In this example, `position` is a simple default value, while `selected_items` uses a factory function wrapped in [`Default`](../../../reference/api#django_components.Default) to ensure a new list is created each time the default is used.
Now, when we render the component, the defaults will be applied:
@@ -65,6 +65,26 @@ and so `selected_items` will be set to `[1, 2, 3]`.
The defaults are aplied only to keyword arguments. They are NOT applied to positional arguments!
+!!! warning
+
+ When [typing](../fundamentals/typing_and_validation.md) your components with [`Args`](../../../reference/api/#django_components.Component.Args),
+ [`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
+ or [`Slots`](../../../reference/api/#django_components.Component.Slots) classes,
+ you may be inclined to define the defaults in the classes.
+
+ ```py
+ class ProfileCard(Component):
+ class Kwargs(NamedTuple):
+ show_details: bool = True
+ ```
+
+ This is **NOT recommended**, because:
+
+ - The defaults will NOT be applied to inputs when using [`self.raw_kwargs`](../../../reference/api/#django_components.Component.raw_kwargs) property.
+ - The defaults will NOT be applied when a field is given but set to `None`.
+
+ Instead, define the defaults in the [`Defaults`](../../../reference/api/#django_components.Component.Defaults) class.
+
### Default factories
For objects such as lists, dictionaries or other instances, you have to be careful - if you simply set a default value, this instance will be shared across all instances of the component!
@@ -78,7 +98,7 @@ class MyTable(Component):
selected_items = [1, 2, 3]
```
-To avoid this, you can use a factory function wrapped in `Default`.
+To avoid this, you can use a factory function wrapped in [`Default`](../../../reference/api#django_components.Default).
```py
from django_components import Component, Default
@@ -104,7 +124,7 @@ class MyTable(Component):
### Accessing defaults
-Since the defaults are defined on the component class, you can access the defaults for a component with the `Component.Defaults` property.
+Since the defaults are defined on the component class, you can access the defaults for a component with the [`Component.Defaults`](../../../reference/api#django_components.Component.Defaults) property.
So if we have a component like this:
@@ -118,10 +138,10 @@ class MyTable(Component):
position = "left"
selected_items = Default(lambda: [1, 2, 3])
- def get_context_data(self, position, selected_items):
+ def get_template_data(self, args, kwargs, slots, context):
return {
- "position": position,
- "selected_items": selected_items,
+ "position": kwargs["position"],
+ "selected_items": kwargs["selected_items"],
}
```
diff --git a/docs/concepts/fundamentals/component_views_urls.md b/docs/concepts/fundamentals/component_views_urls.md
new file mode 100644
index 00000000..f8398749
--- /dev/null
+++ b/docs/concepts/fundamentals/component_views_urls.md
@@ -0,0 +1,151 @@
+_New in version 0.34_
+
+_Note: Since 0.92, `Component` is no longer a subclass of Django's `View`. Instead, the nested
+[`Component.View`](../../../reference/api#django_components.Component.View) class is a subclass of Django's `View`._
+
+---
+
+For web applications, it's common to define endpoints that serve HTML content (AKA views).
+
+django-components has a suite of features that help you write and manage views and their URLs:
+
+- For each component, you can define methods for handling HTTP requests (GET, POST, etc.) - `get()`, `post()`, etc.
+
+- Use [`Component.as_view()`](../../../reference/api#django_components.Component.as_view) to be able to use your Components with Django's [`urlpatterns`](https://docs.djangoproject.com/en/5.2/topics/http/urls/). This works the same way as [`View.as_view()`](https://docs.djangoproject.com/en/5.2/ref/class-based-views/base/#django.views.generic.base.View.as_view).
+
+- To avoid having to manually define the endpoints for each component, you can set the component to be "public" with [`Component.View.public = True`](../../../reference/api#django_components.ComponentView.public). This will automatically create a URL for the component. To retrieve the component URL, use [`get_component_url()`](../../../reference/api#django_components.get_component_url).
+
+- In addition, [`Component`](../../../reference/api#django_components.Component) has a [`render_to_response()`](../../../reference/api#django_components.Component.render_to_response) method that renders the component template based on the provided input and returns an `HttpResponse` object.
+
+## Define handlers
+
+Here's an example of a calendar component defined as a view. Simply define a `View` class with your custom `get()` method to handle GET requests:
+
+```djc_py title="[project root]/components/calendar.py"
+from django_components import Component, ComponentView, register
+
+@register("calendar")
+class Calendar(Component):
+ template = """
+
+
+ {% slot "header" / %}
+
+
+ Today's date is {{ date }}
+
+
+ """
+
+ class View:
+ # Handle GET requests
+ def get(self, request, *args, **kwargs):
+ # Return HttpResponse with the rendered content
+ return Calendar.render_to_response(
+ request=request,
+ kwargs={
+ "date": request.GET.get("date", "2020-06-06"),
+ },
+ slots={
+ "header": "Calendar header",
+ },
+ )
+```
+
+!!! info
+
+ The View class supports all the same HTTP methods as Django's [`View`](https://docs.djangoproject.com/en/5.2/ref/class-based-views/base/#django.views.generic.base.View) class. These are:
+
+ `get()`, `post()`, `put()`, `patch()`, `delete()`, `head()`, `options()`, `trace()`
+
+ Each of these receive the [`HttpRequest`](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpRequest) object as the first argument.
+
+
+
+
+!!! warning
+
+ **Deprecation warning:**
+
+ Previously, the handler methods such as `get()` and `post()` could be defined directly on the `Component` class:
+
+ ```py
+ class Calendar(Component):
+ def get(self, request, *args, **kwargs):
+ return self.render_to_response(
+ kwargs={
+ "date": request.GET.get("date", "2020-06-06"),
+ }
+ )
+ ```
+
+ This is deprecated from v0.137 onwards, and will be removed in v1.0.
+
+### Acccessing component class
+
+You can access the component class from within the View methods by using the [`View.component_cls`](../../../reference/api#django_components.ComponentView.component_cls) attribute:
+
+```py
+class Calendar(Component):
+ ...
+
+ class View:
+ def get(self, request):
+ return self.component_cls.render_to_response(request=request)
+```
+
+## Register URLs manually
+
+To register the component as a route / endpoint in Django, add an entry to your
+[`urlpatterns`](https://docs.djangoproject.com/en/5.2/topics/http/urls/).
+In place of the view function, create a view object with [`Component.as_view()`](../../../reference/api#django_components.Component.as_view):
+
+```python title="[project root]/urls.py"
+from django.urls import path
+from components.calendar.calendar import Calendar
+
+urlpatterns = [
+ path("calendar/", Calendar.as_view()),
+]
+```
+
+[`Component.as_view()`](../../../reference/api#django_components.Component.as_view)
+internally calls [`View.as_view()`](https://docs.djangoproject.com/en/5.2/ref/class-based-views/base/#django.views.generic.base.View.as_view), passing the component
+instance as one of the arguments.
+
+## Register URLs automatically
+
+If you don't care about the exact URL of the component, you can let django-components manage the URLs for you by setting the [`Component.View.public`](../../../reference/api#django_components.ComponentView.public) attribute to `True`:
+
+```py
+class MyComponent(Component):
+ class View:
+ public = True
+
+ def get(self, request):
+ return self.component_cls.render_to_response(request=request)
+ ...
+```
+
+Then, to get the URL for the component, use [`get_component_url()`](../../../reference/api#django_components.get_component_url):
+
+```py
+from django_components import get_component_url
+
+url = get_component_url(MyComponent)
+```
+
+This way you don't have to mix your app URLs with component URLs.
+
+!!! info
+
+ If you need to pass query parameters or a fragment to the component URL, you can do so by passing the `query` and `fragment` arguments to [`get_component_url()`](../../../reference/api#django_components.get_component_url):
+
+ ```py
+ url = get_component_url(
+ MyComponent,
+ query={"foo": "bar"},
+ fragment="baz",
+ )
+ # /components/ext/view/components/c1ab2c3?foo=bar#baz
+ ```
diff --git a/docs/concepts/fundamentals/components_as_views.md b/docs/concepts/fundamentals/components_as_views.md
deleted file mode 100644
index a9f66042..00000000
--- a/docs/concepts/fundamentals/components_as_views.md
+++ /dev/null
@@ -1,148 +0,0 @@
-_New in version 0.34_
-
-_Note: Since 0.92, Component no longer subclasses View. To configure the View class, set the nested `Component.View` class_
-
-Components can now be used as views:
-
-- Components define the `Component.as_view()` class method that can be used the same as [`View.as_view()`](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.View.as_view).
-
-- By default, you can define GET, POST or other HTTP handlers directly on the Component, same as you do with [View](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#view). For example, you can override `get` and `post` to handle GET and POST requests, respectively.
-
-- In addition, `Component` now has a [`render_to_response`](#inputs-of-render-and-render_to_response) method that renders the component template based on the provided context and slots' data and returns an `HttpResponse` object.
-
-## Component as view example
-
-Here's an example of a calendar component defined as a view:
-
-```djc_py
-# In a file called [project root]/components/calendar.py
-from django_components import Component, ComponentView, register
-
-@register("calendar")
-class Calendar(Component):
-
- template = """
-
-
- {% slot "header" / %}
-
-
- Today's date is {{ date }}
-
-
- """
-
- # Handle GET requests
- def get(self, request, *args, **kwargs):
- context = {
- "date": request.GET.get("date", "2020-06-06"),
- }
- slots = {
- "header": "Calendar header",
- }
- # Return HttpResponse with the rendered content
- return self.render_to_response(
- context=context,
- slots=slots,
- )
-```
-
-Then, to use this component as a view, you should create a `urls.py` file in your components directory, and add a path to the component's view:
-
-```python
-# In a file called [project root]/components/urls.py
-from django.urls import path
-from components.calendar.calendar import Calendar
-
-urlpatterns = [
- path("calendar/", Calendar.as_view()),
-]
-```
-
-`Component.as_view()` is a shorthand for calling [`View.as_view()`](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.View.as_view) and passing the component
-instance as one of the arguments.
-
-Remember to add `__init__.py` to your components directory, so that Django can find the `urls.py` file.
-
-Finally, include the component's urls in your project's `urls.py` file:
-
-```python
-# In a file called [project root]/urls.py
-from django.urls import include, path
-
-urlpatterns = [
- path("components/", include("components.urls")),
-]
-```
-
-Note: Slots content are automatically escaped by default to prevent XSS attacks. To disable escaping, set `escape_slots_content=False` in the `render_to_response` method. If you do so, you should make sure that any content you pass to the slots is safe, especially if it comes from user input.
-
-If you're planning on passing an HTML string, check Django's use of [`format_html`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.html.format_html) and [`mark_safe`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.safestring.mark_safe).
-
-## Modifying the View class
-
-The View class that handles the requests is defined on `Component.View`.
-
-When you define a GET or POST handlers on the `Component` class, like so:
-
-```py
-class MyComponent(Component):
- def get(self, request, *args, **kwargs):
- return self.render_to_response(
- context={
- "date": request.GET.get("date", "2020-06-06"),
- },
- )
-
- def post(self, request, *args, **kwargs) -> HttpResponse:
- variable = request.POST.get("variable")
- return self.render_to_response(
- kwargs={"variable": variable}
- )
-```
-
-Then the request is still handled by `Component.View.get()` or `Component.View.post()`
-methods. However, by default, `Component.View.get()` points to `Component.get()`, and so on.
-
-```py
-class ComponentView(View):
- component: Component = None
- ...
-
- def get(self, request, *args, **kwargs):
- return self.component.get(request, *args, **kwargs)
-
- def post(self, request, *args, **kwargs):
- return self.component.post(request, *args, **kwargs)
-
- ...
-```
-
-If you want to define your own `View` class, you need to:
-
-1. Set the class as `Component.View`
-2. Subclass from `ComponentView`, so the View instance has access to the component instance.
-
-In the example below, we added extra logic into `View.setup()`.
-
-Note that the POST handler is still defined at the top. This is because `View` subclasses `ComponentView`, which defines the `post()` method that calls `Component.post()`.
-
-If you were to overwrite the `View.post()` method, then `Component.post()` would be ignored.
-
-```py
-from django_components import Component, ComponentView
-
-class MyComponent(Component):
-
- def post(self, request, *args, **kwargs) -> HttpResponse:
- variable = request.POST.get("variable")
- return self.component.render_to_response(
- kwargs={"variable": variable}
- )
-
- class View(ComponentView):
- def setup(self, request, *args, **kwargs):
- super(request, *args, **kwargs)
-
- do_something_extra(request, *args, **kwargs)
-```
diff --git a/docs/concepts/fundamentals/components_in_python.md b/docs/concepts/fundamentals/components_in_python.md
deleted file mode 100644
index 1cf569f1..00000000
--- a/docs/concepts/fundamentals/components_in_python.md
+++ /dev/null
@@ -1,134 +0,0 @@
-_New in version 0.81_
-
-Components can be rendered outside of Django templates, calling them as regular functions ("React-style").
-
-The component class defines `render` and `render_to_response` class methods. These methods accept positional args, kwargs, and slots, offering the same flexibility as the `{% component %}` tag:
-
-```djc_py
-class SimpleComponent(Component):
- template = """
- {% load component_tags %}
- hello: {{ hello }}
- foo: {{ foo }}
- kwargs: {{ kwargs|safe }}
- slot_first: {% slot "first" required / %}
- """
-
- def get_context_data(self, arg1, arg2, **kwargs):
- return {
- "hello": arg1,
- "foo": arg2,
- "kwargs": kwargs,
- }
-
-rendered = SimpleComponent.render(
- args=["world", "bar"],
- kwargs={"kw1": "test", "kw2": "ooo"},
- slots={"first": "FIRST_SLOT"},
- context={"from_context": 98},
-)
-```
-
-Renders:
-
-```
-hello: world
-foo: bar
-kwargs: {'kw1': 'test', 'kw2': 'ooo'}
-slot_first: FIRST_SLOT
-```
-
-## Inputs of `render` and `render_to_response`
-
-Both `render` and `render_to_response` accept the same input:
-
-```py
-Component.render(
- context: Mapping | django.template.Context | None = None,
- args: List[Any] | None = None,
- kwargs: Dict[str, Any] | None = None,
- slots: Dict[str, str | SafeString | SlotFunc] | None = None,
- escape_slots_content: bool = True
-) -> str:
-```
-
-- _`args`_ - Positional args for the component. This is the same as calling the component
- as `{% component "my_comp" arg1 arg2 ... %}`
-
-- _`kwargs`_ - Keyword args 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 [`SlotFunc`](#slotfunc).
-
-- _`escape_slots_content`_ - Whether the content from `slots` should be escaped. `True` by default to prevent XSS attacks. If you disable escaping, you should make sure that any content you pass to the slots is safe, especially if it comes from user input.
-
-- _`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.
-
-- _`request`_ - A Django request object. This is used to enable Django template `context_processors` to run,
- allowing for template tags like `{% csrf_token %}` and variables like `{{ debug }}`.
- - Similar behavior can be achieved with [provide / inject](#how-to-use-provide--inject).
- - This is used internally to convert `context` to a RequestContext. It does nothing if `context` is already
- a `Context` instance.
-
-### `SlotFunc`
-
-When rendering components with slots in `render` or `render_to_response`, you can pass either a string or a function.
-
-The function has following signature:
-
-```py
-def render_func(
- context: Context,
- data: Dict[str, Any],
- slot_ref: SlotRef,
-) -> str | SafeString:
- return nodelist.render(ctx)
-```
-
-- _`context`_ - Django's Context available to the Slot Node.
-- _`data`_ - Data passed to the `{% slot %}` tag. See [Scoped Slots](#scoped-slots).
-- _`slot_ref`_ - The default slot content. See [Accessing original content of slots](#accessing-original-content-of-slots).
- - NOTE: The slot is lazily evaluated. To render the slot, convert it to string with `str(slot_ref)`.
-
-Example:
-
-```py
-def footer_slot(ctx, data, slot_ref):
- return f"""
- SLOT_DATA: {data['abc']}
- ORIGINAL: {slot_ref}
- """
-
-MyComponent.render_to_response(
- slots={
- "footer": footer_slot,
- },
-)
-```
-
-## Response class of `render_to_response`
-
-While `render` method returns a plain string, `render_to_response` wraps the rendered content in a "Response" class. By default, this is `django.http.HttpResponse`.
-
-If you want to use a different Response class in `render_to_response`, set the `Component.response_class` attribute:
-
-```py
-class MyResponse(HttpResponse):
- def __init__(self, *args, **kwargs) -> None:
- super().__init__(*args, **kwargs)
- # Configure response
- self.headers = ...
- self.status = ...
-
-class SimpleComponent(Component):
- response_class = MyResponse
- template: types.django_html = "HELLO"
-
-response = SimpleComponent.render_to_response()
-assert isinstance(response, MyResponse)
-```
diff --git a/docs/concepts/fundamentals/html_attributes.md b/docs/concepts/fundamentals/html_attributes.md
index 8cde6912..22b111d3 100644
--- a/docs/concepts/fundamentals/html_attributes.md
+++ b/docs/concepts/fundamentals/html_attributes.md
@@ -47,10 +47,10 @@ You can use this for example to allow users of your component to add extra attri
```djc_py
@register("my_comp")
class MyComp(Component):
- # Capture extra kwargs in `attrs`
- def get_context_data(self, **attrs):
+ # Pass all kwargs as `attrs`
+ def get_template_data(self, args, kwargs, slots, context):
return {
- "attrs": attrs,
+ "attrs": kwargs,
"classes": "text-red",
"my_id": 123,
}
@@ -329,7 +329,7 @@ Renders:
```
-### Merging `style` Attributes
+### Merging `style` attributes
The `style` attribute can be specified as a string of style properties as usual.
@@ -364,8 +364,8 @@ If you want granular control over individual style properties, you can use a dic
If a style property is specified multiple times, the last value is used.
-- If the last time the property is set is `False`, the property is removed.
- Properties set to `None` are ignored.
+- If the last non-`None` instance of the property is set to `False`, the property is removed.
**Example:**
@@ -607,10 +607,11 @@ class MyComp(Component):
"""
- def get_context_data(self, date: Date, attrs: dict):
+ def get_template_data(self, args, kwargs, slots, context):
+ date = kwargs.pop("date")
return {
"date": date,
- "attrs": attrs,
+ "attrs": kwargs,
"class_from_var": "extra-class"
}
@@ -625,7 +626,7 @@ class Parent(Component):
/ %}
"""
- def get_context_data(self, date: Date):
+ def get_template_data(self, args, kwargs, slots, context):
return {
"date": datetime.now(),
"json_data": json.dumps({"value": 456})
@@ -669,7 +670,7 @@ So all kwargs that start with `attrs:` will be collected into an `attrs` dict.
attrs:@click="(e) => onClick(e, 'from_parent')"
```
-And `get_context_data` of `MyComp` will receive `attrs` input with following keys:
+And `get_template_data` of `MyComp` will receive a kwarg named `attrs` with following keys:
```py
attrs = {
diff --git a/docs/concepts/fundamentals/html_js_css_files.md b/docs/concepts/fundamentals/html_js_css_files.md
new file mode 100644
index 00000000..296f02e1
--- /dev/null
+++ b/docs/concepts/fundamentals/html_js_css_files.md
@@ -0,0 +1,441 @@
+## Overview
+
+Each component can have single "primary" HTML, CSS and JS file associated with them.
+
+Each of these can be either defined inline, or in a separate file:
+
+- HTML files are defined using [`Component.template`](../../reference/api.md#django_components.Component.template) or [`Component.template_file`](../../reference/api.md#django_components.Component.template_file)
+- CSS files are defined using [`Component.css`](../../reference/api.md#django_components.Component.css) or [`Component.css_file`](../../reference/api.md#django_components.Component.css_file)
+- JS files are defined using [`Component.js`](../../reference/api.md#django_components.Component.js) or [`Component.js_file`](../../reference/api.md#django_components.Component.js_file)
+
+```py
+@register("calendar")
+class Calendar(Component):
+ template_file = "calendar.html"
+ css_file = "calendar.css"
+ js_file = "calendar.js"
+```
+
+or
+
+```djc_py
+@register("calendar")
+class Calendar(Component):
+ template = """
+
+ Hi there!
+
+ """
+ css = """
+ .welcome {
+ color: red;
+ }
+ """
+ js = """
+ console.log("Hello, world!");
+ """
+```
+
+These "primary" files will have special behavior. For example, each will receive variables from the component's data methods.
+Read more about each file type below:
+
+- [HTML](#html)
+- [CSS](#css)
+- [JS](#js)
+
+In addition, you can define extra "secondary" CSS / JS files using the nested [`Component.Media`](../../reference/api.md#django_components.Component.Media) class,
+by setting [`Component.Media.js`](../../reference/api.md#django_components.ComponentMediaInput.js) and [`Component.Media.css`](../../reference/api.md#django_components.ComponentMediaInput.css).
+
+Single component can have many secondary files. There is no special behavior for them.
+
+You can use these for third-party libraries, or for shared CSS / JS files.
+
+Read more about [Secondary JS / CSS files](../secondary_js_css_files).
+
+!!! warning
+
+ You **cannot** use both inlined code **and** separate file for a single language type (HTML, CSS, JS).
+
+ However, you can freely mix these for different languages:
+
+ ```djc_py
+ class MyTable(Component):
+ template: types.django_html = """
+
+ Hi there!
+
+ """
+ js_file = "my_table.js"
+ css_file = "my_table.css"
+ ```
+
+## HTML
+
+Components use Django's template system to define their HTML.
+This means that you can use [Django's template syntax](https://docs.djangoproject.com/en/5.2/ref/templates/language/) to define your HTML.
+
+Inside the template, you can access the data returned from the [`get_template_data()`](../../../reference/api/#django_components.Component.get_template_data) method.
+
+You can define the HTML directly in your Python code using the [`template`](../../reference/api.md#django_components.Component.template) attribute:
+
+```djc_py
+class Button(Component):
+ template = """
+
+ """
+
+ def get_template_data(self, args, kwargs, slots, context):
+ return {
+ "text": kwargs.get("text", "Click me"),
+ "icon": kwargs.get("icon", None),
+ }
+```
+
+Or you can define the HTML in a separate file and reference it using [`template_file`](../../reference/api.md#django_components.Component.template_file):
+
+```python
+class Button(Component):
+ template_file = "button.html"
+
+ def get_template_data(self, args, kwargs, slots, context):
+ return {
+ "text": kwargs.get("text", "Click me"),
+ "icon": kwargs.get("icon", None),
+ }
+```
+
+```django title="button.html"
+
+```
+
+### Dynamic templates
+
+Each component has only a single template associated with it.
+
+However, whether it's for A/B testing or for preserving public API
+when sharing your components, sometimes you may need to render different templates
+based on the input to your component.
+
+You can use [`Component.on_render()`](../../reference/api.md#django_components.Component.on_render)
+to dynamically override what template gets rendered.
+
+By default, the component's template is rendered as-is.
+
+```py
+class Table(Component):
+ def on_render(self, context: Context, template: Optional[Template]):
+ if template is not None:
+ return template.render(context)
+```
+
+If you want to render a different template in its place,
+we recommended you to:
+
+1. Wrap the substitute templates as new Components
+2. Then render those Components inside [`Component.on_render()`](../../reference/api.md#django_components.Component.on_render):
+
+```py
+class TableNew(Component):
+ template_file = "table_new.html"
+
+class TableOld(Component):
+ template_file = "table_old.html"
+
+class Table(Component):
+ def on_render(self, context, template):
+ if self.kwargs.get("feat_table_new_ui"):
+ return TableNew.render(
+ args=self.args,
+ kwargs=self.kwargs,
+ slots=self.slots,
+ )
+ else:
+ return TableOld.render(
+ args=self.args,
+ kwargs=self.kwargs,
+ slots=self.slots,
+ )
+```
+
+!!! warning
+
+ If you do not wrap the templates as Components,
+ there is a risk that some [extensions](../../advanced/extensions) will not work as expected.
+
+ ```py
+ new_template = Template("""
+ {% load django_components %}
+
+ """)
+
+ class Table(Component):
+ def on_render(self, context, template):
+ if self.kwargs.get("feat_table_new_ui"):
+ return new_template.render(context)
+ else:
+ return template.render(context)
+ ```
+
+### Template-less components
+
+Since you can use [`Component.on_render()`](../../reference/api.md#django_components.Component.on_render)
+to render *other* components, there is no need to define a template for the component.
+
+So even an empty component like this is valid:
+
+```py
+class MyComponent(Component):
+ pass
+```
+
+These "template-less" components can be useful as base classes for other components, or as mixins.
+
+### HTML processing
+
+Django Components expects the rendered template to be a valid HTML. This is needed to enable features like [CSS / JS variables](../html_js_css_variables).
+
+Here is how the HTML is post-processed:
+
+1. **Insert component ID**: Each root element in the rendered HTML automatically receives a `data-djc-id-cxxxxxx` attribute containing a unique component instance ID.
+
+ ```html
+
+
+ ...
+
+
+ ...
+
+ ```
+
+2. **Insert CSS ID**: If the component defines CSS variables through [`get_css_data()`](../../../reference/api/#django_components.Component.get_css_data), the root elements also receive a `data-djc-css-xxxxxx` attribute. This attribute links the element to its specific CSS variables.
+
+ ```html
+
+
+
+
+ ```
+
+3. **Insert JS and CSS**: After the HTML is rendered, Django Components handles inserting JS and CSS dependencies into the page based on the [dependencies rendering strategy](../rendering_components/#dependencies-rendering) (document, fragment, or inline).
+
+ For example, if your component contains the
+ [`{% component_js_dependencies %}`](../../reference/template_tags.md#component_js_dependencies)
+ or
+ [`{% component_css_dependencies %}`](../../reference/template_tags.md#component_css_dependencies)
+ tags, or the `` and `` elements, the JS and CSS scripts will be inserted into the HTML.
+
+ For more information on how JS and CSS dependencies are rendered, see [Rendering JS / CSS](../../advanced/rendering_js_css).
+
+## JS
+
+The component's JS script is executed in the browser:
+
+- It is executed AFTER the "secondary" JS files from [`Component.Media.js`](../../reference/api.md#django_components.ComponentMediaInput.js) are loaded.
+- The script is only executed once, even if there are multiple instances of the component on the page.
+- Component JS scripts are executed in the order how they appeared in the template / HTML (top to bottom).
+
+You can define the JS directly in your Python code using the [`js`](../../reference/api.md#django_components.Component.js) attribute:
+
+```djc_py
+class Button(Component):
+ js = """
+ console.log("Hello, world!");
+ """
+
+ def get_js_data(self, args, kwargs, slots, context):
+ return {
+ "text": kwargs.get("text", "Click me"),
+ }
+```
+
+Or you can define the JS in a separate file and reference it using [`js_file`](../../reference/api.md#django_components.Component.js_file):
+
+```python
+class Button(Component):
+ js_file = "button.js"
+
+ def get_js_data(self, args, kwargs, slots, context):
+ return {
+ "text": kwargs.get("text", "Click me"),
+ }
+```
+
+```django title="button.js"
+console.log("Hello, world!");
+```
+
+## CSS
+
+You can define the CSS directly in your Python code using the [`css`](../../reference/api.md#django_components.Component.css) attribute:
+
+```djc_py
+class Button(Component):
+ css = """
+ .btn {
+ width: 100px;
+ color: var(--color);
+ }
+ """
+
+ def get_css_data(self, args, kwargs, slots, context):
+ return {
+ "color": kwargs.get("color", "red"),
+ }
+```
+
+Or you can define the CSS in a separate file and reference it using [`css_file`](../../reference/api.md#django_components.Component.css_file):
+
+```python
+class Button(Component):
+ css_file = "button.css"
+
+ def get_css_data(self, args, kwargs, slots, context):
+ return {
+ "text": kwargs.get("text", "Click me"),
+ }
+```
+
+```django title="button.css"
+.btn {
+ color: red;
+}
+```
+
+## File paths
+
+Compared to the [secondary JS / CSS files](../secondary_js_css_files), the definition of file paths for the main HTML / JS / CSS files is quite simple - just strings, without any lists, objects, or globs.
+
+However, similar to the secondary JS / CSS files, you can specify the file paths [relative to the component's directory](../secondary_js_css_files/#relative-to-component).
+
+So if you have a directory with following files:
+
+```
+[project root]/components/calendar/
+├── calendar.html
+├── calendar.css
+├── calendar.js
+└── calendar.py
+```
+
+You can define the component like this:
+
+```py title="[project root]/components/calendar/calendar.py"
+from django_components import Component, register
+
+@register("calendar")
+class Calendar(Component):
+ template_file = "calendar.html"
+ css_file = "calendar.css"
+ js_file = "calendar.js"
+```
+
+Assuming that
+[`COMPONENTS.dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs)
+contains path `[project root]/components`, the example above is the same as writing out:
+
+```py title="[project root]/components/calendar/calendar.py"
+from django_components import Component, register
+
+@register("calendar")
+class Calendar(Component):
+ template_file = "calendar/template.html"
+ css_file = "calendar/style.css"
+ js_file = "calendar/script.js"
+```
+
+If the path cannot be resolved relative to the component, django-components will attempt
+to resolve the path relative to the component directories, as set in
+[`COMPONENTS.dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs)
+or
+[`COMPONENTS.app_dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.app_dirs).
+
+Read more about [file path resolution](../secondary_js_css_files/#relative-to-component).
+
+## Access component definition
+
+Component's HTML / CSS / JS is resolved and loaded lazily.
+
+This means that, when you specify any of
+[`template_file`](../../reference/api.md#django_components.Component.template_file),
+[`js_file`](../../reference/api.md#django_components.Component.js_file),
+[`css_file`](../../reference/api.md#django_components.Component.css_file),
+or [`Media.js/css`](../../reference/api.md#django_components.Component.Media),
+these file paths will be resolved only once you either:
+
+1. Access any of the following attributes on the component:
+
+ - [`media`](../../reference/api.md#django_components.Component.media),
+ [`template`](../../reference/api.md#django_components.Component.template),
+ [`template_file`](../../reference/api.md#django_components.Component.template_file),
+ [`js`](../../reference/api.md#django_components.Component.js),
+ [`js_file`](../../reference/api.md#django_components.Component.js_file),
+ [`css`](../../reference/api.md#django_components.Component.css),
+ [`css_file`](../../reference/api.md#django_components.Component.css_file)
+
+2. Render the component.
+
+Once the component's media files have been loaded once, they will remain in-memory
+on the Component class:
+
+- HTML from [`Component.template_file`](../../reference/api.md#django_components.Component.template_file)
+ will be available under [`Component.template`](../../reference/api.md#django_components.Component.template)
+- CSS from [`Component.css_file`](../../reference/api.md#django_components.Component.css_file)
+ will be available under [`Component.css`](../../reference/api.md#django_components.Component.css)
+- JS from [`Component.js_file`](../../reference/api.md#django_components.Component.js_file)
+ will be available under [`Component.js`](../../reference/api.md#django_components.Component.js)
+
+Thus, whether you define HTML via
+[`Component.template_file`](../../reference/api.md#django_components.Component.template_file)
+or [`Component.template`](../../reference/api.md#django_components.Component.template),
+you can always access the HTML content under [`Component.template`](../../reference/api.md#django_components.Component.template).
+And the same applies for JS and CSS.
+
+**Example:**
+
+```py
+# When we create Calendar component, the files like `calendar/template.html`
+# are not yet loaded!
+@register("calendar")
+class Calendar(Component):
+ template_file = "calendar/template.html"
+ css_file = "calendar/style.css"
+ js_file = "calendar/script.js"
+
+ class Media:
+ css = "calendar/style1.css"
+ js = "calendar/script2.js"
+
+# It's only at this moment that django-components reads the files like `calendar/template.html`
+print(Calendar.css)
+# Output:
+# .calendar {
+# width: 200px;
+# background: pink;
+# }
+```
+
+!!! warning
+
+ **Do NOT modify HTML / CSS / JS after it has been loaded**
+
+ django-components assumes that the component's media files like `js_file` or `Media.js/css` are static.
+
+ If you need to dynamically change these media files, consider instead defining multiple Components.
+
+ Modifying these files AFTER the component has been loaded at best does nothing. However, this is
+ an untested behavior, which may lead to unexpected errors.
diff --git a/docs/concepts/fundamentals/html_js_css_variables.md b/docs/concepts/fundamentals/html_js_css_variables.md
new file mode 100644
index 00000000..b7164e06
--- /dev/null
+++ b/docs/concepts/fundamentals/html_js_css_variables.md
@@ -0,0 +1,496 @@
+When a component recieves input through [`{% component %}`](../../../reference/template_tags/#component) tag,
+or the [`Component.render()`](../../../reference/api/#django_components.Component.render) or [`Component.render_to_response()`](../../../reference/api/#django_components.Component.render_to_response) methods, you can define how the input is handled, and what variables will be available to the template, JavaScript and CSS.
+
+## Overview
+
+Django Components offers three key methods for passing variables to different parts of your component:
+
+- [`get_template_data()`](../../../reference/api/#django_components.Component.get_template_data) - Provides variables to your HTML template
+- [`get_js_data()`](../../../reference/api/#django_components.Component.get_js_data) - Provides variables to your JavaScript code
+- [`get_css_data()`](../../../reference/api/#django_components.Component.get_css_data) - Provides variables to your CSS styles
+
+These methods let you pre-process inputs before they're used in rendering.
+
+Each method handles the data independently - you can define different data for the template, JS, and CSS.
+
+```python
+class ProfileCard(Component):
+ class Kwargs(NamedTuple):
+ user_id: int
+ show_details: bool
+
+ class Defaults:
+ show_details = True
+
+ def get_template_data(self, args, kwargs: Kwargs, slots, context):
+ user = User.objects.get(id=kwargs.user_id)
+ return {
+ "user": user,
+ "show_details": kwargs.show_details,
+ }
+
+ def get_js_data(self, args, kwargs: Kwargs, slots, context):
+ return {
+ "user_id": kwargs.user_id,
+ }
+
+ def get_css_data(self, args, kwargs: Kwargs, slots, context):
+ text_color = "red" if kwargs.show_details else "blue"
+ return {
+ "text_color": text_color,
+ }
+```
+
+## Template variables
+
+The [`get_template_data()`](../../../reference/api/#django_components.Component.get_template_data) method is the primary way to provide variables to your HTML template. It receives the component inputs and returns a dictionary of data that will be available in the template.
+
+If [`get_template_data()`](../../../reference/api/#django_components.Component.get_template_data) returns `None`, an empty dictionary will be used.
+
+```python
+class ProfileCard(Component):
+ template_file = "profile_card.html"
+
+ class Kwargs(NamedTuple):
+ user_id: int
+ show_details: bool
+
+ def get_template_data(self, args, kwargs: Kwargs, slots, context):
+ user = User.objects.get(id=kwargs.user_id)
+
+ # Process and transform inputs
+ return {
+ "user": user,
+ "show_details": kwargs.show_details,
+ "user_joined_days": (timezone.now() - user.date_joined).days,
+ }
+```
+
+In your template, you can then use these variables:
+
+```django
+
+
{{ user.username }}
+
+ {% if show_details %}
+
Member for {{ user_joined_days }} days
+
Email: {{ user.email }}
+ {% endif %}
+
+```
+
+### Legacy `get_context_data()`
+
+The [`get_context_data()`](../../../reference/api/#django_components.Component.get_context_data) method is the legacy way to provide variables to your HTML template. It serves the same purpose as [`get_template_data()`](../../../reference/api/#django_components.Component.get_template_data) - it receives the component inputs and returns a dictionary of data that will be available in the template.
+
+However, [`get_context_data()`](../../../reference/api/#django_components.Component.get_context_data) has a few drawbacks:
+
+- It does NOT receive the `slots` and `context` parameters.
+- The `args` and `kwargs` parameters are given as variadic `*args` and `**kwargs` parameters. As such, they cannot be typed.
+
+```python
+class ProfileCard(Component):
+ template_file = "profile_card.html"
+
+ def get_context_data(self, user_id, show_details=False, *args, **kwargs):
+ user = User.objects.get(id=user_id)
+ return {
+ "user": user,
+ "show_details": show_details,
+ }
+```
+
+There is a slight difference between [`get_context_data()`](../../../reference/api/#django_components.Component.get_context_data) and [`get_template_data()`](../../../reference/api/#django_components.Component.get_template_data)
+when rendering a component with the [`{% component %}`](../../../reference/template_tags/#component) tag.
+
+For example if you have component that accepts kwarg `date`:
+
+```py
+class MyComponent(Component):
+ def get_context_data(self, date, *args, **kwargs):
+ return {
+ "date": date,
+ }
+
+ def get_template_data(self, args, kwargs, slots, context):
+ return {
+ "date": kwargs["date"],
+ }
+```
+
+The difference is that:
+
+- With [`get_context_data()`](../../../reference/api/#django_components.Component.get_context_data), you can pass `date` either as arg or kwarg:
+
+ ```django
+ ✅
+ {% component "my_component" date=some_date %}
+ {% component "my_component" some_date %}
+ ```
+
+- But with [`get_template_data()`](../../../reference/api/#django_components.Component.get_template_data), `date` MUST be passed as kwarg:
+
+ ```django
+ ✅
+ {% component "my_component" date=some_date %}
+
+ ❌
+ {% component "my_component" some_date %}
+ ```
+
+!!! warning
+
+ [`get_template_data()`](../../../reference/api/#django_components.Component.get_template_data)
+ and [`get_context_data()`](../../../reference/api/#django_components.Component.get_context_data)
+ are mutually exclusive.
+
+ If both methods return non-empty dictionaries, an error will be raised.
+
+!!! note
+
+ The `get_context_data()` method will be removed in v2.
+
+## Accessing component inputs
+
+The component inputs are available in 3 ways:
+
+### Function arguments
+
+The data methods receive the inputs as parameters directly.
+
+```python
+class ProfileCard(Component):
+ # Access inputs directly as parameters
+ def get_template_data(self, args, kwargs, slots, context):
+ return {
+ "user_id": args[0],
+ "show_details": kwargs["show_details"],
+ }
+```
+
+!!! info
+
+ By default, the `args` parameter is a list, while `kwargs` and `slots` are dictionaries.
+
+ If you add typing to your component with
+ [`Args`](../../../reference/api/#django_components.Component.Args),
+ [`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
+ or [`Slots`](../../../reference/api/#django_components.Component.Slots) classes,
+ the respective inputs will be given as instances of these classes.
+
+ Learn more about [Component typing](../../fundamentals/typing_and_validation).
+
+ ```py
+ class ProfileCard(Component):
+ class Args(NamedTuple):
+ user_id: int
+
+ class Kwargs(NamedTuple):
+ show_details: bool
+
+ # Access inputs directly as parameters
+ def get_template_data(self, args: Args, kwargs: Kwargs, slots, context):
+ return {
+ "user_id": args.user_id,
+ "show_details": kwargs.show_details,
+ }
+ ```
+
+### `args`, `kwargs`, `slots` properties
+
+In other methods, you can access the inputs via
+[`self.args`](../../../reference/api/#django_components.Component.args),
+[`self.kwargs`](../../../reference/api/#django_components.Component.kwargs),
+and [`self.slots`](../../../reference/api/#django_components.Component.slots) properties:
+
+```py
+class ProfileCard(Component):
+ def on_render_before(self, context: Context, template: Optional[Template]):
+ # Access inputs via self.args, self.kwargs, self.slots
+ self.args[0]
+ self.kwargs.get("show_details", False)
+ self.slots["footer"]
+```
+
+!!! info
+
+ These properties work the same way as `args`, `kwargs`, and `slots` parameters in the data methods:
+
+ By default, the `args` property is a list, while `kwargs` and `slots` are dictionaries.
+
+ If you add typing to your component with
+ [`Args`](../../../reference/api/#django_components.Component.Args),
+ [`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
+ or [`Slots`](../../../reference/api/#django_components.Component.Slots) classes,
+ the respective inputs will be given as instances of these classes.
+
+ Learn more about [Component typing](../../fundamentals/typing_and_validation).
+
+ ```py
+ class ProfileCard(Component):
+ class Args(NamedTuple):
+ user_id: int
+
+ class Kwargs(NamedTuple):
+ show_details: bool
+
+ def get_template_data(self, args: Args, kwargs: Kwargs, slots, context):
+ return {
+ "user_id": self.args.user_id,
+ "show_details": self.kwargs.show_details,
+ }
+ ```
+
+
+
+### `input` property (low-level)
+
+!!! warning
+
+ The `input` property is deprecated and will be removed in v1.
+
+ Instead, use properties defined on the
+ [`Component`](../../../reference/api/#django_components.Component) class
+ directly like
+ [`self.context`](../../../reference/api/#django_components.Component.context).
+
+ To access the unmodified inputs, use
+ [`self.raw_args`](../../../reference/api/#django_components.Component.raw_args),
+ [`self.raw_kwargs`](../../../reference/api/#django_components.Component.raw_kwargs),
+ and [`self.raw_slots`](../../../reference/api/#django_components.Component.raw_slots) properties.
+
+The previous two approaches allow you to access only the most important inputs.
+
+There are additional settings that may be passed to components.
+If you need to access these, you can use [`self.input`](../../../reference/api/#django_components.Component.input) property
+for a low-level access to all the inputs.
+
+The `input` property contains all the inputs passed to the component (instance of [`ComponentInput`](../../../reference/api/#django_components.ComponentInput)).
+
+This includes:
+
+- [`input.args`](../../../reference/api/#django_components.ComponentInput.args) - List of positional arguments
+- [`input.kwargs`](../../../reference/api/#django_components.ComponentInput.kwargs) - Dictionary of keyword arguments
+- [`input.slots`](../../../reference/api/#django_components.ComponentInput.slots) - Dictionary of slots. Values are normalized to [`Slot`](../../../reference/api/#django_components.Slot) instances
+- [`input.context`](../../../reference/api/#django_components.ComponentInput.context) - [`Context`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context) object that should be used to render the component
+- [`input.type`](../../../reference/api/#django_components.ComponentInput.type) - The type of the component (document, fragment)
+- [`input.render_dependencies`](../../../reference/api/#django_components.ComponentInput.render_dependencies) - Whether to render dependencies (CSS, JS)
+
+```python
+class ProfileCard(Component):
+ def get_template_data(self, args, kwargs, slots, context):
+ # Access positional arguments
+ user_id = self.input.args[0] if self.input.args else None
+
+ # Access keyword arguments
+ show_details = self.input.kwargs.get("show_details", False)
+
+ # Render component differently depending on the type
+ if self.input.type == "fragment":
+ ...
+
+ return {
+ "user_id": user_id,
+ "show_details": show_details,
+ }
+```
+
+!!! info
+
+ Unlike the parameters passed to the data methods, the `args`, `kwargs`, and `slots` in `self.input` property are always lists and dictionaries,
+ regardless of whether you added typing classes to your component (like [`Args`](../../../reference/api/#django_components.Component.Args),
+ [`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
+ or [`Slots`](../../../reference/api/#django_components.Component.Slots)).
+
+## Default values
+
+You can use [`Defaults`](../../../reference/api/#django_components.Component.Defaults) class to provide default values for your inputs.
+
+These defaults will be applied either when:
+
+- The input is not provided at rendering time
+- The input is provided as `None`
+
+When you then access the inputs in your data methods, the default values will be already applied.
+
+Read more about [Component Defaults](./component_defaults.md).
+
+```py
+from django_components import Component, Default, register
+
+@register("profile_card")
+class ProfileCard(Component):
+ class Kwargs(NamedTuple):
+ show_details: bool
+
+ class Defaults:
+ show_details = True
+
+ # show_details will be set to True if `None` or missing
+ def get_template_data(self, args, kwargs: Kwargs, slots, context):
+ return {
+ "show_details": kwargs.show_details,
+ }
+
+ ...
+```
+
+!!! warning
+
+ When typing your components with [`Args`](../../../reference/api/#django_components.Component.Args),
+ [`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
+ or [`Slots`](../../../reference/api/#django_components.Component.Slots) classes,
+ you may be inclined to define the defaults in the classes.
+
+ ```py
+ class ProfileCard(Component):
+ class Kwargs(NamedTuple):
+ show_details: bool = True
+ ```
+
+ This is **NOT recommended**, because:
+
+ - The defaults will NOT be applied to inputs when using [`self.raw_kwargs`](../../../reference/api/#django_components.Component.raw_kwargs) property.
+ - The defaults will NOT be applied when a field is given but set to `None`.
+
+ Instead, define the defaults in the [`Defaults`](../../../reference/api/#django_components.Component.Defaults) class.
+
+## Accessing Render API
+
+All three data methods have access to the Component's [Render API](../render_api), which includes:
+
+- [`self.args`](../render_api/#args) - The positional arguments for the current render call
+- [`self.kwargs`](../render_api/#kwargs) - The keyword arguments for the current render call
+- [`self.slots`](../render_api/#slots) - The slots for the current render call
+- [`self.raw_args`](../render_api/#args) - Unmodified positional arguments for the current render call
+- [`self.raw_kwargs`](../render_api/#kwargs) - Unmodified keyword arguments for the current render call
+- [`self.raw_slots`](../render_api/#slots) - Unmodified slots for the current render call
+- [`self.context`](../render_api/#context) - The context for the current render call
+- [`self.id`](../render_api/#component-id) - The unique ID for the current render call
+- [`self.request`](../render_api/#request-and-context-processors) - The request object
+- [`self.context_processors_data`](../render_api/#request-and-context-processors) - Data from Django's context processors
+- [`self.inject()`](../render_api/#provide-inject) - Inject data into the component
+- [`self.registry`](../render_api/#template-tag-metadata) - The [`ComponentRegistry`](../../../reference/api/#django_components.ComponentRegistry) instance
+- [`self.registered_name`](../render_api/#template-tag-metadata) - The name under which the component was registered
+- [`self.outer_context`](../render_api/#template-tag-metadata) - The context outside of the [`{% component %}`](../../../reference/template_tags#component) tag
+- `self.deps_strategy` - The strategy for rendering dependencies
+
+## Type hints
+
+### Typing inputs
+
+You can add type hints for the component inputs to ensure that the component logic is correct.
+
+For this, define the [`Args`](../../../reference/api/#django_components.Component.Args),
+[`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
+and [`Slots`](../../../reference/api/#django_components.Component.Slots) classes,
+and then add type hints to the data methods.
+
+This will also validate the inputs at runtime, as the type classes will be instantiated with the inputs.
+
+Read more about [Component typing](../../fundamentals/typing_and_validation).
+
+```python
+from typing import NamedTuple, Optional
+from django_components import Component, SlotInput
+
+class Button(Component):
+ class Args(NamedTuple):
+ name: str
+
+ class Kwargs(NamedTuple):
+ surname: str
+ maybe_var: Optional[int] = None # May be omitted
+
+ class Slots(NamedTuple):
+ my_slot: Optional[SlotInput] = None
+ footer: SlotInput
+
+ # Use the above classes to add type hints to the data method
+ def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
+ # The parameters are instances of the classes we defined
+ assert isinstance(args, Button.Args)
+ assert isinstance(kwargs, Button.Kwargs)
+ assert isinstance(slots, Button.Slots)
+```
+
+!!! note
+
+ To access "untyped" inputs, use [`self.raw_args`](../../../reference/api/#django_components.Component.raw_args),
+ [`self.raw_kwargs`](../../../reference/api/#django_components.Component.raw_kwargs),
+ and [`self.raw_slots`](../../../reference/api/#django_components.Component.raw_slots) properties.
+
+ These are plain lists and dictionaries, even when you added typing to your component.
+
+### Typing data
+
+In the same fashion, you can add types and validation for the data that should be RETURNED from each data method.
+
+For this, set the [`TemplateData`](../../../reference/api/#django_components.Component.TemplateData),
+[`JsData`](../../../reference/api/#django_components.Component.JsData),
+and [`CssData`](../../../reference/api/#django_components.Component.CssData) classes on the component class.
+
+For each data method, you can either return a plain dictionary with the data, or an instance of the respective data class.
+
+```python
+from typing import NamedTuple
+from django_components import Component
+
+class Button(Component):
+ class TemplateData(NamedTuple):
+ data1: str
+ data2: int
+
+ class JsData(NamedTuple):
+ js_data1: str
+ js_data2: int
+
+ class CssData(NamedTuple):
+ css_data1: str
+ css_data2: int
+
+ def get_template_data(self, args, kwargs, slots, context):
+ return Button.TemplateData(
+ data1="...",
+ data2=123,
+ )
+
+ def get_js_data(self, args, kwargs, slots, context):
+ return Button.JsData(
+ js_data1="...",
+ js_data2=123,
+ )
+
+ def get_css_data(self, args, kwargs, slots, context):
+ return Button.CssData(
+ css_data1="...",
+ css_data2=123,
+ )
+```
+
+## Pass-through kwargs
+
+It's best practice to explicitly define what args and kwargs a component accepts.
+
+However, if you want a looser setup, you can easily write components that accept any number
+of kwargs, and pass them all to the template
+(similar to [django-cotton](https://github.com/wrabit/django-cotton)).
+
+To do that, simply return the `kwargs` dictionary itself from [`get_template_data()`](../../../reference/api/#django_components.Component.get_template_data):
+
+```py
+class MyComponent(Component):
+ def get_template_data(self, args, kwargs, slots, context):
+ return kwargs
+```
+
+You can do the same for [`get_js_data()`](../../../reference/api/#django_components.Component.get_js_data) and [`get_css_data()`](../../../reference/api/#django_components.Component.get_css_data), if needed:
+
+```py
+class MyComponent(Component):
+ def get_js_data(self, args, kwargs, slots, context):
+ return kwargs
+
+ def get_css_data(self, args, kwargs, slots, context):
+ return kwargs
+```
diff --git a/docs/concepts/fundamentals/http_request.md b/docs/concepts/fundamentals/http_request.md
index 06c9d586..148fdc7b 100644
--- a/docs/concepts/fundamentals/http_request.md
+++ b/docs/concepts/fundamentals/http_request.md
@@ -1,25 +1,22 @@
-The most common use of django-components is to render HTML for a given request. As such,
+The most common use of django-components is to render HTML when the server receives a request. As such,
there are a few features that are dependent on the request object.
-## Passing and accessing HttpRequest
+## Passing the HttpRequest object
-In regular Django templates, the request object is available only within the `RequestContext`.
+In regular Django templates, the request object is available only within the [`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext).
-In Components, you can either use `RequestContext`, or pass the `request` object
+In Components, you can either use [`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext), or pass the `request` object
explicitly to [`Component.render()`](../../../reference/api#django_components.Component.render) and
[`Component.render_to_response()`](../../../reference/api#django_components.Component.render_to_response).
-When a component is nested in another, the child component uses parent's `request` object.
+So the request object is available to components either when:
-You can access the request object under [`Component.request`](../../../reference/api#django_components.Component.request):
+- The component is rendered with [`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext) (Regular Django behavior)
+- The component is rendered with a regular [`Context`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context) (or none), but you set the `request` kwarg
+ of [`Component.render()`](../../../reference/api#django_components.Component.render).
+- The component is nested and the parent has access to the request object.
```python
-class MyComponent(Component):
- def get_context_data(self):
- return {
- 'user_id': self.request.GET['user_id'],
- }
-
# ✅ With request
MyComponent.render(request=request)
MyComponent.render(context=RequestContext(request, {}))
@@ -29,31 +26,7 @@ MyComponent.render()
MyComponent.render(context=Context({}))
```
-## Context Processors
-
-Components support Django's [context processors](https://docs.djangoproject.com/en/5.1/ref/templates/api/#using-requestcontext).
-
-In regular Django templates, the context processors are applied only when the template is rendered with `RequestContext`.
-
-Components allow you to pass the `request` object explicitly. Thus, the context processors are applied to components either when:
-
-- The component is rendered with `RequestContext` (Regular Django behavior)
-- The component is rendered with a regular `Context` (or none), but you set the `request` kwarg
- of [`Component.render()`](../../../reference/api#django_components.Component.render).
-- The component is nested in another component that matches one of the two conditions above.
-
-```python
-# ❌ No context processors
-rendered = MyComponent.render()
-rendered = MyComponent.render(Context({}))
-
-# ✅ With context processors
-rendered = MyComponent.render(request=request)
-rendered = MyComponent.render(Context({}), request=request)
-rendered = MyComponent.render(RequestContext(request, {}))
-```
-
-When a component is rendered within a template with [`{% component %}`](../../../reference/template_tags#component) tag, context processors are available depending on whether the template is rendered with `RequestContext` or not.
+When a component is rendered within a template with [`{% component %}`](../../../reference/template_tags#component) tag, the request object is available depending on whether the template is rendered with [`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext) or not.
```python
template = Template("""
@@ -62,13 +35,33 @@ template = Template("""
""")
-# ❌ No context processors
+# ❌ No request
rendered = template.render(Context({}))
-# ✅ With context processors
+# ✅ With request
rendered = template.render(RequestContext(request, {}))
```
+## Accessing the HttpRequest object
+
+When the component has access to the `request` object, the request object will be available in [`Component.request`](../../../reference/api/#django_components.Component.request).
+
+```python
+class MyComponent(Component):
+ def get_template_data(self, args, kwargs, slots, context):
+ return {
+ 'user_id': self.request.GET['user_id'],
+ }
+```
+
+## Context Processors
+
+Components support Django's [context processors](https://docs.djangoproject.com/en/5.2/ref/templates/api/#using-requestcontext).
+
+In regular Django templates, the context processors are applied only when the template is rendered with [`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext).
+
+In Components, the context processors are applied when the component has access to the `request` object.
+
### Accessing context processors data
The data from context processors is automatically available within the component's template.
@@ -84,13 +77,21 @@ class MyComponent(Component):
MyComponent.render(request=request)
```
-You can also access the context processors data from within [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data) and other methods under [`Component.context_processors_data`](../../../reference/api#django_components.Component.context_processors_data).
+You can also access the context processors data from within [`get_template_data()`](../../../reference/api#django_components.Component.get_template_data) and other methods under [`Component.context_processors_data`](../../../reference/api#django_components.Component.context_processors_data).
```python
class MyComponent(Component):
- def get_context_data(self):
+ def get_template_data(self, args, kwargs, slots, context):
csrf_token = self.context_processors_data['csrf_token']
return {
'csrf_token': csrf_token,
}
```
+
+This is a dictionary with the context processors data.
+
+If the request object is not available, then [`self.context_processors_data`](../../../reference/api/#django_components.Component.context_processors_data) will be an empty dictionary.
+
+!!! warning
+
+ The [`self.context_processors_data`](../../../reference/api/#django_components.Component.context_processors_data) object is generated dynamically, so changes to it are not persisted.
diff --git a/docs/concepts/fundamentals/render_api.md b/docs/concepts/fundamentals/render_api.md
new file mode 100644
index 00000000..d41708d4
--- /dev/null
+++ b/docs/concepts/fundamentals/render_api.md
@@ -0,0 +1,376 @@
+When a component is being rendered, whether with [`Component.render()`](../../../reference/api#django_components.Component.render)
+or [`{% component %}`](../../../reference/template_tags#component), a component instance is populated with the current inputs and context. This allows you to access things like component inputs.
+
+We refer to these render-time-only methods and attributes as the "Render API".
+
+Render API is available inside these [`Component`](../../../reference/api#django_components.Component) methods:
+
+- [`get_template_data()`](../../../reference/api#django_components.Component.get_template_data)
+- [`get_js_data()`](../../../reference/api#django_components.Component.get_js_data)
+- [`get_css_data()`](../../../reference/api#django_components.Component.get_css_data)
+- [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data)
+- [`on_render_before()`](../../../reference/api#django_components.Component.on_render_before)
+- [`on_render()`](../../../reference/api#django_components.Component.on_render)
+- [`on_render_after()`](../../../reference/api#django_components.Component.on_render_after)
+
+Example:
+
+```python
+class Table(Component):
+ def on_render_before(self, context, template):
+ # Access component's ID
+ assert self.id == "c1A2b3c"
+
+ # Access component's inputs, slots and context
+ assert self.args == [123, "str"]
+ assert self.kwargs == {"variable": "test", "another": 1}
+ footer_slot = self.slots["footer"]
+ some_var = self.context["some_var"]
+
+ def get_template_data(self, args, kwargs, slots, context):
+ # Access the request object and Django's context processors, if available
+ assert self.request.GET == {"query": "something"}
+ assert self.context_processors_data['user'].username == "admin"
+
+rendered = Table.render(
+ kwargs={"variable": "test", "another": 1},
+ args=(123, "str"),
+ slots={"footer": "MY_SLOT"},
+)
+```
+
+## Overview
+
+The Render API includes:
+
+- Component inputs:
+ - [`self.args`](../render_api/#args) - The positional arguments for the current render call
+ - [`self.kwargs`](../render_api/#kwargs) - The keyword arguments for the current render call
+ - [`self.slots`](../render_api/#slots) - The slots for the current render call
+ - [`self.raw_args`](../render_api/#args) - Unmodified positional arguments for the current render call
+ - [`self.raw_kwargs`](../render_api/#kwargs) - Unmodified keyword arguments for the current render call
+ - [`self.raw_slots`](../render_api/#slots) - Unmodified slots for the current render call
+ - [`self.context`](../render_api/#context) - The context for the current render call
+ - [`self.deps_strategy`](../../advanced/rendering_js_css#dependencies-strategies) - The strategy for rendering dependencies
+
+- Request-related:
+ - [`self.request`](../render_api/#request-and-context-processors) - The request object (if available)
+ - [`self.context_processors_data`](../render_api/#request-and-context-processors) - Data from Django's context processors
+
+- Provide / inject:
+ - [`self.inject()`](../render_api/#provide-inject) - Inject data into the component
+
+- Template tag metadata:
+ - [`self.node`](../render_api/#template-tag-metadata) - The [`ComponentNode`](../../../reference/api/#django_components.ComponentNode) instance
+ - [`self.registry`](../render_api/#template-tag-metadata) - The [`ComponentRegistry`](../../../reference/api/#django_components.ComponentRegistry) instance
+ - [`self.registered_name`](../render_api/#template-tag-metadata) - The name under which the component was registered
+ - [`self.outer_context`](../render_api/#template-tag-metadata) - The context outside of the [`{% component %}`](../../../reference/template_tags#component) tag
+
+- Other metadata:
+ - [`self.id`](../render_api/#component-id) - The unique ID for the current render call
+
+## Component inputs
+
+### Args
+
+The `args` argument as passed to
+[`Component.get_template_data()`](../../../reference/api/#django_components.Component.get_template_data).
+
+If you defined the [`Component.Args`](../../../reference/api/#django_components.Component.Args) class,
+then the [`Component.args`](../../../reference/api/#django_components.Component.args) property will return an instance of that class.
+
+Otherwise, `args` will be a plain list.
+
+Use [`self.raw_args`](../../../reference/api/#django_components.Component.raw_args)
+to access the positional arguments as a plain list irrespective of [`Component.Args`](../../../reference/api/#django_components.Component.Args).
+
+**Example:**
+
+With `Args` class:
+
+```python
+from django_components import Component
+
+class Table(Component):
+ class Args(NamedTuple):
+ page: int
+ per_page: int
+
+ def on_render_before(self, context: Context, template: Optional[Template]) -> None:
+ assert self.args.page == 123
+ assert self.args.per_page == 10
+
+rendered = Table.render(
+ args=[123, 10],
+)
+```
+
+Without `Args` class:
+
+```python
+from django_components import Component
+
+class Table(Component):
+ def on_render_before(self, context: Context, template: Optional[Template]) -> None:
+ assert self.args[0] == 123
+ assert self.args[1] == 10
+```
+
+### Kwargs
+
+The `kwargs` argument as passed to
+[`Component.get_template_data()`](../../../reference/api/#django_components.Component.get_template_data).
+
+If you defined the [`Component.Kwargs`](../../../reference/api/#django_components.Component.Kwargs) class,
+then the [`Component.kwargs`](../../../reference/api/#django_components.Component.kwargs) property will return an instance of that class.
+
+Otherwise, `kwargs` will be a plain dictionary.
+
+Use [`self.raw_kwargs`](../../../reference/api/#django_components.Component.raw_kwargs)
+to access the keyword arguments as a plain dictionary irrespective of [`Component.Kwargs`](../../../reference/api/#django_components.Component.Kwargs).
+
+**Example:**
+
+With `Kwargs` class:
+
+```python
+from django_components import Component
+
+class Table(Component):
+ class Kwargs(NamedTuple):
+ page: int
+ per_page: int
+
+ def on_render_before(self, context: Context, template: Optional[Template]) -> None:
+ assert self.kwargs.page == 123
+ assert self.kwargs.per_page == 10
+
+rendered = Table.render(
+ kwargs={"page": 123, "per_page": 10},
+)
+```
+
+Without `Kwargs` class:
+
+```python
+from django_components import Component
+
+class Table(Component):
+ def on_render_before(self, context: Context, template: Optional[Template]) -> None:
+ assert self.kwargs["page"] == 123
+ assert self.kwargs["per_page"] == 10
+```
+
+### Slots
+
+The `slots` argument as passed to
+[`Component.get_template_data()`](../../../reference/api/#django_components.Component.get_template_data).
+
+If you defined the [`Component.Slots`](../../../reference/api/#django_components.Component.Slots) class,
+then the [`Component.slots`](../../../reference/api/#django_components.Component.slots) property will return an instance of that class.
+
+Otherwise, `slots` will be a plain dictionary.
+
+Use [`self.raw_slots`](../../../reference/api/#django_components.Component.raw_slots)
+to access the slots as a plain dictionary irrespective of [`Component.Slots`](../../../reference/api/#django_components.Component.Slots).
+
+**Example:**
+
+With `Slots` class:
+
+```python
+from django_components import Component, Slot, SlotInput
+
+class Table(Component):
+ class Slots(NamedTuple):
+ header: SlotInput
+ footer: SlotInput
+
+ def on_render_before(self, context: Context, template: Optional[Template]) -> None:
+ assert isinstance(self.slots.header, Slot)
+ assert isinstance(self.slots.footer, Slot)
+
+rendered = Table.render(
+ slots={
+ "header": "MY_HEADER",
+ "footer": lambda ctx: "FOOTER: " + ctx.data["user_id"],
+ },
+)
+```
+
+Without `Slots` class:
+
+```python
+from django_components import Component, Slot, SlotInput
+
+class Table(Component):
+ def on_render_before(self, context: Context, template: Optional[Template]) -> None:
+ assert isinstance(self.slots["header"], Slot)
+ assert isinstance(self.slots["footer"], Slot)
+```
+
+### Context
+
+The `context` argument as passed to
+[`Component.get_template_data()`](../../../reference/api/#django_components.Component.get_template_data).
+
+This is Django's [Context](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context)
+with which the component template is rendered.
+
+If the root component or template was rendered with
+[`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext)
+then this will be an instance of `RequestContext`.
+
+Whether the context variables defined in `context` are available to the template depends on the
+[context behavior mode](../../../reference/settings#django_components.app_settings.ComponentsSettings.context_behavior):
+
+- In `"django"` context behavior mode, the template will have access to the keys of this context.
+
+- In `"isolated"` context behavior mode, the template will NOT have access to this context,
+ and data MUST be passed via component's args and kwargs.
+
+## Component ID
+
+Component ID (or render ID) is a unique identifier for the current render call.
+
+That means that if you call [`Component.render()`](../../../reference/api#django_components.Component.render)
+multiple times, the ID will be different for each call.
+
+It is available as [`self.id`](../../../reference/api#django_components.Component.id).
+
+The ID is a 7-letter alphanumeric string in the format `cXXXXXX`,
+where `XXXXXX` is a random string of 6 alphanumeric characters (case-sensitive).
+
+E.g. `c1a2b3c`.
+
+A single render ID has a chance of collision 1 in 57 billion. However, due to birthday paradox, the chance of collision increases to 1% when approaching ~33K render IDs.
+
+Thus, there is currently a soft-cap of ~30K components rendered on a single page.
+
+If you need to expand this limit, please open an issue on GitHub.
+
+```python
+class Table(Component):
+ def get_template_data(self, args, kwargs, slots, context):
+ # Access component's ID
+ assert self.id == "c1A2b3c"
+```
+
+## Request and context processors
+
+Components have access to the request object and context processors data if the component was:
+
+- Given a [`request`](../../../reference/api/#django_components.Component.render) kwarg directly
+- Rendered with [`RenderContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext)
+- Nested in another component for which any of these conditions is true
+
+Then the request object will be available in [`self.request`](../../../reference/api/#django_components.Component.request).
+
+If the request object is available, you will also be able to access the [`context processors`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#configuring-an-engine) data in [`self.context_processors_data`](../../../reference/api/#django_components.Component.context_processors_data).
+
+This is a dictionary with the context processors data.
+
+If the request object is not available, then [`self.context_processors_data`](../../../reference/api/#django_components.Component.context_processors_data) will be an empty dictionary.
+
+Read more about the request object and context processors in the [HTTP Request](./http_request.md) section.
+
+```python
+from django.http import HttpRequest
+
+class Table(Component):
+ def get_template_data(self, args, kwargs, slots, context):
+ # Access the request object and Django's context processors
+ assert self.request.GET == {"query": "something"}
+ assert self.context_processors_data['user'].username == "admin"
+
+rendered = Table.render(
+ request=HttpRequest(),
+)
+```
+
+## Provide / Inject
+
+Components support a provide / inject system as known from Vue or React.
+
+When rendering the component, you can call [`self.inject()`](../../../reference/api/#django_components.Component.inject) with the key of the data you want to inject.
+
+The object returned by [`self.inject()`](../../../reference/api/#django_components.Component.inject)
+
+To provide data to components, use the [`{% provide %}`](../../../reference/template_tags#provide) template tag.
+
+Read more about [Provide / Inject](../advanced/provide_inject.md).
+
+```python
+class Table(Component):
+ def get_template_data(self, args, kwargs, slots, context):
+ # Access provided data
+ data = self.inject("some_data")
+ assert data.some_data == "some_data"
+```
+
+## Template tag metadata
+
+If the component is rendered with [`{% component %}`](../../../reference/template_tags#component) template tag,
+the following metadata is available:
+
+- [`self.node`](../../../reference/api/#django_components.Component.node) - The [`ComponentNode`](../../../reference/api/#django_components.ComponentNode) instance
+- [`self.registry`](../../../reference/api/#django_components.Component.registry) - The [`ComponentRegistry`](../../../reference/api/#django_components.ComponentRegistry) instance
+ that was used to render the component
+- [`self.registered_name`](../../../reference/api/#django_components.Component.registered_name) - The name under which the component was registered
+- [`self.outer_context`](../../../reference/api/#django_components.Component.outer_context) - The context outside of the [`{% component %}`](../../../reference/template_tags#component) tag
+
+ ```django
+ {% with abc=123 %}
+ {{ abc }} {# <--- This is in outer context #}
+ {% component "my_component" / %}
+ {% endwith %}
+ ```
+
+You can use these to check whether the component was rendered inside a template with [`{% component %}`](../../../reference/template_tags#component) tag
+or in Python with [`Component.render()`](../../../reference/api/#django_components.Component.render).
+
+```python
+class MyComponent(Component):
+ def get_template_data(self, args, kwargs, slots, context):
+ if self.registered_name is None:
+ # Do something for the render() function
+ else:
+ # Do something for the {% component %} template tag
+```
+
+You can access the [`ComponentNode`](../../../reference/api/#django_components.ComponentNode) under [`Component.node`](../../../reference/api/#django_components.Component.node):
+
+```py
+class MyComponent(Component):
+ def get_template_data(self, context, template):
+ if self.node is not None:
+ assert self.node.name == "my_component"
+```
+
+Accessing the [`ComponentNode`](../../../reference/api/#django_components.ComponentNode) is mostly useful for extensions, which can modify their behaviour based on the source of the Component.
+
+For example, if `MyComponent` was used in another component - that is,
+with a `{% component "my_component" %}` tag
+in a template that belongs to another component - then you can use
+[`self.node.template_component`](../../../reference/api/#django_components.ComponentNode.template_component)
+to access the owner [`Component`](../../../reference/api/#django_components.Component) class.
+
+```djc_py
+class Parent(Component):
+ template: types.django_html = """
+
+ {% component "my_component" / %}
+
+ """
+
+@register("my_component")
+class MyComponent(Component):
+ def get_template_data(self, context, template):
+ if self.node is not None:
+ assert self.node.template_component == Parent
+```
+
+!!! info
+
+ `Component.node` is `None` if the component is created by [`Component.render()`](../../../reference/api/#django_components.Component.render)
+ (but you can pass in the `node` kwarg yourself).
diff --git a/docs/concepts/fundamentals/rendering_components.md b/docs/concepts/fundamentals/rendering_components.md
new file mode 100644
index 00000000..38883204
--- /dev/null
+++ b/docs/concepts/fundamentals/rendering_components.md
@@ -0,0 +1,627 @@
+Your components can be rendered either within your Django templates, or directly in Python code.
+
+## Overview
+
+Django Components provides three main methods to render components:
+
+- [`{% component %}` tag](#component-tag) - Renders the component within your Django templates
+- [`Component.render()` method](#render-method) - Renders the component to a string
+- [`Component.render_to_response()` method](#render-to-response-method) - Renders the component and wraps it in an HTTP response
+
+## `{% component %}` tag
+
+Use the [`{% component %}`](../../../reference/template_tags#component) tag to render a component within your Django templates.
+
+The [`{% component %}`](../../../reference/template_tags#component) tag takes:
+
+- Component's registered name as the first positional argument,
+- Followed by any number of positional and keyword arguments.
+
+```django
+{% load component_tags %}
+
+```
+
+To pass in slots content, you can insert [`{% fill %}`](../../../reference/template_tags#fill) tags,
+directly within the [`{% component %}`](../../../reference/template_tags#component) tag to "fill" the slots:
+
+```django
+{% component "my_table" rows=rows headers=headers %}
+ {% fill "pagination" %}
+ < 1 | 2 | 3 >
+ {% endfill %}
+{% endcomponent %}
+```
+
+You can even nest [`{% fill %}`](../../../reference/template_tags#fill) tags within
+[`{% if %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#if),
+[`{% for %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#for)
+and other tags:
+
+```django
+{% component "my_table" rows=rows headers=headers %}
+ {% if rows %}
+ {% fill "pagination" %}
+ < 1 | 2 | 3 >
+ {% endfill %}
+ {% endif %}
+{% endcomponent %}
+```
+
+!!! info "Omitting the `component` keyword"
+
+ If you would like to omit the `component` keyword, and simply refer to your
+ components by their registered names:
+
+ ```django
+ {% button name="John" job="Developer" / %}
+ ```
+
+ You can do so by setting the "shorthand" [Tag formatter](../../advanced/tag_formatters) in the settings:
+
+ ```python
+ # settings.py
+ COMPONENTS = {
+ "tag_formatter": "django_components.component_shorthand_formatter",
+ }
+ ```
+
+!!! info "Extended template tag syntax"
+
+ Unlike regular Django template tags, django-components' tags offer extra features like
+ defining literal lists and dicts, and more. Read more about [Template tag syntax](../template_tag_syntax).
+
+### Registering components
+
+For a component to be renderable with the [`{% component %}`](../../../reference/template_tags#component) tag, it must be first registered with the [`@register()`](../../../reference/api/#django_components.register) decorator.
+
+For example, if you register a component under the name `"button"`:
+
+```python
+from typing import NamedTuple
+from django_components import Component, register
+
+@register("button")
+class Button(Component):
+ template_file = "button.html"
+
+ class Kwargs(NamedTuple):
+ name: str
+ job: str
+
+ def get_template_data(self, args, kwargs, slots, context):
+ ...
+```
+
+Then you can render this component by using its registered name `"button"` in the template:
+
+```django
+{% component "button" name="John" job="Developer" / %}
+```
+
+As you can see above, the args and kwargs passed to the [`{% component %}`](../../../reference/template_tags#component) tag correspond
+to the component's input.
+
+For more details, read [Registering components](../../advanced/component_registry).
+
+!!! note "Why do I need to register components?"
+
+ TL;DR: To be able to share components as libraries, and because components can be registed with multiple registries / libraries.
+
+ Django-components allows to [share components across projects](../../advanced/component_libraries).
+
+ However, different projects may use different settings. For example, one project may prefer the "long" format:
+
+ ```django
+ {% component "button" name="John" job="Developer" / %}
+ ```
+
+ While the other may use the "short" format:
+
+ ```django
+ {% button name="John" job="Developer" / %}
+ ```
+
+ Both approaches are supported simultaneously for backwards compatibility, because django-components
+ started out with only the "long" format.
+
+ To avoid ambiguity, when you use a 3rd party library, it uses the syntax that the author
+ had configured for it.
+
+ So when you are creating a component, django-components need to know which registry the component
+ belongs to, so it knows which syntax to use.
+
+### Rendering templates
+
+If you have embedded the component in a Django template using the
+[`{% component %}`](../../reference/template_tags#component) tag:
+
+```django title="[project root]/templates/my_template.html"
+{% load component_tags %}
+
+ """)
+
+ rendered_template = template.render()
+ ```
+
+### Isolating components
+
+By default, components behave similarly to Django's
+[`{% include %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#include),
+and the template inside the component has access to the variables defined in the outer template.
+
+You can selectively isolate a component, using the `only` flag, so that the inner template
+can access only the data that was explicitly passed to it:
+
+```django
+{% component "name" positional_arg keyword_arg=value ... only / %}
+```
+
+Alternatively, you can set all components to be isolated by default, by setting
+[`context_behavior`](../../../reference/settings#django_components.app_settings.ComponentsSettings.context_behavior)
+to `"isolated"` in your settings:
+
+```python
+# settings.py
+COMPONENTS = {
+ "context_behavior": "isolated",
+}
+```
+
+## `render()` method
+
+The [`Component.render()`](../../../reference/api/#django_components.Component.render) method renders a component to a string.
+
+This is the equivalent of calling the [`{% component %}`](../template_tags#component) tag.
+
+```python
+from typing import NamedTuple, Optional
+from django_components import Component, SlotInput
+
+class Button(Component):
+ template_file = "button.html"
+
+ class Args(NamedTuple):
+ name: str
+
+ class Kwargs(NamedTuple):
+ surname: str
+ age: int
+
+ class Slots(NamedTuple):
+ footer: Optional[SlotInput] = None
+
+ def get_template_data(self, args, kwargs, slots, context):
+ ...
+
+Button.render(
+ args=["John"],
+ kwargs={
+ "surname": "Doe",
+ "age": 30,
+ },
+ slots={
+ "footer": "i AM A SLOT",
+ },
+)
+```
+
+[`Component.render()`](../../../reference/api/#django_components.Component.render) accepts the following arguments:
+
+- `args` - Positional arguments to pass to the component (as a list or tuple)
+- `kwargs` - Keyword arguments to pass to the component (as a dictionary)
+- `slots` - Slot content to pass to the component (as a dictionary)
+- `context` - Django context for rendering (can be a dictionary or a `Context` object)
+- `deps_strategy` - [Dependencies rendering strategy](#dependencies-rendering) (default: `"document"`)
+- `request` - [HTTP request object](../http_request), used for context processors (optional)
+
+All arguments are optional. If not provided, they default to empty values or sensible defaults.
+
+See the API reference for [`Component.render()`](../../../reference/api/#django_components.Component.render)
+for more details on the arguments.
+
+## `render_to_response()` method
+
+The [`Component.render_to_response()`](../../../reference/api/#django_components.Component.render_to_response)
+method works just like [`Component.render()`](../../../reference/api/#django_components.Component.render),
+but wraps the result in an HTTP response.
+
+It accepts all the same arguments as [`Component.render()`](../../../reference/api/#django_components.Component.render).
+
+Any extra arguments are passed to the [`HttpResponse`](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpResponse)
+constructor.
+
+```python
+from typing import NamedTuple, Optional
+from django_components import Component, SlotInput
+
+class Button(Component):
+ template_file = "button.html"
+
+ class Args(NamedTuple):
+ name: str
+
+ class Kwargs(NamedTuple):
+ surname: str
+ age: int
+
+ class Slots(NamedTuple):
+ footer: Optional[SlotInput] = None
+
+ def get_template_data(self, args, kwargs, slots, context):
+ ...
+
+# Render the component to an HttpResponse
+response = Button.render_to_response(
+ args=["John"],
+ kwargs={
+ "surname": "Doe",
+ "age": 30,
+ },
+ slots={
+ "footer": "i AM A SLOT",
+ },
+ # Additional response arguments
+ status=200,
+ headers={"X-Custom-Header": "Value"},
+)
+```
+
+This method is particularly useful in view functions, as you can return the result of the component directly:
+
+```python
+def profile_view(request, user_id):
+ return Button.render_to_response(
+ kwargs={
+ "surname": "Doe",
+ "age": 30,
+ },
+ request=request,
+ )
+```
+
+### Custom response classes
+
+By default, [`Component.render_to_response()`](../../../reference/api/#django_components.Component.render_to_response)
+returns a standard Django [`HttpResponse`](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpResponse).
+
+You can customize this by setting the [`response_class`](../../../reference/api/#django_components.Component.response_class)
+attribute on your component:
+
+```python
+from django.http import HttpResponse
+from django_components import Component
+
+class MyHttpResponse(HttpResponse):
+ ...
+
+class MyComponent(Component):
+ response_class = MyHttpResponse
+
+response = MyComponent.render_to_response()
+assert isinstance(response, MyHttpResponse)
+```
+
+## Dependencies rendering
+
+The rendered HTML may be used in different contexts (browser, email, etc), and each may need different handling of JS and CSS scripts.
+
+[`render()`](../../../reference/api/#django_components.Component.render) and [`render_to_response()`](../../../reference/api/#django_components.Component.render_to_response)
+accept a `deps_strategy` parameter, which controls where and how the JS / CSS are inserted into the HTML.
+
+The `deps_strategy` parameter is ultimately passed to [`render_dependencies()`](../../../reference/api/#django_components.render_dependencies).
+
+Learn more about [Rendering JS / CSS](../../advanced/rendering_js_css).
+
+There are six dependencies rendering strategies:
+
+- [`document`](../../advanced/rendering_js_css#document) (default)
+ - Smartly inserts JS / CSS into placeholders ([`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies)) or into `` and `` tags.
+ - Inserts extra script to allow `fragment` components to work.
+ - Assumes the HTML will be rendered in a JS-enabled browser.
+- [`fragment`](../../advanced/rendering_js_css#fragment)
+ - A lightweight HTML fragment to be inserted into a document with AJAX.
+ - Assumes the page was already rendered with `"document"` strategy.
+ - No JS / CSS included.
+- [`simple`](../../advanced/rendering_js_css#simple)
+ - Smartly insert JS / CSS into placeholders ([`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies)) or into `` and `` tags.
+ - No extra script loaded.
+- [`prepend`](../../advanced/rendering_js_css#prepend)
+ - Insert JS / CSS before the rendered HTML.
+ - Ignores the placeholders ([`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies)) and any ``/`` HTML tags.
+ - No extra script loaded.
+- [`append`](../../advanced/rendering_js_css#append)
+ - Insert JS / CSS after the rendered HTML.
+ - Ignores the placeholders ([`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies)) and any ``/`` HTML tags.
+ - No extra script loaded.
+- [`ignore`](../../advanced/rendering_js_css#ignore)
+ - HTML is left as-is. You can still process it with a different strategy later with
+ [`render_dependencies()`](../../../reference/api/#django_components.render_dependencies).
+ - Used for inserting rendered HTML into other components.
+
+!!! info
+
+ You can use the `"prepend"` and `"append"` strategies to force to output JS / CSS for components
+ that don't have neither the placeholders like [`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies), nor any ``/`` HTML tags:
+
+ ```py
+ rendered = Calendar.render_to_response(
+ request=request,
+ kwargs={
+ "date": request.GET.get("date", ""),
+ },
+ deps_strategy="append",
+ )
+ ```
+
+ Renders something like this:
+
+ ```html
+
+
+ ...
+
+
+
+
+ ```
+
+## Passing context
+
+The [`render()`](../../../reference/api/#django_components.Component.render) and [`render_to_response()`](../../../reference/api/#django_components.Component.render_to_response) methods accept an optional `context` argument.
+This sets the context within which the component is rendered.
+
+When a component is rendered within a template with the [`{% component %}`](../../../reference/template_tags#component)
+tag, this will be automatically set to the
+[Context](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context)
+instance that is used for rendering the template.
+
+When you call [`Component.render()`](../../../reference/api/#django_components.Component.render) directly from Python,
+there is no context object, so you can ignore this input most of the time.
+Instead, use `args`, `kwargs`, and `slots` to pass data to the component.
+
+However, you can pass
+[`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext)
+to the `context` argument, so that the component will gain access to the request object and will use
+[context processors](https://docs.djangoproject.com/en/5.2/ref/templates/api/#using-requestcontext).
+Read more on [Working with HTTP requests](../http_request).
+
+```py
+Button.render(
+ context=RequestContext(request),
+)
+```
+
+For advanced use cases, you can use `context` argument to "pre-render" the component in Python, and then
+pass the rendered output as plain string to the template. With this, the inner component is rendered as if
+it was within the template with [`{% component %}`](../../../reference/template_tags#component).
+
+```py
+class Button(Component):
+ def render(self, context, template):
+ # Pass `context` to Icon component so it is rendered
+ # as if nested within Button.
+ icon = Icon.render(
+ context=context,
+ args=["icon-name"],
+ deps_strategy="ignore",
+ )
+ # Update context with icon
+ with context.update({"icon": icon}):
+ return template.render(context)
+```
+
+!!! warning
+
+ Whether the variables defined in `context` are actually available in the template depends on the
+ [context behavior mode](../../../reference/settings#django_components.app_settings.ComponentsSettings.context_behavior):
+
+ - In `"django"` context behavior mode, the template will have access to the keys of this context.
+
+ - In `"isolated"` context behavior mode, the template will NOT have access to this context,
+ and data MUST be passed via component's args and kwargs.
+
+ Therefore, it's **strongly recommended** to not rely on defining variables on the context object,
+ but instead passing them through as `args` and `kwargs`
+
+ ❌ Don't do this:
+
+ ```python
+ html = ProfileCard.render(
+ context={"name": "John"},
+ )
+ ```
+
+ ✅ Do this:
+
+ ```python
+ html = ProfileCard.render(
+ kwargs={"name": "John"},
+ )
+ ```
+
+## Typing render methods
+
+Neither [`Component.render()`](../../../reference/api/#django_components.Component.render)
+nor [`Component.render_to_response()`](../../../reference/api/#django_components.Component.render_to_response)
+are typed, due to limitations of Python's type system.
+
+To add type hints, you can wrap the inputs
+in component's [`Args`](../../../reference/api/#django_components.Component.Args),
+[`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
+and [`Slots`](../../../reference/api/#django_components.Component.Slots) classes.
+
+Read more on [Typing and validation](../../fundamentals/typing_and_validation).
+
+```python
+from typing import NamedTuple, Optional
+from django_components import Component, Slot, SlotInput
+
+# Define the component with the types
+class Button(Component):
+ class Args(NamedTuple):
+ name: str
+
+ class Kwargs(NamedTuple):
+ surname: str
+ age: int
+
+ class Slots(NamedTuple):
+ my_slot: Optional[SlotInput] = None
+ footer: SlotInput
+
+# Add type hints to the render call
+Button.render(
+ args=Button.Args(
+ name="John",
+ ),
+ kwargs=Button.Kwargs(
+ surname="Doe",
+ age=30,
+ ),
+ slots=Button.Slots(
+ footer=Slot(lambda ctx: "Click me!"),
+ ),
+)
+```
+
+## Components as input
+
+django_components makes it possible to compose components in a "React-like" way,
+where you can render one component and use its output as input to another component:
+
+```python
+from django.utils.safestring import mark_safe
+
+# Render the inner component
+inner_html = InnerComponent.render(
+ kwargs={"some_data": "value"},
+ deps_strategy="ignore", # Important for nesting!
+)
+
+# Use inner component's output in the outer component
+outer_html = OuterComponent.render(
+ kwargs={
+ "content": mark_safe(inner_html), # Mark as safe to prevent escaping
+ },
+)
+```
+
+The key here is setting [`deps_strategy="ignore"`](../../advanced/rendering_js_css#ignore) for the inner component. This prevents duplicate
+rendering of JS / CSS dependencies when the outer component is rendered.
+
+When `deps_strategy="ignore"`:
+
+- No JS or CSS dependencies will be added to the output HTML
+- The component's content is rendered as-is
+- The outer component will take care of including all needed dependencies
+
+Read more about [Rendering JS / CSS](../../advanced/rendering_js_css).
+
+## Dynamic components
+
+Django components defines a special "dynamic" component ([`DynamicComponent`](../../../reference/components#django_components.components.dynamic.DynamicComponent)).
+
+Normally, you have to hard-code the component name in the template:
+
+```django
+{% component "button" / %}
+```
+
+The dynamic component allows you to dynamically render any component based on the `is` kwarg. This is similar
+to [Vue's dynamic components](https://vuejs.org/guide/essentials/component-basics#dynamic-components) (``).
+
+```django
+{% component "dynamic" is=table_comp data=table_data headers=table_headers %}
+ {% fill "pagination" %}
+ {% component "pagination" / %}
+ {% endfill %}
+{% endcomponent %}
+```
+
+The args, kwargs, and slot fills are all passed down to the underlying component.
+
+As with other components, the dynamic component can be rendered from Python:
+
+```py
+from django_components import DynamicComponent
+
+DynamicComponent.render(
+ kwargs={
+ "is": table_comp,
+ "data": table_data,
+ "headers": table_headers,
+ },
+ slots={
+ "pagination": PaginationComponent.render(
+ deps_strategy="ignore",
+ ),
+ },
+)
+```
+
+### Dynamic component name
+
+By default, the dynamic component is registered under the name `"dynamic"`. In case of a conflict,
+you can set the
+[`COMPONENTS.dynamic_component_name`](../../../reference/settings#django_components.app_settings.ComponentsSettings.dynamic_component_name)
+setting to change the name used for the dynamic components.
+
+```py
+# settings.py
+COMPONENTS = ComponentsSettings(
+ dynamic_component_name="my_dynamic",
+)
+```
+
+After which you will be able to use the dynamic component with the new name:
+
+```django
+{% component "my_dynamic" is=table_comp data=table_data headers=table_headers %}
+ {% fill "pagination" %}
+ {% component "pagination" / %}
+ {% endfill %}
+{% endcomponent %}
+```
+
+## HTML fragments
+
+Django-components provides a seamless integration with HTML fragments with AJAX ([HTML over the wire](https://hotwired.dev/)),
+whether you're using jQuery, HTMX, AlpineJS, vanilla JavaScript, or other.
+
+This is achieved by the combination of the [`"document"`](../../advanced/rendering_js_css#document)
+and [`"fragment"`](../../advanced/rendering_js_css#fragment) dependencies rendering strategies.
+
+Read more about [HTML fragments](../../advanced/html_fragments) and [Rendering JS / CSS](../../advanced/rendering_js_css).
diff --git a/docs/concepts/fundamentals/defining_js_css_html_files.md b/docs/concepts/fundamentals/secondary_js_css_files.md
similarity index 61%
rename from docs/concepts/fundamentals/defining_js_css_html_files.md
rename to docs/concepts/fundamentals/secondary_js_css_files.md
index 24f754e5..ad7e5873 100644
--- a/docs/concepts/fundamentals/defining_js_css_html_files.md
+++ b/docs/concepts/fundamentals/secondary_js_css_files.md
@@ -1,48 +1,266 @@
-As you could have seen in [the tutorial](../../getting_started/adding_js_and_css.md), there's multiple ways how you can associate
-HTML / JS / CSS with a component:
+## Overview
-- You can set [`Component.template`](../../reference/api.md#django_components.Component.template),
- [`Component.css`](../../reference/api.md#django_components.Component.css) and
- [`Component.js`](../../reference/api.md#django_components.Component.js) to define the main HTML / CSS / JS for a component
- as inlined code.
-- You can set [`Component.template_file`](../../reference/api.md#django_components.Component.template_file),
- [`Component.css_file`](../../reference/api.md#django_components.Component.css_file) and
- [`Component.js_file`](../../reference/api.md#django_components.Component.js_file) to define the main HTML / CSS / JS
- for a component in separate files.
-- You can link additional CSS / JS files using
- [`Component.Media.js`](../../reference/api.md#django_components.ComponentMediaInput.js)
- and [`Component.Media.css`](../../reference/api.md#django_components.ComponentMediaInput.css).
+Each component can define extra or "secondary" CSS / JS files using the nested [`Component.Media`](../../reference/api.md#django_components.Component.Media) class,
+by setting [`Component.Media.js`](../../reference/api.md#django_components.ComponentMediaInput.js) and [`Component.Media.css`](../../reference/api.md#django_components.ComponentMediaInput.css).
-!!! warning
+The [main HTML / JS / CSS files](../html_js_css_files) are limited to 1 per component. This is not the case for the secondary files,
+where components can have many of them.
- You **cannot** use both inlined code **and** separate file for a single language type:
+There is also no special behavior or post-processing for these secondary files, they are loaded as is.
- - You can only either set `Component.template` or `Component.template_file`
- - You can only either set `Component.css` or `Component.css_file`
- - You can only either set `Component.js` or `Component.js_file`
+You can use these for third-party libraries, or for shared CSS / JS files.
- However, you can freely mix these for different languages:
+These must be set as paths, URLs, or [custom objects](#paths-as-objects).
- ```djc_py
- class MyTable(Component):
- template: types.django_html = """
-
- Hi there!
-
- """
- js_file = "my_table.js"
- css_file = "my_table.css"
- ```
+```py
+@register("calendar")
+class Calendar(Component):
+ class Media:
+ js = [
+ "https://unpkg.com/alpinejs@3.14.7/dist/cdn.min.js",
+ "calendar/script.js",
+ ]
+ css = [
+ "https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css",
+ "calendar/style.css",
+ ]
+```
!!! note
- django-component's management of files is inspired by [Django's `Media` class](https://docs.djangoproject.com/en/5.0/topics/forms/media/).
+ django-component's management of files is inspired by [Django's `Media` class](https://docs.djangoproject.com/en/5.2/topics/forms/media/).
To be familiar with how Django handles static files, we recommend reading also:
- - [How to manage static files (e.g. images, JavaScript, CSS)](https://docs.djangoproject.com/en/5.0/howto/static-files/)
+ - [How to manage static files (e.g. images, JavaScript, CSS)](https://docs.djangoproject.com/en/5.2/howto/static-files/)
-## Defining file paths relative to component
+## `Media` class
+
+
+
+Use the `Media` class to define secondary JS / CSS files for a component.
+
+This `Media` class behaves similarly to
+[Django's Media class](https://docs.djangoproject.com/en/5.2/topics/forms/media/#assets-as-a-static-definition):
+
+- **Static paths** - Paths are handled as static file paths, and are resolved to URLs with Django's
+ [`{% static %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#static) template tag.
+- **URLs** - A path that starts with `http`, `https`, or `/` is considered a URL. URLs are NOT resolved with [`{% static %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#static).
+- **HTML tags** - Both static paths and URLs are rendered to `
+#
+```
+
+When working with component media files, it is important to understand the difference:
+
+- `Component.Media`
+
+ - Is the "raw" media definition, or the input, which holds only the component's **own** media definition
+ - This class is NOT instantiated, it merely holds the JS / CSS files.
+
+- `Component.media`
+ - Returns all resolved media files, **including** those inherited from parent components
+ - Is an instance of [`Component.media_class`](../../reference/api.md#django_components.Component.media_class)
+
+```python
+class ParentComponent(Component):
+ class Media:
+ js = ["parent.js"]
+
+class ChildComponent(ParentComponent):
+ class Media:
+ js = ["child.js"]
+
+# Access only this component's media
+print(ChildComponent.Media.js) # ["child.js"]
+
+# Access all inherited media
+print(ChildComponent.media._js) # ["parent.js", "child.js"]
+```
+
+!!! note
+
+ You should **not** manually modify `Component.media` or `Component.Media` after the component has been resolved, as this may lead to unexpected behavior.
+
+If you want to modify the class that is instantiated for [`Component.media`](../../reference/api.md#django_components.Component.media),
+you can configure [`Component.media_class`](../../reference/api.md#django_components.Component.media_class)
+([See example](#rendering-paths)).
+
+## File paths
+
+Unlike the [main HTML / JS / CSS files](../html_js_css_files), the path definition for the secondary files are quite ergonomic.
+
+### Relative to component
As seen in the [getting started example](../../getting_started/your_first_component.md), to associate HTML / JS / CSS
files with a component, you can set them as
@@ -117,93 +335,30 @@ class Calendar(Component):
NOTE: In case of ambiguity, the preference goes to resolving the files relative to the component's directory.
-## Defining additional JS and CSS files
+### Globs
-Each component can have only a single template, and single main JS and CSS. However, you can define additional JS or CSS
-using the nested [`Component.Media` class](../../../reference/api#django_components.Component.Media).
+Components can have many secondary files. To simplify their declaration, you can use globs.
-This `Media` class behaves similarly to
-[Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#assets-as-a-static-definition):
+Globs MUST be relative to the component's directory.
-- Paths are generally handled as static file paths, and resolved URLs are rendered to HTML with
- `media_class.render_js()` or `media_class.render_css()`.
-- A path that starts with `http`, `https`, or `/` is considered a URL, skipping the static file resolution.
- This path is still rendered to HTML with `media_class.render_js()` or `media_class.render_css()`.
-- A [`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString),
- or a function (with `__html__` method) is considered an already-formatted HTML tag, skipping both static file
- resolution and rendering with `media_class.render_js()` or `media_class.render_css()`.
-- You can set [`extend`](../../../reference/api#django_components.ComponentMediaInput.extend) to configure
- whether to inherit JS / CSS from parent components. See
- [Controlling Media Inheritance](../../fundamentals/defining_js_css_html_files/#controlling-media-inheritance).
+```py title="[project root]/components/calendar/calendar.py"
+from django_components import Component, register
-However, there's a few differences from Django's Media class:
-
-1. Our Media class accepts various formats for the JS and CSS files: either a single file, a list,
- or (CSS-only) a dictonary (See [`ComponentMediaInput`](../../../reference/api#django_components.ComponentMediaInput)).
-2. Individual JS / CSS files can be any of `str`, `bytes`, `Path`,
- [`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString), or a function
- (See [`ComponentMediaInputPath`](../../../reference/api#django_components.ComponentMediaInputPath)).
-3. Individual JS / CSS files can be glob patterns, e.g. `*.js` or `styles/**/*.css`.
-4. If you set [`Media.extend`](../../../reference/api/#django_components.ComponentMediaInput.extend) to a list,
- it should be a list of [`Component`](../../../reference/api/#django_components.Component) classes.
-
-```py
-class MyTable(Component):
+@register("calendar")
+class Calendar(Component):
class Media:
js = [
- "path/to/script.js",
- "path/to/*.js", # Or as a glob
- "https://unpkg.com/alpinejs@3.14.7/dist/cdn.min.js", # AlpineJS
+ "path/to/*.js",
+ "another/path/*.js",
]
- css = {
- "all": [
- "path/to/style.css",
- "path/to/*.css", # Or as a glob
- "https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", # TailwindCSS
- ],
- "print": ["path/to/style2.css"],
- }
+ css = "*.css"
```
-## Configuring CSS Media Types
+How this works is that django-components will detect that the path is a glob, and will try to resolve all files matching the glob pattern relative to the component's directory.
-You can define which stylesheets will be associated with which
-[CSS Media types](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries#targeting_media_types). You do so by defining CSS files as a dictionary.
+After that, the file paths are handled the same way as if you defined them explicitly.
-See the corresponding [Django Documentation](https://docs.djangoproject.com/en/5.0/topics/forms/media/#css).
-
-Again, you can set either a single file or a list of files per media type:
-
-```py
-class MyComponent(Component):
- class Media:
- css = {
- "all": "path/to/style1.css",
- "print": ["path/to/style2.css", "path/to/style3.css"],
- }
-```
-
-!!! note
-
- When you define CSS as a string or a list, the `all` media type is implied.
-
- So these two examples are the same:
-
- ```py
- class MyComponent(Component):
- class Media:
- css = "path/to/style1.css"
- ```
-
- ```py
- class MyComponent(Component):
- class Media:
- css = {
- "all": ["path/to/style1.css"],
- }
- ```
-
-## Supported types for file paths
+### Supported types
File paths can be any of:
@@ -213,7 +368,7 @@ File paths can be any of:
- `SafeData` (`__html__` method)
- `Callable` that returns any of the above, evaluated at class creation (`__new__`)
-See [`ComponentMediaInputPath`](../../../reference/api#django_components.ComponentMediaInputPath).
+To help with typing the union, use [`ComponentMediaInputPath`](../../../reference/api#django_components.ComponentMediaInputPath).
```py
from pathlib import Path
@@ -238,18 +393,18 @@ class SimpleComponent(Component):
]
```
-## Paths as objects
+### Paths as objects
-In the example [above](#supported-types-for-file-paths), you can see that when we used Django's
-[`mark_safe()`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.mark_safe)
-to mark a string as a [`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString),
-we had to define the full `
-#
-```
-
-### `Component.Media` vs `Component.media`
-
-When working with component media files, there are a few important concepts to understand:
-
-- `Component.Media`
-
- - Is the "raw" media definition, or the input, which holds only the component's **own** media definition
- - This class is NOT instantiated, it merely holds the JS / CSS files.
-
-- `Component.media`
- - Returns all resolved media files, **including** those inherited from parent components
- - Is an instance of [`Component.media_class`](../../reference/api.md#django_components.Component.media_class)
-
-```python
-class ParentComponent(Component):
- class Media:
- js = ["parent.js"]
-
-class ChildComponent(ParentComponent):
- class Media:
- js = ["child.js"]
-
-# Access only this component's media
-print(ChildComponent.Media.js) # ["child.js"]
-
-# Access all inherited media
-print(ChildComponent.media._js) # ["parent.js", "child.js"]
-```
-
-!!! note
-
- You should **not** manually modify `Component.media` or `Component.Media` after the component has been resolved, as this may lead to unexpected behavior.
-
-If you want to modify the class that is instantiated for [`Component.media`](../../reference/api.md#django_components.Component.media),
-you can configure [`Component.media_class`](../../reference/api.md#django_components.Component.media_class)
-([See example](#customize-how-paths-are-rendered-into-html-tags)).
-
-## Controlling Media Inheritance
-
-By default, the media files are inherited from the parent component.
-
-```python
-class ParentComponent(Component):
- class Media:
- js = ["parent.js"]
-
-class MyComponent(ParentComponent):
- class Media:
- js = ["script.js"]
-
-print(MyComponent.media._js) # ["parent.js", "script.js"]
-```
-
-You can set the component NOT to inherit from the parent component by setting the [`extend`](../../reference/api.md#django_components.ComponentMediaInput.extend) attribute to `False`:
-
-```python
-class ParentComponent(Component):
- class Media:
- js = ["parent.js"]
-
-class MyComponent(ParentComponent):
- class Media:
- extend = False # Don't inherit parent media
- js = ["script.js"]
-
-print(MyComponent.media._js) # ["script.js"]
-```
-
-Alternatively, you can specify which components to inherit from. In such case, the media files are inherited ONLY from the specified components, and NOT from the original parent components:
-
-```python
-class ParentComponent(Component):
- class Media:
- js = ["parent.js"]
-
-class MyComponent(ParentComponent):
- class Media:
- # Only inherit from these, ignoring the files from the parent
- extend = [OtherComponent1, OtherComponent2]
-
- js = ["script.js"]
-
-print(MyComponent.media._js) # ["script.js", "other1.js", "other2.js"]
-```
-
-!!! info
-
- The `extend` behaves consistently with
- [Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#extend),
- with one exception:
-
- - When you set `extend` to a list, the list is expected to contain Component classes (or other classes that have a nested `Media` class).
diff --git a/docs/concepts/fundamentals/single_file_components.md b/docs/concepts/fundamentals/single_file_components.md
index d380dca2..6f9b0765 100644
--- a/docs/concepts/fundamentals/single_file_components.md
+++ b/docs/concepts/fundamentals/single_file_components.md
@@ -1,17 +1,38 @@
-Components can be defined in a single file, which is useful for small components. To do this, you can use the `template`, `js`, and `css` class attributes instead of the `template_file`, `js_file`, and `css_file`.
+Components can be defined in a single file, inlining the HTML, JS and CSS within the Python code.
+
+## Writing single file components
+
+To do this, you can use the
+[`template`](../../../reference/api#django_components.Component.template),
+[`js`](../../../reference/api#django_components.Component.js),
+and [`css`](../../../reference/api#django_components.Component.css)
+class attributes instead of the
+[`template_file`](../../../reference/api#django_components.Component.template_file),
+[`js_file`](../../../reference/api#django_components.Component.js_file),
+and [`css_file`](../../../reference/api#django_components.Component.css_file).
For example, here's the calendar component from
-the [Getting started](../../getting_started/your_first_component.md) tutorial,
-defined in a single file:
+the [Getting started](../../getting_started/your_first_component.md) tutorial:
+
+```py title="calendar.py"
+from django_components import Component
+
+class Calendar(Component):
+ template_file = "calendar.html"
+ js_file = "calendar.js"
+ css_file = "calendar.css"
+```
+
+And here is the same component, rewritten in a single file:
```djc_py title="[project root]/components/calendar.py"
from django_components import Component, register, types
@register("calendar")
class Calendar(Component):
- def get_context_data(self, date):
+ def get_template_data(self, args, kwargs, slots, context):
return {
- "date": date,
+ "date": kwargs["date"],
}
template: types.django_html = """
@@ -41,6 +62,194 @@ class Calendar(Component):
"""
```
-This makes it easy to create small components without having to create a separate template, CSS, and JS file.
+You can mix and match, so you can have a component with inlined HTML,
+while the JS and CSS are in separate files:
-To add syntax highlighting to these snippets, head over to [Syntax highlighting](../../guides/setup/syntax_highlight.md).
+```djc_py title="[project root]/components/calendar.py"
+from django_components import Component, register, types
+
+@register("calendar")
+class Calendar(Component):
+ js_file = "calendar.js"
+ css_file = "calendar.css"
+
+ template: types.django_html = """
+
+ Today's date is {{ date }}
+
+ """
+```
+
+## Syntax highlighting
+
+If you "inline" the HTML, JS and CSS code into the Python class, you should set up
+syntax highlighting to let your code editor know that the inlined code is HTML, JS and CSS.
+
+In the examples above, we've annotated the
+[`template`](../../../reference/api#django_components.Component.template),
+[`js`](../../../reference/api#django_components.Component.js),
+and [`css`](../../../reference/api#django_components.Component.css)
+attributes with
+the `types.django_html`, `types.js` and `types.css` types. These are used for syntax highlighting in VSCode.
+
+!!! warning
+
+ Autocompletion / intellisense does not work in the inlined code.
+
+ Help us add support for intellisense in the inlined code! Start a conversation in the
+ [GitHub Discussions](https://github.com/django-components/django-components/discussions).
+
+### VSCode
+
+1. First install [Python Inline Source Syntax Highlighting](https://marketplace.visualstudio.com/items?itemName=samwillis.python-inline-source) extension, it will give you syntax highlighting for the template, CSS, and JS.
+
+2. Next, in your component, set typings of
+[`Component.template`](../../../reference/api#django_components.Component.template),
+[`Component.js`](../../../reference/api#django_components.Component.js),
+[`Component.css`](../../../reference/api#django_components.Component.css)
+to `types.django_html`, `types.css`, and `types.js` respectively. The extension will recognize these and will activate syntax highlighting.
+
+```djc_py title="[project root]/components/calendar.py"
+from django_components import Component, register, types
+
+@register("calendar")
+class Calendar(Component):
+ def get_template_data(self, args, kwargs, slots, context):
+ return {
+ "date": kwargs["date"],
+ }
+
+ template: types.django_html = """
+
+ Today's date is {{ date }}
+
+ """
+
+ css: types.css = """
+ .calendar-component {
+ width: 200px;
+ background: pink;
+ }
+ .calendar-component span {
+ font-weight: bold;
+ }
+ """
+
+ js: types.js = """
+ (function(){
+ if (document.querySelector(".calendar-component")) {
+ document.querySelector(".calendar-component").onclick = function(){ alert("Clicked calendar!"); };
+ }
+ })()
+ """
+```
+
+### Pycharm (or other Jetbrains IDEs)
+
+With PyCharm (or any other editor from Jetbrains), you don't need to use `types.django_html`, `types.css`, `types.js` since Pycharm uses [language injections](https://www.jetbrains.com/help/pycharm/using-language-injections.html).
+
+You only need to write the comments `# language=` above the variables.
+
+```djc_py
+from django_components import Component, register
+
+@register("calendar")
+class Calendar(Component):
+ def get_template_data(self, args, kwargs, slots, context):
+ return {
+ "date": kwargs["date"],
+ }
+
+ # language=HTML
+ template= """
+
+ Today's date is {{ date }}
+
+ """
+
+ # language=CSS
+ css = """
+ .calendar-component {
+ width: 200px;
+ background: pink;
+ }
+ .calendar-component span {
+ font-weight: bold;
+ }
+ """
+
+ # language=JS
+ js = """
+ (function(){
+ if (document.querySelector(".calendar-component")) {
+ document.querySelector(".calendar-component").onclick = function(){ alert("Clicked calendar!"); };
+ }
+ })()
+ """
+```
+
+### Markdown code blocks with Pygments
+
+[Pygments](https://pygments.org/) is a syntax highlighting library written in Python. It's also what's used by this documentation site ([mkdocs-material](https://squidfunk.github.io/mkdocs-material/)) to highlight code blocks.
+
+To write code blocks with syntax highlighting, you need to install the [`pygments-djc`](https://pypi.org/project/pygments-djc/) package.
+
+```bash
+pip install pygments-djc
+```
+
+And then initialize it by importing `pygments_djc` somewhere in your project:
+
+```python
+import pygments_djc
+```
+
+Now you can use the `djc_py` code block to write code blocks with syntax highlighting for components.
+
+```txt
+\```djc_py
+from django_components import Component, register
+
+@register("calendar")
+class Calendar(Component):
+ template = """
+
+ Today's date is {{ date }}
+
+ """
+
+ css = """
+ .calendar-component {
+ width: 200px;
+ background: pink;
+ }
+ .calendar-component span {
+ font-weight: bold;
+ }
+ """
+\```
+```
+
+Will be rendered as below. Notice that the CSS and HTML are highlighted correctly:
+
+```djc_py
+from django_components import Component, register
+
+@register("calendar")
+class Calendar(Component):
+ template= """
+
+ Today's date is {{ date }}
+
+ """
+
+ css = """
+ .calendar-component {
+ width: 200px;
+ background: pink;
+ }
+ .calendar-component span {
+ font-weight: bold;
+ }
+ """
+```
diff --git a/docs/concepts/fundamentals/slots.md b/docs/concepts/fundamentals/slots.md
index fd757694..7a65dcc4 100644
--- a/docs/concepts/fundamentals/slots.md
+++ b/docs/concepts/fundamentals/slots.md
@@ -1,46 +1,74 @@
-_New in version 0.26_:
+django-components has the most extensive slot system of all the popular Python templating engines.
-- The `slot` tag now serves only to declare new slots inside the component template.
- - To override the content of a declared slot, use the newly introduced `fill` tag instead.
-- Whereas unfilled slots used to raise a warning, filling a slot is now optional by default.
- - To indicate that a slot must be filled, the new `required` option should be added at the end of the `slot` tag.
+The slot system is based on [Vue](https://vuejs.org/guide/components/slots.html), and works across both Django templates and Python code.
----
+## What are slots?
Components support something called 'slots'.
-When a component is used inside another template, slots allow the parent template to override specific parts of the child component by passing in different content.
-This mechanism makes components more reusable and composable.
-This behavior is similar to [slots in Vue](https://vuejs.org/guide/components/slots.html).
-In the example below we introduce two block tags that work hand in hand to make this work. These are...
+When you write a component, you define its template. The template will always be
+the same each time you render the component.
-- `{% slot %}`/`{% endslot %}`: Declares a new slot in the component template.
-- `{% fill %}`/`{% endfill %}`: (Used inside a `{% component %}` tag pair.) Fills a declared slot with the specified content.
+However, sometimes you may want to customize the component slightly to change the
+content of the component. This is where slots come in.
-Let's update our calendar component to support more customization. We'll add `slot` tag pairs to its template, _template.html_.
+Slots allow you to insert parts of HTML into the component.
+This makes components more reusable and composable.
-```htmldjango
+```django
- {% slot "body" %}Today's date is {{ date }}{% endslot %}
+ {# This is where the component will insert the content #}
+ {% slot "header" / %}
```
-When using the component, you specify which slots you want to fill and where you want to use the defaults from the template. It looks like this:
+## Slot anatomy
+
+Slots consists of two parts:
+
+1. [`{% slot %}`](../../../reference/template_tags#slot) tag - Inside your component you decide where you want to insert the content.
+2. [`{% fill %}`](../../../reference/template_tags#fill) tag - In the parent template (outside the component) you decide what content to insert into the slot.
+ It "fills" the slot with the specified content.
+
+Let's look at an example:
+
+First, we define the component template. This component contains two slots, `header` and `body`.
+
+```htmldjango
+
+
+ {% slot "body" %}
+ Today's date is {{ date }}
+ {% endslot %}
+
+
+```
+
+Next, when using the component, we can insert our own content into the slots. It looks like this:
```htmldjango
{% component "calendar" date="2020-06-06" %}
{% fill "body" %}
- Can you believe it's already {{ date }}??
+ Can you believe it's already
+ {{ date }}??
{% endfill %}
{% endcomponent %}
```
-Since the 'header' fill is unspecified, it's taken from the base template. If you put this in a template, and pass in `date=2020-06-06`, this is what gets rendered:
+Since the `'header'` fill is unspecified, it's [default value](#default-slot) is used.
+
+When rendered, notice that:
+
+- The body is filled with the content we specified,
+- The header is still the default value we defined in the component template.
```htmldjango
@@ -53,205 +81,825 @@ Since the 'header' fill is unspecified, it's taken from the base template. If yo
```
-### Named slots
+## Slots overview
-As seen in the previouse section, you can use `{% fill slot_name %}` to insert content into a specific
-slot.
+### Slot definition
-You can define fills for multiple slot simply by defining them all within the `{% component %} {% endcomponent %}`
-tags:
+Slots are defined with the [`{% slot %}`](../../../reference/template_tags#slot) tag:
-```htmldjango
-{% component "calendar" date="2020-06-06" %}
- {% fill "header" %}
- Hi this is header!
+```django
+{% slot "name" %}
+ Default content
+{% endslot %}
+```
+
+Single component can have multiple slots:
+
+```django
+{% slot "name" %}
+ Default content
+{% endslot %}
+
+{% slot "other_name" / %}
+```
+
+And you can even define the same slot in multiple places:
+
+```django
+
+```
+
+!!! info
+
+ If you define the same slot in multiple places, you must mark each slot individually
+ when setting `default` or `required` flags, e.g.:
+
+ ```htmldjango
+
+ ```
+
+### Slot filling
+
+Fill can be defined with the [`{% fill %}`](../../../reference/template_tags#fill) tag:
+
+```django
+{% component "calendar" %}
+ {% fill "name" %}
+ Filled content
{% endfill %}
- {% fill "body" %}
- Can you believe it's already {{ date }}??
+ {% fill "other_name" %}
+ Filled content
{% endfill %}
{% endcomponent %}
```
-You can also use `{% for %}`, `{% with %}`, or other non-component tags (even `{% include %}`)
-to construct the `{% fill %}` tags, **as long as these other tags do not leave any text behind!**
+Or in Python with the [`slots`](../../../reference/api#django_components.Component.render) argument:
-```django
-{% component "table" %}
- {% for slot_name in slots %}
- {% fill name=slot_name %}
- {{ slot_name }}
- {% endfill %}
- {% endfor %}
-
- {% with slot_name="abc" %}
- {% fill name=slot_name %}
- {{ slot_name }}
- {% endfill %}
- {% endwith %}
-{% endcomponent %}
+```py
+Calendar.render(
+ slots={
+ "name": "Filled content",
+ "other_name": "Filled content",
+ },
+)
```
### Default slot
-_Added in version 0.28_
+You can make the syntax shorter by marking the slot as [`default`](../../../reference/template_tags#slot):
-As you can see, component slots lets you write reusable containers that you fill in when you use a component. This makes for highly reusable components that can be used in different circumstances.
-
-It can become tedious to use `fill` tags everywhere, especially when you're using a component that declares only one slot. To make things easier, `slot` tags can be marked with an optional keyword: `default`.
-
-When added to the tag (as shown below), this option lets you pass filling content directly in the body of a `component` tag pair – without using a `fill` tag. Choose carefully, though: a component template may contain at most one slot that is marked as `default`. The `default` option can be combined with other slot options, e.g. `required`.
-
-Here's the same example as before, except with default slots and implicit filling.
-
-The template:
-
-```htmldjango
-
- {% slot "body" default %}Today's date is {{ date }}{% endslot %}
-
-
+```django
+{% slot "name" default %}
+ Default content
+{% endslot %}
```
-Including the component (notice how the `fill` tag is omitted):
+This allows you to fill the slot directly in the [`{% component %}`](../../../reference/template_tags#component) tag,
+omitting the `{% fill %}` tag:
-```htmldjango
-{% component "calendar" date="2020-06-06" %}
- Can you believe it's already {{ date }}??
+```django
+{% component "calendar" %}
+ Filled content
{% endcomponent %}
```
-The rendered result (exactly the same as before):
+To target the default slot in Python, you can use the `"default"` slot name:
-```html
-
-
Calendar header
-
Can you believe it's already 2020-06-06??
-
+```py
+Calendar.render(
+ slots={"default": "Filled content"},
+)
```
-You may be tempted to combine implicit fills with explicit `fill` tags. This will not work. The following component template will raise an error when rendered.
+!!! info "Accessing default slot in Python"
-```htmldjango
-{# DON'T DO THIS #}
-{% component "calendar" date="2020-06-06" %}
- {% fill "header" %}Totally new header!{% endfill %}
- Can you believe it's already {{ date }}??
-{% endcomponent %}
-```
+ Since the default slot is stored under the slot name `default`, you can access the default slot
+ in Python under the `"default"` key:
-Instead, you can use a named fill with name `default` to target the default fill:
+ ```py
+ class MyTable(Component):
+ def get_template_data(self, args, kwargs, slots, context):
+ default_slot = slots["default"]
+ return {
+ "default_slot": default_slot,
+ }
+ ```
-```htmldjango
-{# THIS WORKS #}
-{% component "calendar" date="2020-06-06" %}
- {% fill "header" %}Totally new header!{% endfill %}
- {% fill "default" %}
+!!! warning
+
+ Only one [`{% slot %}`](../../../reference/template_tags#slot) can be marked as `default`.
+ But you can have multiple slots with the same name all marked as `default`.
+
+ If you define multiple **different** slots as `default`, this will raise an error.
+
+ ❌ Don't do this
+
+ ```django
+ {% slot "name" default %}
+ Default content
+ {% endslot %}
+ {% slot "other_name" default %}
+ Default content
+ {% endslot %}
+ ```
+
+ ✅ Do this instead
+
+ ```django
+ {% slot "name" default %}
+ Default content
+ {% endslot %}
+ {% slot "name" default %}
+ Default content
+ {% endslot %}
+ ```
+
+!!! warning
+
+ Do NOT combine default fills with explicit named [`{% fill %}`](../../../reference/template_tags#fill) tags.
+
+ The following component template will raise an error when rendered:
+
+ ❌ Don't do this
+
+ ```django
+ {% component "calendar" date="2020-06-06" %}
+ {% fill "header" %}Totally new header!{% endfill %}
Can you believe it's already {{ date }}??
+ {% endcomponent %}
+ ```
+
+ ✅ Do this instead
+
+ ```django
+ {% component "calendar" date="2020-06-06" %}
+ {% fill "header" %}Totally new header!{% endfill %}
+ {% fill "default" %}
+ Can you believe it's already {{ date }}??
+ {% endfill %}
+ {% endcomponent %}
+ ```
+
+!!! warning
+
+ You cannot double-fill a slot.
+
+ That is, if both `{% fill "default" %}` and `{% fill "header" %}` point to the same slot,
+ this will raise an error when rendered.
+
+### Required slot
+
+You can make the slot required by adding the [`required`](../../../reference/template_tags#slot) keyword:
+
+```django
+{% slot "name" required %}
+ Default content
+{% endslot %}
+```
+
+This will raise an error if the slot is not filled.
+
+### Access fills
+
+You can access the fills with the
+[`{{ component_vars.slots. }}`](../../../reference/template_vars#slots) template variable:
+
+```django
+{% if component_vars.slots.my_slot %}
+
+{% endif %}
+```
+
+And in Python with the [`Component.slots`](../../../reference/api#django_components.Component.slots) property:
+
+```py
+class Calendar(Component):
+ # `get_template_data` receives the `slots` argument directly
+ def get_template_data(self, args, kwargs, slots, context):
+ if "my_slot" in slots:
+ content = "Filled content"
+ else:
+ content = "Default content"
+
+ return {
+ "my_slot": content,
+ }
+
+ # In other methods you can still access the slots with `Component.slots`
+ def on_render_before(self, context, template):
+ if "my_slot" in self.slots:
+ # Do something
+```
+
+### Dynamic fills
+
+The slot and fill names can be set as variables. This way you can fill slots dynamically:
+
+```django
+{% with "body" as slot_name %}
+ {% component "calendar" %}
+ {% fill slot_name %}
+ Filled content
+ {% endfill %}
+ {% endcomponent %}
+{% endwith %}
+```
+
+You can even use [`{% if %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#std-templatetag-if)
+and [`{% for %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#std-templatetag-for)
+tags inside the [`{% component %}`](../../../reference/template_tags#component) tag to fill slots with more control:
+
+```django
+{% component "calendar" %}
+ {% if condition %}
+ {% fill "name" %}
+ Filled content
+ {% endfill %}
+ {% endif %}
+
+ {% for item in items %}
+ {% fill item.name %}
+ Item: {{ item.value }}
+ {% endfill %}
+ {% endfor %}
+{% endcomponent %}
+```
+
+You can also use [`{% with %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#std-templatetag-with)
+or even custom tags to generate the fills dynamically:
+
+```django
+{% component "calendar" %}
+ {% with item.name as name %}
+ {% fill name %}
+ Item: {{ item.value }}
+ {% endfill %}
+ {% endwith %}
+{% endcomponent %}
+```
+
+!!! warning
+
+ If you dynamically generate `{% fill %}` tags, be careful to render text only inside the `{% fill %}` tags.
+
+ Any text rendered outside `{% fill %}` tags will be considered a default fill and will raise an error
+ if combined with explicit fills. (See [Default slot](#default-slot))
+
+### Slot data
+
+Sometimes the slots need to access data from the component. Imagine an HTML table component
+which has a slot to configure how to render the rows. Each row has a different data, so you need
+to pass the data to the slot.
+
+Similarly to [Vue's scoped slots](https://vuejs.org/guide/components/slots#scoped-slots),
+you can pass data to the slot, and then access it in the fill.
+
+This consists of two steps:
+
+1. Passing data to [`{% slot %}`](../../../reference/template_tags#slot) tag
+2. Accessing data in [`{% fill %}`](../../../reference/template_tags#fill) tag
+
+The data is passed to the slot as extra keyword arguments. Below we set two extra arguments: `first_name` and `job`.
+
+```django
+{# Pass data to the slot #}
+{% slot "name" first_name="John" job="Developer" %}
+ {# Fallback implementation #}
+ Name: {{ first_name }}
+ Job: {{ job }}
+{% endslot %}
+```
+
+!!! note
+
+ `name` kwarg is already used for slot name, so you cannot pass it as slot data.
+
+To access the slot's data in the fill, use the [`data`](../../../reference/template_tags#fill) keyword. This sets the name
+of the variable that holds the data in the fill:
+
+```django
+{# Access data in the fill #}
+{% component "profile" %}
+ {% fill "name" data="d" %}
+ Hello, my name is
{{ d.first_name }}
+ and I'm a
{{ d.job }}
{% endfill %}
{% endcomponent %}
```
-NOTE: If you doubly-fill a slot, that is, that both `{% fill "default" %}` and `{% fill "header" %}`
-would point to the same slot, this will raise an error when rendered.
-
-#### Accessing default slot in Python
-
-Since the default slot is stored under the slot name `default`, you can access the default slot
-like so:
+To access the slot data in Python, use the `data` attribute in [slot functions](#slot-functions).
```py
-class MyTable(Component):
- def get_context_data(self, *args, **kwargs):
- default_slot = self.input.slots["default"]
+def my_slot(ctx):
+ return f"""
+ Hello, my name is
{ctx.data["first_name"]}
+ and I'm a
{ctx.data["job"]}
+ """
+
+Profile.render(
+ slots={
+ "name": my_slot,
+ },
+)
+```
+
+Slot data can be set also when rendering a slot in Python:
+
+```py
+slot = Slot(lambda ctx: f"Hello, {ctx.data['name']}!")
+
+# Render the slot
+html = slot({"name": "John"})
+```
+
+!!! info
+
+ To access slot data on a [default slot](#default-slot), you have to explictly define the `{% fill %}` tags
+ with name `"default"`.
+
+ ```django
+ {% component "my_comp" %}
+ {% fill "default" data="slot_data" %}
+ {{ slot_data.input }}
+ {% endfill %}
+ {% endcomponent %}
+ ```
+
+!!! warning
+
+ You cannot set the `data` attribute and
+ [`fallback` attribute](#slot-fallback)
+ to the same name. This raises an error:
+
+ ```django
+ {% component "my_comp" %}
+ {% fill "content" data="slot_var" fallback="slot_var" %}
+ {{ slot_var.input }}
+ {% endfill %}
+ {% endcomponent %}
+ ```
+
+### Slot fallback
+
+The content between the `{% slot %}..{% endslot %}` tags is the *fallback* content that
+will be rendered if no fill is given for the slot.
+
+```django
+{% slot "name" %}
+ Hello, my name is {{ name }}
+{% endslot %}
+```
+
+Sometimes you may want to keep the fallback content, but only wrap it in some other content.
+
+To do so, you can access the fallback content via the [`fallback`](../../../reference/template_tags#fill) kwarg.
+This sets the name of the variable that holds the fallback content in the fill:
+
+```django
+{% component "profile" %}
+ {% fill "name" fallback="fb" %}
+ Original content:
+
+ {{ fb }}
+
+ {% endfill %}
+{% endcomponent %}
+```
+
+To access the fallback content in Python, use the [`fallback`](../../../reference/api#django_components.SlotContext.fallback)
+attribute in [slot functions](#slot-functions).
+
+The fallback value is rendered lazily. Coerce the fallback to a string to render it.
+
+```py
+def my_slot(ctx):
+ # Coerce the fallback to a string
+ fallback = str(ctx.fallback)
+ return f"Original content: " + fallback
+
+Profile.render(
+ slots={
+ "name": my_slot,
+ },
+)
+```
+
+Fallback can be set also when rendering a slot in Python:
+
+```py
+slot = Slot(lambda ctx: f"Hello, {ctx.data['name']}!")
+
+# Render the slot
+html = slot({"name": "John"}, fallback="Hello, world!")
+```
+
+!!! info
+
+ To access slot fallback on a [default slot](#default-slot), you have to explictly define the `{% fill %}` tags
+ with name `"default"`.
+
+ ```django
+ {% component "my_comp" %}
+ {% fill "default" fallback="fallback" %}
+ {{ fallback }}
+ {% endfill %}
+ {% endcomponent %}
+ ```
+
+!!! warning
+
+ You cannot set the [`data`](#slot-data) attribute and
+ `fallback` attribute
+ to the same name. This raises an error:
+
+ ```django
+ {% component "my_comp" %}
+ {% fill "content" data="slot_var" fallback="slot_var" %}
+ {{ slot_var.input }}
+ {% endfill %}
+ {% endcomponent %}
+ ```
+
+### Slot functions
+
+In Python code, slot fills can be defined as strings, functions, or
+[`Slot`](../../../reference/api#django_components.Slot) instances that wrap the two.
+Slot functions have access to slot [`data`](../../../reference/api#django_components.SlotContext.data),
+[`fallback`](../../../reference/api#django_components.SlotContext.fallback),
+and [`context`](../../../reference/api#django_components.SlotContext.context).
+
+```py
+def row_slot(ctx):
+ if ctx.data["disabled"]:
+ return ctx.fallback
+
+ item = ctx.data["item"]
+ if ctx.data["type"] == "table":
+ return f"
+ Django automatically executes functions when it comes across them in templates.
+
+ Because of this you MUST wrap the function in [`Slot`](../../../reference/api#django_components.Slot)
+ instance to prevent it from being called.
+
+ Read more about Django's [`do_not_call_in_templates`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#variables-and-lookups).
+
+## Slot class
+
+The [`Slot`](../../../reference/api#django_components.Slot) class is a wrapper around a function that can be used to fill a slot.
+
+```py
+from django_components import Component, Slot
+
+def footer(ctx):
+ return f"Hello, {ctx.data['name']}!"
+
+Table.render(
+ slots={
+ "footer": Slot(footer),
+ },
+)
```
-#### Default and required slots
+Slot class can be instantiated with a function or a string:
-If you use a slot multiple times, you can still mark the slot as `default` or `required`.
-For that, you must mark each slot individually, e.g.:
-
-```htmldjango
-
+```py
+slot1 = Slot(lambda ctx: f"Hello, {ctx.data['name']}!")
+slot2 = Slot("Hello, world!")
```
-Which you can then use as regular default slot:
+!!! warning
-```htmldjango
-{% component "calendar" date="2020-06-06" %}
-
-{% endcomponent %}
+ Passing a [`Slot`](../../../reference/api#django_components.Slot) instance to the `Slot`
+ constructor results in an error:
+
+ ```py
+ slot = Slot("Hello")
+
+ # Raises an error
+ slot2 = Slot(slot)
+ ```
+
+### Rendering slots
+
+**Python**
+
+You can render a [`Slot`](../../../reference/api#django_components.Slot) instance by simply calling it with data:
+
+```py
+slot = Slot(lambda ctx: f"Hello, {ctx.data['name']}!")
+
+# Render the slot with data
+html = slot({"name": "John"})
```
-Since each slot is tagged individually, you can have multiple slots
+Optionally you can pass the [fallback](#slot-fallback) value to the slot. Fallback should be a string.
+
+```py
+html = slot({"name": "John"}, fallback="Hello, world!")
+```
+
+**Template**
+
+Alternatively, you can pass the [`Slot`](../../../reference/api#django_components.Slot) instance to the
+[`{% fill %}`](../../../reference/template_tags#fill) tag:
+
+```django
+{% fill "name" body=slot / %}
+```
+
+### Slot context
+
+If a slot function is rendered by the [`{% slot %}`](../../../reference/template_tags#slot) tag,
+you can access the current [Context](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context)
+using the `context` attribute.
+
+```py
+class Table(Component):
+ template = """
+ {% with "abc" as my_var %}
+ {% slot "name" %}
+ Hello!
+ {% endslot %}
+ {% endwith %}
+ """
+
+def slot_func(ctx):
+ return f"Hello, {ctx.context['my_var']}!"
+
+slot = Slot(slot_func)
+html = slot()
+```
+
+!!! warning
+
+ While available, try to avoid using the `context` attribute in slot functions.
+
+ Instead, prefer using the `data` and `fallback` attributes.
+
+
+ Access to `context` may be removed in future versions (v2, v3).
+
+### Slot metadata
+
+When accessing slots from within [`Component`](../../../reference/api#django_components.Component) methods,
+the [`Slot`](../../../reference/api#django_components.Slot) instances are populated
+with extra metadata:
+
+- [`component_name`](../../../reference/api#django_components.Slot.component_name)
+- [`slot_name`](../../../reference/api#django_components.Slot.slot_name)
+- [`nodelist`](../../../reference/api#django_components.Slot.nodelist)
+- [`fill_node`](../../../reference/api#django_components.Slot.fill_node)
+- [`extra`](../../../reference/api#django_components.Slot.extra)
+
+These are populated the first time a slot is passed to a component.
+
+So if you pass the same slot through multiple nested components, the metadata will
+still point to the first component that received the slot.
+
+You can use these for debugging, such as printing out the slot's component name and slot name.
+
+**Fill node**
+
+Components or extensions can use [`Slot.fill_node`](../../../reference/api#django_components.Slot.fill_node)
+to handle slots differently based on whether the slot
+was defined in the template with [`{% fill %}`](../../../reference/template_tags#fill) and
+[`{% component %}`](../../../reference/template_tags#component) tags,
+or in the component's Python code.
+
+If the slot was created from a [`{% fill %}`](../../../reference/template_tags#fill) tag,
+this will be the [`FillNode`](../../../reference/api#django_components.FillNode) instance.
+
+If the slot was a default slot created from a [`{% component %}`](../../../reference/template_tags#component) tag,
+this will be the [`ComponentNode`](../../../reference/api#django_components.ComponentNode) instance.
+
+You can use this to find the [`Component`](../../../reference/api#django_components.Component) in whose
+template the [`{% fill %}`](../../../reference/template_tags#fill) tag was defined:
+
+```python
+class MyTable(Component):
+ def get_template_data(self, args, kwargs, slots, context):
+ footer_slot = slots.get("footer")
+ if footer_slot is not None and footer_slot.fill_node is not None:
+ owner_component = footer_slot.fill_node.template_component
+ # ...
+```
+
+**Extra**
+
+You can also pass any additional data along with the slot by setting it in [`Slot.extra`](../../../reference/api#django_components.Slot.extra):
+
+```py
+slot = Slot(
+ lambda ctx: f"Hello, {ctx.data['name']}!",
+ extra={"foo": "bar"},
+)
+```
+
+When you create a slot, you can set any of these fields too:
+
+```py
+# Either at slot creation
+slot = Slot(
+ lambda ctx: f"Hello, {ctx.data['name']}!",
+ # Optional
+ component_name="table",
+ slot_name="name",
+ extra={},
+)
+
+# Or later
+slot.component_name = "table"
+slot.slot_name = "name"
+slot.extra["foo"] = "bar"
+```
+
+Read more in [Pass slot metadata](../../advanced/extensions#pass-slot-metadata).
+
+### Slot contents
+
+Whether you create a slot from a function, a string, or from the [`{% fill %}`](../../../reference/template_tags#fill) tags,
+the [`Slot`](../../../reference/api#django_components.Slot) class normalizes its contents to a function.
+
+Use [`Slot.contents`](../../../reference/api#django_components.Slot.contents) to access the original value that was passed to the Slot constructor.
+
+```py
+slot = Slot("Hello!")
+print(slot.contents) # "Hello!"
+```
+
+If the slot was created from a string or from the [`{% fill %}`](../../../reference/template_tags#fill) tags,
+the contents will be accessible also as a Nodelist under [`Slot.nodelist`](../../../reference/api#django_components.Slot.nodelist).
+
+```py
+slot = Slot("Hello!")
+print(slot.nodelist) #
+```
+
+### Escaping slots content
+
+Slots content are automatically escaped by default to prevent XSS attacks.
+
+In other words, it's as if you would be using Django's [`escape()`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#std-templatefilter-escape) on the slot contents / result:
+
+```python
+from django.utils.html import escape
+
+class Calendar(Component):
+ template = """
+
+ {% slot "date" default date=date / %}
+
+ """
+
+Calendar.render(
+ slots={
+ "date": escape("Hello"),
+ }
+)
+```
+
+To disable escaping, you can wrap the slot string or slot result in Django's [`mark_safe()`](https://docs.djangoproject.com/en/5.2/ref/utils/#django.utils.safestring.mark_safe):
+
+```py
+Calendar.render(
+ slots={
+ # string
+ "date": mark_safe("Hello"),
+
+ # function
+ "date": lambda ctx: mark_safe("Hello"),
+ }
+)
+```
+
+!!! info
+
+ Read more about Django's
+ [`format_html`](https://docs.djangoproject.com/en/5.2/ref/utils/#django.utils.html.format_html)
+ and [`mark_safe`](https://docs.djangoproject.com/en/5.2/ref/utils/#django.utils.safestring.mark_safe).
+
+## Examples
+
+### Pass through all the slots
+
+You can dynamically pass all slots to a child component. This is similar to
+[passing all slots in Vue](https://vue-land.github.io/faq/forwarding-slots#passing-all-slots):
+
+```djc_py
+class MyTable(Component):
+ template = """
+
+ {% component "child" %}
+ {% for slot_name, slot in component_vars.slots.items %}
+ {% fill name=slot_name body=slot / %}
+ {% endfor %}
+ {% endcomponent %}
+
+ """
+```
+
+### Required and default slots
+
+Since each [`{% slot %}`](../../../reference/template_tags#slot) is tagged
+with [`required`](#required-slot) and [`default`](#default-slot) individually, you can have multiple slots
with the same name but different conditions.
-E.g. in this example, we have a component that renders a user avatar - a small circular image with a profile picture or name initials.
+In this example, we have a component that renders a user avatar - a small circular image with a profile picture or name initials.
If the component is given `image_src` or `name_initials` variables,
-the `image` slot is optional. But if neither of those are provided,
-you MUST fill the `image` slot.
+the `image` slot is optional.
+
+But if neither of those are provided, you MUST fill the `image` slot.
```htmldjango
+ {# Image given, so slot is optional #}
{% if image_src %}
{% slot "image" default %}
{% endslot %}
+
+ {# Image not given, but we can make image from initials, so slot is optional #}
{% elif name_initials %}
{% slot "image" default %}
```
-### Accessing original content of slots
+### Dynamic slots in table component
-_Added in version 0.26_
+Sometimes you may want to generate slots based on the given input. One example of this is [Vuetify's table component](https://vuetifyjs.com/en/api/v-data-table/), which creates a header and an item slots for each user-defined column.
-> NOTE: In version 0.77, the syntax was changed from
->
-> ```django
-> {% fill "my_slot" as "alias" %} {{ alias.default }}
-> ```
->
-> to
->
-> ```django
-> {% fill "my_slot" default="slot_default" %} {{ slot_default }}
-> ```
-
-Sometimes you may want to keep the original slot, but only wrap or prepend/append content to it. To do so, you can access the default slot via the `default` kwarg.
-
-Similarly to the `data` attribute, you specify the variable name through which the default slot will be made available.
-
-For instance, let's say you're filling a slot called 'body'. To render the original slot, assign it to a variable using the `'default'` keyword. You then render this variable to insert the default content:
-
-```htmldjango
-{% component "calendar" date="2020-06-06" %}
- {% fill "body" default="body_default" %}
- {{ body_default }}. Have a great day!
- {% endfill %}
-{% endcomponent %}
-```
-
-This produces:
-
-```htmldjango
-
-
- Calendar header
-
-
- Today's date is 2020-06-06. Have a great day!
-
-
-```
-
-To access the original content of a default slot, set the name to `default`:
-
-```htmldjango
-{% component "calendar" date="2020-06-06" %}
- {% fill "default" default="slot_default" %}
- {{ slot_default }}. Have a great day!
- {% endfill %}
-{% endcomponent %}
-```
-
-### Conditional slots
-
-_Added in version 0.26._
-
-> NOTE: In version 0.70, `{% if_filled %}` tags were replaced with `{{ component_vars.is_filled }}` variables. If your slot name contained special characters, see the section [Accessing `is_filled` of slot names with special characters](#accessing-is_filled-of-slot-names-with-special-characters).
-
-In certain circumstances, you may want the behavior of slot filling to depend on
-whether or not a particular slot is filled.
-
-For example, suppose we have the following component template:
-
-```htmldjango
-
-```
-
-By default the slot named 'subtitle' is empty. Yet when the component is used without
-explicit fills, the div containing the slot is still rendered, as shown below:
-
-```html
-
-
Title
-
-
-```
-
-This may not be what you want. What if instead the outer 'subtitle' div should only
-be included when the inner slot is in fact filled?
-
-The answer is to use the `{{ component_vars.is_filled. }}` variable. You can use this together with Django's `{% if/elif/else/endif %}` tags to define a block whose contents will be rendered only if the component slot with
-the corresponding 'name' is filled.
-
-This is what our example looks like with `component_vars.is_filled`.
-
-```htmldjango
-
-```
-
-Sometimes you're not interested in whether a slot is filled, but rather that it _isn't_.
-To negate the meaning of `component_vars.is_filled`, simply treat it as boolean and negate it with `not`:
-
-```htmldjango
-{% if not component_vars.is_filled.subtitle %}
-
- {% slot "subtitle" / %}
-
-{% endif %}
-```
-
-#### Accessing `is_filled` of slot names with special characters
-
-To be able to access a slot name via `component_vars.is_filled`, the slot name needs to be composed of only alphanumeric characters and underscores (e.g. `this__isvalid_123`).
-
-However, you can still define slots with other special characters. In such case, the slot name in `component_vars.is_filled` is modified to replace all invalid characters into `_`.
-
-So a slot named `"my super-slot :)"` will be available as `component_vars.is_filled.my_super_slot___`.
-
-Same applies when you are accessing `is_filled` from within the Python, e.g.:
+So if you pass columns named `name` and `age` to the table component:
```py
-class MyTable(Component):
- def on_render_before(self, context, template) -> None:
- # ✅ Works
- if self.is_filled["my_super_slot___"]:
- # Do something
-
- # ❌ Does not work
- if self.is_filled["my super-slot :)"]:
- # Do something
+[
+ {"key": "name", "title": "Name"},
+ {"key": "age", "title": "Age"},
+]
```
-### Conditional fills
-
-Similarly, you can use `{% if %}` and `{% for %}` when defining the `{% fill %}` tags, to conditionally fill the slots when using the componnet:
-
-In the example below, the `{% fill "footer" %}` fill is used only if the condition is true. If falsy, the fill is ignored, and so the `my_table` component will use its default content for the `footer` slot.
-
-```django_html
-{% component "my_table" %}
- {% if editable %}
- {% fill "footer" %}
-
- {% endfill %}
- {% endif %}
-{% endcomponent %}
-```
-
-You can even combine `{% if %}` and `{% for %}`:
-
-```django_html
-{% component "my_table" %}
- {% for header in headers %}
- {% if header != "hyperlink" %}
- {# Generate fill name like `header.my_column` #}
- {% fill name="header."|add:header" %}
- {{ header }}
- {% endfill %}
- {% endif %}
- {% endfor %}
-{% endcomponent %}
-```
-
-### Scoped slots
-
-_Added in version 0.76_:
-
-Consider a component with slot(s). This component may do some processing on the inputs, and then use the processed variable in the slot's default template:
-
-```djc_py
-@register("my_comp")
-class MyComp(Component):
- template = """
-
- """
-
- def get_context_data(self, input):
- processed_input = do_something(input)
- return {"input": processed_input}
-```
-
-You may want to design a component so that users of your component can still access the `input` variable, so they don't have to recompute it.
-
-This behavior is called "scoped slots". This is inspired by [Vue scoped slots](https://vuejs.org/guide/components/slots.html#scoped-slots) and [scoped slots of django-web-components](https://github.com/Xzya/django-web-components/tree/master?tab=readme-ov-file#scoped-slots).
-
-Using scoped slots consists of two steps:
-
-1. Passing data to `slot` tag
-2. Accessing data in `fill` tag
-
-#### Passing data to slots
-
-To pass the data to the `slot` tag, simply pass them as keyword attributes (`key=value`):
-
-```djc_py
-@register("my_comp")
-class MyComp(Component):
- template = """
-
- """
-
- def get_context_data(self, input):
- processed_input = do_something(input)
- return {
- "input": processed_input,
- }
-```
-
-#### Accessing slot data in fill
-
-Next, we head over to where we define a fill for this slot. Here, to access the slot data
-we set the `data` attribute to the name of the variable through which we want to access
-the slot data. In the example below, we set it to `data`:
+Then the component will accept fills named `header-name` and `header-age` (among others):
```django
-{% component "my_comp" %}
- {% fill "content" data="slot_data" %}
- {{ slot_data.input }}
- {% endfill %}
-{% endcomponent %}
+{% fill "header-name" data="data" %}
+ {{ data.value }}
+{% endfill %}
+
+{% fill "header-age" data="data" %}
+ {{ data.value }}
+{% endfill %}
```
-To access slot data on a default slot, you have to explictly define the `{% fill %}` tags.
-
-So this works:
-
-```django
-{% component "my_comp" %}
- {% fill "content" data="slot_data" %}
- {{ slot_data.input }}
- {% endfill %}
-{% endcomponent %}
-```
-
-While this does not:
-
-```django
-{% component "my_comp" data="data" %}
- {{ data.input }}
-{% endcomponent %}
-```
-
-Note: You cannot set the `data` attribute and
-[`default` attribute)](#accessing-original-content-of-slots)
-to the same name. This raises an error:
-
-```django
-{% component "my_comp" %}
- {% fill "content" data="slot_var" default="slot_var" %}
- {{ slot_var.input }}
- {% endfill %}
-{% endcomponent %}
-```
-
-#### Slot data of default slots
-
-To access data of a default slot, you can specify `{% fill name="default" %}`:
-
-```htmldjango
-{% component "my_comp" %}
- {% fill "default" data="slot_data" %}
- {{ slot_data.input }}
- {% endfill %}
-{% endcomponent %}
-```
-
-### Dynamic slots and fills
-
-Until now, we were declaring slot and fill names statically, as a string literal, e.g.
-
-```django
-{% slot "content" / %}
-```
-
-However, sometimes you may want to generate slots based on the given input. One example of this is [a table component like that of Vuetify](https://vuetifyjs.com/en/api/v-data-table/), which creates a header and an item slots for each user-defined column.
-
-In django_components you can achieve the same, simply by using a variable (or a [template expression](#use-template-tags-inside-component-inputs)) instead of a string literal:
+In django-components you can achieve the same, simply by using a variable or a [template expression](../template_tag_syntax#template-tags-inside-literal-strings) instead of a string literal:
```django
@@ -599,9 +964,9 @@ When using the component, you can either set the fill explicitly:
```django
{% component "table" headers=headers items=items %}
- {% fill "header-name" data="data" %}
- {{ data.value }}
- {% endfill %}
+ {% fill "header-name" data="data" %}
+ {{ data.value }}
+ {% endfill %}
{% endcomponent %}
```
@@ -609,16 +974,22 @@ Or also use a variable:
```django
{% component "table" headers=headers items=items %}
- {# Make only the active column bold #}
- {% fill "header-{{ active_header_name }}" data="data" %}
- {{ data.value }}
- {% endfill %}
+ {% fill "header-{{ active_header_name }}" data="data" %}
+ {{ data.value }}
+ {% endfill %}
{% endcomponent %}
```
-> NOTE: It's better to use static slot names whenever possible for clarity. The dynamic slot names should be reserved for advanced use only.
+!!! note
-Lastly, in rare cases, you can also pass the slot name via [the spread operator](#spread-operator). This is possible, because the slot name argument is actually a shortcut for a `name` keyword argument.
+ It's better to use literal slot names whenever possible for clarity.
+ The dynamic slot names should be reserved for advanced use only.
+
+### Spread operator
+
+Lastly, you can also pass the slot name through the [spread operator](../template_tag_syntax#spread-operator).
+
+When you define a slot name, it's actually a shortcut for a `name` keyword argument.
So this:
@@ -639,27 +1010,85 @@ So it's possible to define a `name` key on a dictionary, and then spread that on
{% slot ...slot_props / %}
```
-### Pass through all the slots
-
-You can dynamically pass all slots to a child component. This is similar to
-[passing all slots in Vue](https://vue-land.github.io/faq/forwarding-slots#passing-all-slots):
+Full example:
```djc_py
class MyTable(Component):
- def get_context_data(self, *args, **kwargs):
- return {
- "slots": self.input.slots,
- }
-
template = """
-
+ {% slot ...slot_props / %}
"""
+
+ def get_template_data(self, args, kwargs, slots, context):
+ return {
+ "slot_props": {"name": "content", "extra_field": 123},
+ }
+```
+
+!!! info
+
+ This applies for both [`{% slot %}`](../../../reference/template_tags#slot)
+ and [`{% fill %}`](../../../reference/template_tags#fill) tags.
+
+
+
+## Legacy conditional slots
+
+> Since version 0.70, you could check if a slot was filled with
+>
+> `{{ component_vars.is_filled. }}`
+>
+> Since version 0.140, this has been deprecated and superseded with
+>
+> [`{% component_vars.slots. %}`](../../../reference/template_vars#slots)
+>
+> The `component_vars.is_filled` variable is still available, but will be removed in v1.0.
+>
+> NOTE: `component_vars.slots` no longer escapes special characters in slot names.
+
+You can use `{{ component_vars.is_filled. }}` together with Django's `{% if / elif / else / endif %}` tags
+to define a block whose contents will be rendered only if the component slot with the corresponding 'name' is filled.
+
+This is what our example looks like with `component_vars.is_filled`.
+
+```htmldjango
+
+```
+
+### Accessing `is_filled` of slot names with special characters
+
+To be able to access a slot name via `component_vars.is_filled`, the slot name needs to be composed of only alphanumeric characters and underscores (e.g. `this__isvalid_123`).
+
+However, you can still define slots with other special characters. In such case, the slot name in `component_vars.is_filled` is modified to replace all invalid characters into `_`.
+
+So a slot named `"my super-slot :)"` will be available as `component_vars.is_filled.my_super_slot___`.
+
+Same applies when you are accessing `is_filled` from within the Python, e.g.:
+
+```py
+class MyTable(Component):
+ def on_render_before(self, context, template) -> None:
+ # ✅ Works
+ if self.is_filled["my_super_slot___"]:
+ # Do something
+
+ # ❌ Does not work
+ if self.is_filled["my super-slot :)"]:
+ # Do something
```
diff --git a/docs/concepts/fundamentals/subclassing_components.md b/docs/concepts/fundamentals/subclassing_components.md
index 7a8bf950..3d1f0dcb 100644
--- a/docs/concepts/fundamentals/subclassing_components.md
+++ b/docs/concepts/fundamentals/subclassing_components.md
@@ -4,7 +4,7 @@ In such cases, you can extract shared behavior into a standalone component class
When subclassing a component, there's a couple of things to keep in mind:
-### Template, JS, and CSS Inheritance
+## Template, JS, and CSS inheritance
When it comes to the pairs:
@@ -52,13 +52,13 @@ class CustomCard(BaseCard):
"""
```
-### Media Class Inheritance
+## Media inheritance
The [`Component.Media`](../../reference/api.md#django_components.Component.Media) nested class follows Django's media inheritance rules:
- If both parent and child define a `Media` class, the child's media will automatically include both its own and the parent's JS and CSS files.
- This behavior can be configured using the [`extend`](../../reference/api.md#django_components.Component.Media.extend) attribute in the Media class, similar to Django's forms.
- Read more on this in [Controlling Media Inheritance](./defining_js_css_html_files.md#controlling-media-inheritance).
+ Read more on this in [Media inheritance](./secondary_js_css_files/#media-inheritance).
For example:
@@ -83,7 +83,35 @@ class SimpleModal(BaseModal):
js = ["simple_modal.js"] # Only this JS will be included
```
-### Regular Python Inheritance
+## Opt out of inheritance
+
+For the following media attributes, when you don't want to inherit from the parent,
+but you also don't need to set the template / JS / CSS to any specific value,
+you can set these attributes to `None`.
+
+- [`template`](../../reference/api.md#django_components.Component.template) / [`template_file`](../../reference/api.md#django_components.Component.template_file)
+- [`js`](../../reference/api.md#django_components.Component.js) / [`js_file`](../../reference/api.md#django_components.Component.js_file)
+- [`css`](../../reference/api.md#django_components.Component.css) / [`css_file`](../../reference/api.md#django_components.Component.css_file)
+- [`Media`](../../reference/api.md#django_components.Component.Media) class
+
+For example:
+
+```djc_py
+class BaseForm(Component):
+ template = "..."
+ css = "..."
+ js = "..."
+
+ class Media:
+ js = ["form.js"]
+
+# Use parent's template and CSS, but no JS
+class ContactForm(BaseForm):
+ js = None
+ Media = None
+```
+
+## Regular Python inheritance
All other attributes and methods (including the [`Component.View`](../../reference/api.md#django_components.ComponentView) class and its methods) follow standard Python inheritance rules.
@@ -100,7 +128,7 @@ class BaseForm(Component):
"""
- def get_context_data(self, **kwargs):
+ def get_template_data(self, args, kwargs, slots, context):
return {
"form_content": self.get_form_content(),
"submit_text": "Submit"
@@ -112,8 +140,8 @@ class BaseForm(Component):
class ContactForm(BaseForm):
# Extend parent's "context"
# but override "submit_text"
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
+ def get_template_data(self, args, kwargs, slots, context):
+ context = super().get_template_data(args, kwargs, slots, context)
context["submit_text"] = "Send Message"
return context
diff --git a/docs/concepts/fundamentals/template_tag_syntax.md b/docs/concepts/fundamentals/template_tag_syntax.md
index 58c9c620..54f59697 100644
--- a/docs/concepts/fundamentals/template_tag_syntax.md
+++ b/docs/concepts/fundamentals/template_tag_syntax.md
@@ -30,14 +30,12 @@ so are still valid:
```
-These can then be accessed inside `get_context_data` so:
+These can then be accessed inside `get_template_data` so:
```py
@register("calendar")
class Calendar(Component):
- # Since # . @ - are not valid identifiers, we have to
- # use `**kwargs` so the method can accept these args.
- def get_context_data(self, **kwargs):
+ def get_template_data(self, args, kwargs, slots, context):
return {
"date": kwargs["my-date"],
"id": kwargs["#some_id"],
@@ -85,24 +83,24 @@ Other than that, you can use spread operators multiple times, and even put keywo
In a case of conflicts, the values added later (right-most) overwrite previous values.
-## Use template tags inside component inputs
+## Template tags inside literal strings
_New in version 0.93_
When passing data around, sometimes you may need to do light transformations, like negating booleans or filtering lists.
Normally, what you would have to do is to define ALL the variables
-inside `get_context_data()`. But this can get messy if your components contain a lot of logic.
+inside `get_template_data()`. But this can get messy if your components contain a lot of logic.
```py
@register("calendar")
class Calendar(Component):
- def get_context_data(self, id: str, editable: bool):
+ def get_template_data(self, args, kwargs, slots, context):
return {
- "editable": editable,
- "readonly": not editable,
- "input_id": f"input-{id}",
- "icon_id": f"icon-{id}",
+ "editable": kwargs["editable"],
+ "readonly": not kwargs["editable"],
+ "input_id": f"input-{kwargs['id']}",
+ "icon_id": f"icon-{kwargs['id']}",
...
}
```
@@ -119,26 +117,22 @@ Instead, template tags in django_components (`{% component %}`, `{% slot %}`, `{
/ %}
```
-In the example above:
+In the example above, the component receives:
-- Component `test` receives a positional argument with value `"As positional arg "`. The comment is omitted.
-- Kwarg `title` is passed as a string, e.g. `John Doe`
-- Kwarg `id` is passed as `int`, e.g. `15`
-- Kwarg `readonly` is passed as `bool`, e.g. `False`
-- Kwarg `author` is passed as a string, e.g. `John Wick ` (Comment omitted)
+- Positional argument `"As positional arg "` (Comment omitted)
+- `title` - passed as `str`, e.g. `John Doe`
+- `id` - passed as `int`, e.g. `15`
+- `readonly` - passed as `bool`, e.g. `False`
+- `author` - passed as `str`, e.g. `John Wick ` (Comment omitted)
This is inspired by [django-cotton](https://github.com/wrabit/django-cotton#template-expressions-in-attributes).
### Passing data as string vs original values
-Sometimes you may want to use the template tags to transform
-or generate the data that is then passed to the component.
+In the example above, the kwarg `id` was passed as an integer, NOT a string.
-The data doesn't necessarily have to be strings. In the example above, the kwarg `id` was passed as an integer, NOT a string.
-
-Although the string literals for components inputs are treated as regular Django templates, there is one special case:
-
-When the string literal contains only a single template tag, with no extra text, then the value is passed as the original type instead of a string.
+When the string literal contains only a single template tag, with no extra text (and no extra whitespace),
+then the value is passed as the original type instead of a string.
Here, `page` is an integer:
@@ -204,10 +198,10 @@ class MyComp(Component):
{% component "other" attrs=attrs / %}
"""
- def get_context_data(self, some_id: str):
+ def get_template_data(self, args, kwargs, slots, context):
attrs = {
"class": "pa-4 flex",
- "data-some-id": some_id,
+ "data-some-id": kwargs["some_id"],
"@click.stop": "onClickHandler",
}
return {"attrs": attrs}
@@ -235,8 +229,8 @@ class MyComp(Component):
/ %}
"""
- def get_context_data(self, some_id: str):
- return {"some_id": some_id}
+ def get_template_data(self, args, kwargs, slots, context):
+ return {"some_id": kwargs["some_id"]}
```
Sweet! Now all the relevant HTML is inside the template, and we can move it to a separate file with confidence:
diff --git a/docs/concepts/fundamentals/typing_and_validation.md b/docs/concepts/fundamentals/typing_and_validation.md
new file mode 100644
index 00000000..59c36de0
--- /dev/null
+++ b/docs/concepts/fundamentals/typing_and_validation.md
@@ -0,0 +1,668 @@
+## Typing overview
+
+
+
+!!! warning
+
+ In versions 0.92 to 0.139 (inclusive), the component typing was specified through generics.
+
+ Since v0.140, the types must be specified as class attributes of the Component class - `Args`, `Kwargs`, `Slots`, `TemplateData`, `JsData`, and `CssData`.
+
+ See [Migrating from generics to class attributes](#migrating-from-generics-to-class-attributes) for more info.
+
+!!! warning
+
+ Input validation was NOT part of Django Components between versions 0.136 and 0.139 (inclusive).
+
+The [`Component`](../../../reference/api#django_components.Component) class optionally accepts class attributes
+that allow you to define the types of args, kwargs, slots, as well as the data returned from the data methods.
+
+Use this to add type hints to your components, to validate the inputs at runtime, and to document them.
+
+```py
+from typing import NamedTuple, Optional
+from django.template import Context
+from django_components import Component, SlotInput
+
+class Button(Component):
+ class Args(NamedTuple):
+ size: int
+ text: str
+
+ class Kwargs(NamedTuple):
+ variable: str
+ maybe_var: Optional[int] = None # May be omitted
+
+ class Slots(NamedTuple):
+ my_slot: Optional[SlotInput] = None
+
+ def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
+ ...
+
+ template_file = "button.html"
+```
+
+The class attributes are:
+
+- [`Args`](../../../reference/api#django_components.Component.Args) - Type for positional arguments.
+- [`Kwargs`](../../../reference/api#django_components.Component.Kwargs) - Type for keyword arguments.
+- [`Slots`](../../../reference/api#django_components.Component.Slots) - Type for slots.
+- [`TemplateData`](../../../reference/api#django_components.Component.TemplateData) - Type for data returned from [`get_template_data()`](../../../reference/api#django_components.Component.get_template_data).
+- [`JsData`](../../../reference/api#django_components.Component.JsData) - Type for data returned from [`get_js_data()`](../../../reference/api#django_components.Component.get_js_data).
+- [`CssData`](../../../reference/api#django_components.Component.CssData) - Type for data returned from [`get_css_data()`](../../../reference/api#django_components.Component.get_css_data).
+
+You can specify as many or as few of these as you want, the rest will default to `None`.
+
+## Typing inputs
+
+You can use [`Component.Args`](../../../reference/api#django_components.Component.Args),
+[`Component.Kwargs`](../../../reference/api#django_components.Component.Kwargs),
+and [`Component.Slots`](../../../reference/api#django_components.Component.Slots) to type the component inputs.
+
+When you set these classes, at render time the `args`, `kwargs`, and `slots` parameters of the data methods
+([`get_template_data()`](../../../reference/api#django_components.Component.get_template_data),
+[`get_js_data()`](../../../reference/api#django_components.Component.get_js_data),
+[`get_css_data()`](../../../reference/api#django_components.Component.get_css_data))
+will be instances of these classes.
+
+This way, each component can have runtime validation of the inputs:
+
+- When you use [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
+ or [`@dataclass`](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass),
+ instantiating these classes will check ONLY for the presence of the attributes.
+- When you use [Pydantic models](https://docs.pydantic.dev/latest/concepts/models/),
+ instantiating these classes will check for the presence AND type of the attributes.
+
+If you omit the [`Args`](../../../reference/api#django_components.Component.Args),
+[`Kwargs`](../../../reference/api#django_components.Component.Kwargs), or
+[`Slots`](../../../reference/api#django_components.Component.Slots) classes,
+or set them to `None`, the inputs will be passed as plain lists or dictionaries,
+and will not be validated.
+
+```python
+from typing_extensions import NamedTuple, TypedDict
+from django.template import Context
+from django_components import Component, Slot, SlotInput
+
+# The data available to the `footer` scoped slot
+class ButtonFooterSlotData(TypedDict):
+ value: int
+
+# Define the component with the types
+class Button(Component):
+ class Args(NamedTuple):
+ name: str
+
+ class Kwargs(NamedTuple):
+ surname: str
+ age: int
+ maybe_var: Optional[int] = None # May be omitted
+
+ class Slots(NamedTuple):
+ # Use `SlotInput` to allow slots to be given as `Slot` instance,
+ # plain string, or a function that returns a string.
+ my_slot: Optional[SlotInput] = None
+ # Use `Slot` to allow ONLY `Slot` instances.
+ # The generic is optional, and it specifies the data available
+ # to the slot function.
+ footer: Slot[ButtonFooterSlotData]
+
+ # Add type hints to the data method
+ def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
+ # The parameters are instances of the classes we defined
+ assert isinstance(args, Button.Args)
+ assert isinstance(kwargs, Button.Kwargs)
+ assert isinstance(slots, Button.Slots)
+
+ args.name # str
+ kwargs.age # int
+ slots.footer # Slot[ButtonFooterSlotData]
+
+# Add type hints to the render call
+Button.render(
+ args=Button.Args(
+ name="John",
+ ),
+ kwargs=Button.Kwargs(
+ surname="Doe",
+ age=30,
+ ),
+ slots=Button.Slots(
+ footer=Slot(lambda ctx: "Click me!"),
+ ),
+)
+```
+
+If you don't want to validate some parts, set them to `None` or omit them.
+
+The following will validate only the keyword inputs:
+
+```python
+class Button(Component):
+ # We could also omit these
+ Args = None
+ Slots = None
+
+ class Kwargs(NamedTuple):
+ name: str
+ age: int
+
+ # Only `kwargs` is instantiated. `args` and `slots` are not.
+ def get_template_data(self, args, kwargs: Kwargs, slots, context: Context):
+ assert isinstance(args, list)
+ assert isinstance(slots, dict)
+ assert isinstance(kwargs, Button.Kwargs)
+
+ args[0] # str
+ slots["footer"] # Slot[ButtonFooterSlotData]
+ kwargs.age # int
+```
+
+!!! info
+
+ Components can receive slots as strings, functions, or instances of [`Slot`](../../../reference/api#django_components.Slot).
+
+ Internally these are all normalized to instances of [`Slot`](../../../reference/api#django_components.Slot).
+
+ Therefore, the `slots` dictionary available in data methods (like
+ [`get_template_data()`](../../../reference/api#django_components.Component.get_template_data))
+ will always be a dictionary of [`Slot`](../../../reference/api#django_components.Slot) instances.
+
+ To correctly type this dictionary, you should set the fields of `Slots` to
+ [`Slot`](../../../reference/api#django_components.Slot) or [`SlotInput`](../../../reference/api#django_components.SlotInput):
+
+ [`SlotInput`](../../../reference/api#django_components.SlotInput) is a union of `Slot`, string, and function types.
+
+## Typing data
+
+You can use [`Component.TemplateData`](../../../reference/api#django_components.Component.TemplateData),
+[`Component.JsData`](../../../reference/api#django_components.Component.JsData),
+and [`Component.CssData`](../../../reference/api#django_components.Component.CssData) to type the data returned from [`get_template_data()`](../../../reference/api#django_components.Component.get_template_data), [`get_js_data()`](../../../reference/api#django_components.Component.get_js_data), and [`get_css_data()`](../../../reference/api#django_components.Component.get_css_data).
+
+When you set these classes, at render time they will be instantiated with the data returned from these methods.
+
+This way, each component can have runtime validation of the returned data:
+
+- When you use [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
+ or [`@dataclass`](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass),
+ instantiating these classes will check ONLY for the presence of the attributes.
+- When you use [Pydantic models](https://docs.pydantic.dev/latest/concepts/models/),
+ instantiating these classes will check for the presence AND type of the attributes.
+
+If you omit the [`TemplateData`](../../../reference/api#django_components.Component.TemplateData),
+[`JsData`](../../../reference/api#django_components.Component.JsData), or
+[`CssData`](../../../reference/api#django_components.Component.CssData) classes,
+or set them to `None`, the validation and instantiation will be skipped.
+
+```python
+from typing import NamedTuple
+from django_components import Component
+
+class Button(Component):
+ class TemplateData(NamedTuple):
+ data1: str
+ data2: int
+
+ class JsData(NamedTuple):
+ js_data1: str
+ js_data2: int
+
+ class CssData(NamedTuple):
+ css_data1: str
+ css_data2: int
+
+ def get_template_data(self, args, kwargs, slots, context):
+ return {
+ "data1": "...",
+ "data2": 123,
+ }
+
+ def get_js_data(self, args, kwargs, slots, context):
+ return {
+ "js_data1": "...",
+ "js_data2": 123,
+ }
+
+ def get_css_data(self, args, kwargs, slots, context):
+ return {
+ "css_data1": "...",
+ "css_data2": 123,
+ }
+```
+
+For each data method, you can either return a plain dictionary with the data, or an instance of the respective data class directly.
+
+```python
+from typing import NamedTuple
+from django_components import Component
+
+class Button(Component):
+ class TemplateData(NamedTuple):
+ data1: str
+ data2: int
+
+ class JsData(NamedTuple):
+ js_data1: str
+ js_data2: int
+
+ class CssData(NamedTuple):
+ css_data1: str
+ css_data2: int
+
+ def get_template_data(self, args, kwargs, slots, context):
+ return Button.TemplateData(
+ data1="...",
+ data2=123,
+ )
+
+ def get_js_data(self, args, kwargs, slots, context):
+ return Button.JsData(
+ js_data1="...",
+ js_data2=123,
+ )
+
+ def get_css_data(self, args, kwargs, slots, context):
+ return Button.CssData(
+ css_data1="...",
+ css_data2=123,
+ )
+```
+
+## Custom types
+
+We recommend to use [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
+for the `Args` class, and [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple),
+[dataclasses](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass),
+or [Pydantic models](https://docs.pydantic.dev/latest/concepts/models/)
+for `Kwargs`, `Slots`, `TemplateData`, `JsData`, and `CssData` classes.
+
+However, you can use any class, as long as they meet the conditions below.
+
+For example, here is how you can use [Pydantic models](https://docs.pydantic.dev/latest/concepts/models/)
+to validate the inputs at runtime.
+
+```py
+from django_components import Component
+from pydantic import BaseModel
+
+class Table(Component):
+ class Kwargs(BaseModel):
+ name: str
+ age: int
+
+ def get_template_data(self, args, kwargs, slots, context):
+ assert isinstance(kwargs, Table.Kwargs)
+
+Table.render(
+ kwargs=Table.Kwargs(name="John", age=30),
+)
+```
+
+### `Args` class
+
+The [`Args`](../../../reference/api#django_components.Component.Args) class
+represents a list of positional arguments. It must meet two conditions:
+
+1. The constructor for the `Args` class must accept positional arguments.
+
+ ```py
+ Args(*args)
+ ```
+
+2. The `Args` instance must be convertable to a list.
+
+ ```py
+ list(Args(1, 2, 3))
+ ```
+
+To implement the conversion to a list, you can implement the `__iter__()` method:
+
+```py
+class MyClass:
+ def __init__(self):
+ self.x = 1
+ self.y = 2
+
+ def __iter__(self):
+ return iter([('x', self.x), ('y', self.y)])
+```
+
+### Dictionary classes
+
+On the other hand, other types
+([`Kwargs`](../../../reference/api#django_components.Component.Kwargs),
+[`Slots`](../../../reference/api#django_components.Component.Slots),
+[`TemplateData`](../../../reference/api#django_components.Component.TemplateData),
+[`JsData`](../../../reference/api#django_components.Component.JsData),
+and [`CssData`](../../../reference/api#django_components.Component.CssData))
+represent dictionaries. They must meet these two conditions:
+
+1. The constructor must accept keyword arguments.
+
+ ```py
+ Kwargs(**kwargs)
+ Slots(**slots)
+ ```
+
+2. The instance must be convertable to a dictionary.
+
+ ```py
+ dict(Kwargs(a=1, b=2))
+ dict(Slots(a=1, b=2))
+ ```
+
+To implement the conversion to a dictionary, you can implement either:
+
+1. `_asdict()` method
+ ```py
+ class MyClass:
+ def __init__(self):
+ self.x = 1
+ self.y = 2
+
+ def _asdict(self):
+ return {'x': self.x, 'y': self.y}
+ ```
+
+2. Or make the class dict-like with `__iter__()` and `__getitem__()`
+ ```py
+ class MyClass:
+ def __init__(self):
+ self.x = 1
+ self.y = 2
+
+ def __iter__(self):
+ return iter([('x', self.x), ('y', self.y)])
+
+ def __getitem__(self, key):
+ return getattr(self, key)
+ ```
+
+## Passing variadic args and kwargs
+
+You may have a component that accepts any number of args or kwargs.
+
+However, this cannot be described with the current Python's typing system (as of v0.140).
+
+As a workaround:
+
+- For a variable number of positional arguments (`*args`), set a positional argument that accepts a list of values:
+
+ ```py
+ class Table(Component):
+ class Args(NamedTuple):
+ args: List[str]
+
+ Table.render(
+ args=Table.Args(args=["a", "b", "c"]),
+ )
+ ```
+
+- For a variable number of keyword arguments (`**kwargs`), set a keyword argument that accepts a dictionary of values:
+
+ ```py
+ class Table(Component):
+ class Kwargs(NamedTuple):
+ variable: str
+ another: int
+ # Pass any extra keys under `extra`
+ extra: Dict[str, any]
+
+ Table.render(
+ kwargs=Table.Kwargs(
+ variable="a",
+ another=1,
+ extra={"foo": "bar"},
+ ),
+ )
+ ```
+
+## Handling no args or no kwargs
+
+To declare that a component accepts no args, kwargs, etc, define the types with no attributes using the `pass` keyword:
+
+```py
+from typing import NamedTuple
+from django_components import Component
+
+class Button(Component):
+ class Args(NamedTuple):
+ pass
+
+ class Kwargs(NamedTuple):
+ pass
+
+ class Slots(NamedTuple):
+ pass
+```
+
+This can get repetitive, so we added a [`Empty`](../../../reference/api#django_components.Empty) type to make it easier:
+
+```py
+from django_components import Component, Empty
+
+class Button(Component):
+ Args = Empty
+ Kwargs = Empty
+ Slots = Empty
+```
+
+## Subclassing
+
+Subclassing components with types is simple.
+
+Since each type class is a separate class attribute, you can just override them in the Component subclass.
+
+In the example below, `ButtonExtra` inherits `Kwargs` from `Button`, but overrides the `Args` class.
+
+```py
+from django_components import Component, Empty
+
+class Button(Component):
+ class Args(NamedTuple):
+ size: int
+
+ class Kwargs(NamedTuple):
+ color: str
+
+class ButtonExtra(Button):
+ class Args(NamedTuple):
+ name: str
+ size: int
+
+# Stil works the same way!
+ButtonExtra.render(
+ args=ButtonExtra.Args(name="John", size=30),
+ kwargs=ButtonExtra.Kwargs(color="red"),
+)
+```
+
+The only difference is when it comes to type hints to the data methods like
+[`get_template_data()`](../../../reference/api#django_components.Component.get_template_data).
+
+When you define the nested classes like `Args` and `Kwargs` directly on the class, you
+can reference them just by their class name (`Args` and `Kwargs`).
+
+But when you have a Component subclass, and it uses `Args` or `Kwargs` from the parent,
+you will have to reference the type as a [forward reference](https://peps.python.org/pep-0563/#forward-references), including the full name of the component
+(`Button.Args` and `Button.Kwargs`).
+
+Compare the following:
+
+```py
+class Button(Component):
+ class Args(NamedTuple):
+ size: int
+
+ class Kwargs(NamedTuple):
+ color: str
+
+ # Both `Args` and `Kwargs` are defined on the class
+ def get_template_data(self, args: Args, kwargs: Kwargs, slots, context):
+ pass
+
+class ButtonExtra(Button):
+ class Args(NamedTuple):
+ name: str
+ size: int
+
+ # `Args` is defined on the subclass, `Kwargs` is defined on the parent
+ def get_template_data(self, args: Args, kwargs: "ButtonExtra.Kwargs", slots, context):
+ pass
+
+class ButtonSame(Button):
+ # Both `Args` and `Kwargs` are defined on the parent
+ def get_template_data(self, args: "ButtonSame.Args", kwargs: "ButtonSame.Kwargs", slots, context):
+ pass
+```
+
+## Runtime type validation
+
+When you add types to your component, and implement
+them as [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) or [`dataclass`](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass), the validation will check only for the presence of the attributes.
+
+So this will not catch if you pass a string to an `int` attribute.
+
+To enable runtime type validation, you need to use [Pydantic models](https://docs.pydantic.dev/latest/concepts/models/), and install the [`djc-ext-pydantic`](https://github.com/django-components/djc-ext-pydantic) extension.
+
+The `djc-ext-pydantic` extension ensures compatibility between django-components' classes such as `Component`, or `Slot` and Pydantic models.
+
+First install the extension:
+
+```bash
+pip install djc-ext-pydantic
+```
+
+And then add the extension to your project:
+
+```py
+COMPONENTS = {
+ "extensions": [
+ "djc_pydantic.PydanticExtension",
+ ],
+}
+```
+
+
+
+## Migrating from generics to class attributes
+
+In versions 0.92 to 0.139 (inclusive), the component typing was specified through generics.
+
+Since v0.140, the types must be specified as class attributes of the [Component](../../../reference/api#django_components.Component) class -
+[`Args`](../../../reference/api#django_components.Component.Args),
+[`Kwargs`](../../../reference/api#django_components.Component.Kwargs),
+[`Slots`](../../../reference/api#django_components.Component.Slots),
+[`TemplateData`](../../../reference/api#django_components.Component.TemplateData),
+[`JsData`](../../../reference/api#django_components.Component.JsData),
+and [`CssData`](../../../reference/api#django_components.Component.CssData).
+
+This change was necessary to make it possible to subclass components. Subclassing with generics was otherwise too complicated. Read the discussion [here](https://github.com/django-components/django-components/issues/1122).
+
+Because of this change, the [`Component.render()`](../../../reference/api#django_components.Component.render)
+method is no longer typed.
+To type-check the inputs, you should wrap the inputs in [`Component.Args`](../../../reference/api#django_components.Component.Args),
+[`Component.Kwargs`](../../../reference/api#django_components.Component.Kwargs),
+[`Component.Slots`](../../../reference/api#django_components.Component.Slots), etc.
+
+For example, if you had a component like this:
+
+```py
+from typing import NotRequired, Tuple, TypedDict
+from django_components import Component, Slot, SlotInput
+
+ButtonArgs = Tuple[int, str]
+
+class ButtonKwargs(TypedDict):
+ variable: str
+ another: int
+ maybe_var: NotRequired[int] # May be omitted
+
+class ButtonSlots(TypedDict):
+ # Use `SlotInput` to allow slots to be given as `Slot` instance,
+ # plain string, or a function that returns a string.
+ my_slot: NotRequired[SlotInput]
+ # Use `Slot` to allow ONLY `Slot` instances.
+ another_slot: Slot
+
+ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots]
+
+class Button(ButtonType):
+ def get_context_data(self, *args, **kwargs):
+ self.input.args[0] # int
+ self.input.kwargs["variable"] # str
+ self.input.slots["my_slot"] # Slot[MySlotData]
+
+Button.render(
+ args=(1, "hello"),
+ kwargs={
+ "variable": "world",
+ "another": 123,
+ },
+ slots={
+ "my_slot": "...",
+ "another_slot": Slot(lambda ctx: ...),
+ },
+)
+```
+
+The steps to migrate are:
+
+1. Convert all the types (`ButtonArgs`, `ButtonKwargs`, `ButtonSlots`) to subclasses
+ of [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple).
+2. Move these types inside the Component class (`Button`), and rename them to `Args`, `Kwargs`, and `Slots`.
+3. If you defined typing for the data methods (like [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data)), move them inside the Component class, and rename them to `TemplateData`, `JsData`, and `CssData`.
+4. Remove the `Component` generic.
+5. If you accessed the `args`, `kwargs`, or `slots` attributes via
+ [`self.input`](../../../reference/api#django_components.Component.input), you will need to add the type hints yourself, because [`self.input`](../../../reference/api#django_components.Component.input) is no longer typed.
+
+ Otherwise, you may use [`Component.get_template_data()`](../../../reference/api#django_components.Component.get_template_data) instead of [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data), as `get_template_data()` receives `args`, `kwargs`, `slots` and `context` as arguments. You will still need to add the type hints yourself.
+
+6. Lastly, you will need to update the [`Component.render()`](../../../reference/api#django_components.Component.render)
+ calls to wrap the inputs in [`Component.Args`](../../../reference/api#django_components.Component.Args), [`Component.Kwargs`](../../../reference/api#django_components.Component.Kwargs), and [`Component.Slots`](../../../reference/api#django_components.Component.Slots), to manually add type hints.
+
+Thus, the code above will become:
+
+```py
+from typing import NamedTuple, Optional
+from django.template import Context
+from django_components import Component, Slot, SlotInput
+
+# The Component class does not take any generics
+class Button(Component):
+ # Types are now defined inside the component class
+ class Args(NamedTuple):
+ size: int
+ text: str
+
+ class Kwargs(NamedTuple):
+ variable: str
+ another: int
+ maybe_var: Optional[int] = None # May be omitted
+
+ class Slots(NamedTuple):
+ # Use `SlotInput` to allow slots to be given as `Slot` instance,
+ # plain string, or a function that returns a string.
+ my_slot: Optional[SlotInput] = None
+ # Use `Slot` to allow ONLY `Slot` instances.
+ another_slot: Slot
+
+ # The args, kwargs, slots are instances of the component's Args, Kwargs, and Slots classes
+ def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
+ args.size # int
+ kwargs.variable # str
+ slots.my_slot # Slot[MySlotData]
+
+Button.render(
+ # Wrap the inputs in the component's Args, Kwargs, and Slots classes
+ args=Button.Args(1, "hello"),
+ kwargs=Button.Kwargs(
+ variable="world",
+ another=123,
+ ),
+ slots=Button.Slots(
+ my_slot="...",
+ another_slot=Slot(lambda ctx: ...),
+ ),
+)
+```
diff --git a/docs/getting_started/adding_js_and_css.md b/docs/getting_started/adding_js_and_css.md
index 0b097935..cc3e675a 100644
--- a/docs/getting_started/adding_js_and_css.md
+++ b/docs/getting_started/adding_js_and_css.md
@@ -52,7 +52,8 @@ Be sure to prefix your rules with unique CSS class like `calendar`, so the CSS d
This CSS will be inserted into the page as an inlined `
+
+ {output}
+
+ """
+
+ return output
+
+
+class HighlightComponentsDescriptor:
+ def __get__(self, obj: Optional[Any], objtype: Type) -> bool:
+ return app_settings.DEBUG_HIGHLIGHT_COMPONENTS
+
+
+class HighlightSlotsDescriptor:
+ def __get__(self, obj: Optional[Any], objtype: Type) -> bool:
+ return app_settings.DEBUG_HIGHLIGHT_SLOTS
+
+
+class ComponentDebugHighlight(ExtensionComponentConfig):
+ """
+ The interface for `Component.DebugHighlight`.
+
+ The fields of this class are used to configure the component debug highlighting for this component
+ and its direct slots.
+
+ Read more about [Component debug highlighting](../../guides/other/troubleshooting#component-and-slot-highlighting).
+
+ **Example:**
+
+ ```python
+ from django_components import Component
+
+ class MyComponent(Component):
+ class DebugHighlight:
+ highlight_components = True
+ highlight_slots = True
+ ```
+
+ To highlight ALL components and slots, set
+ [extension defaults](../../reference/settings/#django_components.app_settings.ComponentsSettings.extensions_defaults)
+ in your settings:
+
+ ```python
+ from django_components import ComponentsSettings
+
+ COMPONENTS = ComponentsSettings(
+ extensions_defaults={
+ "debug_highlight": {
+ "highlight_components": True,
+ "highlight_slots": True,
+ },
+ },
+ )
+ ```
+ """ # noqa: E501
+
+ # TODO_v1 - Remove `DEBUG_HIGHLIGHT_COMPONENTS` and `DEBUG_HIGHLIGHT_SLOTS`
+ # Instead set this as plain boolean fields.
+ highlight_components = HighlightComponentsDescriptor()
+ """Whether to highlight this component in the rendered output."""
+ highlight_slots = HighlightSlotsDescriptor()
+ """Whether to highlight slots of this component in the rendered output."""
+
+
+# TODO_v1 - Move into standalone extension (own repo?) and ask people to manually add this extension in settings.
+class DebugHighlightExtension(ComponentExtension):
+ """
+ This extension adds the ability to highlight components and slots in the rendered output.
+
+ To highlight slots, set `ComponentsSettings.DEBUG_HIGHLIGHT_SLOTS` to `True` in your settings.
+
+ To highlight components, set `ComponentsSettings.DEBUG_HIGHLIGHT_COMPONENTS` to `True`.
+
+ Highlighting is done by wrapping the content in a `
` with a border and a highlight color.
+
+ This extension is automatically added to all components.
+ """
+
+ name = "debug_highlight"
+ ComponentConfig = ComponentDebugHighlight
+
+ # Apply highlight to the slot's rendered output
+ def on_slot_rendered(self, ctx: OnSlotRenderedContext) -> Optional[str]:
+ debug_cls: Optional[ComponentDebugHighlight] = getattr(ctx.component_cls, "DebugHighlight", None)
+ if not debug_cls or not debug_cls.highlight_slots:
+ return None
+
+ return apply_component_highlight("slot", ctx.result, f"{ctx.component_cls.__name__} - {ctx.slot_name}")
+
+ # Apply highlight to the rendered component
+ def on_component_rendered(self, ctx: OnComponentRenderedContext) -> Optional[str]:
+ debug_cls: Optional[ComponentDebugHighlight] = getattr(ctx.component_cls, "DebugHighlight", None)
+ if not debug_cls or not debug_cls.highlight_components or ctx.result is None:
+ return None
+
+ return apply_component_highlight("component", ctx.result, f"{ctx.component.name} ({ctx.component_id})")
diff --git a/src/django_components/extensions/defaults.py b/src/django_components/extensions/defaults.py
index e356c02b..a2781d92 100644
--- a/src/django_components/extensions/defaults.py
+++ b/src/django_components/extensions/defaults.py
@@ -3,7 +3,12 @@ from dataclasses import MISSING, Field, dataclass
from typing import TYPE_CHECKING, Any, Callable, Dict, List, NamedTuple, Optional, Type
from weakref import WeakKeyDictionary
-from django_components.extension import ComponentExtension, OnComponentClassCreatedContext, OnComponentInputContext
+from django_components.extension import (
+ ComponentExtension,
+ ExtensionComponentConfig,
+ OnComponentClassCreatedContext,
+ OnComponentInputContext,
+)
if TYPE_CHECKING:
from django_components.component import Component
@@ -62,7 +67,8 @@ def _extract_defaults(defaults: Optional[Type]) -> List[ComponentDefaultField]:
for default_field_key in dir(defaults):
# Iterate only over fields set by the user (so non-dunder fields).
# Plus ignore `component_class` because that was set by the extension system.
- if default_field_key.startswith("__") or default_field_key == "component_class":
+ # TODO_V1 - Remove `component_class`
+ if default_field_key.startswith("__") or default_field_key in {"component_class", "component_cls"}:
continue
default_field = getattr(defaults, default_field_key)
@@ -119,6 +125,29 @@ def _apply_defaults(kwargs: Dict, defaults: List[ComponentDefaultField]) -> None
kwargs[default_field.key] = default_value
+class ComponentDefaults(ExtensionComponentConfig):
+ """
+ The interface for `Component.Defaults`.
+
+ The fields of this class are used to set default values for the component's kwargs.
+
+ Read more about [Component defaults](../../concepts/fundamentals/component_defaults).
+
+ **Example:**
+
+ ```python
+ from django_components import Component, Default
+
+ class MyComponent(Component):
+ class Defaults:
+ position = "left"
+ selected_items = Default(lambda: [1, 2, 3])
+ ```
+ """
+
+ pass
+
+
class DefaultsExtension(ComponentExtension):
"""
This extension adds a nested `Defaults` class to each `Component`.
@@ -141,6 +170,7 @@ class DefaultsExtension(ComponentExtension):
"""
name = "defaults"
+ ComponentConfig = ComponentDefaults
# Preprocess the `Component.Defaults` class, if given, so we don't have to do it
# each time a component is rendered.
diff --git a/src/django_components/extensions/dependencies.py b/src/django_components/extensions/dependencies.py
new file mode 100644
index 00000000..20c4d444
--- /dev/null
+++ b/src/django_components/extensions/dependencies.py
@@ -0,0 +1,21 @@
+from django_components.dependencies import cache_component_css, cache_component_js
+from django_components.extension import (
+ ComponentExtension,
+ OnComponentClassCreatedContext,
+)
+
+
+class DependenciesExtension(ComponentExtension):
+ """
+ This extension adds a nested `Dependencies` class to each `Component`.
+
+ This extension is automatically added to all components.
+ """
+
+ name = "dependencies"
+
+ # Cache the component's JS and CSS scripts when the class is created, so that
+ # components' JS/CSS files are accessible even before having to render the component first.
+ def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
+ cache_component_js(ctx.component_cls, force=True)
+ cache_component_css(ctx.component_cls, force=True)
diff --git a/src/django_components/extensions/view.py b/src/django_components/extensions/view.py
index 71dfae36..68b7d47f 100644
--- a/src/django_components/extensions/view.py
+++ b/src/django_components/extensions/view.py
@@ -1,80 +1,300 @@
-from typing import TYPE_CHECKING, Any, Protocol, cast
+import sys
+from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional, Protocol, Type, Union, cast
+from weakref import WeakKeyDictionary
+import django.urls
from django.http import HttpRequest, HttpResponse
from django.views.generic import View
-from django_components.extension import ComponentExtension
+from django_components.extension import (
+ ComponentExtension,
+ ExtensionComponentConfig,
+ OnComponentClassCreatedContext,
+ OnComponentClassDeletedContext,
+ URLRoute,
+ extensions,
+)
+from django_components.util.misc import format_url
if TYPE_CHECKING:
from django_components.component import Component
+# NOTE: `WeakKeyDictionary` is NOT a generic pre-3.9
+if sys.version_info >= (3, 9):
+ ComponentRouteCache = WeakKeyDictionary[Type["Component"], URLRoute]
+else:
+ ComponentRouteCache = WeakKeyDictionary
+
class ViewFn(Protocol):
def __call__(self, request: HttpRequest, *args: Any, **kwargs: Any) -> Any: ... # noqa: E704
-class ComponentView(ComponentExtension.ExtensionClass, View): # type: ignore
+def _get_component_route_name(component: Union[Type["Component"], "Component"]) -> str:
+ return f"__component_url__{component.class_id}"
+
+
+def get_component_url(
+ component: Union[Type["Component"], "Component"],
+ query: Optional[Dict] = None,
+ fragment: Optional[str] = None,
+) -> str:
"""
- Subclass of `django.views.View` where the `Component` instance is available
- via `self.component`.
+ Get the URL for a [`Component`](../api#django_components.Component).
+
+ Raises `RuntimeError` if the component is not public.
+
+ Read more about [Component views and URLs](../../concepts/fundamentals/component_views_urls).
+
+ `get_component_url()` optionally accepts `query` and `fragment` arguments.
+
+ **Example:**
+
+ ```py
+ from django_components import Component, get_component_url
+
+ class MyComponent(Component):
+ class View:
+ public = True
+
+ # Get the URL for the component
+ url = get_component_url(
+ MyComponent,
+ query={"foo": "bar"},
+ fragment="baz",
+ )
+ # /components/ext/view/components/c1ab2c3?foo=bar#baz
+ ```
+ """
+ view_cls: Optional[Type[ComponentView]] = getattr(component, "View", None)
+ if not _is_view_public(view_cls):
+ raise RuntimeError("Component URL is not available - Component is not public")
+
+ route_name = _get_component_route_name(component)
+ url = django.urls.reverse(route_name)
+ return format_url(url, query=query, fragment=fragment)
+
+
+class ComponentView(ExtensionComponentConfig, View):
+ """
+ The interface for `Component.View`.
+
+ The fields of this class are used to configure the component views and URLs.
+
+ This class is a subclass of
+ [`django.views.View`](https://docs.djangoproject.com/en/5.2/ref/class-based-views/base/#view).
+ The [`Component`](../api#django_components.Component) class is available
+ via `self.component_cls`.
+
+ Override the methods of this class to define the behavior of the component.
+
+ Read more about [Component views and URLs](../../concepts/fundamentals/component_views_urls).
+
+ **Example:**
+
+ ```python
+ class MyComponent(Component):
+ class View:
+ def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
+ return HttpResponse("Hello, world!")
+ ```
+
+ **Component URL:**
+
+ If the `public` attribute is set to `True`, the component will have its own URL
+ that will point to the Component's View.
+
+ ```py
+ from django_components import Component
+
+ class MyComponent(Component):
+ class View:
+ public = True
+
+ def get(self, request, *args, **kwargs):
+ return HttpResponse("Hello, world!")
+ ```
+
+ Will create a URL route like `/components/ext/view/components/a1b2c3/`.
+
+ To get the URL for the component, use [`get_component_url()`](../api#django_components.get_component_url):
+
+ ```py
+ url = get_component_url(MyComponent)
+ ```
"""
- # NOTE: This attribute must be declared on the class for `View.as_view()` to allow
- # us to pass `component` kwarg.
+ # NOTE: The `component` / `component_cls` attributes are NOT user input, but still must be declared
+ # on this class for Django's `View.as_view()` to allow us to pass `component` kwarg.
+
+ # TODO_v1 - Remove. Superseded by `component_cls` attribute because we don't actually have access to an instance.
component = cast("Component", None)
+ """
+ DEPRECATED: Will be removed in v1.0.
+ Use [`component_cls`](../api#django_components.ComponentView.component_cls) instead.
+
+ This is a dummy instance created solely for the View methods.
+
+ It is the same as if you instantiated the component class directly:
+
+ ```py
+ component = Calendar()
+ component.render_to_response(request=request)
+ ```
+ """
+
+ component_cls = cast(Type["Component"], None)
+ """
+ The parent component class.
+
+ **Example:**
+
+ ```py
+ class MyComponent(Component):
+ class View:
+ def get(self, request):
+ return self.component_cls.render_to_response(request=request)
+ ```
+ """
def __init__(self, component: "Component", **kwargs: Any) -> None:
- ComponentExtension.ExtensionClass.__init__(self, component)
+ ComponentExtension.ComponentConfig.__init__(self, component)
View.__init__(self, **kwargs)
+ @property
+ def url(self) -> str:
+ """
+ The URL for the component.
+
+ Raises `RuntimeError` if the component is not public.
+
+ This is the same as calling [`get_component_url()`](../api#django_components.get_component_url)
+ with the parent [`Component`](../api#django_components.Component) class:
+
+ ```py
+ class MyComponent(Component):
+ class View:
+ def get(self, request):
+ assert self.url == get_component_url(self.component_cls)
+ ```
+ """
+ return get_component_url(self.component_cls)
+
+ # #####################################
+ # PUBLIC API (Configurable by users)
+ # #####################################
+
+ public: ClassVar[bool] = False
+ """
+ Whether the component should be available via a URL.
+
+ **Example:**
+
+ ```py
+ from django_components import Component
+
+ class MyComponent(Component):
+ class View:
+ public = True
+ ```
+
+ Will create a URL route like `/components/ext/view/components/a1b2c3/`.
+
+ To get the URL for the component, use [`get_component_url()`](../api#django_components.get_component_url):
+
+ ```py
+ url = get_component_url(MyComponent)
+ ```
+ """
+
# NOTE: The methods below are defined to satisfy the `View` class. All supported methods
# are defined in `View.http_method_names`.
#
# Each method actually delegates to the component's method of the same name.
# E.g. When `get()` is called, it delegates to `component.get()`.
+ # TODO_V1 - In v1 handlers like `get()` should be defined on the Component.View class,
+ # not the Component class directly. This is to align Views with the extensions API
+ # where each extension should keep its methods in the extension class.
+ # Instead, the defaults for these methods should be something like
+ # `return self.component_cls.render_to_response(request, *args, **kwargs)` or similar
+ # or raise NotImplementedError.
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
- component: "Component" = self.component
- return getattr(component, "get")(request, *args, **kwargs)
+ return getattr(self.component_cls(), "get")(request, *args, **kwargs)
def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
- component: "Component" = self.component
- return getattr(component, "post")(request, *args, **kwargs)
+ return getattr(self.component_cls(), "post")(request, *args, **kwargs)
def put(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
- component: "Component" = self.component
- return getattr(component, "put")(request, *args, **kwargs)
+ return getattr(self.component_cls(), "put")(request, *args, **kwargs)
def patch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
- component: "Component" = self.component
- return getattr(component, "patch")(request, *args, **kwargs)
+ return getattr(self.component_cls(), "patch")(request, *args, **kwargs)
def delete(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
- component: "Component" = self.component
- return getattr(component, "delete")(request, *args, **kwargs)
+ return getattr(self.component_cls(), "delete")(request, *args, **kwargs)
def head(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
- component: "Component" = self.component
- return getattr(component, "head")(request, *args, **kwargs)
+ return getattr(self.component_cls(), "head")(request, *args, **kwargs)
def options(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
- component: "Component" = self.component
- return getattr(component, "options")(request, *args, **kwargs)
+ return getattr(self.component_cls(), "options")(request, *args, **kwargs)
def trace(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
- component: "Component" = self.component
- return getattr(component, "trace")(request, *args, **kwargs)
+ return getattr(self.component_cls(), "trace")(request, *args, **kwargs)
class ViewExtension(ComponentExtension):
"""
This extension adds a nested `View` class to each `Component`.
+
This nested class is a subclass of `django.views.View`, and allows the component
to be used as a view by calling `ComponentView.as_view()`.
+ This extension also allows the component to be available via a unique URL.
+
This extension is automatically added to all components.
"""
name = "view"
- ExtensionClass = ComponentView
+ ComponentConfig = ComponentView
+
+ def __init__(self) -> None:
+ # Remember which route belongs to which component
+ self.routes_by_component: ComponentRouteCache = WeakKeyDictionary()
+
+ # Create URL route on creation
+ def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
+ comp_cls = ctx.component_cls
+ view_cls: Optional[Type[ComponentView]] = getattr(comp_cls, "View", None)
+ if not _is_view_public(view_cls):
+ return
+
+ # Create a URL route like `components/MyTable_a1b2c3/`
+ # And since this is within the `view` extension, the full URL path will then be:
+ # `/components/ext/view/components/MyTable_a1b2c3/`
+ route_path = f"components/{comp_cls.class_id}/"
+ route_name = _get_component_route_name(comp_cls)
+ route = URLRoute(
+ path=route_path,
+ handler=comp_cls.as_view(),
+ name=route_name,
+ )
+
+ self.routes_by_component[comp_cls] = route
+ extensions.add_extension_urls(self.name, [route])
+
+ # Remove URL route on deletion
+ def on_component_class_deleted(self, ctx: OnComponentClassDeletedContext) -> None:
+ comp_cls = ctx.component_cls
+ route = self.routes_by_component.pop(comp_cls, None)
+ if route is None:
+ return
+ extensions.remove_extension_urls(self.name, [route])
+
+
+def _is_view_public(view_cls: Optional[Type[ComponentView]]) -> bool:
+ if view_cls is None:
+ return False
+ return getattr(view_cls, "public", False)
diff --git a/src/django_components/finders.py b/src/django_components/finders.py
index 06029459..c91b7382 100644
--- a/src/django_components/finders.py
+++ b/src/django_components/finders.py
@@ -2,6 +2,7 @@ import os
import re
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
+from django import VERSION as DJANGO_VERSION
from django.contrib.staticfiles.finders import BaseFinder
from django.contrib.staticfiles.utils import get_files
from django.core.checks import CheckMessage, Error, Warning
@@ -90,17 +91,32 @@ class ComponentsFileSystemFinder(BaseFinder):
return errors
# NOTE: Same as `FileSystemFinder.find`
- def find(self, path: str, all: bool = False) -> Union[List[str], str]:
+ def find(self, path: str, **kwargs: Any) -> Union[List[str], str]:
"""
Look for files in the extra locations as defined in COMPONENTS.dirs.
"""
+ # Handle deprecated `all` parameter:
+ # - In Django 5.2, the `all` parameter was deprecated in favour of `find_all`.
+ # - Between Django 5.2 (inclusive) and 6.1 (exclusive), the `all` parameter was still
+ # supported, but an error was raised if both were provided.
+ # - In Django 6.1, the `all` parameter was removed.
+ #
+ # See https://github.com/django/django/blob/5.2/django/contrib/staticfiles/finders.py#L58C9-L58C37
+ # And https://github.com/django-components/django-components/issues/1119
+ if DJANGO_VERSION >= (5, 2) and DJANGO_VERSION < (6, 1):
+ find_all = self._check_deprecated_find_param(**kwargs) # type: ignore
+ elif DJANGO_VERSION >= (6, 1):
+ find_all = kwargs.get("find_all", False)
+ else:
+ find_all = kwargs.get("all", False)
+
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:
+ if not find_all:
return matched_path
matches.append(matched_path)
return matches
diff --git a/src/django_components/library.py b/src/django_components/library.py
index cb90a297..5cff7d5f 100644
--- a/src/django_components/library.py
+++ b/src/django_components/library.py
@@ -11,9 +11,9 @@ class TagProtectedError(Exception):
The way the [`TagFormatter`](../../concepts/advanced/tag_formatter) works is that,
based on which start and end tags are used for rendering components,
the [`ComponentRegistry`](../api#django_components.ComponentRegistry) behind the scenes
- [un-/registers the template tags](https://docs.djangoproject.com/en/5.1/howto/custom-template-tags/#registering-the-tag)
+ [un-/registers the template tags](https://docs.djangoproject.com/en/5.2/howto/custom-template-tags/#registering-the-tag)
with the associated instance of Django's
- [`Library`](https://docs.djangoproject.com/en/5.1/howto/custom-template-tags/#code-layout).
+ [`Library`](https://docs.djangoproject.com/en/5.2/howto/custom-template-tags/#code-layout).
In other words, if I have registered a component `"table"`, and I use the shorthand
syntax:
diff --git a/src/django_components/middleware.py b/src/django_components/middleware.py
deleted file mode 100644
index b18e2e41..00000000
--- a/src/django_components/middleware.py
+++ /dev/null
@@ -1,4 +0,0 @@
-# These middlewares are part of public API
-from django_components.dependencies import ComponentDependencyMiddleware
-
-__all__ = ["ComponentDependencyMiddleware"]
diff --git a/src/django_components/node.py b/src/django_components/node.py
index 71f545db..f458c939 100644
--- a/src/django_components/node.py
+++ b/src/django_components/node.py
@@ -1,7 +1,7 @@
import functools
import inspect
import keyword
-from typing import Any, Callable, Dict, List, Optional, Tuple, Type, cast
+from typing import TYPE_CHECKING, Any, Callable, ClassVar, Dict, List, Optional, Tuple, Type, cast
from django.template import Context, Library
from django.template.base import Node, NodeList, Parser, Token
@@ -15,6 +15,9 @@ from django_components.util.template_tag import (
validate_params,
)
+if TYPE_CHECKING:
+ from django_components.component import Component
+
# Normally, when `Node.render()` is called, it receives only a single argument `context`.
#
@@ -100,9 +103,9 @@ class NodeMeta(type):
#
# ```py
# class MyComponent(Component):
- # def get_context_data(self, name: str, **kwargs: Any) -> str:
+ # def get_template_data(self, args, kwargs, slots, context) -> str:
# return {
- # "name": name,
+ # "name": kwargs.pop("name"),
# "attrs": kwargs,
# }
# template = """
@@ -252,32 +255,77 @@ class BaseNode(Node, metaclass=NodeMeta):
# PUBLIC API (Configurable by users)
# #####################################
- tag: str
+ tag: ClassVar[str]
"""
The tag name.
E.g. `"component"` or `"slot"` will make this class match
template tags `{% component %}` or `{% slot %}`.
+
+ ```python
+ class SlotNode(BaseNode):
+ tag = "slot"
+ end_tag = "endslot"
+ ```
+
+ This will allow the template tag `{% slot %}` to be used like this:
+
+ ```django
+ {% slot %} ... {% endslot %}
+ ```
"""
- end_tag: Optional[str] = None
+ end_tag: ClassVar[Optional[str]] = None
"""
The end tag name.
E.g. `"endcomponent"` or `"endslot"` will make this class match
template tags `{% endcomponent %}` or `{% endslot %}`.
+ ```python
+ class SlotNode(BaseNode):
+ tag = "slot"
+ end_tag = "endslot"
+ ```
+
+ This will allow the template tag `{% slot %}` to be used like this:
+
+ ```django
+ {% slot %} ... {% endslot %}
+ ```
+
If not set, then this template tag has no end tag.
So instead of `{% component %} ... {% endcomponent %}`, you'd use only
`{% component %}`.
+
+ ```python
+ class MyNode(BaseNode):
+ tag = "mytag"
+ end_tag = None
+ ```
"""
- allowed_flags: Optional[List[str]] = None
+ allowed_flags: ClassVar[Optional[List[str]]] = None
"""
- The allowed flags for this tag.
+ The list of all *possible* flags for this tag.
E.g. `["required"]` will allow this tag to be used like `{% slot required %}`.
+
+ ```python
+ class SlotNode(BaseNode):
+ tag = "slot"
+ end_tag = "endslot"
+ allowed_flags = ["required", "default"]
+ ```
+
+ This will allow the template tag `{% slot %}` to be used like this:
+
+ ```django
+ {% slot required %} ... {% endslot %}
+ {% slot default %} ... {% endslot %}
+ {% slot required default %} ... {% endslot %}
+ ```
"""
def render(self, context: Context, *args: Any, **kwargs: Any) -> str:
@@ -303,6 +351,133 @@ class BaseNode(Node, metaclass=NodeMeta):
"""
return self.nodelist.render(context)
+ # #####################################
+ # Attributes
+ # #####################################
+
+ params: List[TagAttr]
+ """
+ The parameters to the tag in the template.
+
+ A single param represents an arg or kwarg of the template tag.
+
+ E.g. the following tag:
+
+ ```django
+ {% component "my_comp" key=val key2='val2 two' %}
+ ```
+
+ Has 3 params:
+
+ - Posiitonal arg `"my_comp"`
+ - Keyword arg `key=val`
+ - Keyword arg `key2='val2 two'`
+ """
+
+ flags: Dict[str, bool]
+ """
+ Dictionary of all [`allowed_flags`](../api#django_components.BaseNode.allowed_flags)
+ that were set on the tag.
+
+ Flags that were set are `True`, and the rest are `False`.
+
+ E.g. the following tag:
+
+ ```python
+ class SlotNode(BaseNode):
+ tag = "slot"
+ end_tag = "endslot"
+ allowed_flags = ["default", "required"]
+ ```
+
+ ```django
+ {% slot "content" default %}
+ ```
+
+ Has 2 flags, `default` and `required`, but only `default` was set.
+
+ The `flags` dictionary will be:
+
+ ```python
+ {
+ "default": True,
+ "required": False,
+ }
+ ```
+
+ You can check if a flag is set by doing:
+
+ ```python
+ if node.flags["default"]:
+ ...
+ ```
+ """
+
+ nodelist: NodeList
+ """
+ The nodelist of the tag.
+
+ This is the text between the opening and closing tags, e.g.
+
+ ```django
+ {% slot "content" default required %}
+
+ ...
+
+ {% endslot %}
+ ```
+
+ The `nodelist` will contain the `
...
` part.
+
+ Unlike [`contents`](../api#django_components.BaseNode.contents),
+ the `nodelist` contains the actual Nodes, not just the text.
+ """
+
+ contents: Optional[str]
+ """
+ The contents of the tag.
+
+ This is the text between the opening and closing tags, e.g.
+
+ ```django
+ {% slot "content" default required %}
+
+ ...
+
+ {% endslot %}
+ ```
+
+ The `contents` will be `"
...
"`.
+ """
+
+ node_id: str
+ """
+ The unique ID of the node.
+
+ Extensions can use this ID to store additional information.
+ """
+
+ template_name: Optional[str]
+ """
+ The name of the [`Template`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template)
+ that contains this node.
+
+ The template name is set by Django's
+ [template loaders](https://docs.djangoproject.com/en/5.2/topics/templates/#loaders).
+
+ For example, the filesystem template loader will set this to the absolute path of the template file.
+
+ ```
+ "/home/user/project/templates/my_template.html"
+ ```
+ """
+
+ template_component: Optional[Type["Component"]]
+ """
+ If the template that contains this node belongs to a [`Component`](../api#django_components.Component),
+ then this will be the [`Component`](../api#django_components.Component) class.
+ """
+
# #####################################
# MISC
# #####################################
@@ -313,11 +488,17 @@ class BaseNode(Node, metaclass=NodeMeta):
flags: Optional[Dict[str, bool]] = None,
nodelist: Optional[NodeList] = None,
node_id: Optional[str] = None,
+ contents: Optional[str] = None,
+ template_name: Optional[str] = None,
+ template_component: Optional[Type["Component"]] = None,
):
self.params = params
self.flags = flags or {flag: False for flag in self.allowed_flags or []}
self.nodelist = nodelist or NodeList()
self.node_id = node_id or gen_id()
+ self.contents = contents
+ self.template_name = template_name
+ self.template_component = template_component
def __repr__(self) -> str:
return (
@@ -327,7 +508,21 @@ class BaseNode(Node, metaclass=NodeMeta):
@property
def active_flags(self) -> List[str]:
- """Flags that were set for this specific instance."""
+ """
+ Flags that were set for this specific instance as a list of strings.
+
+ E.g. the following tag:
+
+ ```django
+ {% slot "content" default required / %}
+ ```
+
+ Will have the following flags:
+
+ ```python
+ ["default", "required"]
+ ```
+ """
flags = []
for flag, value in self.flags.items():
if value:
@@ -338,24 +533,30 @@ class BaseNode(Node, metaclass=NodeMeta):
def parse(cls, parser: Parser, token: Token, **kwargs: Any) -> "BaseNode":
"""
This function is what is passed to Django's `Library.tag()` when
- [registering the tag](https://docs.djangoproject.com/en/5.1/howto/custom-template-tags/#registering-the-tag).
+ [registering the tag](https://docs.djangoproject.com/en/5.2/howto/custom-template-tags/#registering-the-tag).
In other words, this method is called by Django's template parser when we encounter
a tag that matches this node's tag, e.g. `{% component %}` or `{% slot %}`.
To register the tag, you can use [`BaseNode.register()`](../api#django_components.BaseNode.register).
"""
+ # NOTE: Avoids circular import
+ from django_components.template import get_component_from_origin
+
tag_id = gen_id()
tag = parse_template_tag(cls.tag, cls.end_tag, cls.allowed_flags, parser, token)
trace_node_msg("PARSE", cls.tag, tag_id)
- body = tag.parse_body()
+ body, contents = tag.parse_body()
node = cls(
nodelist=body,
node_id=tag_id,
params=tag.params,
flags=tag.flags,
+ contents=contents,
+ template_name=parser.origin.name if parser.origin else None,
+ template_component=get_component_from_origin(parser.origin) if parser.origin else None,
**kwargs,
)
@@ -431,7 +632,7 @@ def template_tag(
The function MUST accept at least two positional arguments: `node` and `context`
- `node` is the [`BaseNode`](../api#django_components.BaseNode) instance.
- - `context` is the [`Context`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context)
+ - `context` is the [`Context`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context)
of the template.
Any extra parameters defined on this function will be part of the tag's input parameters.
diff --git a/src/django_components/perfutil/component.py b/src/django_components/perfutil/component.py
index 35728441..0af91856 100644
--- a/src/django_components/perfutil/component.py
+++ b/src/django_components/perfutil/component.py
@@ -1,13 +1,16 @@
import re
from collections import deque
-from typing import TYPE_CHECKING, Callable, Deque, Dict, List, NamedTuple, Optional, Tuple
+from typing import TYPE_CHECKING, Callable, Deque, Dict, List, NamedTuple, Optional, Set, Tuple, Union
from django.utils.safestring import mark_safe
+from django_components.constants import COMP_ID_LENGTH
from django_components.util.exception import component_error_message
if TYPE_CHECKING:
- from django_components.component import ComponentContext
+ from django_components.component import ComponentContext, OnRenderGenerator
+
+OnComponentRenderedResult = Tuple[Optional[str], Optional[Exception]]
# When we're inside a component's template, we need to acccess some component data,
# as defined by `ComponentContext`. If we have nested components, then
@@ -28,39 +31,60 @@ if TYPE_CHECKING:
component_context_cache: Dict[str, "ComponentContext"] = {}
-class PostRenderQueueItem(NamedTuple):
- content_before_component: str
- child_id: Optional[str]
+class ComponentPart(NamedTuple):
+ """Queue item where a component is nested in another component."""
+
+ child_id: str
parent_id: Optional[str]
- grandparent_id: Optional[str]
component_name_path: List[str]
def __repr__(self) -> str:
return (
- f"PostRenderQueueItem(child_id={self.child_id!r}, parent_id={self.parent_id!r}, "
- f"grandparent_id={self.grandparent_id!r}, component_name_path={self.component_name_path!r}, "
- f"content_before_component={self.content_before_component[:10]!r})"
+ f"ComponentPart(child_id={self.child_id!r}, parent_id={self.parent_id!r}, "
+ f"component_name_path={self.component_name_path!r})"
)
+class TextPart(NamedTuple):
+ """Queue item where a text is between two components."""
+
+ text: str
+ is_last: bool
+ parent_id: str
+
+
+class ErrorPart(NamedTuple):
+ """Queue item where a component has thrown an error."""
+
+ child_id: str
+ error: Exception
+
+
# Function that accepts a list of extra HTML attributes to be set on the component's root elements
# and returns the component's HTML content and a dictionary of child components' IDs
# and their root elements' HTML attributes.
#
# In other words, we use this to "delay" the actual rendering of the component's HTML content,
# until we know what HTML attributes to apply to the root elements.
-ComponentRenderer = Callable[[Optional[List[str]]], Tuple[str, Dict[str, List[str]]]]
+ComponentRenderer = Callable[
+ [Optional[List[str]]],
+ Tuple[str, Dict[str, List[str]], Optional["OnRenderGenerator"]],
+]
# Render-time cache for component rendering
# See component_post_render()
component_renderer_cache: Dict[str, Tuple[ComponentRenderer, str]] = {}
child_component_attrs: Dict[str, List[str]] = {}
-nested_comp_pattern = re.compile(r']*?djc-render-id="\w{6}"[^>]*?>')
-render_id_pattern = re.compile(r'djc-render-id="(?P\w{6})"')
+nested_comp_pattern = re.compile(
+ r']*?djc-render-id="\w{{{COMP_ID_LENGTH}}}"[^>]*?>'.format(COMP_ID_LENGTH=COMP_ID_LENGTH)
+)
+render_id_pattern = re.compile(
+ r'djc-render-id="(?P\w{{{COMP_ID_LENGTH}}})"'.format(COMP_ID_LENGTH=COMP_ID_LENGTH)
+)
-# When a component is rendered, we want to apply HTML attributes like `data-djc-id-a1b3cf`
+# When a component is rendered, we want to apply HTML attributes like `data-djc-id-ca1b3cf`
# to all root elements. However, we have to approach it smartly, to minimize the HTML parsing.
#
# If we naively first rendered the child components, and then the parent component, then we would
@@ -90,8 +114,8 @@ render_id_pattern = re.compile(r'djc-render-id="(?P\w{6})"')
# The actual HTML output is stored in `component_renderer_cache`.
# 2. The parent of the child component is rendered normally.
# 3. If the placeholder for the child component is at root of the parent component,
-# then the placeholder may be tagged with extra attributes, e.g. `data-djc-id-a1b3cf`.
-# ``.
+# then the placeholder may be tagged with extra attributes, e.g. `data-djc-id-ca1b3cf`.
+# ``.
# 4. When the parent is done rendering, we go back to step 1., the parent component
# either returns the actual HTML, or a placeholder.
# 5. Only once we get to the root component, that has no further parents, is when we finally
@@ -110,7 +134,9 @@ def component_post_render(
render_id: str,
component_name: str,
parent_id: Optional[str],
- on_component_rendered_callbacks: Dict[str, Callable[[str], str]],
+ on_component_rendered_callbacks: Dict[
+ str, Callable[[Optional[str], Optional[Exception]], OnComponentRenderedResult]
+ ],
on_html_rendered: Callable[[str], str],
) -> str:
# Instead of rendering the component's HTML content immediately, we store it,
@@ -118,9 +144,34 @@ def component_post_render(
# to be applied to the resulting HTML.
component_renderer_cache[render_id] = (renderer, component_name)
+ # Case: Nested component
+ # If component is nested, return a placeholder
+ #
+ # How this works is that we have nested components:
+ # ```
+ # ComponentA
+ # ComponentB
+ # ComponentC
+ # ```
+ #
+ # And these components are embedded one in another using the `{% component %}` tag.
+ # ```django
+ #
+ #
+ # {% component "ComponentB" / %}
+ #
+ # ```
+ #
+ # Then the order in which components call `component_post_render()` is:
+ # 1. ComponentB - Triggered by `{% component "ComponentB" / %}` while A's template is being rendered,
+ # returns only a placeholder.
+ # 2. ComponentA - Triggered by the end of A's template. A isn't nested, so it starts full component
+ # tree render. This replaces B's placeholder with actual HTML and introduces C's placeholder.
+ # And so on...
+ # 3. ComponentC - Triggered by `{% component "ComponentC" / %}` while B's template is being rendered
+ # as part of full component tree render. Returns only a placeholder, to be replaced in next
+ # step.
if parent_id is not None:
- # Case: Nested component
- # If component is nested, return a placeholder
return mark_safe(f'')
# Case: Root component - Construct the final HTML by recursively replacing placeholders
@@ -128,13 +179,25 @@ def component_post_render(
# We first generate the component's HTML content, by calling the renderer.
#
# Then we process the component's HTML from root-downwards, going depth-first.
- # So if we have a structure:
+ # So if we have a template:
+ # ```django
+ #
+ # ```
+ #
+ # Then component's template is rendered, replacing nested components with placeholders:
+ # ```html
#
#
...
#
# ...
#
#
+ # ```
#
# Then we first split up the current HTML into parts, splitting at placeholders:
# -
...
@@ -156,14 +219,12 @@ def component_post_render(
# repeating this whole process until we've processed all nested components.
# 5. If the placeholder ID is None, then we've reached the end of the component's HTML content,
# and we can go one level up to continue the process with component's parent.
- process_queue: Deque[PostRenderQueueItem] = deque()
+ process_queue: Deque[Union[ErrorPart, TextPart, ComponentPart]] = deque()
process_queue.append(
- PostRenderQueueItem(
- content_before_component="",
+ ComponentPart(
child_id=render_id,
parent_id=None,
- grandparent_id=None,
component_name_path=[],
)
)
@@ -182,85 +243,180 @@ def component_post_render(
#
# Then we end up with 3 bits - 1. text before, 2. component, and 3. text after
#
- # We know when we've arrived at component's end, because `child_id` will be set to `None`.
- # So we can collect the HTML parts by the component ID, and when we hit the end, we join
- # all the bits that belong to the same component.
+ # We know when we've arrived at component's end. We then collect the HTML parts by the component ID,
+ # and when we hit the end, we join all the bits that belong to the same component.
#
# Once the component's HTML is joined, we can call the callback for the component, and
# then add the joined HTML to the cache for the parent component to continue the cycle.
html_parts_by_component_id: Dict[str, List[str]] = {}
content_parts: List[str] = []
+ # Remember which component ID had which parent ID, so we can bubble up errors
+ # to the parent component.
+ child_id_to_parent_id: Dict[str, Optional[str]] = {}
+
def get_html_parts(component_id: str) -> List[str]:
if component_id not in html_parts_by_component_id:
html_parts_by_component_id[component_id] = []
return html_parts_by_component_id[component_id]
+ def handle_error(component_id: str, error: Exception) -> None:
+ # Cleanup
+ # Remove any HTML parts that were already rendered for this component
+ html_parts_by_component_id.pop(component_id, None)
+ # Mark any remaining parts of this component (that may be still in the queue) as errored
+ ignored_ids.add(component_id)
+ # Also mark as ignored any remaining parts of the PARENT component.
+ # The reason is because due to the error, parent's rendering flow was disrupted.
+ # Even if parent recovers from the error by returning a new HTML, this new HTML
+ # may have nothing in common with the original HTML.
+ parent_id = child_id_to_parent_id[component_id]
+ if parent_id is not None:
+ ignored_ids.add(parent_id)
+
+ # Add error item to the queue so we handle it in next iteration
+ process_queue.appendleft(
+ ErrorPart(
+ child_id=component_id,
+ error=error,
+ )
+ )
+
+ def finalize_component(component_id: str, error: Optional[Exception]) -> None:
+ parent_id = child_id_to_parent_id[component_id]
+
+ component_parts = html_parts_by_component_id.pop(component_id, [])
+ if error is None:
+ component_html = "".join(component_parts)
+ else:
+ component_html = None
+
+ # Allow to optionally override/modify the rendered content from `Component.on_render()`
+ # and by extensions' `on_component_rendered` hooks.
+ on_component_rendered = on_component_rendered_callbacks[component_id]
+ component_html, error = on_component_rendered(component_html, error)
+
+ # If this component had an error, then we ignore this component's HTML, and instead
+ # bubble the error up to the parent component.
+ if error is not None:
+ handle_error(component_id=component_id, error=error)
+ return
+
+ if component_html is None:
+ raise RuntimeError("Unexpected `None` from `Component.on_render()`")
+
+ # At this point we have a component, and we've resolved all its children into strings.
+ # So the component's full HTML is now only strings.
+ #
+ # Hence we can transfer the child component's HTML to parent, treating it as if
+ # the parent component had the rendered HTML in child's place.
+ if parent_id is not None:
+ target_list = get_html_parts(parent_id)
+ target_list.append(component_html)
+ # If there is no parent, then we're at the root component, and we can add the
+ # component's HTML to the final output.
+ else:
+ content_parts.append(component_html)
+
+ # To avoid having to iterate over the queue multiple times to remove from it those
+ # entries that belong to components that have thrown error, we instead keep track of which
+ # components have thrown error, and skip any remaining parts of the component.
+ ignored_ids: Set[str] = set()
+
while len(process_queue):
curr_item = process_queue.popleft()
- # In this case we've reached the end of the component's HTML content, and there's
- # no more subcomponents to process.
- if curr_item.child_id is None:
- # Parent ID must NOT be None in this branch
- if curr_item.parent_id is None:
- raise RuntimeError("Parent ID is None")
+ # NOTE: When an error is bubbling up, then the flow goes between `handle_error()`, `finalize_component()`,
+ # and this branch, until we reach the root component, where the error is finally raised.
+ #
+ # Any ancestor component of the one that raised can intercept the error and instead return a new string
+ # (or a new error).
+ if isinstance(curr_item, ErrorPart):
+ parent_id = child_id_to_parent_id[curr_item.child_id]
- parent_parts = html_parts_by_component_id.pop(curr_item.parent_id, [])
+ # If there is no parent, then we're at the root component, so we simply propagate the error.
+ # This ends the error bubbling.
+ if parent_id is None:
+ raise curr_item.error from None # Re-raise
- # Add the left-over content
- parent_parts.append(curr_item.content_before_component)
+ # This will make the parent component either handle the error and return a new string instead,
+ # or propagate the error to its parent.
+ finalize_component(component_id=parent_id, error=curr_item.error)
+ continue
- # Allow to optionally override/modify the rendered content from outside
- component_html = "".join(parent_parts)
- on_component_rendered = on_component_rendered_callbacks[curr_item.parent_id]
- component_html = on_component_rendered(component_html) # type: ignore[arg-type]
+ # Skip parts of errored components
+ elif curr_item.parent_id in ignored_ids:
+ continue
- # Add the component's HTML to parent's parent's HTML parts
- if curr_item.grandparent_id is not None:
- target_list = get_html_parts(curr_item.grandparent_id)
- target_list.append(component_html)
- else:
- content_parts.append(component_html)
+ # Process text parts
+ elif isinstance(curr_item, TextPart):
+ parent_html_parts = get_html_parts(curr_item.parent_id)
+ parent_html_parts.append(curr_item.text)
+
+ # In this case we've reached the end of the component's HTML content, and there's
+ # no more subcomponents to process. We can call `finalize_component()` to process
+ # the component's HTML and eventually trigger `on_component_rendered` hook.
+ if curr_item.is_last:
+ finalize_component(component_id=curr_item.parent_id, error=None)
continue
- # Process content before the component
- if curr_item.content_before_component:
- if curr_item.parent_id is None:
- raise RuntimeError("Parent ID is None")
- parent_html_parts = get_html_parts(curr_item.parent_id)
- parent_html_parts.append(curr_item.content_before_component)
+ # The rest of this branch assumes `curr_item` is a `ComponentPart`
+ component_id = curr_item.child_id
+
+ # Remember which component ID had which parent ID, so we can bubble up errors
+ # to the parent component.
+ child_id_to_parent_id[component_id] = curr_item.parent_id
# Generate component's content, applying the extra HTML attributes set by the parent component
- curr_comp_renderer, curr_comp_name = component_renderer_cache.pop(curr_item.child_id)
- # NOTE: This may be undefined, because this is set only for components that
- # are also root elements in their parent's HTML
- curr_comp_attrs = child_component_attrs.pop(curr_item.child_id, None)
+ curr_comp_renderer, curr_comp_name = component_renderer_cache.pop(component_id)
+ # NOTE: Attributes passed from parent to current component are `None` for the root component.
+ curr_comp_attrs = child_component_attrs.pop(component_id, None)
full_path = [*curr_item.component_name_path, curr_comp_name]
# This is where we actually render the component
#
# NOTE: [1:] because the root component will be yet again added to the error's
- # `components` list in `_render` so we remove the first element from the path.
- with component_error_message(full_path[1:]):
- curr_comp_content, grandchild_component_attrs = curr_comp_renderer(curr_comp_attrs)
+ # `components` list in `_render_with_error_trace` so we remove the first element from the path.
+ try:
+ with component_error_message(full_path[1:]):
+ comp_content, grandchild_component_attrs, on_render_generator = curr_comp_renderer(curr_comp_attrs)
+ # This error may be triggered when any of following raises:
+ # - `Component.on_render()` (first part - before yielding)
+ # - `Component.on_render_before()`
+ # - Rendering of component's template
+ #
+ # In all cases, we want to mark the component as errored, and let the parent handle it.
+ except Exception as err:
+ handle_error(component_id=component_id, error=err)
+ continue
- # Exclude the `data-djc-scope-...` attribute from being applied to the child component's HTML
- for key in list(grandchild_component_attrs.keys()):
- if key.startswith("data-djc-scope-"):
- grandchild_component_attrs.pop(key, None)
+ # To access the *final* output (with all its children rendered) from within `Component.on_render()`,
+ # users may convert it to a generator by including a `yield` keyword. If they do so, the part of code
+ # AFTER the yield will be called once, when the component's HTML is fully rendered.
+ #
+ # We want to make sure we call the second part of `Component.on_render()` BEFORE
+ # we call `Component.on_render_after()`. The latter will be triggered by calling
+ # corresponding `on_component_rendered`.
+ #
+ # So we want to wrap the `on_component_rendered` callback, so we get to call the generator first.
+ if on_render_generator is not None:
+ unwrapped_on_component_rendered = on_component_rendered_callbacks[component_id]
+ on_component_rendered_callbacks[component_id] = _call_generator_before_callback(
+ on_render_generator,
+ unwrapped_on_component_rendered,
+ )
child_component_attrs.update(grandchild_component_attrs)
- # Process the component's content
+ # Split component's content by placeholders, and put the pairs of
+ # `(text_between_components, placeholder_id)`
+ # into the queue.
last_index = 0
- parts_to_process: List[PostRenderQueueItem] = []
-
- # Split component's content by placeholders, and put the pairs of (content, placeholder_id) into the queue
- for match in nested_comp_pattern.finditer(curr_comp_content):
- part_before_component = curr_comp_content[last_index : match.start()] # noqa: E203
+ parts_to_process: List[Union[TextPart, ComponentPart]] = []
+ for match in nested_comp_pattern.finditer(comp_content):
+ part_before_component = comp_content[last_index : match.start()] # noqa: E203
last_index = match.end()
comp_part = match[0]
@@ -269,27 +425,31 @@ def component_post_render(
if grandchild_id_match is None:
raise ValueError(f"No placeholder ID found in {comp_part}")
grandchild_id = grandchild_id_match.group("render_id")
- parts_to_process.append(
- PostRenderQueueItem(
- content_before_component=part_before_component,
- child_id=grandchild_id,
- parent_id=curr_item.child_id,
- grandparent_id=curr_item.parent_id,
- component_name_path=full_path,
- )
+
+ parts_to_process.extend(
+ [
+ TextPart(
+ text=part_before_component,
+ is_last=False,
+ parent_id=component_id,
+ ),
+ ComponentPart(
+ child_id=grandchild_id,
+ parent_id=component_id,
+ component_name_path=full_path,
+ ),
+ ]
)
# Append any remaining text
- parts_to_process.append(
- PostRenderQueueItem(
- content_before_component=curr_comp_content[last_index:],
- # Setting `child_id` to None means that this is the last part of the component's HTML
- # and we're done with this component
- child_id=None,
- parent_id=curr_item.child_id,
- grandparent_id=curr_item.parent_id,
- component_name_path=full_path,
- )
+ parts_to_process.extend(
+ [
+ TextPart(
+ text=comp_content[last_index:],
+ is_last=True,
+ parent_id=component_id,
+ ),
+ ]
)
process_queue.extendleft(reversed(parts_to_process))
@@ -300,3 +460,44 @@ def component_post_render(
output = on_html_rendered(output)
return mark_safe(output)
+
+
+def _call_generator_before_callback(
+ on_render_generator: Optional["OnRenderGenerator"],
+ inner_fn: Callable[[Optional[str], Optional[Exception]], OnComponentRenderedResult],
+) -> Callable[[Optional[str], Optional[Exception]], OnComponentRenderedResult]:
+ if on_render_generator is None:
+ return inner_fn
+
+ def on_component_rendered_wrapper(
+ html: Optional[str],
+ error: Optional[Exception],
+ ) -> OnComponentRenderedResult:
+ try:
+ on_render_generator.send((html, error))
+ # `Component.on_render()` should contain only one `yield` statement, so calling `.send()`
+ # should reach `return` statement in `Component.on_render()`, which triggers `StopIteration`.
+ # In that case, the value returned from `Component.on_render()` with the `return` keyword
+ # is the new output (if not `None`).
+ except StopIteration as generator_err:
+ # To override what HTML / error gets returned, user may either:
+ # - Return a new HTML at the end of `Component.on_render()` (after yielding),
+ # - Raise a new error
+ new_output = generator_err.value
+ if new_output is not None:
+ html = new_output
+ error = None
+
+ # Catch if `Component.on_render()` raises an exception, in which case this becomes
+ # the new error.
+ except Exception as new_error:
+ error = new_error
+ html = None
+ # This raises if `StopIteration` was not raised, which may be if `Component.on_render()`
+ # contains more than one `yield` statement.
+ else:
+ raise RuntimeError("`Component.on_render()` must include only one `yield` statement")
+
+ return inner_fn(html, error)
+
+ return on_component_rendered_wrapper
diff --git a/src/django_components/provide.py b/src/django_components/provide.py
index 55b0f1b3..41e2367f 100644
--- a/src/django_components/provide.py
+++ b/src/django_components/provide.py
@@ -12,8 +12,11 @@ from django_components.util.misc import gen_id
class ProvideNode(BaseNode):
"""
- The "provider" part of the [provide / inject feature](../../concepts/advanced/provide_inject).
+ The [`{% provide %}`](../template_tags#provide) tag is part of the "provider" part of
+ the [provide / inject feature](../../concepts/advanced/provide_inject).
+
Pass kwargs to this tag to define the provider's data.
+
Any components defined within the `{% provide %}..{% endprovide %}` tags will be able to access this data
with [`Component.inject()`](../api#django_components.Component.inject).
@@ -30,7 +33,7 @@ class ProvideNode(BaseNode):
Provide the "user_data" in parent component:
- ```python
+ ```djc_py
@register("parent")
class Parent(Component):
template = \"\"\"
@@ -41,16 +44,16 @@ class ProvideNode(BaseNode):
\"\"\"
- def get_context_data(self, user: User):
+ def get_template_data(self, args, kwargs, slots, context):
return {
- "user": user,
+ "user": kwargs["user"],
}
```
Since the "child" component is used within the `{% provide %} / {% endprovide %}` tags,
we can request the "user_data" using `Component.inject("user_data")`:
- ```python
+ ```djc_py
@register("child")
class Child(Component):
template = \"\"\"
@@ -59,14 +62,14 @@ class ProvideNode(BaseNode):
\"\"\"
- def get_context_data(self):
+ def get_template_data(self, args, kwargs, slots, context):
user = self.inject("user_data").user
return {
"user": user,
}
```
- Notice that the keys defined on the `{% provide %}` tag are then accessed as attributes
+ Notice that the keys defined on the [`{% provide %}`](../template_tags#provide) tag are then accessed as attributes
when accessing them with [`Component.inject()`](../api#django_components.Component.inject).
✅ Do this
@@ -138,7 +141,7 @@ def set_provided_context_var(
) -> str:
"""
'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
+ using `self.inject(key)` inside of `get_template_data()` method of components that
are nested inside the `{% provide %}` tag.
"""
# NOTE: We raise TemplateSyntaxError since this func should be called only from
@@ -155,8 +158,8 @@ def set_provided_context_var(
# 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)
+ tuple_cls = namedtuple("DepInject", provided_kwargs.keys()) # type: ignore[misc]
+ payload = tuple_cls(**provided_kwargs)
# Instead of storing the provided data on the Context object, we store it
# in a separate dictionary, and we set only the key to the data on the Context.
diff --git a/src/django_components/slots.py b/src/django_components/slots.py
index 2a26b0e0..69c60b10 100644
--- a/src/django_components/slots.py
+++ b/src/django_components/slots.py
@@ -1,6 +1,7 @@
import difflib
import re
-from dataclasses import dataclass
+from dataclasses import dataclass, field
+from dataclasses import replace as dataclass_replace
from typing import (
TYPE_CHECKING,
Any,
@@ -13,6 +14,7 @@ from typing import (
Optional,
Protocol,
Set,
+ Tuple,
TypeVar,
Union,
cast,
@@ -22,73 +24,328 @@ from typing import (
from django.template import Context, Template
from django.template.base import NodeList, TextNode
from django.template.exceptions import TemplateSyntaxError
+from django.utils.html import conditional_escape
from django.utils.safestring import SafeString, mark_safe
-from django_components.app_settings import ContextBehavior, app_settings
-from django_components.context import _COMPONENT_CONTEXT_KEY, _INJECT_CONTEXT_KEY_PREFIX
+from django_components.app_settings import ContextBehavior
+from django_components.context import _COMPONENT_CONTEXT_KEY, _INJECT_CONTEXT_KEY_PREFIX, COMPONENT_IS_NESTED_KEY
+from django_components.extension import OnSlotRenderedContext, extensions
from django_components.node import BaseNode
from django_components.perfutil.component import component_context_cache
-from django_components.util.component_highlight import apply_component_highlight
from django_components.util.exception import add_slot_to_error_message
from django_components.util.logger import trace_component_msg
-from django_components.util.misc import get_index, get_last_index, is_identifier
+from django_components.util.misc import default, get_index, get_last_index, is_identifier
if TYPE_CHECKING:
- from django_components.component import ComponentContext
+ from django_components.component import Component, ComponentNode
-TSlotData = TypeVar("TSlotData", bound=Mapping, contravariant=True)
+TSlotData = TypeVar("TSlotData", bound=Mapping)
DEFAULT_SLOT_KEY = "default"
FILL_GEN_CONTEXT_KEY = "_DJANGO_COMPONENTS_GEN_FILL"
-SLOT_DATA_KWARG = "data"
SLOT_NAME_KWARG = "name"
-SLOT_DEFAULT_KWARG = "default"
-SLOT_REQUIRED_KEYWORD = "required"
-SLOT_DEFAULT_KEYWORD = "default"
+SLOT_REQUIRED_FLAG = "required"
+SLOT_DEFAULT_FLAG = "default"
+FILL_DATA_KWARG = "data"
+FILL_FALLBACK_KWARG = "fallback"
+FILL_BODY_KWARG = "body"
# Public types
SlotResult = Union[str, SafeString]
+"""
+Type representing the result of a slot render function.
+
+**Example:**
+
+```python
+from django_components import SlotContext, SlotResult
+
+def my_slot_fn(ctx: SlotContext) -> SlotResult:
+ return "Hello, world!"
+
+my_slot = Slot(my_slot_fn)
+html = my_slot() # Output: Hello, world!
+```
+
+Read more about [Slot functions](../../concepts/fundamentals/slots#slot-functions).
+"""
+
+
+@dataclass(frozen=True)
+class SlotContext(Generic[TSlotData]):
+ """
+ Metadata available inside slot functions.
+
+ Read more about [Slot functions](../../concepts/fundamentals/slots#slot-class).
+
+ **Example:**
+
+ ```python
+ from django_components import SlotContext, SlotResult
+
+ def my_slot(ctx: SlotContext) -> SlotResult:
+ return f"Hello, {ctx.data['name']}!"
+ ```
+
+ You can pass a type parameter to the `SlotContext` to specify the type of the data passed to the slot:
+
+ ```python
+ class MySlotData(TypedDict):
+ name: str
+
+ def my_slot(ctx: SlotContext[MySlotData]):
+ return f"Hello, {ctx.data['name']}!"
+ ```
+ """
+
+ data: TSlotData
+ """
+ Data passed to the slot.
+
+ Read more about [Slot data](../../concepts/fundamentals/slots#slot-data).
+
+ **Example:**
+
+ ```python
+ def my_slot(ctx: SlotContext):
+ return f"Hello, {ctx.data['name']}!"
+ ```
+ """
+ fallback: Optional[Union[str, "SlotFallback"]] = None
+ """
+ Slot's fallback content. Lazily-rendered - coerce this value to string to force it to render.
+
+ Read more about [Slot fallback](../../concepts/fundamentals/slots#slot-fallback).
+
+ **Example:**
+
+ ```python
+ def my_slot(ctx: SlotContext):
+ return f"Hello, {ctx.fallback}!"
+ ```
+
+ May be `None` if you call the slot fill directly, without using [`{% slot %}`](../template_tags#slot) tags.
+ """
+ context: Optional[Context] = None
+ """
+ Django template [`Context`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context)
+ available inside the [`{% fill %}`](../template_tags#fill) tag.
+
+ May be `None` if you call the slot fill directly, without using [`{% slot %}`](../template_tags#slot) tags.
+ """
@runtime_checkable
class SlotFunc(Protocol, Generic[TSlotData]):
- def __call__(self, ctx: Context, slot_data: TSlotData, slot_ref: "SlotRef") -> SlotResult: ... # noqa E704
+ """
+ When rendering components with
+ [`Component.render()`](../api#django_components.Component.render)
+ or
+ [`Component.render_to_response()`](../api#django_components.Component.render_to_response),
+ the slots can be given either as strings or as functions.
+
+ If a slot is given as a function, it will have the signature of `SlotFunc`.
+
+ Read more about [Slot functions](../../concepts/fundamentals/slots#slot-functions).
+
+ Args:
+ ctx (SlotContext): Single named tuple that holds the slot data and metadata.
+
+ Returns:
+ (str | SafeString): The rendered slot content.
+
+ **Example:**
+
+ ```python
+ from django_components import SlotContext, SlotResult
+
+ def header(ctx: SlotContext) -> SlotResult:
+ if ctx.data.get("name"):
+ return f"Hello, {ctx.data['name']}!"
+ else:
+ return ctx.fallback
+
+ html = MyTable.render(
+ slots={
+ "header": header,
+ },
+ )
+ ```
+ """
+
+ def __call__(self, ctx: SlotContext[TSlotData]) -> SlotResult: ... # noqa E704
@dataclass
class Slot(Generic[TSlotData]):
- """This class holds the slot content function along with related metadata."""
+ """
+ This class is the main way for defining and handling slots.
- content_func: SlotFunc[TSlotData]
- escaped: bool = False
- """Whether the slot content has been escaped."""
+ It holds the slot content function along with related metadata.
+
+ Read more about [Slot class](../../concepts/fundamentals/slots#slot-class).
+
+ **Example:**
+
+ Passing slots to components:
+
+ ```python
+ from django_components import Slot
+
+ slot = Slot(lambda ctx: f"Hello, {ctx.data['name']}!")
+
+ MyComponent.render(
+ slots={
+ "my_slot": slot,
+ },
+ )
+ ```
+
+ Accessing slots inside the components:
+
+ ```python
+ from django_components import Component
+
+ class MyComponent(Component):
+ def get_template_data(self, args, kwargs, slots, context):
+ my_slot = slots["my_slot"]
+ return {
+ "my_slot": my_slot,
+ }
+ ```
+
+ Rendering slots:
+
+ ```python
+ from django_components import Slot
+
+ slot = Slot(lambda ctx: f"Hello, {ctx.data['name']}!")
+ html = slot({"name": "John"}) # Output: Hello, John!
+ ```
+ """
+
+ contents: Any
+ """
+ The original value that was passed to the `Slot` constructor.
+
+ - If Slot was created from [`{% fill %}`](../template_tags#fill) tag, `Slot.contents` will contain
+ the body (string) of that `{% fill %}` tag.
+ - If Slot was created from string as `Slot("...")`, `Slot.contents` will contain that string.
+ - If Slot was created from a function, `Slot.contents` will contain that function.
+
+ Read more about [Slot contents](../../concepts/fundamentals/slots#slot-contents).
+ """
+ content_func: SlotFunc[TSlotData] = cast(SlotFunc[TSlotData], None)
+ """
+ The actual slot function.
+
+ Do NOT call this function directly, instead call the `Slot` instance as a function.
+
+ Read more about [Rendering slot functions](../../concepts/fundamentals/slots#rendering-slots).
+ """
# Following fields are only for debugging
component_name: Optional[str] = None
- """Name of the component that originally defined or accepted this slot fill."""
+ """
+ Name of the component that originally received this slot fill.
+
+ See [Slot metadata](../../concepts/fundamentals/slots#slot-metadata).
+ """
slot_name: Optional[str] = None
- """Name of the slot that originally defined or accepted this slot fill."""
+ """
+ Slot name to which this Slot was initially assigned.
+
+ See [Slot metadata](../../concepts/fundamentals/slots#slot-metadata).
+ """
nodelist: Optional[NodeList] = None
- """Nodelist of the slot content."""
+ """
+ If the slot was defined with [`{% fill %}`](../template_tags#fill) tag,
+ this will be the Nodelist of the fill's content.
+
+ See [Slot metadata](../../concepts/fundamentals/slots#slot-metadata).
+ """
+ fill_node: Optional[Union["FillNode", "ComponentNode"]] = None
+ """
+ If the slot was created from a [`{% fill %}`](../template_tags#fill) tag,
+ this will be the [`FillNode`](../api/#django_components.FillNode) instance.
+
+ If the slot was a default slot created from a [`{% component %}`](../template_tags#component) tag,
+ this will be the [`ComponentNode`](../api/#django_components.ComponentNode) instance.
+
+ Otherwise, this will be `None`.
+
+ Extensions can use this info to handle slots differently based on their source.
+
+ See [Slot metadata](../../concepts/fundamentals/slots#slot-metadata).
+
+ **Example:**
+
+ You can use this to find the [`Component`](../api/#django_components.Component) in whose
+ template the [`{% fill %}`](../template_tags#fill) tag was defined:
+
+ ```python
+ class MyTable(Component):
+ def get_template_data(self, args, kwargs, slots, context):
+ footer_slot = slots.get("footer")
+ if footer_slot is not None and footer_slot.fill_node is not None:
+ owner_component = footer_slot.fill_node.template_component
+ # ...
+ ```
+ """
+ extra: Dict[str, Any] = field(default_factory=dict)
+ """
+ Dictionary that can be used to store arbitrary metadata about the slot.
+
+ See [Slot metadata](../../concepts/fundamentals/slots#slot-metadata).
+
+ See [Pass slot metadata](../../concepts/advanced/extensions#pass-slot-metadata)
+ for usage for extensions.
+
+ **Example:**
+
+ ```python
+ # Either at slot creation
+ slot = Slot(lambda ctx: "Hello, world!", extra={"foo": "bar"})
+
+ # Or later
+ slot.extra["baz"] = "qux"
+ ```
+ """
def __post_init__(self) -> None:
- if not callable(self.content_func):
- raise ValueError(f"Slot content must be a callable, got: {self.content_func}")
+ # Raise if Slot received another Slot instance as `contents`,
+ # because this leads to ambiguity about how to handle the metadata.
+ if isinstance(self.contents, Slot):
+ raise ValueError("Slot received another Slot instance as `contents`")
- # Allow passing Slot instances as content functions
- if isinstance(self.content_func, Slot):
- inner_slot = self.content_func
- self.content_func = inner_slot.content_func
+ if self.content_func is None:
+ self.contents, new_nodelist, self.content_func = self._resolve_contents(self.contents)
+ if self.nodelist is None:
+ self.nodelist = new_nodelist
+
+ if not callable(self.content_func):
+ raise ValueError(f"Slot 'content_func' must be a callable, got: {self.content_func}")
# Allow to treat the instances as functions
- def __call__(self, ctx: Context, slot_data: TSlotData, slot_ref: "SlotRef") -> SlotResult:
- return self.content_func(ctx, slot_data, slot_ref)
+ def __call__(
+ self,
+ data: Optional[TSlotData] = None,
+ fallback: Optional[Union[str, "SlotFallback"]] = None,
+ context: Optional[Context] = None,
+ ) -> SlotResult:
+ slot_ctx: SlotContext = SlotContext(context=context, data=data or {}, fallback=fallback)
+ result = self.content_func(slot_ctx)
+ return conditional_escape(result)
# Make Django pass the instances of this class within the templates without calling
# the instances as a function.
@property
def do_not_call_in_templates(self) -> bool:
+ """
+ Django special property to prevent calling the instance as a function
+ inside Django templates.
+ """
return True
def __repr__(self) -> str:
@@ -96,66 +353,167 @@ class Slot(Generic[TSlotData]):
slot_name = f"'{self.slot_name}'" if self.slot_name else None
return f"<{self.__class__.__name__} component_name={comp_name} slot_name={slot_name}>"
+ def _resolve_contents(self, contents: Any) -> Tuple[Any, NodeList, SlotFunc[TSlotData]]:
+ # Case: Content is a string / scalar, so we can use `TextNode` to render it.
+ if not callable(contents):
+ contents = str(contents) if not isinstance(contents, (str, SafeString)) else contents
+ contents = conditional_escape(contents)
+ slot = _nodelist_to_slot(
+ component_name=self.component_name or "",
+ slot_name=self.slot_name,
+ nodelist=NodeList([TextNode(contents)]),
+ contents=contents,
+ data_var=None,
+ fallback_var=None,
+ )
+ return slot.contents, slot.nodelist, slot.content_func
+
+ # Otherwise, we're dealing with a function.
+ return contents, None, contents
+
# NOTE: This must be defined here, so we don't have any forward references
# otherwise Pydantic has problem resolving the types.
-SlotContent = Union[SlotResult, SlotFunc[TSlotData], Slot[TSlotData]]
+SlotInput = Union[SlotResult, SlotFunc[TSlotData], Slot[TSlotData]]
+"""
+Type representing all forms in which slot content can be passed to a component.
+
+When rendering a component with [`Component.render()`](../api#django_components.Component.render)
+or [`Component.render_to_response()`](../api#django_components.Component.render_to_response),
+the slots may be given a strings, functions, or [`Slot`](../api#django_components.Slot) instances.
+This type describes that union.
+
+Use this type when typing the slots in your component.
+
+`SlotInput` accepts an optional type parameter to specify the data dictionary that will be passed to the
+slot content function.
+
+**Example:**
+
+```python
+from typing import NamedTuple
+from typing_extensions import TypedDict
+from django_components import Component, SlotInput
+
+class TableFooterSlotData(TypedDict):
+ page_number: int
+
+class Table(Component):
+ class Slots(NamedTuple):
+ header: SlotInput
+ footer: SlotInput[TableFooterSlotData]
+
+ template = "
{% slot 'footer' %}
"
+
+html = Table.render(
+ slots={
+ # As a string
+ "header": "Hello, World!",
+
+ # Safe string
+ "header": mark_safe(""),
+
+ # Function
+ "footer": lambda ctx: f"Page: {ctx.data['page_number']}!",
+
+ # Slot instance
+ "footer": Slot(lambda ctx: f"Page: {ctx.data['page_number']}!"),
+
+ # None (Same as no slot)
+ "header": None,
+ },
+)
+```
+"""
+# TODO_V1 - REMOVE, superseded by SlotInput
+SlotContent = SlotInput[TSlotData]
+"""
+DEPRECATED: Use [`SlotInput`](../api#django_components.SlotInput) instead. Will be removed in v1.
+"""
# Internal type aliases
SlotName = str
-@dataclass(frozen=True)
-class SlotFill(Generic[TSlotData]):
+class SlotFallback:
"""
- SlotFill describes what WILL be rendered.
+ The content between the `{% slot %}..{% endslot %}` tags is the *fallback* content that
+ will be rendered if no fill is given for the slot.
- The fill may be provided by the user from the outside (`is_filled=True`),
- or it may be the default content of the slot (`is_filled=False`).
- """
+ ```django
+ {% slot "name" %}
+ Hello, my name is {{ name }}
+ {% endslot %}
+ ```
- name: str
- """Name of the slot."""
- is_filled: bool
- slot: Slot[TSlotData]
+ Because the fallback is defined as a piece of the template
+ ([`NodeList`](https://github.com/django/django/blob/ddb85294159185c5bd5cae34c9ef735ff8409bfe/django/template/base.py#L1017)),
+ we want to lazily render it only when needed.
+ `SlotFallback` type allows to pass around the slot fallback as a variable.
-class SlotRef:
- """
- SlotRef allows to treat a slot as a variable. The slot is rendered only once
- the instance is coerced to string.
+ To force the fallback to render, coerce it to string to trigger the `__str__()` method.
- 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.
- """
+ **Example:**
+
+ ```py
+ def slot_function(self, ctx: SlotContext):
+ return f"Hello, {ctx.fallback}!"
+ ```
+ """ # noqa: E501
def __init__(self, slot: "SlotNode", context: Context):
self._slot = slot
self._context = context
- # Render the slot when the template coerces SlotRef to string
+ # Render the slot when the template coerces SlotFallback to string
def __str__(self) -> str:
return mark_safe(self._slot.nodelist.render(self._context))
+# TODO_v1 - REMOVE - superseded by SlotFallback
+SlotRef = SlotFallback
+"""
+DEPRECATED: Use [`SlotFallback`](../api#django_components.SlotFallback) instead. Will be removed in v1.
+"""
+
+
+name_escape_re = re.compile(r"[^\w]")
+
+
+# TODO_v1 - Remove, superseded by `Component.slots` and `component_vars.slots`
class SlotIsFilled(dict):
"""
Dictionary that returns `True` if the slot is filled (key is found), `False` otherwise.
"""
def __init__(self, fills: Dict, *args: Any, **kwargs: Any) -> None:
- escaped_fill_names = {_escape_slot_name(fill_name): True for fill_name in fills.keys()}
+ escaped_fill_names = {self._escape_slot_name(fill_name): True for fill_name in fills.keys()}
super().__init__(escaped_fill_names, *args, **kwargs)
def __missing__(self, key: Any) -> bool:
return False
+ def _escape_slot_name(self, name: str) -> str:
+ """
+ Users may define slots with names which are invalid identifiers like 'my slot'.
+ But these cannot be used as keys in the template context, e.g. `{{ component_vars.is_filled.'my slot' }}`.
+ So as workaround, we instead use these escaped names which are valid identifiers.
+
+ So e.g. `my slot` should be escaped as `my_slot`.
+ """
+ # NOTE: Do a simple substitution where we replace all non-identifier characters with `_`.
+ # Identifiers consist of alphanum (a-zA-Z0-9) and underscores.
+ # We don't check if these escaped names conflict with other existing slots in the template,
+ # we leave this obligation to the user.
+ escaped_name = name_escape_re.sub("_", name)
+ return escaped_name
+
class SlotNode(BaseNode):
"""
- Slot tag marks a place inside a component where content can be inserted
+ [`{% slot %}`](../template_tags#slot) tag marks a place inside a component where content can be inserted
from outside.
[Learn more](../../concepts/fundamentals/slots) about using slots.
@@ -169,14 +527,14 @@ class SlotNode(BaseNode):
- `name` (str, required): Registered name of the component to render
- `default`: Optional flag. If there is a default slot, you can pass the component slot content
- without using the [`{% fill %}`](#fill) tag. See
+ without using the [`{% fill %}`](../template_tags#fill) tag. See
[Default slot](../../concepts/fundamentals/slots#default-slot)
- `required`: Optional flag. Will raise an error if a slot is required but not given.
- `**kwargs`: Any extra kwargs will be passed as the slot data.
**Example:**
- ```python
+ ```djc_py
@register("child")
class Child(Component):
template = \"\"\"
@@ -191,7 +549,7 @@ class SlotNode(BaseNode):
\"\"\"
```
- ```python
+ ```djc_py
@register("parent")
class Parent(Component):
template = \"\"\"
@@ -209,12 +567,14 @@ class SlotNode(BaseNode):
\"\"\"
```
- ### Passing data to slots
+ ### Slot data
- Any extra kwargs will be considered as slot data, and will be accessible in the [`{% fill %}`](#fill)
- tag via fill's `data` kwarg:
+ Any extra kwargs will be considered as slot data, and will be accessible
+ in the [`{% fill %}`](../template_tags#fill) tag via fill's `data` kwarg:
- ```python
+ Read more about [Slot data](../../concepts/fundamentals/slots#slot-data).
+
+ ```djc_py
@register("child")
class Child(Component):
template = \"\"\"
@@ -227,7 +587,7 @@ class SlotNode(BaseNode):
\"\"\"
```
- ```python
+ ```djc_py
@register("parent")
class Parent(Component):
template = \"\"\"
@@ -242,35 +602,35 @@ class SlotNode(BaseNode):
\"\"\"
```
- ### Accessing default slot content
+ ### Slot fallback
- The content between the `{% slot %}..{% endslot %}` tags is the default content that
+ The content between the `{% slot %}..{% endslot %}` tags is the fallback content that
will be rendered if no fill is given for the slot.
- This default content can then be accessed from within the [`{% fill %}`](#fill) tag using
- the fill's `default` kwarg.
+ This fallback content can then be accessed from within the [`{% fill %}`](../template_tags#fill) tag
+ using the fill's `fallback` kwarg.
This is useful if you need to wrap / prepend / append the original slot's content.
- ```python
+ ```djc_py
@register("child")
class Child(Component):
template = \"\"\"
{% slot "content" %}
- This is default content!
+ This is fallback content!
{% endslot %}
\"\"\"
```
- ```python
+ ```djc_py
@register("parent")
class Parent(Component):
template = \"\"\"
- {# Parent can access the slot's default content #}
+ {# Parent can access the slot's fallback content #}
{% component "child" %}
- {% fill "content" default="default" %}
- {{ default }}
+ {% fill "content" fallback="fallback" %}
+ {{ fallback }}
{% endfill %}
{% endcomponent %}
\"\"\"
@@ -279,7 +639,7 @@ class SlotNode(BaseNode):
tag = "slot"
end_tag = "endslot"
- allowed_flags = [SLOT_DEFAULT_KEYWORD, SLOT_REQUIRED_KEYWORD]
+ allowed_flags = [SLOT_DEFAULT_FLAG, SLOT_REQUIRED_FLAG]
# NOTE:
# In the current implementation, the slots are resolved only at the render time.
@@ -316,14 +676,22 @@ class SlotNode(BaseNode):
f"SlotNode: {self.__repr__()}"
)
+ # Component info
component_id: str = context[_COMPONENT_CONTEXT_KEY]
component_ctx = component_context_cache[component_id]
- component_name = component_ctx.component_name
+ component = component_ctx.component
+ component_name = component.name
component_path = component_ctx.component_path
- slot_fills = component_ctx.fills
+ is_dynamic_component = getattr(component, "_is_dynamic_component", False)
+ # NOTE: Use `ComponentContext.outer_context`, and NOT `Component.outer_context`.
+ # The first is a SNAPSHOT of the outer context.
+ outer_context = component_ctx.outer_context
+
+ # Slot info
+ slot_fills = component.raw_slots
slot_name = name
- is_default = self.flags[SLOT_DEFAULT_KEYWORD]
- is_required = self.flags[SLOT_REQUIRED_KEYWORD]
+ is_default = self.flags[SLOT_DEFAULT_FLAG]
+ is_required = self.flags[SLOT_REQUIRED_FLAG]
trace_component_msg(
"RENDER_SLOT_START",
@@ -336,7 +704,7 @@ class SlotNode(BaseNode):
)
# Check for errors
- if is_default and not component_ctx.is_dynamic_component:
+ if is_default and not is_dynamic_component:
# Allow one slot to be marked as 'default', or multiple slots but with
# the same name. If there is multiple 'default' slots with different names, raise.
default_slot_name = component_ctx.default_slot
@@ -374,7 +742,7 @@ class SlotNode(BaseNode):
# 1. Using the "django" context behavior
# 2. AND the slot fill is defined in the root template
#
- # Then `ctx_with_fills.fills` does NOT contain any fills (`{% fill %}`). So in this case,
+ # Then `ctx_with_fills.component.raw_slots` does NOT contain any fills (`{% fill %}`). So in this case,
# we need to use a different strategy to find the fills Context layer that contains the fills.
#
# ------------------------------------------------------------------------------------------
@@ -396,9 +764,9 @@ class SlotNode(BaseNode):
# In this case, we need to find the context that was used to render the component,
# and use the fills from that context.
if (
- component_ctx.registry.settings.context_behavior == ContextBehavior.DJANGO
- and component_ctx.outer_context is None
- and (slot_name not in component_ctx.fills)
+ component.registry.settings.context_behavior == ContextBehavior.DJANGO
+ and outer_context is None
+ and (slot_name not in slot_fills)
):
# When we have nested components with fills, the context layers are added in
# the following order:
@@ -445,10 +813,10 @@ class SlotNode(BaseNode):
trace_component_msg(
"SLOT_PARENT_INDEX",
- component_name=component_ctx.component_name,
- component_id=component_ctx.component_id,
+ component_name=component_name,
+ component_id=component_id,
slot_name=name,
- component_path=component_ctx.component_path,
+ component_path=component_path,
extra=(
f"Parent index: {parent_index}, Current index: {curr_index}, "
f"Context stack: {[d.get(_COMPONENT_CONTEXT_KEY) for d in context.dicts]}"
@@ -457,7 +825,7 @@ class SlotNode(BaseNode):
if parent_index is not None:
ctx_id_with_fills = context.dicts[parent_index][_COMPONENT_CONTEXT_KEY]
ctx_with_fills = component_context_cache[ctx_id_with_fills]
- slot_fills = ctx_with_fills.fills
+ slot_fills = ctx_with_fills.component.raw_slots
# Add trace message when slot_fills are overwritten
trace_component_msg(
@@ -470,24 +838,18 @@ class SlotNode(BaseNode):
)
if fill_name in slot_fills:
- slot_fill_fn = slot_fills[fill_name]
- slot_fill = SlotFill(
- name=slot_name,
- is_filled=True,
- slot=slot_fill_fn,
- )
+ slot_is_filled = True
+ slot = slot_fills[fill_name]
else:
- # No fill was supplied, render the slot's default content
- slot_fill = SlotFill(
- name=slot_name,
- is_filled=False,
- slot=_nodelist_to_slot_render_func(
- component_name=component_name,
- slot_name=slot_name,
- nodelist=self.nodelist,
- data_var=None,
- default_var=None,
- ),
+ # No fill was supplied, render the slot's fallback content
+ slot_is_filled = False
+ slot = _nodelist_to_slot(
+ component_name=component_name,
+ slot_name=slot_name,
+ nodelist=self.nodelist,
+ contents=self.contents,
+ data_var=None,
+ fallback_var=None,
)
# Check: If a slot is marked as 'required', it must be filled.
@@ -500,7 +862,7 @@ class SlotNode(BaseNode):
# Note: Finding a good `cutoff` value may require further trial-and-error.
# Higher values make matching stricter. This is probably preferable, as it
# reduces false positives.
- if is_required and not slot_fill.is_filled and not component_ctx.is_dynamic_component:
+ if is_required and not slot_is_filled and not is_dynamic_component:
msg = (
f"Slot '{slot_name}' is marked as 'required' (i.e. non-optional), "
f"yet no fill is provided. Check template.'"
@@ -534,13 +896,13 @@ class SlotNode(BaseNode):
#
# Hence, even in the "django" mode, we MUST use slots of the context of the parent component.
if (
- component_ctx.registry.settings.context_behavior == ContextBehavior.DJANGO
- and component_ctx.outer_context is not None
- and _COMPONENT_CONTEXT_KEY in component_ctx.outer_context
+ component.registry.settings.context_behavior == ContextBehavior.DJANGO
+ and outer_context is not None
+ and _COMPONENT_CONTEXT_KEY in outer_context
):
- extra_context[_COMPONENT_CONTEXT_KEY] = component_ctx.outer_context[_COMPONENT_CONTEXT_KEY]
- # This ensures that `component_vars.is_filled`is accessible in the fill
- extra_context["component_vars"] = component_ctx.outer_context["component_vars"]
+ extra_context[_COMPONENT_CONTEXT_KEY] = outer_context[_COMPONENT_CONTEXT_KEY]
+ # This ensures that the ComponentVars API (e.g. `{{ component_vars.is_filled }}`) is accessible in the fill
+ extra_context["component_vars"] = outer_context["component_vars"]
# Irrespective of which context we use ("root" context or the one passed to this
# render function), pass down the keys used by inject/provide feature. This makes it
@@ -552,11 +914,11 @@ class SlotNode(BaseNode):
if key.startswith(_INJECT_CONTEXT_KEY_PREFIX):
extra_context[key] = value
- slot_ref = SlotRef(self, context)
+ fallback = SlotFallback(self, context)
# For the user-provided slot fill, we want to use the context of where the slot
# came from (or current context if configured so)
- used_ctx = self._resolve_slot_context(context, slot_fill, component_ctx)
+ used_ctx = self._resolve_slot_context(context, slot_is_filled, component, outer_context)
with used_ctx.update(extra_context):
# Required for compatibility with Django's {% extends %} tag
# This makes sure that the render context used outside of a component
@@ -571,12 +933,24 @@ class SlotNode(BaseNode):
with used_ctx.render_context.push(render_ctx_layer):
with add_slot_to_error_message(component_name, slot_name):
# Render slot as a function
- # NOTE: While `{% fill %}` tag has to opt in for the `default` and `data` variables,
+ # NOTE: While `{% fill %}` tag has to opt in for the `fallback` and `data` variables,
# the render function ALWAYS receives them.
- output = slot_fill.slot(used_ctx, kwargs, slot_ref)
+ output = slot(data=kwargs, fallback=fallback, context=used_ctx)
- if app_settings.DEBUG_HIGHLIGHT_SLOTS:
- output = apply_component_highlight("slot", output, f"{component_name} - {slot_name}")
+ # Allow plugins to post-process the slot's rendered output
+ output = extensions.on_slot_rendered(
+ OnSlotRenderedContext(
+ component=component,
+ component_cls=component.__class__,
+ component_id=component_id,
+ slot=slot,
+ slot_name=slot_name,
+ slot_node=self,
+ slot_is_required=is_required,
+ slot_is_default=is_default,
+ result=output,
+ ),
+ )
trace_component_msg(
"RENDER_SLOT_END",
@@ -592,21 +966,21 @@ class SlotNode(BaseNode):
def _resolve_slot_context(
self,
context: Context,
- slot_fill: "SlotFill",
- component_ctx: "ComponentContext",
+ slot_is_filled: bool,
+ component: "Component",
+ outer_context: Optional[Context],
) -> Context:
"""Prepare the context used in a slot fill based on the settings."""
- # If slot is NOT filled, we use the slot's default AKA content between
+ # If slot is NOT filled, we use the slot's fallback AKA content between
# the `{% slot %}` tags. These should be evaluated as if the `{% slot %}`
# tags weren't even there, which means that we use the current context.
- if not slot_fill.is_filled:
+ if not slot_is_filled:
return context
- registry_settings = component_ctx.registry.settings
+ registry_settings = component.registry.settings
if registry_settings.context_behavior == ContextBehavior.DJANGO:
return context
elif registry_settings.context_behavior == ContextBehavior.ISOLATED:
- outer_context = component_ctx.outer_context
return outer_context if outer_context is not None else Context()
else:
raise ValueError(f"Unknown value for context_behavior: '{registry_settings.context_behavior}'")
@@ -614,24 +988,23 @@ class SlotNode(BaseNode):
class FillNode(BaseNode):
"""
- Use this tag to insert content into component's slots.
+ Use [`{% fill %}`](../template_tags#fill) tag to insert content into component's
+ [slots](../../concepts/fundamentals/slots).
- `{% fill %}` tag may be used only within a `{% component %}..{% endcomponent %}` block.
- Runtime checks should prohibit other usages.
+ [`{% fill %}`](../template_tags#fill) tag may be used only within a `{% component %}..{% endcomponent %}` block,
+ and raises a `TemplateSyntaxError` if used outside of a component.
**Args:**
- `name` (str, required): Name of the slot to insert this content into. Use `"default"` for
- the default slot.
- - `default` (str, optional): This argument allows you to access the original content of the slot
- under the specified variable name. See
- [Accessing original content of slots](../../concepts/fundamentals/slots#accessing-original-content-of-slots)
+ the [default slot](../../concepts/fundamentals/slots#default-slot).
- `data` (str, optional): This argument allows you to access the data passed to the slot
- under the specified variable name. See [Scoped slots](../../concepts/fundamentals/slots#scoped-slots)
+ under the specified variable name. See [Slot data](../../concepts/fundamentals/slots#slot-data).
+ - `fallback` (str, optional): This argument allows you to access the original content of the slot
+ under the specified variable name. See [Slot fallback](../../concepts/fundamentals/slots#slot-fallback).
- **Examples:**
+ **Example:**
- Basic usage:
```django
{% component "my_table" %}
{% fill "pagination" %}
@@ -640,7 +1013,15 @@ class FillNode(BaseNode):
{% endcomponent %}
```
- ### Accessing slot's default content with the `default` kwarg
+ ### Access slot fallback
+
+ Use the `fallback` kwarg to access the original content of the slot.
+
+ The `fallback` kwarg defines the name of the variable that will contain the slot's fallback content.
+
+ Read more about [Slot fallback](../../concepts/fundamentals/slots#slot-fallback).
+
+ Component template:
```django
{# my_table.html #}
@@ -652,17 +1033,27 @@ class FillNode(BaseNode):
{% endfill %}
{% endcomponent %}
```
- ### Accessing slot's data with the `data` kwarg
+ ### Access slot data
+
+ Use the `data` kwarg to access the data passed to the slot.
+
+ The `data` kwarg defines the name of the variable that will contain the slot's data.
+
+ Read more about [Slot data](../../concepts/fundamentals/slots#slot-data).
+
+ Component template:
```django
{# my_table.html #}
@@ -674,6 +1065,8 @@ class FillNode(BaseNode):
```
+ Fill:
+
```django
{% component "my_table" %}
{% fill "pagination" data="slot_data" %}
@@ -686,26 +1079,85 @@ class FillNode(BaseNode):
{% endcomponent %}
```
- ### Accessing slot data and default content on the default slot
+ ### Using default slot
- To access slot data and the default slot content on the default slot,
- use `{% fill %}` with `name` set to `"default"`:
+ To access slot data and the fallback slot content on the default slot,
+ use [`{% fill %}`](../template_tags#fill) with `name` set to `"default"`:
```django
{% component "button" %}
- {% fill name="default" data="slot_data" default="default_slot" %}
+ {% fill name="default" data="slot_data" fallback="slot_fallback" %}
You clicked me {{ slot_data.count }} times!
- {{ default_slot }}
+ {{ slot_fallback }}
{% endfill %}
{% endcomponent %}
```
+
+ ### Slot fills from Python
+
+ You can pass a slot fill from Python to a component by setting the `body` kwarg
+ on the [`{% fill %}`](../template_tags#fill) tag.
+
+ First pass a [`Slot`](../api#django_components.Slot) instance to the template
+ with the [`get_template_data()`](../api#django_components.Component.get_template_data)
+ method:
+
+ ```python
+ from django_components import component, Slot
+
+ class Table(Component):
+ def get_template_data(self, args, kwargs, slots, context):
+ return {
+ "my_slot": Slot(lambda ctx: "Hello, world!"),
+ }
+ ```
+
+ Then pass the slot to the [`{% fill %}`](../template_tags#fill) tag:
+
+ ```django
+ {% component "table" %}
+ {% fill "pagination" body=my_slot / %}
+ {% endcomponent %}
+ ```
+
+ !!! warning
+
+ If you define both the `body` kwarg and the [`{% fill %}`](../template_tags#fill) tag's body,
+ an error will be raised.
+
+ ```django
+ {% component "table" %}
+ {% fill "pagination" body=my_slot %}
+ ...
+ {% endfill %}
+ {% endcomponent %}
+ ```
"""
tag = "fill"
end_tag = "endfill"
allowed_flags = []
- def render(self, context: Context, name: str, *, data: Optional[str] = None, default: Optional[str] = None) -> str:
+ def render(
+ self,
+ context: Context,
+ name: str,
+ *,
+ data: Optional[str] = None,
+ fallback: Optional[str] = None,
+ body: Optional[SlotInput] = None,
+ # TODO_V1: Use `fallback` kwarg instead of `default`
+ default: Optional[str] = None,
+ ) -> str:
+ # TODO_V1: Use `fallback` kwarg instead of `default`
+ if fallback is not None and default is not None:
+ raise TemplateSyntaxError(
+ f"Fill tag received both 'default' and '{FILL_FALLBACK_KWARG}' kwargs. "
+ f"Use '{FILL_FALLBACK_KWARG}' instead."
+ )
+ elif fallback is None and default is not None:
+ fallback = default
+
if not _is_extracting_fill(context):
raise TemplateSyntaxError(
"FillNode.render() (AKA {% fill ... %} block) cannot be rendered outside of a Component context. "
@@ -718,36 +1170,43 @@ class FillNode(BaseNode):
if data is not None:
if not isinstance(data, str):
- raise TemplateSyntaxError(f"Fill tag '{SLOT_DATA_KWARG}' kwarg must resolve to a string, got {data}")
+ raise TemplateSyntaxError(f"Fill tag '{FILL_DATA_KWARG}' kwarg must resolve to a string, got {data}")
if not is_identifier(data):
raise RuntimeError(
- f"Fill tag kwarg '{SLOT_DATA_KWARG}' does not resolve to a valid Python identifier, got '{data}'"
+ f"Fill tag kwarg '{FILL_DATA_KWARG}' does not resolve to a valid Python identifier, got '{data}'"
)
- if default is not None:
- if not isinstance(default, str):
+ if fallback is not None:
+ if not isinstance(fallback, str):
raise TemplateSyntaxError(
- f"Fill tag '{SLOT_DEFAULT_KWARG}' kwarg must resolve to a string, got {default}"
+ f"Fill tag '{FILL_FALLBACK_KWARG}' kwarg must resolve to a string, got {fallback}"
)
- if not is_identifier(default):
+ if not is_identifier(fallback):
raise RuntimeError(
- f"Fill tag kwarg '{SLOT_DEFAULT_KWARG}' does not resolve to a valid Python identifier,"
- f" got '{default}'"
+ f"Fill tag kwarg '{FILL_FALLBACK_KWARG}' does not resolve to a valid Python identifier,"
+ f" got '{fallback}'"
)
- # data and default cannot be bound to the same variable
- if data and default and data == default:
+ # data and fallback cannot be bound to the same variable
+ if data and fallback and data == fallback:
raise RuntimeError(
- f"Fill '{name}' received the same string for slot default ({SLOT_DEFAULT_KWARG}=...)"
- f" and slot data ({SLOT_DATA_KWARG}=...)"
+ f"Fill '{name}' received the same string for slot fallback ({FILL_FALLBACK_KWARG}=...)"
+ f" and slot data ({FILL_DATA_KWARG}=...)"
+ )
+
+ if body is not None and self.contents:
+ raise TemplateSyntaxError(
+ f"Fill '{name}' received content both through '{FILL_BODY_KWARG}' kwarg and '{{% fill %}}' body. "
+ f"Use only one method."
)
fill_data = FillWithData(
fill=self,
name=name,
- default_var=default,
+ fallback_var=fallback,
data_var=data,
extra_context={},
+ body=body,
)
self._extract_fill(context, fill_data)
@@ -763,10 +1222,13 @@ class FillNode(BaseNode):
# ...
# {% endfill %}
# {% endfor %}
- collected_fills: List[FillWithData] = context.get(FILL_GEN_CONTEXT_KEY, None)
+ captured_fills: Optional[List[FillWithData]] = context.get(FILL_GEN_CONTEXT_KEY, None)
- if collected_fills is None:
- return
+ if captured_fills is None:
+ raise RuntimeError(
+ "FillNode.render() (AKA {% fill ... %} block) cannot be rendered outside of a Component context. "
+ "Make sure that the {% fill %} tags are nested within {% component %} tags."
+ )
# To allow using variables which were defined within the template and to which
# the `{% fill %}` tag has access, we need to capture those variables too.
@@ -786,7 +1248,8 @@ class FillNode(BaseNode):
# `{% component %} ... {% endcomponent %}`. Hence we search for the last
# index of `FILL_GEN_CONTEXT_KEY`.
index_of_new_layers = get_last_index(context.dicts, lambda d: FILL_GEN_CONTEXT_KEY in d)
- for dict_layer in context.dicts[index_of_new_layers:]:
+ context_dicts: List[Dict[str, Any]] = context.dicts
+ for dict_layer in context_dicts[index_of_new_layers:]:
for key, value in dict_layer.items():
if not key.startswith("_"):
data.extra_context[key] = value
@@ -821,25 +1284,61 @@ class FillNode(BaseNode):
layer["forloop"] = layer["forloop"].copy()
data.extra_context.update(layer)
- collected_fills.append(data)
+ captured_fills.append(data)
#######################################
# EXTRACTING {% fill %} FROM TEMPLATES
+# (internal)
#######################################
class FillWithData(NamedTuple):
fill: FillNode
name: str
- default_var: Optional[str]
+ """Name of the slot to be filled, as set on the `{% fill %}` tag."""
+ body: Optional[SlotInput]
+ """
+ Slot fill as set by the `body` kwarg on the `{% fill %}` tag.
+
+ E.g.
+ ```django
+ {% component "mycomponent" %}
+ {% fill "footer" body=my_slot / %}
+ {% endcomponent %}
+ ```
+ """
+ fallback_var: Optional[str]
+ """Name of the FALLBACK variable, as set on the `{% fill %}` tag."""
data_var: Optional[str]
+ """Name of the DATA variable, as set on the `{% fill %}` tag."""
extra_context: Dict[str, Any]
+ """
+ Extra context variables that will be available inside the `{% fill %}` tag.
+
+ For example, if the `{% fill %}` tags are nested within `{% with %}` or `{% for %}` tags,
+ then the variables defined within those tags will be available inside the `{% fill %}` tags:
+
+ ```django
+ {% component "mycomponent" %}
+ {% with extra_var="extra_value" %}
+ {% fill "my_fill" %}
+ {{ extra_var }}
+ {% endfill %}
+ {% endwith %}
+ {% for item in items %}
+ {% fill "my_fill" %}
+ {{ item }}
+ {% endfill %}
+ {% endfor %}
+ {% endcomponent %}
+ ```
+ """
def resolve_fills(
context: Context,
- nodelist: NodeList,
+ component_node: "ComponentNode",
component_name: str,
) -> Dict[SlotName, Slot]:
"""
@@ -890,6 +1389,9 @@ def resolve_fills(
"""
slots: Dict[SlotName, Slot] = {}
+ nodelist = component_node.nodelist
+ contents = component_node.contents
+
if not nodelist:
return slots
@@ -907,12 +1409,14 @@ def resolve_fills(
)
if not nodelist_is_empty:
- slots[DEFAULT_SLOT_KEY] = _nodelist_to_slot_render_func(
+ slots[DEFAULT_SLOT_KEY] = _nodelist_to_slot(
component_name=component_name,
slot_name=None, # Will be populated later
nodelist=nodelist,
+ contents=contents,
data_var=None,
- default_var=None,
+ fallback_var=None,
+ fill_node=component_node,
)
# The content has fills
@@ -920,14 +1424,30 @@ def resolve_fills(
# NOTE: If slot fills are explicitly defined, we use them even if they are empty (or only whitespace).
# This is different from the default slot, where we ignore empty content.
for fill in maybe_fills:
- slots[fill.name] = _nodelist_to_slot_render_func(
- component_name=component_name,
- slot_name=fill.name,
- nodelist=fill.fill.nodelist,
- data_var=fill.data_var,
- default_var=fill.default_var,
- extra_context=fill.extra_context,
- )
+ # Case: Slot fill was explicitly defined as `{% fill body=... / %}`
+ if fill.body is not None:
+ # Set `Slot.fill_node` so the slot fill behaves the same as if it was defined inside
+ # a `{% fill %}` tag.
+ # This for example allows CSS scoping to work even on slots that are defined
+ # as `{% fill ... body=... / %}`
+ if isinstance(fill.body, Slot):
+ # Make a copy of the Slot instance and set its `fill_node`.
+ slot_fill = dataclass_replace(fill.body, fill_node=fill.fill)
+ else:
+ slot_fill = Slot(fill.body, fill_node=fill.fill)
+ # Case: Slot fill was defined as the body of `{% fill / %}...{% endfill %}`
+ else:
+ slot_fill = _nodelist_to_slot(
+ component_name=component_name,
+ slot_name=fill.name,
+ nodelist=fill.fill.nodelist,
+ contents=fill.fill.contents,
+ data_var=fill.data_var,
+ fallback_var=fill.fallback_var,
+ extra_context=fill.extra_context,
+ fill_node=fill.fill,
+ )
+ slots[fill.name] = slot_fill
return slots
@@ -973,32 +1493,81 @@ def _extract_fill_content(
#######################################
-name_escape_re = re.compile(r"[^\w]")
+def normalize_slot_fills(
+ fills: Mapping[SlotName, SlotInput],
+ component_name: Optional[str] = None,
+) -> Dict[SlotName, Slot]:
+ norm_fills = {}
+
+ # NOTE: `copy_slot` is defined as a separate function, instead of being inlined within
+ # the forloop, because the value the forloop variable points to changes with each loop iteration.
+ def copy_slot(content: Union[SlotFunc, Slot], slot_name: str) -> Slot:
+ # Case: Already Slot and names assigned, so nothing to do.
+ if isinstance(content, Slot) and content.slot_name and content.component_name:
+ return content
+
+ # Otherwise, we create a new instance of Slot, whether we've been given Slot or not,
+ # so we can assign metadata to our internal copies without affecting the original.
+ if not isinstance(content, Slot):
+ content_func = content
+ else:
+ content_func = content.content_func
+
+ # Populate potentially missing fields so we can trace the component and slot
+ if isinstance(content, Slot):
+ used_component_name = content.component_name or component_name
+ used_slot_name = content.slot_name or slot_name
+ used_nodelist = content.nodelist
+ used_contents = content.contents if content.contents is not None else content_func
+ used_fill_node = content.fill_node
+ used_extra = content.extra.copy()
+ else:
+ used_component_name = component_name
+ used_slot_name = slot_name
+ used_nodelist = None
+ used_contents = content_func
+ used_fill_node = None
+ used_extra = {}
+
+ slot = Slot(
+ contents=used_contents,
+ content_func=content_func,
+ component_name=used_component_name,
+ slot_name=used_slot_name,
+ nodelist=used_nodelist,
+ fill_node=used_fill_node,
+ extra=used_extra,
+ )
+
+ return slot
+
+ for slot_name, content in fills.items():
+ # Case: No content, so nothing to do.
+ if content is None:
+ continue
+ # Case: Content is a string / non-slot / non-callable
+ elif not callable(content):
+ # NOTE: `Slot.content_func` and `Slot.nodelist` will be set in `Slot.__init__()`
+ slot: Slot = Slot(contents=content, component_name=component_name, slot_name=slot_name)
+ # Case: Content is a callable, so either a plain function or a `Slot` instance.
+ else:
+ slot = copy_slot(content, slot_name)
+
+ norm_fills[slot_name] = slot
+
+ return norm_fills
-def _escape_slot_name(name: str) -> str:
- """
- Users may define slots with names which are invalid identifiers like 'my slot'.
- But these cannot be used as keys in the template context, e.g. `{{ component_vars.is_filled.'my slot' }}`.
- So as workaround, we instead use these escaped names which are valid identifiers.
-
- So e.g. `my slot` should be escaped as `my_slot`.
- """
- # NOTE: Do a simple substitution where we replace all non-identifier characters with `_`.
- # Identifiers consist of alphanum (a-zA-Z0-9) and underscores.
- # We don't check if these escaped names conflict with other existing slots in the template,
- # we leave this obligation to the user.
- escaped_name = name_escape_re.sub("_", name)
- return escaped_name
-
-
-def _nodelist_to_slot_render_func(
+def _nodelist_to_slot(
component_name: str,
slot_name: Optional[str],
nodelist: NodeList,
+ contents: Optional[str] = None,
data_var: Optional[str] = None,
- default_var: Optional[str] = None,
+ fallback_var: Optional[str] = None,
extra_context: Optional[Dict[str, Any]] = None,
+ fill_node: Optional[Union[FillNode, "ComponentNode"]] = None,
+ extra: Optional[Dict[str, Any]] = None,
) -> Slot:
if data_var:
if not data_var.isidentifier():
@@ -1006,30 +1575,30 @@ def _nodelist_to_slot_render_func(
f"Slot data alias in fill '{slot_name}' must be a valid identifier. Got '{data_var}'"
)
- if default_var:
- if not default_var.isidentifier():
+ if fallback_var:
+ if not fallback_var.isidentifier():
raise TemplateSyntaxError(
- f"Slot default alias in fill '{slot_name}' must be a valid identifier. Got '{default_var}'"
+ f"Slot fallback alias in fill '{slot_name}' must be a valid identifier. Got '{fallback_var}'"
)
# We use Template.render() to render the nodelist, so that Django correctly sets up
# and binds the context.
template = Template("")
template.nodelist = nodelist
- # This allows the template to access current RenderContext layer.
- template._djc_is_component_nested = True
- def render_func(ctx: Context, slot_data: Dict[str, Any], slot_ref: SlotRef) -> SlotResult:
+ def render_func(ctx: SlotContext) -> SlotResult:
+ context = ctx.context or Context()
+
# Expose the kwargs that were passed to the `{% slot %}` tag. These kwargs
# are made available through a variable name that was set on the `{% fill %}`
# tag.
if data_var:
- ctx[data_var] = slot_data
+ context[data_var] = ctx.data
- # If slot fill is using `{% fill "myslot" default="abc" %}`, then set the "abc" to
- # the context, so users can refer to the default slot from within the fill content.
- if default_var:
- ctx[default_var] = slot_ref
+ # If slot fill is using `{% fill "myslot" fallback="abc" %}`, then set the "abc" to
+ # the context, so users can refer to the fallback slot from within the fill content.
+ if fallback_var:
+ context[fallback_var] = ctx.fallback
# NOTE: If a `{% fill %}` tag inside a `{% component %}` tag is inside a forloop,
# the `extra_context` contains the forloop variables. We want to make these available
@@ -1049,29 +1618,35 @@ def _nodelist_to_slot_render_func(
#
# And so we want to put the `extra_context` into the same layer that contains `_COMPONENT_CONTEXT_KEY`.
#
- # HOWEVER, the layer with `_COMPONENT_CONTEXT_KEY` also contains user-defined data from `get_context_data()`.
- # Data from `get_context_data()` should take precedence over `extra_context`. So we have to insert
+ # HOWEVER, the layer with `_COMPONENT_CONTEXT_KEY` also contains user-defined data from `get_template_data()`.
+ # Data from `get_template_data()` should take precedence over `extra_context`. So we have to insert
# the forloop variables BEFORE that.
- index_of_last_component_layer = get_last_index(ctx.dicts, lambda d: _COMPONENT_CONTEXT_KEY in d)
+ index_of_last_component_layer = get_last_index(context.dicts, lambda d: _COMPONENT_CONTEXT_KEY in d)
if index_of_last_component_layer is None:
index_of_last_component_layer = 0
- # TODO: Currently there's one more layer before the `_COMPONENT_CONTEXT_KEY` layer, which is
+ # TODO_V1: Currently there's one more layer before the `_COMPONENT_CONTEXT_KEY` layer, which is
# pushed in `_prepare_template()` in `component.py`.
# That layer should be removed when `Component.get_template()` is removed, after which
# the following line can be removed.
index_of_last_component_layer -= 1
- # Insert the `extra_context` layer BEFORE the layer that defines the variables from get_context_data.
- # Thus, get_context_data will overshadow these on conflict.
- ctx.dicts.insert(index_of_last_component_layer, extra_context or {})
+ # Insert the `extra_context` layer BEFORE the layer that defines the variables from get_template_data.
+ # Thus, get_template_data will overshadow these on conflict.
+ context.dicts.insert(index_of_last_component_layer, extra_context or {})
trace_component_msg("RENDER_NODELIST", component_name, component_id=None, slot_name=slot_name)
- rendered = template.render(ctx)
+ # NOTE 1: We wrap the slot nodelist in Template. However, we also override Django's `Template.render()`
+ # to call `render_dependencies()` on the results. So we need to set the strategy to `ignore`
+ # so that the dependencies are processed only once the whole component tree is rendered.
+ # NOTE 2: We also set `_DJC_COMPONENT_IS_NESTED` to `True` so that the template can access
+ # current RenderContext layer.
+ with context.push({"DJC_DEPS_STRATEGY": "ignore", COMPONENT_IS_NESTED_KEY: True}):
+ rendered = template.render(context)
# After the rendering is done, remove the `extra_context` from the context stack
- ctx.dicts.pop(index_of_last_component_layer)
+ context.dicts.pop(index_of_last_component_layer)
return rendered
@@ -1079,8 +1654,14 @@ def _nodelist_to_slot_render_func(
content_func=cast(SlotFunc, render_func),
component_name=component_name,
slot_name=slot_name,
- escaped=False,
nodelist=nodelist,
+ # The `contents` param passed to this function may be `None`, because it's taken from
+ # `BaseNode.contents` which is `None` for self-closing tags like `{% fill "footer" / %}`.
+ # But `Slot(contents=None)` would result in `Slot.contents` being the render function.
+ # So we need to special-case this.
+ contents=default(contents, ""),
+ fill_node=default(fill_node, None),
+ extra=default(extra, {}),
)
diff --git a/src/django_components/tag_formatter.py b/src/django_components/tag_formatter.py
index 449c4e2c..5daacc39 100644
--- a/src/django_components/tag_formatter.py
+++ b/src/django_components/tag_formatter.py
@@ -237,27 +237,11 @@ class ComponentFormatter(TagFormatterABC):
if not args:
raise TemplateSyntaxError(f"{self.__class__.__name__}: Component tag did not receive tag name")
- # If the first arg is a kwarg, not a positional arg, then look for the "name" kwarg
- # for component name.
+ # If the first arg is a kwarg, then clearly the component name is not set.
if "=" in args[0]:
comp_name = None
- final_args = []
- for kwarg in args:
- if not kwarg.startswith("name="):
- final_args.append(kwarg)
- continue
-
- if comp_name:
- raise TemplateSyntaxError(
- f"ComponentFormatter: 'name' kwarg for component '{comp_name}'" " was defined more than once."
- )
-
- # NOTE: We intentionally do NOT add to `final_args` here
- # because we want to remove the the `name=` kwarg from args list
- comp_name = kwarg[5:]
else:
comp_name = args.pop(0)
- final_args = args
if not comp_name:
raise TemplateSyntaxError("Component name must be a non-empty quoted string, e.g. 'my_comp'")
@@ -268,7 +252,7 @@ class ComponentFormatter(TagFormatterABC):
# Remove the quotes
comp_name = comp_name[1:-1]
- return TagResult(comp_name, final_args)
+ return TagResult(comp_name, args)
class ShorthandComponentFormatter(TagFormatterABC):
diff --git a/src/django_components/template.py b/src/django_components/template.py
index 5dad8dac..6b6ebb24 100644
--- a/src/django_components/template.py
+++ b/src/django_components/template.py
@@ -1,14 +1,24 @@
-from typing import Any, Optional, Type, TypeVar
+import sys
+from contextlib import contextmanager
+from typing import TYPE_CHECKING, Any, Dict, Generator, List, Optional, Type, Union
+from weakref import ReferenceType, ref
-from django.template import Origin, Template
+from django.core.exceptions import ImproperlyConfigured
+from django.template import Context, Origin, Template
+from django.template.loader import get_template as django_get_template
from django_components.cache import get_template_cache
-from django_components.util.misc import get_import_path
+from django_components.util.django_monkeypatch import is_cls_patched
+from django_components.util.loader import get_component_dirs
+from django_components.util.logger import trace_component_msg
+from django_components.util.misc import get_import_path, get_module_info
-TTemplate = TypeVar("TTemplate", bound=Template)
+if TYPE_CHECKING:
+ from django_components.component import Component
-# Central logic for creating Templates from string, so we can cache the results
+# TODO_V1 - Remove, won't be needed once we remove `get_template_string()`, `get_template_name()`, `get_template()`
+# Legacy logic for creating Templates from string
def cached_template(
template_string: str,
template_cls: Optional[Type[Template]] = None,
@@ -17,17 +27,19 @@ def cached_template(
engine: Optional[Any] = None,
) -> Template:
"""
+ DEPRECATED. Template caching will be removed in v1.
+
Create a Template instance that will be cached as per the
[`COMPONENTS.template_cache_size`](../settings#django_components.app_settings.ComponentsSettings.template_cache_size)
setting.
Args:
template_string (str): Template as a string, same as the first argument to Django's\
- [`Template`](https://docs.djangoproject.com/en/5.1/topics/templates/#template). Required.
+ [`Template`](https://docs.djangoproject.com/en/5.2/topics/templates/#template). Required.
template_cls (Type[Template], optional): Specify the Template class that should be instantiated.\
- Defaults to Django's [`Template`](https://docs.djangoproject.com/en/5.1/topics/templates/#template) class.
+ Defaults to Django's [`Template`](https://docs.djangoproject.com/en/5.2/topics/templates/#template) class.
origin (Type[Origin], optional): Sets \
- [`Template.Origin`](https://docs.djangoproject.com/en/5.1/howto/custom-template-backend/#origin-api-and-3rd-party-integration).
+ [`Template.Origin`](https://docs.djangoproject.com/en/5.2/howto/custom-template-backend/#origin-api-and-3rd-party-integration).
name (Type[str], optional): Sets `Template.name`
engine (Type[Any], optional): Sets `Template.engine`
@@ -64,3 +76,403 @@ def cached_template(
template = maybe_cached_template
return template
+
+
+########################################################
+# PREPARING COMPONENT TEMPLATES FOR RENDERING
+########################################################
+
+
+@contextmanager
+def prepare_component_template(
+ component: "Component",
+ template_data: Any,
+) -> Generator[Optional[Template], Any, None]:
+ context = component.context
+ with context.update(template_data):
+ template = _get_component_template(component)
+
+ if template is None:
+ # If template is None, then the component is "template-less",
+ # and we skip template processing.
+ yield template
+ return
+
+ if not is_cls_patched(template):
+ raise RuntimeError(
+ "Django-components received a Template instance which was not patched."
+ "If you are using Django's Template class, check if you added django-components"
+ "to INSTALLED_APPS. If you are using a custom template class, then you need to"
+ "manually patch the class."
+ )
+
+ with _maybe_bind_template(context, template):
+ yield template
+
+
+# `_maybe_bind_template()` handles two problems:
+#
+# 1. Initially, the binding the template was needed for the context processor data
+# to work when using `RequestContext` (See `RequestContext.bind_template()` in e.g. Django v4.2 or v5.1).
+# But as of djc v0.140 (possibly earlier) we generate and apply the context processor data
+# ourselves in `Component._render_impl()`.
+#
+# Now, we still want to "bind the template" by setting the `Context.template` attribute.
+# This is for compatibility with Django, because we don't know if there isn't some code that relies
+# on the `Context.template` attribute being set.
+#
+# But we don't call `context.bind_template()` explicitly. If we did, then we would
+# be generating and applying the context processor data twice if the context was `RequestContext`.
+# Instead, we only run the same logic as `Context.bind_template()` but inlined.
+#
+# The downstream effect of this is that if the user or some third-party library
+# uses custom subclass of `Context` with custom logic for `Context.bind_template()`,
+# then this custom logic will NOT be applied. In such case they should open an issue.
+#
+# See https://github.com/django-components/django-components/issues/580
+# and https://github.com/django-components/django-components/issues/634
+#
+# 2. Not sure if I (Juro) remember right, but I think that with the binding of templates
+# there was also an issue that in *some* cases the template was already bound to the context
+# by the time we got to rendering the component. This is why we need to check if `context.template`
+# is already set.
+#
+# The cause of this may have been compatibility with Django's `{% extends %}` tag, or
+# maybe when using the "isolated" context behavior. But not sure.
+@contextmanager
+def _maybe_bind_template(context: Context, template: Template) -> Generator[None, Any, None]:
+ if context.template is not None:
+ yield
+ return
+
+ # This code is taken from `Context.bind_template()` from Django v5.1
+ context.template = template
+ try:
+ yield
+ finally:
+ context.template = None
+
+
+########################################################
+# LOADING TEMPLATES FROM FILEPATH
+########################################################
+
+
+# Remember which Component class is currently being loaded
+# This is important, because multiple Components may define the same `template_file`.
+# So we need this global state to help us decide which Component class of the list of components
+# that matched for the given `template_file` should be associated with the template.
+#
+# NOTE: Implemented as a list (stack) to handle the case when calling Django's `get_template()`
+# could lead to more components being loaded at once.
+# (For this to happen, user would have to define a Django template loader that renders other components
+# while resolving the template file.)
+loading_components: List["ComponentRef"] = []
+
+
+def load_component_template(
+ component_cls: Type["Component"],
+ filepath: Optional[str] = None,
+ content: Optional[str] = None,
+) -> Template:
+ if filepath is None and content is None:
+ raise ValueError("Either `filepath` or `content` must be provided.")
+
+ loading_components.append(ref(component_cls))
+
+ if filepath is not None:
+ # Use Django's `get_template()` to load the template file
+ template = _load_django_template(filepath)
+ template = ensure_unique_template(component_cls, template)
+
+ elif content is not None:
+ template = _create_template_from_string(component_cls, content, is_component_template=True)
+ else:
+ raise ValueError("Received both `filepath` and `content`. These are mutually exclusive.")
+
+ loading_components.pop()
+
+ return template
+
+
+# When loading a Template instance, it may be cached by Django / template loaders.
+# In that case we want to make a copy of the template which would
+# be owned by the current Component class.
+# Thus each Component has it's own Template instance with their own Origins
+# pointing to the correct Component class.
+def ensure_unique_template(component_cls: Type["Component"], template: Template) -> Template:
+ # Use `template.origin.component_cls` to check if the template was cached by Django / template loaders.
+ if get_component_from_origin(template.origin) is None:
+ set_component_to_origin(template.origin, component_cls)
+ else:
+ origin_copy = Origin(template.origin.name, template.origin.template_name, template.origin.loader)
+ set_component_to_origin(origin_copy, component_cls)
+ template = Template(template.source, origin=origin_copy, name=template.name, engine=template.engine)
+
+ return template
+
+
+def _get_component_template(component: "Component") -> Optional[Template]:
+ trace_component_msg("COMP_LOAD", component_name=component.name, component_id=component.id, slot_name=None)
+
+ # TODO_V1 - Remove, not needed once we remove `get_template_string()`, `get_template_name()`, `get_template()`
+ template_sources: Dict[str, Optional[Union[str, Template]]] = {}
+
+ # TODO_V1 - Remove `get_template_name()` in v1
+ template_sources["get_template_name"] = component.get_template_name(component.context)
+
+ # TODO_V1 - Remove `get_template_string()` in v1
+ if hasattr(component, "get_template_string"):
+ template_string_getter = getattr(component, "get_template_string")
+ template_body_from_getter = template_string_getter(component.context)
+ else:
+ template_body_from_getter = None
+ template_sources["get_template_string"] = template_body_from_getter
+
+ # TODO_V1 - Remove `get_template()` in v1
+ template_sources["get_template"] = component.get_template(component.context)
+
+ # NOTE: `component.template` should be populated whether user has set `template` or `template_file`
+ # so we discern between the two cases by checking `component.template_file`
+ if component.template_file is not None:
+ template_sources["template_file"] = component.template_file
+ else:
+ template_sources["template"] = component.template
+
+ # TODO_V1 - Remove this check in v1
+ # Raise if there are multiple sources for the component template
+ sources_with_values = [k for k, v in template_sources.items() if v is not None]
+ if len(sources_with_values) > 1:
+ raise ImproperlyConfigured(
+ f"Component template was set multiple times in Component {component.name}."
+ f"Sources: {sources_with_values}"
+ )
+
+ # Load the template based on the source
+ if template_sources["get_template_name"]:
+ template_name = template_sources["get_template_name"]
+ template: Optional[Template] = _load_django_template(template_name)
+ template_string: Optional[str] = None
+ elif template_sources["get_template_string"]:
+ template_string = template_sources["get_template_string"]
+ template = None
+ elif template_sources["get_template"]:
+ # `Component.get_template()` returns either string or Template instance
+ if hasattr(template_sources["get_template"], "render"):
+ template = template_sources["get_template"]
+ template_string = None
+ else:
+ template = None
+ template_string = template_sources["get_template"]
+ elif component.template or component.template_file:
+ # If the template was loaded from `Component.template` or `Component.template_file`,
+ # then the Template instance was already created and cached in `Component._template`.
+ #
+ # NOTE: This is important to keep in mind, because the implication is that we should
+ # treat Templates AND their nodelists as IMMUTABLE.
+ template = component.__class__._component_media._template # type: ignore[attr-defined]
+ template_string = None
+ # No template
+ else:
+ template = None
+ template_string = None
+
+ # We already have a template instance, so we can return it
+ if template is not None:
+ return template
+ # Create the template from the string
+ elif template_string is not None:
+ return _create_template_from_string(component.__class__, template_string)
+
+ # Otherwise, Component has no template - this is valid, as it may be instead rendered
+ # via `Component.on_render()`
+ return None
+
+
+def _create_template_from_string(
+ component: Type["Component"],
+ template_string: str,
+ is_component_template: bool = False,
+) -> Template:
+ # Generate a valid Origin instance.
+ # When an Origin instance is created by Django when using Django's loaders, it looks like this:
+ # ```
+ # {
+ # 'name': '/path/to/project/django-components/sampleproject/calendarapp/templates/calendarapp/calendar.html',
+ # 'template_name': 'calendarapp/calendar.html',
+ # 'loader':
+ # }
+ # ```
+ #
+ # Since our template is inlined, we will format as `filepath::ComponentName`
+ #
+ # ```
+ # /path/to/project/django-components/src/calendarapp/calendar.html::Calendar
+ # ```
+ #
+ # See https://docs.djangoproject.com/en/5.2/howto/custom-template-backend/#template-origin-api
+ _, _, module_filepath = get_module_info(component)
+ origin = Origin(
+ name=f"{module_filepath}::{component.__name__}",
+ template_name=None,
+ loader=None,
+ )
+
+ set_component_to_origin(origin, component)
+
+ if is_component_template:
+ template = Template(template_string, name=origin.template_name, origin=origin)
+ else:
+ # TODO_V1 - `cached_template()` won't be needed as there will be only 1 template per component
+ # so we will be able to instead use `template_cache` to store the template
+ template = cached_template(
+ template_string=template_string,
+ name=origin.template_name,
+ origin=origin,
+ )
+
+ return template
+
+
+# When loading a template, use Django's `get_template()` to ensure it triggers Django template loaders
+# See https://github.com/django-components/django-components/issues/901
+#
+# This may raise `TemplateDoesNotExist` if the template doesn't exist.
+# See https://docs.djangoproject.com/en/5.2/ref/templates/api/#template-loaders
+# And https://docs.djangoproject.com/en/5.2/ref/templates/api/#custom-template-loaders
+#
+# TODO_v3 - Instead of loading templates with Django's `get_template()`,
+# we should simply read the files directly (same as we do for JS and CSS).
+# This has the implications that:
+# - We would no longer support Django's template loaders
+# - Instead if users are using template loaders, they should re-create them as djc extensions
+# - We would no longer need to set `TEMPLATES.OPTIONS.loaders` to include
+# `django_components.template_loader.Loader`
+def _load_django_template(template_name: str) -> Template:
+ return django_get_template(template_name).template
+
+
+########################################################
+# ASSOCIATING COMPONENT CLASSES WITH TEMPLATES
+#
+# See https://github.com/django-components/django-components/pull/1222
+########################################################
+
+# NOTE: `ReferenceType` is NOT a generic pre-3.9
+if sys.version_info >= (3, 9):
+ ComponentRef = ReferenceType[Type["Component"]]
+else:
+ ComponentRef = ReferenceType
+
+
+# Remember which Component classes defined `template_file`. Since multiple Components may
+# define the same `template_file`, we store a list of weak references to the Component classes.
+component_template_file_cache: Dict[str, List[ComponentRef]] = {}
+component_template_file_cache_initialized = False
+
+
+# Remember the mapping of `Component.template_file` -> `Component` class, so that we can associate
+# the `Template` instances with the correct Component class in our monkepatched `Template.__init__()`.
+def cache_component_template_file(component_cls: Type["Component"]) -> None:
+ # When a Component class is created before Django is set up,
+ # then `component_template_file_cache_initialized` is False and we leave it for later.
+ # This is necessary because:
+ # 1. We might need to resolve the template_file as relative to the file where the Component class is defined.
+ # 2. To be able to resolve the template_file, Django needs to be set up, because we need to access Django settings.
+ # 3. Django settings may not be available at the time of Component class creation.
+ if not component_template_file_cache_initialized:
+ return
+
+ # NOTE: Avoids circular import
+ from django_components.component_media import ComponentMedia, Unset, _resolve_component_relative_files, is_set
+
+ # If we access the `Component.template_file` attribute, then this triggers media resolution if it was not done yet.
+ # The problem is that this also causes the loading of the Template, if Component has defined `template_file`.
+ # This triggers `Template.__init__()`, which then triggers another call to `cache_component_template_file()`.
+ #
+ # At the same time, at this point we don't need the media files to be loaded. But we DO need for the relative
+ # file path to be resolved.
+ #
+ # So for this reason, `ComponentMedia.resolved_relative_files` was added to track if the media files were resolved.
+ # Once relative files were resolved, we can safely access the template file from `ComponentMedia` instance
+ # directly, thus avoiding the triggering of the Template loading.
+ comp_media: ComponentMedia = component_cls._component_media # type: ignore[attr-defined]
+ if comp_media.resolved and comp_media.resolved_relative_files:
+ template_file: Union[str, Unset, None] = component_cls.template_file
+ else:
+ # NOTE: This block of code is based on `_resolve_media()` in `component_media.py`
+ if not comp_media.resolved_relative_files:
+ comp_dirs = get_component_dirs()
+ _resolve_component_relative_files(component_cls, comp_media, comp_dirs=comp_dirs)
+
+ template_file = comp_media.template_file
+
+ if not is_set(template_file):
+ return
+
+ if template_file not in component_template_file_cache:
+ component_template_file_cache[template_file] = []
+
+ component_template_file_cache[template_file].append(ref(component_cls))
+
+
+def get_component_by_template_file(template_file: str) -> Optional[Type["Component"]]:
+ # This function is called from within `Template.__init__()`. At that point, Django MUST be already set up,
+ # because Django's `Template.__init__()` accesses the templating engines.
+ #
+ # So at this point we want to call `cache_component_template_file()` for all Components for which
+ # we skipped it earlier.
+ global component_template_file_cache_initialized
+ if not component_template_file_cache_initialized:
+ component_template_file_cache_initialized = True
+
+ # NOTE: Avoids circular import
+ from django_components.component import all_components
+
+ components = all_components()
+ for component in components:
+ cache_component_template_file(component)
+
+ if template_file not in component_template_file_cache or not len(component_template_file_cache[template_file]):
+ return None
+
+ # There is at least one Component class that has this `template_file`.
+ matched_component_refs = component_template_file_cache[template_file]
+
+ # There may be multiple components that define the same `template_file`.
+ # So to find the correct one, we need to check if the currently loading component
+ # is one of the ones that define the `template_file`.
+ #
+ # If there are NO currently loading components, then `Template.__init__()` was NOT triggered by us,
+ # in which case we don't associate any Component class with this Template.
+ if not len(loading_components):
+ return None
+
+ loading_component = loading_components[-1]()
+ if loading_component is None:
+ return None
+
+ for component_ref in matched_component_refs:
+ comp_cls = component_ref()
+ if comp_cls is loading_component:
+ return comp_cls
+
+ return None
+
+
+# NOTE: Used by `@djc_test` to reset the component template file cache
+def _reset_component_template_file_cache() -> None:
+ global component_template_file_cache
+ component_template_file_cache = {}
+
+ global component_template_file_cache_initialized
+ component_template_file_cache_initialized = False
+
+
+# Helpers so we know where in the codebase we set / access the `Origin.component_cls` attribute
+def set_component_to_origin(origin: Origin, component_cls: Type["Component"]) -> None:
+ origin.component_cls = component_cls
+
+
+def get_component_from_origin(origin: Origin) -> Optional[Type["Component"]]:
+ return getattr(origin, "component_cls", None)
diff --git a/src/django_components/template_loader.py b/src/django_components/template_loader.py
index b6d48097..4545ffda 100644
--- a/src/django_components/template_loader.py
+++ b/src/django_components/template_loader.py
@@ -10,7 +10,7 @@ from django.template.loaders.filesystem import Loader as FilesystemLoader
from django_components.util.loader import get_component_dirs
-class Loader(FilesystemLoader):
+class DjcLoader(FilesystemLoader):
def get_dirs(self, include_apps: bool = True) -> List[Path]:
"""
Prepare directories that may contain component files:
@@ -26,3 +26,10 @@ class Loader(FilesystemLoader):
`BASE_DIR` setting is required.
"""
return get_component_dirs(include_apps)
+
+
+# NOTE: Django's template loaders have the pattern of using the `Loader` class name.
+# However, this then makes it harder to track and distinguish between different loaders.
+# So internally we use the name `DjcLoader` instead.
+# But for public API we use the name `Loader` to match Django.
+Loader = DjcLoader
diff --git a/src/django_components/templatetags/component_tags.py b/src/django_components/templatetags/component_tags.py
index bc4868d5..e3f2117e 100644
--- a/src/django_components/templatetags/component_tags.py
+++ b/src/django_components/templatetags/component_tags.py
@@ -7,7 +7,7 @@ from django_components.provide import ProvideNode
from django_components.slots import FillNode, SlotNode
# NOTE: Variable name `register` is required by Django to recognize this as a template tag library
-# See https://docs.djangoproject.com/en/dev/howto/custom-template-tags
+# See https://docs.djangoproject.com/en/5.2/howto/custom-template-tags
register = django.template.Library()
diff --git a/src/django_components/types.py b/src/django_components/types.py
index b7a55ab8..61e38ccb 100644
--- a/src/django_components/types.py
+++ b/src/django_components/types.py
@@ -1,6 +1,6 @@
"""Helper types for IDEs."""
-from django_components.util.types import Annotated
+from typing_extensions import Annotated
css = Annotated[str, "css"]
django_html = Annotated[str, "django_html"]
diff --git a/src/django_components/util/command.py b/src/django_components/util/command.py
index e04f8ac0..e4de943c 100644
--- a/src/django_components/util/command.py
+++ b/src/django_components/util/command.py
@@ -1,11 +1,35 @@
import sys
from argparse import Action, ArgumentParser
from dataclasses import asdict, dataclass
-from typing import TYPE_CHECKING, Any, Callable, Dict, List, Literal, Optional, Protocol, Sequence, Type, Union
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Callable,
+ Dict,
+ List,
+ Literal,
+ Optional,
+ Protocol,
+ Sequence,
+ Type,
+ TypeVar,
+ Union,
+)
if TYPE_CHECKING:
from argparse import _ArgumentGroup, _FormatterClass
+
+TClass = TypeVar("TClass", bound=Type[Any])
+
+
+# Mark object as related to extension commands so we can place these in
+# a separate documentation section
+def mark_extension_command_api(obj: TClass) -> TClass:
+ obj._extension_command_api = True
+ return obj
+
+
#############################
# Argparse typing
#############################
@@ -19,8 +43,10 @@ The basic type of action to be taken when this argument is encountered at the co
This is a subset of the values for `action` in
[`ArgumentParser.add_argument()`](https://docs.python.org/3/library/argparse.html#the-add-argument-method).
"""
+mark_extension_command_api(CommandLiteralAction) # type: ignore
+@mark_extension_command_api
@dataclass
class CommandArg:
"""
@@ -77,6 +103,7 @@ class CommandArg:
return _remove_none_values(asdict(self))
+@mark_extension_command_api
@dataclass
class CommandArgGroup:
"""
@@ -102,6 +129,7 @@ class CommandArgGroup:
return _remove_none_values(asdict(self))
+@mark_extension_command_api
@dataclass
class CommandSubcommand:
"""
@@ -158,6 +186,7 @@ class CommandSubcommand:
return _remove_none_values(asdict(self))
+@mark_extension_command_api
@dataclass
class CommandParserInput:
"""
@@ -203,16 +232,18 @@ class CommandParserInput:
#############################
+@mark_extension_command_api
class CommandHandler(Protocol):
def __call__(self, *args: Any, **kwargs: Any) -> None: ... # noqa: E704
+@mark_extension_command_api
class ComponentCommand:
"""
Definition of a CLI command.
This class is based on Python's [`argparse`](https://docs.python.org/3/library/argparse.html)
- module and Django's [`BaseCommand`](https://docs.djangoproject.com/en/5.1/howto/custom-management-commands/)
+ module and Django's [`BaseCommand`](https://docs.djangoproject.com/en/5.2/howto/custom-management-commands/)
class. `ComponentCommand` allows you to define:
- Command name, description, and help text
diff --git a/src/django_components/util/component_highlight.py b/src/django_components/util/component_highlight.py
deleted file mode 100644
index 7e9adae0..00000000
--- a/src/django_components/util/component_highlight.py
+++ /dev/null
@@ -1,43 +0,0 @@
-from typing import Literal, NamedTuple
-
-from django_components.util.misc import gen_id
-
-
-class HighlightColor(NamedTuple):
- text_color: str
- border_color: str
-
-
-COLORS = {
- "component": HighlightColor(text_color="#2f14bb", border_color="blue"),
- "slot": HighlightColor(text_color="#bb1414", border_color="#e40c0c"),
-}
-
-
-def apply_component_highlight(type: Literal["component", "slot"], output: str, name: str) -> str:
- """
- Wrap HTML (string) in a div with a border and a highlight color.
-
- This is part of the component / slot highlighting feature. User can toggle on
- to see the component / slot boundaries.
- """
- color = COLORS[type]
-
- # Because the component / slot name is set via styling as a `::before` pseudo-element,
- # we need to generate a unique ID for each component / slot to avoid conflicts.
- highlight_id = gen_id()
-
- output = f"""
-
-
- {output}
-
- """
-
- return output
diff --git a/src/django_components/util/context.py b/src/django_components/util/context.py
index 43a54bad..71e33488 100644
--- a/src/django_components/util/context.py
+++ b/src/django_components/util/context.py
@@ -131,7 +131,7 @@ def _copy_block_context(block_context: BlockContext) -> BlockContext:
# See https://github.com/django/django/blame/2d34ebe49a25d0974392583d5bbd954baf742a32/django/template/context.py#L255
def gen_context_processors_data(context: BaseContext, request: HttpRequest) -> Dict[str, Any]:
if request in context_processors_data:
- return context_processors_data[request]
+ return context_processors_data[request].copy()
# TODO_REMOVE_IN_V2 - In v2, if we still support context processors,
# it should be set on our settings, so we wouldn't have to get the Engine for that.
@@ -153,4 +153,6 @@ def gen_context_processors_data(context: BaseContext, request: HttpRequest) -> D
except TypeError as e:
raise TypeError(f"Context processor {processor.__qualname__} didn't return a " "dictionary.") from e
+ context_processors_data[request] = processors_data
+
return processors_data
diff --git a/src/django_components/util/django_monkeypatch.py b/src/django_components/util/django_monkeypatch.py
index 1e2bdafa..722eba02 100644
--- a/src/django_components/util/django_monkeypatch.py
+++ b/src/django_components/util/django_monkeypatch.py
@@ -1,18 +1,92 @@
-from typing import Any, Type
+from typing import Any, Optional, Type
from django.template import Context, NodeList, Template
-from django.template.base import Parser
+from django.template.base import Node, Origin, Parser
+from django.template.loader_tags import IncludeNode
+from django_components.context import _COMPONENT_CONTEXT_KEY, _STRATEGY_CONTEXT_KEY, COMPONENT_IS_NESTED_KEY
+from django_components.dependencies import COMPONENT_COMMENT_REGEX, render_dependencies
+from django_components.extension import OnTemplateCompiledContext, OnTemplateLoadedContext, extensions
from django_components.util.template_parser import parse_template
# In some cases we can't work around Django's design, and need to patch the template class.
def monkeypatch_template_cls(template_cls: Type[Template]) -> None:
+ if is_cls_patched(template_cls):
+ return
+
+ monkeypatch_template_init(template_cls)
monkeypatch_template_compile_nodelist(template_cls)
monkeypatch_template_render(template_cls)
template_cls._djc_patched = True
+# Patch `Template.__init__` to apply `on_template_loaded()` and `on_template_compiled()`
+# extension hooks if the template belongs to a Component.
+def monkeypatch_template_init(template_cls: Type[Template]) -> None:
+ original_init = template_cls.__init__
+
+ # NOTE: Function signature of Template.__init__ hasn't changed in 11 years, so we can safely patch it.
+ # See https://github.com/django/django/blame/main/django/template/base.py#L139
+ def __init__(
+ self: Template,
+ template_string: Any,
+ origin: Optional[Origin] = None,
+ name: Optional[str] = None,
+ *args: Any,
+ **kwargs: Any,
+ ) -> None:
+ # NOTE: Avoids circular import
+ from django_components.template import (
+ get_component_by_template_file,
+ get_component_from_origin,
+ set_component_to_origin,
+ )
+
+ # If this Template instance was created by us when loading a template file for a component
+ # with `load_component_template()`, then we do 2 things:
+ #
+ # 1. Associate the Component class with the template by setting it on the `Origin` instance
+ # (`template.origin.component_cls`). This way the `{% component%}` and `{% slot %}` tags
+ # will know inside which Component class they were defined.
+ #
+ # 2. Apply `extensions.on_template_preprocess()` to the template, so extensions can modify
+ # the template string before it's compiled into a nodelist.
+ if get_component_from_origin(origin) is not None:
+ component_cls = get_component_from_origin(origin)
+ elif origin is not None and origin.template_name is not None:
+ component_cls = get_component_by_template_file(origin.template_name)
+ if component_cls is not None:
+ set_component_to_origin(origin, component_cls)
+ else:
+ component_cls = None
+
+ if component_cls is not None:
+ template_string = str(template_string)
+ template_string = extensions.on_template_loaded(
+ OnTemplateLoadedContext(
+ component_cls=component_cls,
+ content=template_string,
+ origin=origin,
+ name=name,
+ )
+ )
+
+ # Calling original `Template.__init__` should also compile the template into a Nodelist
+ # via `Template.compile_nodelist()`.
+ original_init(self, template_string, origin, name, *args, **kwargs) # type: ignore[misc]
+
+ if component_cls is not None:
+ extensions.on_template_compiled(
+ OnTemplateCompiledContext(
+ component_cls=component_cls,
+ template=self,
+ )
+ )
+
+ template_cls.__init__ = __init__
+
+
# Patch `Template.compile_nodelist` to use our custom parser. Our parser makes it possible
# to use template tags as inputs to the component tag:
#
@@ -58,7 +132,7 @@ def monkeypatch_template_compile_nodelist(template_cls: Type[Template]) -> None:
def monkeypatch_template_render(template_cls: Type[Template]) -> None:
# Modify `Template.render` to set `isolated_context` kwarg of `push_state`
- # based on our custom `Template._djc_is_component_nested`.
+ # based on our custom `_DJC_COMPONENT_IS_NESTED`.
#
# Part of fix for https://github.com/django-components/django-components/issues/508
#
@@ -73,38 +147,101 @@ def monkeypatch_template_render(template_cls: Type[Template]) -> None:
# doesn't require the source to be parsed multiple times. User can pass extra args/kwargs,
# and can modify the rendering behavior by overriding the `_render` method.
#
- # NOTE 2: Instead of setting `Template._djc_is_component_nested`, alternatively we could
+ # NOTE 2: Instead of setting `_DJC_COMPONENT_IS_NESTED` context key, alternatively we could
# have passed the value to `monkeypatch_template_render` directly. However, we intentionally
# did NOT do that, so the monkey-patched method is more robust, and can be e.g. copied
# to other.
- if is_template_cls_patched(template_cls):
+ if is_cls_patched(template_cls):
# Do not patch if done so already. This helps us avoid RecursionError
return
+ # NOTE: This implementation is based on Django v5.1.3)
def _template_render(self: Template, context: Context, *args: Any, **kwargs: Any) -> str:
"Display stage -- can be called many times"
- # ---------------- ORIGINAL (Django v5.1.3) ----------------
- # with context.render_context.push_state(self):
- # ---------------- OUR CHANGES START ----------------
# We parametrized `isolated_context`, which was `True` in the original method.
- if not hasattr(self, "_djc_is_component_nested"):
+ if COMPONENT_IS_NESTED_KEY not in context:
isolated_context = True
else:
# MUST be `True` for templates that are NOT import with `{% extends %}` tag,
# and `False` otherwise.
- isolated_context = not self._djc_is_component_nested
+ isolated_context = not context[COMPONENT_IS_NESTED_KEY]
+ # This is original implementation, except we override `isolated_context`,
+ # and we post-process the result with `render_dependencies()`.
with context.render_context.push_state(self, isolated_context=isolated_context):
- # ---------------- OUR CHANGES END ----------------
if context.template is None:
with context.bind_template(self):
context.template_name = self.name
- return self._render(context, *args, **kwargs)
+ result: str = self._render(context, *args, **kwargs)
else:
- return self._render(context, *args, **kwargs)
+ result = self._render(context, *args, **kwargs)
+
+ # If the key is present, that means this Template is rendered as part of `Component.render()`
+ # or `{% component %}`. In that case the parent component will take care of rendering the
+ # dependencies, so we don't need to do that here.
+ if _COMPONENT_CONTEXT_KEY in context:
+ return result
+
+ # NOTE: Only process dependencies if the rendered result contains AT LEAST ONE rendered component.
+ # This has two reasons:
+ # 1. To keep the behavior consistent with the previous implementation, when `Template.render()`
+ # didn't call `render_dependencies()`.
+ # 2. To avoid unnecessary processing which otherwise has a considerable perf overhead.
+ # See https://github.com/django-components/django-components/pull/1166#issuecomment-2850899765
+ if not COMPONENT_COMMENT_REGEX.search(result.encode("utf-8")):
+ return result
+
+ # Allow users to configure the `deps_strategy` kwarg of `render_dependencies()`, even if
+ # they render a Template directly with `Template.render()` or Django's `django.shortcuts.render()`.
+ #
+ # Example:
+ # ```
+ # result = render_dependencies(
+ # result,
+ # Context({ "DJC_DEPS_STRATEGY": "fragment" }),
+ # )
+ # ```
+ if _STRATEGY_CONTEXT_KEY in context and context[_STRATEGY_CONTEXT_KEY] is not None:
+ strategy = context[_STRATEGY_CONTEXT_KEY]
+ result = render_dependencies(result, strategy)
+ else:
+ result = render_dependencies(result)
+ return result
template_cls.render = _template_render
-def is_template_cls_patched(template_cls: Type[Template]) -> bool:
- return getattr(template_cls, "_djc_patched", False)
+def monkeypatch_include_node(include_node_cls: Type[Node]) -> None:
+ if is_cls_patched(include_node_cls):
+ return
+
+ monkeypatch_include_render(include_node_cls)
+ include_node_cls._djc_patched = True
+
+
+def monkeypatch_include_render(include_node_cls: Type[Node]) -> None:
+ # Modify `IncludeNode.render()` (what renders `{% include %}` tag) so that the included
+ # template does NOT render the JS/CSS by itself.
+ #
+ # Instead, we want the parent template
+ # (which contains the `{% component %}` tag) to decide whether to render the JS/CSS.
+ #
+ # We achieve this by setting `DJC_DEPS_STRATEGY` to `ignore` in the context.
+ #
+ # Fix for https://github.com/django-components/django-components/issues/1296
+ if is_cls_patched(include_node_cls):
+ # Do not patch if done so already. This helps us avoid RecursionError
+ return
+
+ orig_include_render = include_node_cls.render
+
+ # NOTE: This implementation is based on Django v5.1.3)
+ def _include_render(self: IncludeNode, context: Context, *args: Any, **kwargs: Any) -> str:
+ with context.update({_STRATEGY_CONTEXT_KEY: "ignore"}):
+ return orig_include_render(self, context, *args, **kwargs)
+
+ include_node_cls.render = _include_render
+
+
+def is_cls_patched(cls: Type[Any]) -> bool:
+ return getattr(cls, "_djc_patched", False)
diff --git a/src/django_components/util/exception.py b/src/django_components/util/exception.py
index b7dbb97b..dc57b7c3 100644
--- a/src/django_components/util/exception.py
+++ b/src/django_components/util/exception.py
@@ -25,7 +25,7 @@ def component_error_message(component_path: List[str]) -> Generator[None, None,
if not components:
orig_msg = str(err.args[0])
else:
- orig_msg = err.args[0].split("\n", 1)[-1]
+ orig_msg = str(err.args[0]).split("\n", 1)[-1]
else:
orig_msg = str(err)
diff --git a/src/django_components/util/loader.py b/src/django_components/util/loader.py
index f99bf122..00e646a5 100644
--- a/src/django_components/util/loader.py
+++ b/src/django_components/util/loader.py
@@ -87,7 +87,7 @@ def get_component_dirs(include_apps: bool = True) -> List[Path]:
# 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
+ # See https://docs.djangoproject.com/en/5.2/ref/settings/#prefixes-optional
if isinstance(component_dir, (tuple, list)):
component_dir = component_dir[1]
try:
diff --git a/src/django_components/util/misc.py b/src/django_components/util/misc.py
index 1ac8698f..def9d6ee 100644
--- a/src/django_components/util/misc.py
+++ b/src/django_components/util/misc.py
@@ -1,17 +1,21 @@
import re
import sys
+from dataclasses import asdict, is_dataclass
from hashlib import md5
from importlib import import_module
from itertools import chain
from types import ModuleType
-from typing import TYPE_CHECKING, Any, Callable, Iterable, List, Optional, Tuple, Type, TypeVar, Union
+from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, Tuple, Type, TypeVar, Union, cast
+from urllib import parse
+from django_components.constants import UID_LENGTH
from django_components.util.nanoid import generate
if TYPE_CHECKING:
from django_components.component import Component
T = TypeVar("T")
+U = TypeVar("U")
# Based on nanoid implementation from
@@ -24,7 +28,7 @@ def gen_id() -> str:
# See https://zelark.github.io/nano-id-cc/
return generate(
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
- size=6,
+ size=UID_LENGTH,
)
@@ -88,8 +92,13 @@ def get_module_info(
return module, module_name, module_file_path
-def default(val: Optional[T], default: T) -> T:
- return val if val is not None else default
+def default(val: Optional[T], default: Union[U, Callable[[], U], Type[T]], factory: bool = False) -> Union[T, U]:
+ if val is not None:
+ return val
+ if factory:
+ default_func = cast(Callable[[], U], default)
+ return default_func()
+ return cast(U, default)
def get_index(lst: List, key: Callable[[Any], bool]) -> Optional[int]:
@@ -112,10 +121,11 @@ def is_nonempty_str(txt: Optional[str]) -> bool:
return txt is not None and bool(txt.strip())
+# Convert Component class to something like `TableComp_a91d03`
def hash_comp_cls(comp_cls: Type["Component"]) -> str:
full_name = get_import_path(comp_cls)
- comp_cls_hash = md5(full_name.encode()).hexdigest()[0:6]
- return comp_cls.__name__ + "_" + comp_cls_hash
+ name_hash = md5(full_name.encode()).hexdigest()[0:6]
+ return comp_cls.__name__ + "_" + name_hash
# String is a glob if it contains at least one of `?`, `*`, or `[`
@@ -128,3 +138,91 @@ def is_glob(filepath: str) -> bool:
def flatten(lst: Iterable[Iterable[T]]) -> List[T]:
return list(chain.from_iterable(lst))
+
+
+def to_dict(data: Any) -> dict:
+ """
+ Convert object to a dict.
+
+ Handles `dict`, `NamedTuple`, and `dataclass`.
+ """
+ if isinstance(data, dict):
+ return data
+ elif hasattr(data, "_asdict"): # Case: NamedTuple
+ return data._asdict()
+ elif is_dataclass(data): # Case: dataclass
+ return asdict(data) # type: ignore[arg-type]
+
+ return dict(data)
+
+
+def format_url(url: str, query: Optional[Dict] = None, fragment: Optional[str] = None) -> str:
+ """
+ Given a URL, add to it query parameters and a fragment, returning an updated URL.
+
+ ```py
+ url = format_url(url="https://example.com", query={"foo": "bar"}, fragment="baz")
+ # https://example.com?foo=bar#baz
+ ```
+
+ `query` and `fragment` are optional, and not applied if `None`.
+ """
+ parts = parse.urlsplit(url)
+ fragment_enc = parse.quote(fragment or parts.fragment, safe="")
+ base_qs = dict(parse.parse_qsl(parts.query))
+ merged = {**base_qs, **(query or {})}
+ encoded_qs = parse.urlencode(merged, safe="")
+
+ return parse.urlunsplit(parts._replace(query=encoded_qs, fragment=fragment_enc))
+
+
+def format_as_ascii_table(data: List[Dict[str, Any]], headers: List[str], include_headers: bool = True) -> str:
+ """
+ Format a list of dictionaries as an ASCII table.
+
+ Example:
+
+ ```python
+ data = [
+ {"name": "ProjectPage", "full_name": "project.pages.project.ProjectPage", "path": "./project/pages/project"},
+ {"name": "ProjectDashboard", "full_name": "project.components.dashboard.ProjectDashboard", "path": "./project/components/dashboard"},
+ {"name": "ProjectDashboardAction", "full_name": "project.components.dashboard_action.ProjectDashboardAction", "path": "./project/components/dashboard_action"},
+ ]
+ headers = ["name", "full_name", "path"]
+ print(format_as_ascii_table(data, headers))
+ ```
+
+ Which prints:
+
+ ```txt
+ name full_name path
+ ==================================================================================================
+ ProjectPage project.pages.project.ProjectPage ./project/pages/project
+ ProjectDashboard project.components.dashboard.ProjectDashboard ./project/components/dashboard
+ ProjectDashboardAction project.components.dashboard_action.ProjectDashboardAction ./project/components/dashboard_action
+ ```
+ """ # noqa: E501
+ # Calculate the width of each column
+ column_widths = {header: len(header) for header in headers}
+ for row in data:
+ for header in headers:
+ row_value = str(row.get(header, ""))
+ column_widths[header] = max(column_widths[header], len(row_value))
+
+ # Create the header row
+ header_row = " ".join(f"{header:<{column_widths[header]}}" for header in headers)
+ separator = "=" * len(header_row)
+
+ # Create the data rows
+ data_rows = []
+ for row in data:
+ row_values = [str(row.get(header, "")) for header in headers]
+ data_row = " ".join(f"{value:<{column_widths[header]}}" for value, header in zip(row_values, headers))
+ data_rows.append(data_row)
+
+ # Combine all parts into the final table
+ if include_headers:
+ table = "\n".join([header_row, separator] + data_rows)
+ else:
+ table = "\n".join(data_rows)
+ return table
diff --git a/src/django_components/util/routing.py b/src/django_components/util/routing.py
index 470018d3..c8da0d4b 100644
--- a/src/django_components/util/routing.py
+++ b/src/django_components/util/routing.py
@@ -1,20 +1,31 @@
from dataclasses import dataclass, field
-from typing import Any, Dict, List, Optional, Protocol
+from typing import Any, Dict, Iterable, Optional, Protocol, Type, TypeVar
+
+TClass = TypeVar("TClass", bound=Type[Any])
+# Mark object as related to extension URLs so we can place these in
+# a separate documentation section
+def mark_extension_url_api(obj: TClass) -> TClass:
+ obj._extension_url_api = True
+ return obj
+
+
+@mark_extension_url_api
class URLRouteHandler(Protocol):
"""Framework-agnostic 'view' function for routes"""
def __call__(self, request: Any, *args: Any, **kwargs: Any) -> Any: ... # noqa: E704
+@mark_extension_url_api
@dataclass
class URLRoute:
"""
Framework-agnostic route definition.
This is similar to Django's `URLPattern` object created with
- [`django.urls.path()`](https://docs.djangoproject.com/en/5.1/ref/urls/#path).
+ [`django.urls.path()`](https://docs.djangoproject.com/en/5.2/ref/urls/#path).
The `URLRoute` must either define a `handler` function or have a list of child routes `children`.
If both are defined, an error will be raised.
@@ -53,10 +64,14 @@ class URLRoute:
path: str
handler: Optional[URLRouteHandler] = None
- children: List["URLRoute"] = field(default_factory=list)
+ children: Iterable["URLRoute"] = field(default_factory=list)
name: Optional[str] = None
extra: Dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
if self.handler is not None and self.children:
raise ValueError("Cannot have both handler and children")
+
+ # Allow to use `URLRoute` objects in sets and dictionaries
+ def __hash__(self) -> int:
+ return hash(self.path)
diff --git a/src/django_components/util/tag_parser.py b/src/django_components/util/tag_parser.py
index c25a47df..83153b9d 100644
--- a/src/django_components/util/tag_parser.py
+++ b/src/django_components/util/tag_parser.py
@@ -811,7 +811,7 @@ def parse_tag(text: str, parser: Optional[Parser]) -> Tuple[str, List[TagAttr]]:
# or here: ^
if is_next_token(["'", '"', "_("]):
# NOTE: Strings may be wrapped in `_()` to allow for translation.
- # See https://docs.djangoproject.com/en/5.1/topics/i18n/translation/#string-literals-passed-to-tags-and-filters # noqa: E501
+ # See https://docs.djangoproject.com/en/5.2/topics/i18n/translation/#string-literals-passed-to-tags-and-filters # noqa: E501
# NOTE 2: We could potentially raise if this token is supposed to be a filter
# name (after `|`) and we got a translation or a quoted string instead. But we
# leave that up for Django.
diff --git a/src/django_components/util/template_tag.py b/src/django_components/util/template_tag.py
index 3e9184ba..6f382384 100644
--- a/src/django_components/util/template_tag.py
+++ b/src/django_components/util/template_tag.py
@@ -104,7 +104,7 @@ def resolve_params(
class ParsedTag(NamedTuple):
flags: Dict[str, bool]
params: List[TagAttr]
- parse_body: Callable[[], NodeList]
+ parse_body: Callable[[], Tuple[NodeList, Optional[str]]]
def parse_template_tag(
@@ -140,13 +140,15 @@ def parse_template_tag(
raw_params, flags = _extract_flags(tag_name, attrs, allowed_flags or [])
- def _parse_tag_body(parser: Parser, end_tag: str, inline: bool) -> NodeList:
+ def _parse_tag_body(parser: Parser, end_tag: str, inline: bool) -> Tuple[NodeList, Optional[str]]:
if inline:
body = NodeList()
+ contents: Optional[str] = None
else:
+ contents = _extract_contents_until(parser, [end_tag])
body = parser.parse(parse_until=[end_tag])
parser.delete_first_token()
- return body
+ return body, contents
return ParsedTag(
params=raw_params,
@@ -155,10 +157,53 @@ def parse_template_tag(
# loggers before the parsing. This is because, if the body contains any other
# tags, it will trigger their tag handlers. So the code called AFTER
# `parse_body()` is already after all the nested tags were processed.
- parse_body=lambda: _parse_tag_body(parser, end_tag, is_inline) if end_tag else NodeList(),
+ parse_body=lambda: _parse_tag_body(parser, end_tag, is_inline) if end_tag else (NodeList(), None),
)
+# Similar to `parser.parse(parse_until=[end_tag])`, except:
+# 1. Does not remove the token it goes over (unlike `parser.parse()`, which mutates the parser state)
+# 2. Returns a string, instead of a NodeList
+#
+# This is used so we can access the contents of the tag body as strings, for example
+# to be used for caching slots.
+#
+# See https://github.com/django/django/blob/1fb3f57e81239a75eb8f873b392e11534c041fdc/django/template/base.py#L471
+def _extract_contents_until(parser: Parser, until_blocks: List[str]) -> str:
+ contents: List[str] = []
+ for token in reversed(parser.tokens):
+ # Use the raw values here for TokenType.* for a tiny performance boost.
+ token_type = token.token_type.value
+ if token_type == 0: # TokenType.TEXT
+ contents.append(token.contents)
+ elif token_type == 1: # TokenType.VAR
+ contents.append("{{ " + token.contents + " }}")
+ elif token_type == 2: # TokenType.BLOCK
+ try:
+ command = token.contents.split()[0]
+ except IndexError:
+ # NOTE: Django's `Parser.parse()` raises a `TemplateSyntaxError` when there
+ # was a an empty block tag, e.g. `{% %}`.
+ # We skip raising an error here and let `Parser.parse()` raise it.
+ contents.append("{% " + token.contents + " %}")
+ if command in until_blocks:
+ return "".join(contents)
+ else:
+ contents.append("{% " + token.contents + " %}")
+ elif token_type == 3: # TokenType.COMMENT
+ contents.append("{# " + token.contents + " #}")
+ else:
+ raise ValueError(f"Unknown token type {token_type}")
+
+ # NOTE: If we got here, then we've reached the end of the tag body without
+ # encountering any of the `until_blocks`.
+ # Django's `Parser.parse()` raises a `TemplateSyntaxError` in such case.
+ #
+ # Currently `_extract_contents_until()` runs right before `parser.parse()`,
+ # so we skip raising an error here.
+ return "".join(contents)
+
+
def _extract_flags(
tag_name: str, attrs: List[TagAttr], allowed_flags: List[str]
) -> Tuple[List[TagAttr], Dict[str, bool]]:
diff --git a/src/django_components/util/testing.py b/src/django_components/util/testing.py
index 2af46f52..9117692d 100644
--- a/src/django_components/util/testing.py
+++ b/src/django_components/util/testing.py
@@ -6,14 +6,19 @@ from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Set,
from unittest.mock import patch
from weakref import ReferenceType
+import django
from django.conf import settings as _django_settings
+from django.core.cache import BaseCache, caches
from django.template import engines
+from django.template.loaders.base import Loader
from django.test import override_settings
from django_components.component import ALL_COMPONENTS, Component, component_node_subclasses_by_name
from django_components.component_media import ComponentMedia
from django_components.component_registry import ALL_REGISTRIES, ComponentRegistry
from django_components.extension import extensions
+from django_components.perfutil.provide import provide_cache
+from django_components.template import _reset_component_template_file_cache, loading_components
# NOTE: `ReferenceType` is NOT a generic pre-3.9
if sys.version_info >= (3, 9):
@@ -173,7 +178,7 @@ def djc_test(
**Arguments:**
- `django_settings`: Django settings, a dictionary passed to Django's
- [`@override_settings`](https://docs.djangoproject.com/en/5.1/topics/testing/tools/#django.test.override_settings).
+ [`@override_settings`](https://docs.djangoproject.com/en/5.2/topics/testing/tools/#django.test.override_settings).
The test runs within the context of these overridden settings.
If `django_settings` contains django-components settings (`COMPONENTS` field), these are merged.
@@ -236,7 +241,7 @@ def djc_test(
)
)
def test_context_behavior(components_settings):
- rendered = MyComponent().render()
+ rendered = MyComponent.render()
...
```
@@ -296,6 +301,11 @@ def djc_test(
# Contents of this function will run as the test
def _wrapper_impl(*args: Any, **kwargs: Any) -> Any:
+ # If Django is not yet configured, do so now, because we'll need to access
+ # Django's settings when merging the given settings.
+ if not _django_settings.configured:
+ django.setup()
+
# Merge the settings
current_django_settings = django_settings if not callable(django_settings) else None
current_django_settings = current_django_settings.copy() if current_django_settings else {}
@@ -450,7 +460,9 @@ def _clear_djc_global_state(
# beause the IDs count will reset to 0, but we won't generate IDs for the Nodes of the cached
# templates. Thus, the IDs will be out of sync between the tests.
for engine in engines.all():
- engine.engine.template_loaders[0].reset()
+ for loader in engine.engine.template_loaders:
+ if isinstance(loader, Loader):
+ loader.reset()
# NOTE: There are 1-2 tests which check Templates, so we need to clear the cache
from django_components.cache import component_media_cache, template_cache
@@ -461,6 +473,9 @@ def _clear_djc_global_state(
if component_media_cache:
component_media_cache.clear()
+ if provide_cache:
+ provide_cache.clear()
+
# Remove cached Node subclasses
component_node_subclasses_by_name.clear()
@@ -523,6 +538,18 @@ def _clear_djc_global_state(
sys.modules.pop(mod, None)
LOADED_MODULES.clear()
+ # Clear extensions caches
+ extensions._route_to_url.clear()
+
+ # Clear other djc state
+ _reset_component_template_file_cache()
+ loading_components.clear()
+
+ # Clear Django caches
+ all_caches: List[BaseCache] = list(caches.all())
+ for cache in all_caches:
+ cache.clear()
+
# Force garbage collection, so that any finalizers are run.
# If garbage collection is skipped, then in some cases the finalizers
# are run too late, in the context of the next test, causing flaky tests.
diff --git a/src/django_components/util/types.py b/src/django_components/util/types.py
index ab463534..4d490d9c 100644
--- a/src/django_components/util/types.py
+++ b/src/django_components/util/types.py
@@ -1,143 +1,30 @@
-import sys
-import typing
-from typing import Any, Tuple
-
-# See https://peps.python.org/pep-0655/#usage-in-python-3-11
-if sys.version_info >= (3, 11):
- from typing import TypedDict
-else:
- from typing_extensions import TypedDict as TypedDict # for Python <3.11 with (Not)Required
-
-try:
- from typing import Annotated # type: ignore
-except ImportError:
-
- @typing.no_type_check
- class Annotated: # type: ignore
- def __init__(self, type_: str, *args: Any, **kwargs: Any):
- self.type_ = type_
- self.metadata = args, kwargs
-
- def __repr__(self) -> str:
- return f"Annotated[{self.type_}, {self.metadata[0]!r}, {self.metadata[1]!r}]"
-
- def __getitem__(self, params: Any) -> "Annotated[Any, Any, Any]": # type: ignore
- if not isinstance(params, tuple):
- params = (params,)
- return Annotated(self.type_, *params, **self.metadata[1]) # type: ignore
-
- def __class_getitem__(self, *params: Any) -> "Annotated[Any, Any, Any]": # type: ignore
- return Annotated(*params) # type: ignore
+from typing import NamedTuple
-EmptyTuple = Tuple[()]
-"""
-Tuple with no members.
-
-You can use this to define a [Component](../api#django_components.Component)
-that accepts NO positional arguments:
-
-```python
-from django_components import Component, EmptyTuple
-
-class Table(Component(EmptyTuple, Any, Any, Any, Any, Any))
- ...
-```
-
-After that, when you call [`Component.render()`](../api#django_components.Component.render)
-or [`Component.render_to_response()`](../api#django_components.Component.render_to_response),
-the `args` parameter will raise type error if `args` is anything else than an empty
-tuple.
-
-```python
-Table.render(
- args: (),
-)
-```
-
-Omitting `args` is also fine:
-
-```python
-Table.render()
-```
-
-Other values are not allowed. This will raise an error with MyPy:
-
-```python
-Table.render(
- args: ("one", 2, "three"),
-)
-```
-"""
-
-
-class EmptyDict(TypedDict):
+class Empty(NamedTuple):
"""
- TypedDict with no members.
+ Type for an object with no members.
- You can use this to define a [Component](../api#django_components.Component)
- that accepts NO kwargs, or NO slots, or returns NO data from
- [`Component.get_context_data()`](../api#django_components.Component.get_context_data)
- /
- [`Component.get_js_data()`](../api#django_components.Component.get_js_data)
- /
- [`Component.get_css_data()`](../api#django_components.Component.get_css_data):
-
- Accepts NO kwargs:
+ You can use this to define [Component](../api#django_components.Component)
+ types that accept NO args, kwargs, slots, etc:
```python
- from django_components import Component, EmptyDict
+ from django_components import Component, Empty
- class Table(Component(Any, EmptyDict, Any, Any, Any, Any))
+ class Table(Component):
+ Args = Empty
+ Kwargs = Empty
...
```
- Accepts NO slots:
+ This class is a shorthand for:
- ```python
- from django_components import Component, EmptyDict
-
- class Table(Component(Any, Any, EmptyDict, Any, Any, Any))
- ...
+ ```py
+ class Empty(NamedTuple):
+ pass
```
- Returns NO data from `get_context_data()`:
-
- ```python
- from django_components import Component, EmptyDict
-
- class Table(Component(Any, Any, Any, EmptyDict, Any, Any))
- ...
- ```
-
- Going back to the example with NO kwargs, when you then call
- [`Component.render()`](../api#django_components.Component.render)
- or [`Component.render_to_response()`](../api#django_components.Component.render_to_response),
- the `kwargs` parameter will raise type error if `kwargs` is anything else than an empty
- dict.
-
- ```python
- Table.render(
- kwargs: {},
- )
- ```
-
- Omitting `kwargs` is also fine:
-
- ```python
- Table.render()
- ```
-
- Other values are not allowed. This will raise an error with MyPy:
-
- ```python
- Table.render(
- kwargs: {
- "one": 2,
- "three": 4,
- },
- )
- ```
+ Read more about [Typing and validation](../../concepts/fundamentals/typing_and_validation).
"""
pass
diff --git a/src/django_components/util/validation.py b/src/django_components/util/validation.py
deleted file mode 100644
index 2ffc8a53..00000000
--- a/src/django_components/util/validation.py
+++ /dev/null
@@ -1,130 +0,0 @@
-import sys
-import typing
-from typing import Any, Mapping, Tuple, get_type_hints
-
-# Get all types that users may use from the `typing` module.
-#
-# These are the types that we do NOT try to resolve when it's a typed generic,
-# e.g. `Union[int, str]`.
-# If we get a typed generic that's NOT part of this set, we assume it's a user-made
-# generic, e.g. `Component[Args, Kwargs]`. In such case we assert that a given value
-# is an instance of the base class, e.g. `Component`.
-_typing_exports = frozenset(
- [
- value
- for value in typing.__dict__.values()
- if isinstance(
- value,
- (
- typing._SpecialForm,
- # Used in 3.8 and 3.9
- getattr(typing, "_GenericAlias", ()),
- # Used in 3.11+ (possibly 3.10?)
- getattr(typing, "_SpecialGenericAlias", ()),
- ),
- )
- ]
-)
-
-
-def _prepare_type_for_validation(the_type: Any) -> Any:
- # If we got a typed generic (AKA "subscripted" generic), e.g.
- # `Component[CompArgs, CompKwargs, ...]`
- # then we cannot use that generic in `isintance()`, because we get this error:
- # `TypeError("Subscripted generics cannot be used with class and instance checks")`
- #
- # Instead, we resolve the generic to its original class, e.g. `Component`,
- # which can then be used in instance assertion.
- if hasattr(the_type, "__origin__"):
- is_custom_typing = the_type.__origin__ not in _typing_exports
- if is_custom_typing:
- return the_type.__origin__
- else:
- return the_type
- else:
- return the_type
-
-
-# NOTE: tuple_type is a _GenericAlias - See https://stackoverflow.com/questions/74412803
-def validate_typed_tuple(
- value: Tuple[Any, ...],
- tuple_type: Any,
- prefix: str,
- kind: str,
-) -> None:
- # `Any` type is the signal that we should skip validation
- if tuple_type == Any:
- return
-
- # We do two kinds of validation with the given Tuple type:
- # 1. We check whether there are any extra / missing positional args
- # 2. We look at the members of the Tuple (which are types themselves),
- # and check if our concrete list / tuple has correct types under correct indices.
- expected_pos_args = len(tuple_type.__args__)
- actual_pos_args = len(value)
- if expected_pos_args > actual_pos_args:
- # Generate errors like below (listed for searchability)
- # `Component 'name' expected 3 positional arguments, got 2`
- raise TypeError(f"{prefix} expected {expected_pos_args} {kind}s, got {actual_pos_args}")
-
- for index, arg_type in enumerate(tuple_type.__args__):
- arg = value[index]
- arg_type = _prepare_type_for_validation(arg_type)
- if sys.version_info >= (3, 11) and not isinstance(arg, arg_type):
- # Generate errors like below (listed for searchability)
- # `Component 'name' expected positional argument at index 0 to be , got 123.5 of type ` # noqa: E501
- raise TypeError(
- f"{prefix} expected {kind} at index {index} to be {arg_type}, got {arg} of type {type(arg)}"
- )
-
-
-# NOTE:
-# - `dict_type` can be a `TypedDict` or `Any` as the types themselves
-# - `value` is expected to be TypedDict, the base `TypedDict` type cannot be used
-# in function signature (only its subclasses can), so we specify the type as Mapping.
-# See https://stackoverflow.com/questions/74412803
-def validate_typed_dict(value: Mapping[str, Any], dict_type: Any, prefix: str, kind: str) -> None:
- # `Any` type is the signal that we should skip validation
- if dict_type == Any:
- return
-
- # See https://stackoverflow.com/a/76527675
- # And https://stackoverflow.com/a/71231688
- required_kwargs = dict_type.__required_keys__
- unseen_keys = set(value.keys())
-
- # For each entry in the TypedDict, we do two kinds of validation:
- # 1. We check whether there are any extra / missing keys
- # 2. We look at the values of TypedDict entries (which are types themselves),
- # and check if our concrete dict has correct types under correct keys.
- for key, kwarg_type in get_type_hints(dict_type).items():
- if key not in value:
- if key in required_kwargs:
- # Generate errors like below (listed for searchability)
- # `Component 'name' is missing a required keyword argument 'key'`
- # `Component 'name' is missing a required slot argument 'key'`
- # `Component 'name' is missing a required data argument 'key'`
- raise TypeError(f"{prefix} is missing a required {kind} '{key}'")
- else:
- unseen_keys.remove(key)
- kwarg = value[key]
- kwarg_type = _prepare_type_for_validation(kwarg_type)
-
- # NOTE: `isinstance()` cannot be used with the version of TypedDict prior to 3.11.
- # So we do type validation for TypedDicts only in 3.11 and later.
- if sys.version_info >= (3, 11) and not isinstance(kwarg, kwarg_type):
- # Generate errors like below (listed for searchability)
- # `Component 'name' expected keyword argument 'key' to be , got 123.4 of type ` # noqa: E501
- # `Component 'name' expected slot 'key' to be , got 123.4 of type `
- # `Component 'name' expected data 'key' to be , got 123.4 of type `
- raise TypeError(
- f"{prefix} expected {kind} '{key}' to be {kwarg_type}, got {kwarg} of type {type(kwarg)}"
- )
-
- if unseen_keys:
- formatted_keys = ", ".join([f"'{key}'" for key in unseen_keys])
- # Generate errors like below (listed for searchability)
- # `Component 'name' got unexpected keyword argument keys 'invalid_key'`
- # `Component 'name' got unexpected slot keys 'invalid_key'`
- # `Component 'name' got unexpected data keys 'invalid_key'`
- raise TypeError(f"{prefix} got unexpected {kind} keys {formatted_keys}")
diff --git a/src/django_components_js/README.md b/src/django_components_js/README.md
index 5c7f0fff..f6eb3ad0 100644
--- a/src/django_components_js/README.md
+++ b/src/django_components_js/README.md
@@ -25,8 +25,8 @@ Components.registerComponentData(
// we can run component's init function
Components.callComponent(
"table", // Component name
- 12345, // Component ID - An HTML element with corresponding
- // attribute (`data-djc-id-12345`) MUST
+ "c123456", // Component ID - An HTML element with corresponding
+ // attribute (`data-djc-id-c123456`) MUST
// be present in the DOM.
"3d09cf", // Input ID
);
diff --git a/tests/__snapshots__/test_benchmark_djc.ambr b/tests/__snapshots__/test_benchmark_djc.ambr
index 52dc2801..4a23fe00 100644
--- a/tests/__snapshots__/test_benchmark_djc.ambr
+++ b/tests/__snapshots__/test_benchmark_djc.ambr
@@ -11,7 +11,7 @@
-
+
@@ -43,7 +43,7 @@