refactor: Instantiate component when rendering, and remove metadata stack (#1212)

* refactor: Instantiate component when rendering, and remove metadata stack

* refactor: update test

* refactor: fix linter errors

* docs: remove example from changelog
This commit is contained in:
Juro Oravec 2025-05-25 23:33:38 +02:00 committed by GitHub
parent 2e08af9a13
commit bae0f28813
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 1123 additions and 707 deletions

View file

@ -52,6 +52,12 @@ See the full list in [Extension Hooks Reference](../../../reference/extension_ho
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:**
@ -61,10 +67,10 @@ 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
@ -78,10 +84,14 @@ You can override the `get()`, `post()`, etc methods to customize the behavior of
class MyTable(Component):
class View:
def get(self, request):
return self.component.get(request)
# TO BE IMPLEMENTED BY USER
# return self.component_cls.render_to_response(request=request)
raise NotImplementedError("You must implement the `get` method.")
def post(self, request):
return self.component.post(request)
# TO BE IMPLEMENTED BY USER
# return self.component_cls.render_to_response(request=request)
raise NotImplementedError("You must implement the `post` method.")
...
```
@ -97,12 +107,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__,
}
}
@ -208,10 +218,14 @@ class ViewExtension(ComponentExtension):
# The default behavior of the `View` extension class.
class ExtensionClass(ComponentExtension.ExtensionClass):
def get(self, request):
return self.component.get(request)
# TO BE IMPLEMENTED BY USER
# return self.component_cls.render_to_response(request=request)
raise NotImplementedError("You must implement the `get` method.")
def post(self, request):
return self.component.post(request)
# TO BE IMPLEMENTED BY USER
# return self.component_cls.render_to_response(request=request)
raise NotImplementedError("You must implement the `post` method.")
...
```

View file

@ -136,7 +136,7 @@ class MyPage(Component):
class View:
def get(self, request):
return self.component.render_to_response(request=request)
return self.component_cls.render_to_response(request=request)
```
### 2. Define fragment HTML
@ -166,7 +166,7 @@ class Frag(Component):
class View:
def get(self, request):
return self.component.render_to_response(
return self.component_cls.render_to_response(
request=request,
# IMPORTANT: Don't forget `deps_strategy="fragment"`
deps_strategy="fragment",
@ -228,7 +228,7 @@ class MyPage(Component):
class View:
def get(self, request):
return self.component.render_to_response(request=request)
return self.component_cls.render_to_response(request=request)
```
### 2. Define fragment HTML
@ -271,7 +271,7 @@ class Frag(Component):
class View:
def get(self, request):
return self.component.render_to_response(
return self.component_cls.render_to_response(
request=request,
# IMPORTANT: Don't forget `deps_strategy="fragment"`
deps_strategy="fragment",
@ -332,7 +332,7 @@ class MyPage(Component):
class View:
def get(self, request):
return self.component.render_to_response(request=request)
return self.component_cls.render_to_response(request=request)
```
### 2. Define fragment HTML
@ -362,7 +362,7 @@ class Frag(Component):
class View:
def get(self, request):
return self.component.render_to_response(
return self.component_cls.render_to_response(
request=request,
# IMPORTANT: Don't forget `deps_strategy="fragment"`
deps_strategy="fragment",

View file

@ -110,6 +110,6 @@ from django_components.testing import djc_test
)
)
def test_context_behavior(components_settings):
rendered = MyComponent().render()
rendered = MyComponent.render()
...
```

View file

@ -81,9 +81,9 @@ class Calendar(Component):
This is deprecated from v0.137 onwards, and will be removed in v1.0.
### Acccessing component instance
### Acccessing component class
You can access the component instance from within the View methods by using the [`View.component`](../../../reference/api#django_components.ComponentView.component) attribute:
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):
@ -91,20 +91,9 @@ class Calendar(Component):
class View:
def get(self, request):
return self.component.render_to_response(request=request)
return self.component_cls.render_to_response(request=request)
```
!!! note
The [`View.component`](../../../reference/api#django_components.ComponentView.component) instance 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)
```
## Register URLs manually
To register the component as a route / endpoint in Django, add an entry to your
@ -134,7 +123,7 @@ class MyComponent(Component):
public = True
def get(self, request):
return self.component.render_to_response(request=request)
return self.component_cls.render_to_response(request=request)
...
```

View file

@ -260,7 +260,7 @@ This includes:
- [`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)
For more details, see [Component inputs](../render_api/#component-inputs).
For more details, see [Component inputs](../render_api/#other-inputs).
```python
class ProfileCard(Component):
@ -343,16 +343,20 @@ class ProfileCard(Component):
## Accessing Render API
All three data methods have access to the Component's [Render API](./render_api.md), which includes:
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.input`](./render_api/#component-inputs) - All the component inputs
- [`self.id`](./render_api/#component-id) - The unique ID for the current render call
- [`self.request`](./render_api/#request-object-and-context-processors) - The request object (if available)
- [`self.context_processors_data`](./render_api/#request-object-and-context-processors) - Data from Django's context processors (if request is available)
- [`self.inject()`](./render_api/#provide-inject) - Inject data into the component
- [`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.context`](../render_api/#context) - The context for the current render call
- [`self.input`](../render_api/#other-inputs) - All the component inputs
- [`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
## Type hints

View file

@ -12,10 +12,6 @@ Render API is available inside these [`Component`](../../../reference/api#django
- [`on_render_before()`](../../../reference/api#django_components.Component.on_render_before)
- [`on_render_after()`](../../../reference/api#django_components.Component.on_render_after)
!!! note
If you try to access the Render API outside of these methods, you will get a `RuntimeError`.
Example:
```python
@ -46,16 +42,31 @@ rendered = Table.render(
The Render API 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.input`](../render_api/#component-inputs) - All the component inputs
- [`self.id`](../render_api/#component-id) - The unique ID for the current render call
- [`self.request`](../render_api/#request-object-and-context-processors) - The request object (if available)
- [`self.context_processors_data`](../render_api/#request-object-and-context-processors) - Data from Django's context processors (if request is available)
- [`self.inject()`](../render_api/#provide-inject) - Inject data into the component
- 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.context`](../render_api/#context) - The context for the current render call
- [`self.input`](../render_api/#other-inputs) - All the component inputs
## Args
- 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.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).
@ -65,8 +76,6 @@ then the [`Component.args`](../../../reference/api/#django_components.Component.
Otherwise, `args` will be a plain list.
Raises `RuntimeError` if accessed outside of rendering execution.
**Example:**
With `Args` class:
@ -99,7 +108,7 @@ class Table(Component):
assert self.args[1] == 10
```
## Kwargs
### Kwargs
The `kwargs` argument as passed to
[`Component.get_template_data()`](../../../reference/api/#django_components.Component.get_template_data).
@ -109,8 +118,6 @@ then the [`Component.kwargs`](../../../reference/api/#django_components.Componen
Otherwise, `kwargs` will be a plain dictionary.
Raises `RuntimeError` if accessed outside of rendering execution.
**Example:**
With `Kwargs` class:
@ -143,7 +150,7 @@ class Table(Component):
assert self.kwargs["per_page"] == 10
```
## Slots
### Slots
The `slots` argument as passed to
[`Component.get_template_data()`](../../../reference/api/#django_components.Component.get_template_data).
@ -153,8 +160,6 @@ then the [`Component.slots`](../../../reference/api/#django_components.Component
Otherwise, `slots` will be a plain dictionary.
Raises `RuntimeError` if accessed outside of rendering execution.
**Example:**
With `Slots` class:
@ -190,7 +195,27 @@ class Table(Component):
assert isinstance(self.slots["footer"], Slot)
```
## Component inputs
### 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.
### Other inputs
You can access the most important inputs via [`self.args`](../render_api/#args),
[`self.kwargs`](../render_api/#kwargs),
@ -255,7 +280,7 @@ class Table(Component):
assert self.id == "c1A2b3c"
```
## Request object and context processors
## Request and context processors
Components have access to the request object and context processors data if the component was:
@ -306,3 +331,32 @@ class Table(Component):
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.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
```

View file

@ -246,7 +246,6 @@ Button.render(
- `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)
- `escape_slots_content` - Whether to HTML-escape slot content (default: `True`)
All arguments are optional. If not provided, they default to empty values or sensible defaults.

View file

@ -770,10 +770,10 @@ print(slot.nodelist) # <django.template.Nodelist: ['Hello!']>
Slots content are automatically escaped by default to prevent XSS attacks.
In other words, it's as if you would be using Django's [`mark_safe()`](https://docs.djangoproject.com/en/5.2/ref/utils/#django.utils.safestring.mark_safe) function on the slot content:
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.safestring import mark_safe
from django.utils.html import escape
class Calendar(Component):
template = """
@ -784,24 +784,28 @@ class Calendar(Component):
Calendar.render(
slots={
"date": mark_safe("<b>Hello</b>"),
"date": escape("<b>Hello</b>"),
}
)
```
To disable escaping, you can pass `escape_slots_content=False` to
[`Component.render()`](../../../reference/api#django_components.Component.render)
or [`Component.render_to_response()`](../../../reference/api#django_components.Component.render_to_response)
methods.
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):
!!! warning
```py
Calendar.render(
slots={
# string
"date": mark_safe("<b>Hello</b>"),
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!
# function
"date": lambda ctx: mark_safe("<b>Hello</b>"),
}
)
```
!!! info
If you're planning on passing an HTML string, check Django's use of
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).