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

View file

@ -164,10 +164,10 @@ def my_view(request):
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 = """
@ -178,24 +178,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).

View file

@ -4,6 +4,7 @@ nav:
- Compatibility: compatibility.md
- Installation: installation.md
- Security notes 🚨: security_notes.md
- Migrating: migrating.md
- Community: community.md
- Contributing: contributing.md
- Development: development.md

View file

@ -0,0 +1,25 @@
Django-components is still in active development.
Since django-components is in pre-1.0 development, the public API is not yet frozen.
This means that there may be breaking changes between minor versions.
We try to minimize the number of breaking changes, but sometimes it's unavoidable.
When upgrading, please read the [Release notes](../../release_notes).
## Migrating in pre-v1.0
If you're on older pre-v1.0 versions of django-components, we recommend doing step-wise
upgrades in the following order:
- [v0.26](../../release_notes/#v026)
- [v0.50](../../release_notes/#v050)
- [v0.70](../../release_notes/#v070)
- [v0.77](../../release_notes/#v077)
- [v0.81](../../release_notes/#v081)
- [v0.85](../../release_notes/#v085)
- [v0.92](../../release_notes/#v092)
- [v0.100](../../release_notes/#v0100)
- [v0.110](../../release_notes/#v0110)
- [v0.140](../../release_notes/#v01400)
These versions introduced breaking changes that are not backwards compatible.

View file

@ -208,7 +208,7 @@ 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-object-and-context-processors)
- 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