mirror of
https://github.com/django-components/django-components.git
synced 2025-07-07 17:34:59 +00:00
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:
parent
2e08af9a13
commit
bae0f28813
33 changed files with 1123 additions and 707 deletions
64
CHANGELOG.md
64
CHANGELOG.md
|
@ -430,6 +430,58 @@ Summary:
|
|||
Calendar.render_to_response(deps_strategy="ignore")
|
||||
```
|
||||
|
||||
- Support for `Component` constructor kwargs `registered_name`, `outer_context`, and `registry` is deprecated, and will be removed in v1.
|
||||
|
||||
Before, you could instantiate a standalone component,
|
||||
and then call `render()` on the instance:
|
||||
|
||||
```py
|
||||
comp = MyComponent(
|
||||
registered_name="my_component",
|
||||
outer_context=my_context,
|
||||
registry=my_registry,
|
||||
)
|
||||
comp.render(
|
||||
args=[1, 2, 3],
|
||||
kwargs={"a": 1, "b": 2},
|
||||
slots={"my_slot": "CONTENT"},
|
||||
)
|
||||
```
|
||||
|
||||
Now you should instead pass all that data to `Component.render()` / `Component.render_to_response()`:
|
||||
|
||||
```py
|
||||
MyComponent.render(
|
||||
args=[1, 2, 3],
|
||||
kwargs={"a": 1, "b": 2},
|
||||
slots={"my_slot": "CONTENT"},
|
||||
# NEW
|
||||
registered_name="my_component",
|
||||
outer_context=my_context,
|
||||
registry=my_registry,
|
||||
)
|
||||
```
|
||||
|
||||
- If you are using the Components as views, the way to access the component class is now different.
|
||||
|
||||
Instead of `self.component`, use `self.component_cls`. `self.component` will be removed in v1.
|
||||
|
||||
Before:
|
||||
|
||||
```py
|
||||
class MyView(View):
|
||||
def get(self, request):
|
||||
return self.component.render_to_response(request=request)
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```py
|
||||
class MyView(View):
|
||||
def get(self, request):
|
||||
return self.component_cls.render_to_response(request=request)
|
||||
```
|
||||
|
||||
**Extensions**
|
||||
|
||||
- In the `on_component_data()` extension hook, the `context_data` field of the context object was superseded by `template_data`.
|
||||
|
@ -805,12 +857,24 @@ Summary:
|
|||
|
||||
See all [Extension hooks](https://django-components.github.io/django-components/0.140/reference/extension_hooks/).
|
||||
|
||||
#### 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).
|
||||
|
||||
#### 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
|
||||
|
|
|
@ -218,7 +218,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
|
||||
|
|
|
@ -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.")
|
||||
|
||||
...
|
||||
```
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -110,6 +110,6 @@ from django_components.testing import djc_test
|
|||
)
|
||||
)
|
||||
def test_context_behavior(components_settings):
|
||||
rendered = MyComponent().render()
|
||||
rendered = MyComponent.render()
|
||||
...
|
||||
```
|
||||
|
|
|
@ -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)
|
||||
...
|
||||
```
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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).
|
||||
|
||||
|
|
|
@ -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).
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
25
docs/overview/migrating.md
Normal file
25
docs/overview/migrating.md
Normal 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.
|
|
@ -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
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -118,17 +118,15 @@ class DynamicComponent(Component):
|
|||
|
||||
comp_class = self._resolve_component(comp_name_or_class, registry)
|
||||
|
||||
comp = comp_class(
|
||||
registered_name=self.registered_name,
|
||||
outer_context=self.outer_context,
|
||||
registry=self.registry,
|
||||
)
|
||||
output = comp.render(
|
||||
output = comp_class.render(
|
||||
context=self.input.context,
|
||||
args=self.input.args,
|
||||
kwargs=cleared_kwargs,
|
||||
slots=self.input.slots,
|
||||
deps_strategy=self.input.deps_strategy,
|
||||
registered_name=self.registered_name,
|
||||
outer_context=self.outer_context,
|
||||
registry=self.registry,
|
||||
)
|
||||
|
||||
# Set the output to the context so it can be accessed from within the template.
|
||||
|
|
|
@ -582,9 +582,7 @@ def _process_dep_declarations(content: bytes, strategy: DependenciesStrategy) ->
|
|||
from django_components.component import get_component_by_class_id
|
||||
|
||||
comp_cls = get_component_by_class_id(comp_cls_id)
|
||||
# NOTE: We instantiate the component classes so the `Media` are processed into `media`
|
||||
comp = comp_cls()
|
||||
return comp.media
|
||||
return comp_cls.media
|
||||
|
||||
all_medias = [
|
||||
# JS / CSS files from Component.Media.js/css.
|
||||
|
|
|
@ -129,10 +129,10 @@ class OnComponentRenderedContext(NamedTuple):
|
|||
"""The rendered component"""
|
||||
|
||||
|
||||
# TODO - Add `component` once we create instances inside `render()`
|
||||
# See https://github.com/django-components/django-components/issues/1186
|
||||
@mark_extension_hook_api
|
||||
class OnSlotRenderedContext(NamedTuple):
|
||||
component: "Component"
|
||||
"""The Component instance that contains the `{% slot %}` tag"""
|
||||
component_cls: Type["Component"]
|
||||
"""The Component class that contains the `{% slot %}` tag"""
|
||||
component_id: str
|
||||
|
|
|
@ -3,7 +3,7 @@ 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, OnComponentClassCreatedContext
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django_components.component import Component
|
||||
|
@ -99,7 +99,7 @@ def _extract_defaults(defaults: Optional[Type]) -> List[ComponentDefaultField]:
|
|||
return defaults_fields
|
||||
|
||||
|
||||
def _apply_defaults(kwargs: Dict, defaults: List[ComponentDefaultField]) -> None:
|
||||
def apply_defaults(kwargs: Dict, defaults: List[ComponentDefaultField]) -> None:
|
||||
"""
|
||||
Apply the defaults from `Component.Defaults` to the given `kwargs`.
|
||||
|
||||
|
@ -171,11 +171,3 @@ class DefaultsExtension(ComponentExtension):
|
|||
def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
|
||||
defaults_cls = getattr(ctx.component_cls, "Defaults", None)
|
||||
defaults_by_component[ctx.component_cls] = _extract_defaults(defaults_cls)
|
||||
|
||||
# Apply defaults to missing or `None` values in `kwargs`
|
||||
def on_component_input(self, ctx: OnComponentInputContext) -> None:
|
||||
defaults = defaults_by_component.get(ctx.component_cls, None)
|
||||
if defaults is None:
|
||||
return
|
||||
|
||||
_apply_defaults(ctx.kwargs, defaults)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import sys
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Protocol, Type, Union, cast
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional, Protocol, Type, Union, cast
|
||||
from weakref import WeakKeyDictionary
|
||||
|
||||
import django.urls
|
||||
|
@ -82,8 +82,8 @@ class ComponentView(ComponentExtension.ExtensionClass, View): # type: ignore
|
|||
|
||||
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) instance is available
|
||||
via `self.component`.
|
||||
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.
|
||||
|
||||
|
@ -116,18 +116,21 @@ class ComponentView(ComponentExtension.ExtensionClass, View): # type: ignore
|
|||
|
||||
Will create a URL route like `/components/ext/view/components/a1b2c3/`.
|
||||
|
||||
To get the URL for the component, use `get_component_url`:
|
||||
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 class 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)
|
||||
"""
|
||||
The component instance.
|
||||
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.
|
||||
|
||||
|
@ -139,7 +142,48 @@ class ComponentView(ComponentExtension.ExtensionClass, View): # type: ignore
|
|||
```
|
||||
"""
|
||||
|
||||
public = False
|
||||
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)
|
||||
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.
|
||||
|
||||
|
@ -155,26 +199,13 @@ class ComponentView(ComponentExtension.ExtensionClass, View): # type: ignore
|
|||
|
||||
Will create a URL route like `/components/ext/view/components/a1b2c3/`.
|
||||
|
||||
To get the URL for the component, use `get_component_url`:
|
||||
To get the URL for the component, use [`get_component_url()`](../api#django_components.get_component_url):
|
||||
|
||||
```py
|
||||
url = get_component_url(MyComponent)
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, component: "Component", **kwargs: Any) -> None:
|
||||
ComponentExtension.ExtensionClass.__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.
|
||||
"""
|
||||
return get_component_url(self.component.__class__)
|
||||
|
||||
# NOTE: The methods below are defined to satisfy the `View` class. All supported methods
|
||||
# are defined in `View.http_method_names`.
|
||||
#
|
||||
|
@ -185,30 +216,31 @@ class ComponentView(ComponentExtension.ExtensionClass, View): # type: ignore
|
|||
# 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.render_to_response()` or similar.
|
||||
# `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:
|
||||
return getattr(self.component, "get")(request, *args, **kwargs)
|
||||
return getattr(self.component_cls(), "get")(request, *args, **kwargs)
|
||||
|
||||
def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
return getattr(self.component, "post")(request, *args, **kwargs)
|
||||
return getattr(self.component_cls(), "post")(request, *args, **kwargs)
|
||||
|
||||
def put(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
return getattr(self.component, "put")(request, *args, **kwargs)
|
||||
return getattr(self.component_cls(), "put")(request, *args, **kwargs)
|
||||
|
||||
def patch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
return getattr(self.component, "patch")(request, *args, **kwargs)
|
||||
return getattr(self.component_cls(), "patch")(request, *args, **kwargs)
|
||||
|
||||
def delete(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
return getattr(self.component, "delete")(request, *args, **kwargs)
|
||||
return getattr(self.component_cls(), "delete")(request, *args, **kwargs)
|
||||
|
||||
def head(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
return getattr(self.component, "head")(request, *args, **kwargs)
|
||||
return getattr(self.component_cls(), "head")(request, *args, **kwargs)
|
||||
|
||||
def options(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
return getattr(self.component, "options")(request, *args, **kwargs)
|
||||
return getattr(self.component_cls(), "options")(request, *args, **kwargs)
|
||||
|
||||
def trace(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
return getattr(self.component, "trace")(request, *args, **kwargs)
|
||||
return getattr(self.component_cls(), "trace")(request, *args, **kwargs)
|
||||
|
||||
|
||||
class ViewExtension(ComponentExtension):
|
||||
|
|
|
@ -155,8 +155,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.
|
||||
|
|
|
@ -36,7 +36,7 @@ from django_components.util.logger import trace_component_msg
|
|||
from django_components.util.misc import get_index, get_last_index, is_identifier
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django_components.component import ComponentContext, ComponentNode
|
||||
from django_components.component import Component, ComponentNode
|
||||
|
||||
TSlotData = TypeVar("TSlotData", bound=Mapping)
|
||||
|
||||
|
@ -597,11 +597,19 @@ 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.input.slots
|
||||
slot_name = name
|
||||
is_default = self.flags[SLOT_DEFAULT_FLAG]
|
||||
is_required = self.flags[SLOT_REQUIRED_FLAG]
|
||||
|
@ -617,7 +625,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
|
||||
|
@ -677,9 +685,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:
|
||||
|
@ -726,10 +734,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]}"
|
||||
|
@ -775,7 +783,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_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.'"
|
||||
|
@ -809,13 +817,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]
|
||||
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"] = component_ctx.outer_context["component_vars"]
|
||||
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
|
||||
|
@ -831,7 +839,7 @@ class SlotNode(BaseNode):
|
|||
|
||||
# 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_is_filled, 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
|
||||
|
@ -853,8 +861,9 @@ class SlotNode(BaseNode):
|
|||
# Allow plugins to post-process the slot's rendered output
|
||||
output = extensions.on_slot_rendered(
|
||||
OnSlotRenderedContext(
|
||||
component_cls=component_ctx.component_class,
|
||||
component_id=component_ctx.component_id,
|
||||
component=component,
|
||||
component_cls=component.__class__,
|
||||
component_id=component_id,
|
||||
slot=slot,
|
||||
slot_name=slot_name,
|
||||
slot_is_required=is_required,
|
||||
|
@ -878,7 +887,8 @@ class SlotNode(BaseNode):
|
|||
self,
|
||||
context: Context,
|
||||
slot_is_filled: bool,
|
||||
component_ctx: "ComponentContext",
|
||||
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 fallback AKA content between
|
||||
|
@ -887,11 +897,10 @@ class SlotNode(BaseNode):
|
|||
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}'")
|
||||
|
|
|
@ -5,7 +5,7 @@ from hashlib import md5
|
|||
from importlib import import_module
|
||||
from itertools import chain
|
||||
from types import ModuleType
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, 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
|
||||
|
@ -15,6 +15,7 @@ if TYPE_CHECKING:
|
|||
from django_components.component import Component
|
||||
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U")
|
||||
|
||||
|
||||
# Based on nanoid implementation from
|
||||
|
@ -91,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]:
|
||||
|
|
|
@ -238,7 +238,7 @@ def djc_test(
|
|||
)
|
||||
)
|
||||
def test_context_behavior(components_settings):
|
||||
rendered = MyComponent().render()
|
||||
rendered = MyComponent.render()
|
||||
...
|
||||
```
|
||||
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
from typing import Optional
|
||||
|
||||
from django_components import Component, register
|
||||
|
||||
|
||||
# Used for testing the template_loader
|
||||
@register("app_lvl_comp")
|
||||
class AppLvlCompComponent(Component):
|
||||
template_file: Optional[str] = "app_lvl_comp.html"
|
||||
template_file = "app_lvl_comp.html"
|
||||
js_file = "app_lvl_comp.js"
|
||||
css_file = "app_lvl_comp.css"
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ For tests focusing on the `component` tag, see `test_templatetags_component.py`
|
|||
"""
|
||||
|
||||
import re
|
||||
from typing import NamedTuple
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
|
@ -103,6 +103,55 @@ class TestComponentLegacyApi:
|
|||
""",
|
||||
)
|
||||
|
||||
# TODO_REMOVE_IN_V1 - Registry and registered name should be passed to `Component.render()`,
|
||||
# not to the constructor.
|
||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||
def test_component_instantiation(self, components_settings):
|
||||
class SimpleComponent(Component):
|
||||
template = """
|
||||
<div>
|
||||
Name: {{ name }}
|
||||
</div>
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"name": self.name,
|
||||
}
|
||||
|
||||
# Old syntax
|
||||
rendered = SimpleComponent("simple").render()
|
||||
assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
<div data-djc-id-ca1bc3f>
|
||||
Name: simple
|
||||
</div>
|
||||
""",
|
||||
)
|
||||
|
||||
# New syntax
|
||||
rendered = SimpleComponent.render(registered_name="simple")
|
||||
assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
<div data-djc-id-ca1bc40>
|
||||
Name: simple
|
||||
</div>
|
||||
""",
|
||||
)
|
||||
|
||||
# Sanity check
|
||||
rendered = SimpleComponent.render()
|
||||
assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
<div data-djc-id-ca1bc41>
|
||||
Name: SimpleComponent
|
||||
</div>
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
@djc_test
|
||||
class TestComponent:
|
||||
|
@ -112,7 +161,7 @@ class TestComponent:
|
|||
pass
|
||||
|
||||
with pytest.raises(ImproperlyConfigured):
|
||||
EmptyComponent("empty_component")._get_template(Context({}), "123")
|
||||
EmptyComponent.render(args=["123"])
|
||||
|
||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||
def test_template_string_static_inlined(self, components_settings):
|
||||
|
@ -200,6 +249,7 @@ class TestComponent:
|
|||
css = "style.css"
|
||||
js = "script.js"
|
||||
|
||||
# Access fields on Component class
|
||||
assert SimpleComponent.template_name == "simple_template.html"
|
||||
assert SimpleComponent.template_file == "simple_template.html"
|
||||
|
||||
|
@ -216,13 +266,14 @@ class TestComponent:
|
|||
""",
|
||||
)
|
||||
|
||||
# Access fields on Component instance
|
||||
comp = SimpleComponent()
|
||||
assert comp.template_name == "simple_template.html"
|
||||
assert comp.template_file == "simple_template.html"
|
||||
|
||||
# NOTE: Setting `template_file` on INSTANCE is not supported, as users should work
|
||||
# with classes and not instances. This is tested for completeness.
|
||||
comp.template_name = "other_template_2.html"
|
||||
comp.template_name = "other_template_2.html" # type: ignore[misc]
|
||||
assert comp.template_name == "other_template_2.html"
|
||||
assert comp.template_file == "other_template_2.html"
|
||||
assert SimpleComponent.template_name == "other_template_2.html"
|
||||
|
@ -233,7 +284,7 @@ class TestComponent:
|
|||
assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
Variable: <strong data-djc-id-ca1bc3f>test</strong>
|
||||
Variable: <strong data-djc-id-ca1bc40>test</strong>
|
||||
""",
|
||||
)
|
||||
|
||||
|
@ -422,17 +473,24 @@ class TestComponentRenderAPI:
|
|||
|
||||
assert called
|
||||
|
||||
def test_args_kwargs_slots__raises_outside_render(self):
|
||||
def test_args_kwargs_slots__available_outside_render(self):
|
||||
comp: Any = None
|
||||
|
||||
class TestComponent(Component):
|
||||
template = ""
|
||||
|
||||
comp = TestComponent()
|
||||
with pytest.raises(RuntimeError):
|
||||
comp.args
|
||||
with pytest.raises(RuntimeError):
|
||||
comp.kwargs
|
||||
with pytest.raises(RuntimeError):
|
||||
comp.slots
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
nonlocal comp
|
||||
comp = self
|
||||
|
||||
assert comp is None
|
||||
|
||||
TestComponent.render()
|
||||
|
||||
assert comp.args == [] # type: ignore[attr-defined]
|
||||
assert comp.kwargs == {} # type: ignore[attr-defined]
|
||||
assert comp.slots == {} # type: ignore[attr-defined]
|
||||
assert comp.context == Context() # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@djc_test
|
||||
|
@ -953,7 +1011,7 @@ class TestComponentRender:
|
|||
# """
|
||||
assertInHTML(
|
||||
"""
|
||||
<kbd data-djc-id-ca1bc3e>
|
||||
<kbd data-djc-id-ca1bc3f>
|
||||
Rendered via GET request
|
||||
</kbd>
|
||||
""",
|
||||
|
|
|
@ -48,8 +48,8 @@ class TestComponentCache:
|
|||
# Check if the cache entry is set
|
||||
cache_key = component.cache.get_cache_key([], {}, {})
|
||||
assert cache_key == "components:cache:c98bf483e9a1937732d4542c714462ac"
|
||||
assert component.cache.get_entry(cache_key) == "<!-- _RENDERED TestComponent_c9770f,ca1bc3e,, -->Hello"
|
||||
assert caches["default"].get(cache_key) == "<!-- _RENDERED TestComponent_c9770f,ca1bc3e,, -->Hello"
|
||||
assert component.cache.get_entry(cache_key) == "<!-- _RENDERED TestComponent_c9770f,ca1bc3f,, -->Hello"
|
||||
assert caches["default"].get(cache_key) == "<!-- _RENDERED TestComponent_c9770f,ca1bc3f,, -->Hello"
|
||||
|
||||
# Second render
|
||||
did_call_get = False
|
||||
|
@ -105,7 +105,7 @@ class TestComponentCache:
|
|||
|
||||
cache_instance = component.cache
|
||||
cache_key = cache_instance.get_cache_key([], {}, {})
|
||||
assert cache_instance.get_entry(cache_key) == "<!-- _RENDERED TestComponent_42aca9,ca1bc3e,, -->Hello"
|
||||
assert cache_instance.get_entry(cache_key) == "<!-- _RENDERED TestComponent_42aca9,ca1bc3f,, -->Hello"
|
||||
|
||||
# Wait for TTL to expire
|
||||
time.sleep(0.2)
|
||||
|
@ -140,7 +140,7 @@ class TestComponentCache:
|
|||
assert component.cache.get_cache() is caches["custom"]
|
||||
assert (
|
||||
component.cache.get_entry("components:cache:bcb4b049d8556e06871b39e0e584e452")
|
||||
== "<!-- _RENDERED TestComponent_90ef7a,ca1bc3e,, -->Hello"
|
||||
== "<!-- _RENDERED TestComponent_90ef7a,ca1bc3f,, -->Hello"
|
||||
)
|
||||
|
||||
def test_cache_by_input(self):
|
||||
|
@ -168,11 +168,11 @@ class TestComponentCache:
|
|||
assert len(cache._cache) == 2
|
||||
assert (
|
||||
component.cache.get_entry("components:cache:3535e1d1e5f6fa5bc521e7fe203a68d0")
|
||||
== "<!-- _RENDERED TestComponent_648b95,ca1bc3e,, -->Hello world"
|
||||
== "<!-- _RENDERED TestComponent_648b95,ca1bc3f,, -->Hello world"
|
||||
)
|
||||
assert (
|
||||
component.cache.get_entry("components:cache:a98a8bd5e72a544d7601798d5e777a77")
|
||||
== "<!-- _RENDERED TestComponent_648b95,ca1bc3f,, -->Hello cake"
|
||||
== "<!-- _RENDERED TestComponent_648b95,ca1bc40,, -->Hello cake"
|
||||
)
|
||||
|
||||
def test_cache_input_hashing(self):
|
||||
|
@ -206,7 +206,7 @@ class TestComponentCache:
|
|||
# The key should use the custom hash methods
|
||||
expected_key = "components:cache:3d54974c467a578c509efec189b0d14b"
|
||||
assert component.cache.get_cache_key([1, 2], {"key": "value"}, {}) == expected_key
|
||||
assert component.cache.get_entry(expected_key) == "<!-- _RENDERED TestComponent_28880f,ca1bc3e,, -->Hello"
|
||||
assert component.cache.get_entry(expected_key) == "<!-- _RENDERED TestComponent_28880f,ca1bc3f,, -->Hello"
|
||||
|
||||
def test_cached_component_inside_include(self):
|
||||
|
||||
|
@ -283,7 +283,7 @@ class TestComponentCache:
|
|||
assert len(cache._cache) == 2
|
||||
assert (
|
||||
component.cache.get_entry("components:cache:1d7e3a58972550cf9bec18f457fb1a61")
|
||||
== '<!-- _RENDERED TestComponent_dd1dee,ca1bc44,, -->Hello cake <div data-djc-id-ca1bc44="">\n TWO\n </div>' # noqa: E501
|
||||
== '<!-- _RENDERED TestComponent_dd1dee,ca1bc45,, -->Hello cake <div data-djc-id-ca1bc45="">\n TWO\n </div>' # noqa: E501
|
||||
)
|
||||
|
||||
def test_cache_slots__strings(self):
|
||||
|
@ -324,7 +324,7 @@ class TestComponentCache:
|
|||
assert len(cache._cache) == 2
|
||||
assert (
|
||||
component.cache.get_entry("components:cache:468e3f122ac305cff5d9096a3c548faf")
|
||||
== '<!-- _RENDERED TestComponent_34b6d1,ca1bc41,, -->Hello cake <div data-djc-id-ca1bc41="">TWO</div>'
|
||||
== '<!-- _RENDERED TestComponent_34b6d1,ca1bc42,, -->Hello cake <div data-djc-id-ca1bc42="">TWO</div>'
|
||||
)
|
||||
|
||||
def test_cache_slots_raises_on_func(self):
|
||||
|
|
|
@ -215,10 +215,9 @@ class TestMainMedia:
|
|||
def get_template(self, context):
|
||||
return Template("<div class='variable-html'>{{ variable }}</div>")
|
||||
|
||||
comp = VariableHTMLComponent("variable_html_component")
|
||||
context = Context({"variable": "Dynamic Content"})
|
||||
rendered = VariableHTMLComponent.render(context=Context({"variable": "Dynamic Content"}))
|
||||
assertHTMLEqual(
|
||||
comp.render(context),
|
||||
rendered,
|
||||
'<div class="variable-html" data-djc-id-ca1bc3e>Dynamic Content</div>',
|
||||
)
|
||||
|
||||
|
@ -1047,6 +1046,13 @@ class TestSubclassingMedia:
|
|||
assertInHTML('<script src="child.js"></script>', rendered)
|
||||
assertInHTML('<script src="parent.js"></script>', rendered)
|
||||
|
||||
assert str(ChildComponent.media) == (
|
||||
'<link href="child.css" media="all" rel="stylesheet">\n'
|
||||
'<link href="parent.css" media="all" rel="stylesheet">\n'
|
||||
'<script src="child.js"></script>\n'
|
||||
'<script src="parent.js"></script>'
|
||||
)
|
||||
|
||||
def test_media_in_child_and_grandparent(self):
|
||||
class GrandParentComponent(Component):
|
||||
template: types.django_html = """
|
||||
|
@ -1075,6 +1081,13 @@ class TestSubclassingMedia:
|
|||
assertInHTML('<script src="child.js"></script>', rendered)
|
||||
assertInHTML('<script src="grandparent.js"></script>', rendered)
|
||||
|
||||
assert str(ChildComponent.media) == (
|
||||
'<link href="child.css" media="all" rel="stylesheet">\n'
|
||||
'<link href="grandparent.css" media="all" rel="stylesheet">\n'
|
||||
'<script src="child.js"></script>\n'
|
||||
'<script src="grandparent.js"></script>'
|
||||
)
|
||||
|
||||
def test_media_in_parent_and_grandparent(self):
|
||||
class GrandParentComponent(Component):
|
||||
template: types.django_html = """
|
||||
|
@ -1103,6 +1116,13 @@ class TestSubclassingMedia:
|
|||
assertInHTML('<script src="parent.js"></script>', rendered)
|
||||
assertInHTML('<script src="grandparent.js"></script>', rendered)
|
||||
|
||||
assert str(ChildComponent.media) == (
|
||||
'<link href="parent.css" media="all" rel="stylesheet">\n'
|
||||
'<link href="grandparent.css" media="all" rel="stylesheet">\n'
|
||||
'<script src="parent.js"></script>\n'
|
||||
'<script src="grandparent.js"></script>'
|
||||
)
|
||||
|
||||
def test_media_in_multiple_bases(self):
|
||||
class GrandParent1Component(Component):
|
||||
class Media:
|
||||
|
@ -1154,6 +1174,17 @@ class TestSubclassingMedia:
|
|||
assertInHTML('<script src="grandparent1.js"></script>', rendered)
|
||||
assertInHTML('<script src="grandparent3.js"></script>', rendered)
|
||||
|
||||
assert str(ChildComponent.media) == (
|
||||
'<link href="child.css" media="all" rel="stylesheet">\n'
|
||||
'<link href="grandparent3.css" media="all" rel="stylesheet">\n'
|
||||
'<link href="parent1.css" media="all" rel="stylesheet">\n'
|
||||
'<link href="grandparent1.css" media="all" rel="stylesheet">\n'
|
||||
'<script src="child.js"></script>\n'
|
||||
'<script src="grandparent3.js"></script>\n'
|
||||
'<script src="parent1.js"></script>\n'
|
||||
'<script src="grandparent1.js"></script>'
|
||||
)
|
||||
|
||||
def test_extend_false_in_child(self):
|
||||
class Parent1Component(Component):
|
||||
template: types.django_html = """
|
||||
|
@ -1187,6 +1218,11 @@ class TestSubclassingMedia:
|
|||
assert "parent2.js" not in rendered
|
||||
assertInHTML('<script src="child.js"></script>', rendered)
|
||||
|
||||
assert str(ChildComponent.media) == (
|
||||
'<link href="child.css" media="all" rel="stylesheet">\n'
|
||||
'<script src="child.js"></script>'
|
||||
)
|
||||
|
||||
def test_extend_false_in_parent(self):
|
||||
class GrandParentComponent(Component):
|
||||
class Media:
|
||||
|
@ -1227,6 +1263,15 @@ class TestSubclassingMedia:
|
|||
assertInHTML('<script src="parent2.js"></script>', rendered)
|
||||
assertInHTML('<script src="child.js"></script>', rendered)
|
||||
|
||||
assert str(ChildComponent.media) == (
|
||||
'<link href="child.css" media="all" rel="stylesheet">\n'
|
||||
'<link href="parent2.css" media="all" rel="stylesheet">\n'
|
||||
'<link href="parent1.css" media="all" rel="stylesheet">\n'
|
||||
'<script src="child.js"></script>\n'
|
||||
'<script src="parent2.js"></script>\n'
|
||||
'<script src="parent1.js"></script>'
|
||||
)
|
||||
|
||||
def test_extend_list_in_child(self):
|
||||
class Parent1Component(Component):
|
||||
template: types.django_html = """
|
||||
|
@ -1274,6 +1319,15 @@ class TestSubclassingMedia:
|
|||
assertInHTML('<script src="other2.js"></script>', rendered)
|
||||
assertInHTML('<script src="child.js"></script>', rendered)
|
||||
|
||||
assert str(ChildComponent.media) == (
|
||||
'<link href="child.css" media="all" rel="stylesheet">\n'
|
||||
'<link href="other2.css" media="all" rel="stylesheet">\n'
|
||||
'<link href="other1.css" media="all" rel="stylesheet">\n'
|
||||
'<script src="child.js"></script>\n'
|
||||
'<script src="other2.js"></script>\n'
|
||||
'<script src="other1.js"></script>'
|
||||
)
|
||||
|
||||
def test_extend_list_in_parent(self):
|
||||
class Other1Component(Component):
|
||||
class Media:
|
||||
|
@ -1327,3 +1381,16 @@ class TestSubclassingMedia:
|
|||
assertInHTML('<script src="parent1.js"></script>', rendered)
|
||||
assertInHTML('<script src="parent2.js"></script>', rendered)
|
||||
assertInHTML('<script src="child.js"></script>', rendered)
|
||||
|
||||
assert str(ChildComponent.media) == (
|
||||
'<link href="child.css" media="all" rel="stylesheet">\n'
|
||||
'<link href="parent2.css" media="all" rel="stylesheet">\n'
|
||||
'<link href="parent1.css" media="all" rel="stylesheet">\n'
|
||||
'<link href="other2.css" media="all" rel="stylesheet">\n'
|
||||
'<link href="other1.css" media="all" rel="stylesheet">\n'
|
||||
'<script src="child.js"></script>\n'
|
||||
'<script src="parent2.js"></script>\n'
|
||||
'<script src="parent1.js"></script>\n'
|
||||
'<script src="other2.js"></script>\n'
|
||||
'<script src="other1.js"></script>'
|
||||
)
|
||||
|
|
|
@ -195,11 +195,12 @@ class TestComponentAsView(SimpleTestCase):
|
|||
def get(self, request, *args, **kwargs) -> HttpResponse:
|
||||
return self.render_to_response(kwargs={"variable": self.name})
|
||||
|
||||
client = CustomClient(urlpatterns=[path("test/", MockComponentRequest("my_comp").as_view())])
|
||||
view = MockComponentRequest.as_view()
|
||||
client = CustomClient(urlpatterns=[path("test/", view)])
|
||||
response = client.get("/test/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertInHTML(
|
||||
'<input type="text" name="variable" value="my_comp">',
|
||||
'<input type="text" name="variable" value="MockComponentRequest">',
|
||||
response.content.decode(),
|
||||
)
|
||||
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import re
|
||||
from typing import Dict, Optional, cast
|
||||
|
||||
import pytest
|
||||
from django.http import HttpRequest
|
||||
from django.template import Context, RequestContext, Template
|
||||
from pytest_django.asserts import assertHTMLEqual, assertInHTML
|
||||
|
@ -916,25 +914,24 @@ class TestContextProcessors:
|
|||
assert child_data["dummy"] == "a1bc3f"
|
||||
assert parent_data["csrf_token"] == child_data["csrf_token"]
|
||||
|
||||
def test_raises_on_accessing_context_processors_data_outside_of_rendering(self):
|
||||
def test_context_processors_data_outside_of_rendering(self):
|
||||
class TestComponent(Component):
|
||||
template: types.django_html = """{% csrf_token %}"""
|
||||
|
||||
with pytest.raises(
|
||||
RuntimeError,
|
||||
match=re.escape("Tried to access Component's `context_processors_data` attribute while outside of rendering execution"), # noqa: E501
|
||||
):
|
||||
TestComponent().context_processors_data
|
||||
request = HttpRequest()
|
||||
component = TestComponent(request=request)
|
||||
data = component.context_processors_data
|
||||
|
||||
def test_raises_on_accessing_request_outside_of_rendering(self):
|
||||
assert list(data.keys()) == ["csrf_token"]
|
||||
|
||||
def test_request_outside_of_rendering(self):
|
||||
class TestComponent(Component):
|
||||
template: types.django_html = """{% csrf_token %}"""
|
||||
|
||||
with pytest.raises(
|
||||
RuntimeError,
|
||||
match=re.escape("Tried to access Component's `request` attribute while outside of rendering execution"),
|
||||
):
|
||||
TestComponent().request
|
||||
request = HttpRequest()
|
||||
component = TestComponent(request=request)
|
||||
|
||||
assert component.request == request
|
||||
|
||||
|
||||
@djc_test
|
||||
|
|
|
@ -20,6 +20,7 @@ from django_components.extension import (
|
|||
OnComponentUnregisteredContext,
|
||||
OnComponentInputContext,
|
||||
OnComponentDataContext,
|
||||
OnSlotRenderedContext,
|
||||
)
|
||||
from django_components.extensions.cache import CacheExtension
|
||||
from django_components.extensions.debug_highlight import DebugHighlightExtension
|
||||
|
@ -59,6 +60,7 @@ class DummyExtension(ComponentExtension):
|
|||
"on_component_unregistered": [],
|
||||
"on_component_input": [],
|
||||
"on_component_data": [],
|
||||
"on_slot_rendered": [],
|
||||
}
|
||||
|
||||
urls = [
|
||||
|
@ -94,6 +96,9 @@ class DummyExtension(ComponentExtension):
|
|||
def on_component_data(self, ctx: OnComponentDataContext) -> None:
|
||||
self.calls["on_component_data"].append(ctx)
|
||||
|
||||
def on_slot_rendered(self, ctx: OnSlotRenderedContext) -> None:
|
||||
self.calls["on_slot_rendered"].append(ctx)
|
||||
|
||||
|
||||
class DummyNestedExtension(ComponentExtension):
|
||||
name = "test_nested_extension"
|
||||
|
@ -306,6 +311,36 @@ class TestExtensionHooks:
|
|||
assert data_call.js_data == {"script": "console.log('Hello!')"}
|
||||
assert data_call.css_data == {"style": "body { color: blue; }"}
|
||||
|
||||
@djc_test(components_settings={"extensions": [DummyExtension]})
|
||||
def test_on_slot_rendered(self):
|
||||
@register("test_comp")
|
||||
class TestComponent(Component):
|
||||
template = "Hello {% slot 'content' required default / %}!"
|
||||
|
||||
# Render the component with some args and kwargs
|
||||
test_context = Context({"foo": "bar"})
|
||||
TestComponent.render(
|
||||
context=test_context,
|
||||
args=("arg1", "arg2"),
|
||||
kwargs={"name": "Test"},
|
||||
slots={"content": "Some content"},
|
||||
)
|
||||
|
||||
extension = cast(DummyExtension, app_settings.EXTENSIONS[4])
|
||||
|
||||
# Verify on_slot_rendered was called with correct args
|
||||
assert len(extension.calls["on_slot_rendered"]) == 1
|
||||
slot_call: OnSlotRenderedContext = extension.calls["on_slot_rendered"][0]
|
||||
assert isinstance(slot_call.component, TestComponent)
|
||||
assert slot_call.component_cls == TestComponent
|
||||
|
||||
assert slot_call.component_id == "ca1bc3e"
|
||||
assert isinstance(slot_call.slot, Slot)
|
||||
assert slot_call.slot_name == "content"
|
||||
assert slot_call.slot_is_required is True
|
||||
assert slot_call.slot_is_default is True
|
||||
assert slot_call.result == "Some content"
|
||||
|
||||
|
||||
@djc_test
|
||||
class TestExtensionViews:
|
||||
|
|
|
@ -500,7 +500,7 @@ class TestDynamicComponentTemplateTag:
|
|||
)
|
||||
|
||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||
def test_raises_on_invalid_args(self, components_settings):
|
||||
def test_raises_on_invalid_input(self, components_settings):
|
||||
registry.register(name="test", component=self.SimpleComponent)
|
||||
|
||||
simple_tag_template: types.django_html = """
|
||||
|
|
|
@ -772,7 +772,7 @@ class TestInject:
|
|||
self._assert_clear_cache()
|
||||
|
||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||
def test_inject_raises_on_called_outside_get_context_data(self, components_settings):
|
||||
def test_inject_called_outside_rendering(self, components_settings):
|
||||
@register("injectee")
|
||||
class InjectComponent(Component):
|
||||
template: types.django_html = """
|
||||
|
@ -783,9 +783,8 @@ class TestInject:
|
|||
var = self.inject("abc", "default")
|
||||
return {"var": var}
|
||||
|
||||
comp = InjectComponent("")
|
||||
with pytest.raises(RuntimeError):
|
||||
comp.inject("abc", "def")
|
||||
comp = InjectComponent()
|
||||
comp.inject("abc", "def")
|
||||
|
||||
self._assert_clear_cache()
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue