mirror of
https://github.com/django-components/django-components.git
synced 2025-09-20 12:49:45 +00:00
feat: component URL (#1088)
* feat: allow to set defaults * refactor: remove input validation and link to it * feat: component URL * refactor: fix linter errors * refactor: fix linter errors + update examples to use Component.View..get * docs: update comment * refactor: revert change to hash_comp_cls * docs: update comment
This commit is contained in:
parent
3555411f1e
commit
a49f5e51dd
37 changed files with 987 additions and 314 deletions
53
CHANGELOG.md
53
CHANGELOG.md
|
@ -1,5 +1,56 @@
|
||||||
# Release notes
|
# Release notes
|
||||||
|
|
||||||
|
## v0.137
|
||||||
|
|
||||||
|
#### Feat
|
||||||
|
|
||||||
|
- It's now easier to create URLs for component views.
|
||||||
|
|
||||||
|
Before, you had to call `Component.as_view()` and pass that to `urlpatterns`.
|
||||||
|
|
||||||
|
Now this can be done for you if you set `Component.Url.public` to `True`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class MyComponent(Component):
|
||||||
|
class Url:
|
||||||
|
public = True
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, to get the URL for the component, use `get_component_url()`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from django_components import get_component_url
|
||||||
|
|
||||||
|
url = get_component_url(MyComponent)
|
||||||
|
```
|
||||||
|
|
||||||
|
This way you don't have to mix your app URLs with component URLs.
|
||||||
|
|
||||||
|
Read more on [Component views and URLs](https://django-components.github.io/django-components/0.135/concepts/fundamentals/component_views_urls/).
|
||||||
|
|
||||||
|
#### Deprecation
|
||||||
|
|
||||||
|
- Currently, view request handlers such as `get()` and `post()` methods can be defined
|
||||||
|
directly on the `Component` class:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class MyComponent(Component):
|
||||||
|
def get(self, request):
|
||||||
|
return self.render_to_response()
|
||||||
|
```
|
||||||
|
|
||||||
|
Or, nested within the `Component.View` class:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class MyComponent(Component):
|
||||||
|
class View:
|
||||||
|
def get(self, request):
|
||||||
|
return self.render_to_response()
|
||||||
|
```
|
||||||
|
|
||||||
|
In v1, these methods should be defined only on the `Component.View` class instead.
|
||||||
|
|
||||||
## 🚨📢 v0.136
|
## 🚨📢 v0.136
|
||||||
|
|
||||||
#### 🚨📢 BREAKING CHANGES
|
#### 🚨📢 BREAKING CHANGES
|
||||||
|
@ -77,6 +128,8 @@ where each class name or style property can be managed separately.
|
||||||
%}
|
%}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Read more on [HTML attributes](https://django-components.github.io/django-components/0.135/concepts/fundamentals/html_attributes/).
|
||||||
|
|
||||||
#### Fix
|
#### Fix
|
||||||
|
|
||||||
- Fix compat with Windows when reading component files ([#1074](https://github.com/django-components/django-components/issues/1074))
|
- Fix compat with Windows when reading component files ([#1074](https://github.com/django-components/django-components/issues/1074))
|
||||||
|
|
16
README.md
16
README.md
|
@ -259,13 +259,15 @@ Read more about [HTML attributes](https://django-components.github.io/django-com
|
||||||
class Calendar(Component):
|
class Calendar(Component):
|
||||||
template_file = "calendar.html"
|
template_file = "calendar.html"
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
class View:
|
||||||
page = request.GET.get("page", 1)
|
def get(self, request, *args, **kwargs):
|
||||||
return self.render_to_response(
|
page = request.GET.get("page", 1)
|
||||||
kwargs={
|
return Calendar.render_to_response(
|
||||||
"page": page,
|
request=request,
|
||||||
}
|
kwargs={
|
||||||
)
|
"page": page,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def get_context_data(self, page):
|
def get_context_data(self, page):
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -65,10 +65,10 @@ to configure the extensions on a per-component basis.
|
||||||
|
|
||||||
### Example: Component as View
|
### Example: Component as View
|
||||||
|
|
||||||
The [Components as Views](../../fundamentals/components_as_views) feature is actually implemented as an extension
|
The [Components as Views](../../fundamentals/component_views_urls) feature is actually implemented as an extension
|
||||||
that is configured by a `View` nested class.
|
that is configured by a `View` nested class.
|
||||||
|
|
||||||
You can override the `get`, `post`, etc methods to customize the behavior of the component as a view:
|
You can override the `get()`, `post()`, etc methods to customize the behavior of the component as a view:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class MyTable(Component):
|
class MyTable(Component):
|
||||||
|
|
|
@ -106,8 +106,9 @@ from django_components import Component, types
|
||||||
|
|
||||||
# HTML into which a fragment will be loaded using HTMX
|
# HTML into which a fragment will be loaded using HTMX
|
||||||
class MyPage(Component):
|
class MyPage(Component):
|
||||||
def get(self, request):
|
Class View:
|
||||||
return self.render_to_response()
|
def get(self, request):
|
||||||
|
return self.component.render_to_response(request=request)
|
||||||
|
|
||||||
template = """
|
template = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
|
@ -138,11 +139,13 @@ class MyPage(Component):
|
||||||
|
|
||||||
```djc_py title="[root]/components/demo.py"
|
```djc_py title="[root]/components/demo.py"
|
||||||
class Frag(Component):
|
class Frag(Component):
|
||||||
def get(self, request):
|
class View:
|
||||||
return self.render_to_response(
|
def get(self, request):
|
||||||
# IMPORTANT: Don't forget `type="fragment"`
|
return self.component.render_to_response(
|
||||||
type="fragment",
|
request=request,
|
||||||
)
|
# IMPORTANT: Don't forget `type="fragment"`
|
||||||
|
type="fragment",
|
||||||
|
)
|
||||||
|
|
||||||
template = """
|
template = """
|
||||||
<div class="frag">
|
<div class="frag">
|
||||||
|
@ -184,8 +187,9 @@ from django_components import Component, types
|
||||||
|
|
||||||
# HTML into which a fragment will be loaded using AlpineJS
|
# HTML into which a fragment will be loaded using AlpineJS
|
||||||
class MyPage(Component):
|
class MyPage(Component):
|
||||||
def get(self, request):
|
class View:
|
||||||
return self.render_to_response()
|
def get(self, request):
|
||||||
|
return self.component.render_to_response(request=request)
|
||||||
|
|
||||||
template = """
|
template = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
|
@ -222,11 +226,13 @@ class MyPage(Component):
|
||||||
|
|
||||||
```djc_py title="[root]/components/demo.py"
|
```djc_py title="[root]/components/demo.py"
|
||||||
class Frag(Component):
|
class Frag(Component):
|
||||||
def get(self, request):
|
class View:
|
||||||
# IMPORTANT: Don't forget `type="fragment"`
|
def get(self, request):
|
||||||
return self.render_to_response(
|
return self.component.render_to_response(
|
||||||
type="fragment",
|
request=request,
|
||||||
)
|
# IMPORTANT: Don't forget `type="fragment"`
|
||||||
|
type="fragment",
|
||||||
|
)
|
||||||
|
|
||||||
# NOTE: We wrap the actual fragment in a template tag with x-if="false" to prevent it
|
# NOTE: We wrap the actual fragment in a template tag with x-if="false" to prevent it
|
||||||
# from being rendered until we have registered the component with AlpineJS.
|
# from being rendered until we have registered the component with AlpineJS.
|
||||||
|
@ -281,8 +287,9 @@ from django_components import Component, types
|
||||||
|
|
||||||
# HTML into which a fragment will be loaded using JS
|
# HTML into which a fragment will be loaded using JS
|
||||||
class MyPage(Component):
|
class MyPage(Component):
|
||||||
def get(self, request):
|
class View:
|
||||||
return self.render_to_response()
|
def get(self, request):
|
||||||
|
return self.component.render_to_response(request=request)
|
||||||
|
|
||||||
template = """
|
template = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
|
@ -318,11 +325,13 @@ class MyPage(Component):
|
||||||
|
|
||||||
```djc_py title="[root]/components/demo.py"
|
```djc_py title="[root]/components/demo.py"
|
||||||
class Frag(Component):
|
class Frag(Component):
|
||||||
def get(self, request):
|
class View:
|
||||||
return self.render_to_response(
|
def get(self, request):
|
||||||
# IMPORTANT: Don't forget `type="fragment"`
|
return self.component.render_to_response(
|
||||||
type="fragment",
|
request=request,
|
||||||
)
|
# IMPORTANT: Don't forget `type="fragment"`
|
||||||
|
type="fragment",
|
||||||
|
)
|
||||||
|
|
||||||
template = """
|
template = """
|
||||||
<div class="frag">
|
<div class="frag">
|
||||||
|
|
|
@ -10,6 +10,6 @@ nav:
|
||||||
- HTML attributes: html_attributes.md
|
- HTML attributes: html_attributes.md
|
||||||
- Defining HTML / JS / CSS files: defining_js_css_html_files.md
|
- Defining HTML / JS / CSS files: defining_js_css_html_files.md
|
||||||
- Autodiscovery: autodiscovery.md
|
- Autodiscovery: autodiscovery.md
|
||||||
- Components as views: components_as_views.md
|
- Component views and URLs: component_views_urls.md
|
||||||
- HTTP Request: http_request.md
|
- HTTP Request: http_request.md
|
||||||
- Subclassing components: subclassing_components.md
|
- Subclassing components: subclassing_components.md
|
||||||
|
|
128
docs/concepts/fundamentals/component_views_urls.md
Normal file
128
docs/concepts/fundamentals/component_views_urls.md
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
_New in version 0.34_
|
||||||
|
|
||||||
|
_Note: Since 0.92, `Component` is no longer a subclass of Django's `View`. Instead, the nested
|
||||||
|
[`Component.View`](../../../reference/api#django_components.Component.View) class is a subclass of Django's `View`._
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For web applications, it's common to define endpoints that serve HTML content (AKA views).
|
||||||
|
|
||||||
|
django-components has a suite of features that help you write and manage views and their URLs:
|
||||||
|
|
||||||
|
- For each component, you can define methods for handling HTTP requests (GET, POST, etc.) - `get()`, `post()`, etc.
|
||||||
|
|
||||||
|
- Use [`Component.as_view()`](../../../reference/api#django_components.Component.as_view) to be able to use your Components with Django's [`urlpatterns`](https://docs.djangoproject.com/en/5.1/topics/http/urls/). This works the same way as [`View.as_view()`](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.View.as_view).
|
||||||
|
|
||||||
|
- To avoid having to manually define the endpoints for each component, you can set the component to be "public" with [`Component.Url.public = True`](../../../reference/api#django_components.ComponentUrl.public). This will automatically create a URL for the component. To retrieve the component URL, use [`get_component_url()`](../../../reference/api#django_components.get_component_url).
|
||||||
|
|
||||||
|
- In addition, [`Component`](../../../reference/api#django_components.Component) has a [`render_to_response()`](../../../reference/api#django_components.Component.render_to_response) method that renders the component template based on the provided input and returns an `HttpResponse` object.
|
||||||
|
|
||||||
|
## Component as view example
|
||||||
|
|
||||||
|
### Define handlers
|
||||||
|
|
||||||
|
Here's an example of a calendar component defined as a view. Simply define a `View` class with your custom `get()` method to handle GET requests:
|
||||||
|
|
||||||
|
```djc_py title="[project root]/components/calendar.py"
|
||||||
|
from django_components import Component, ComponentView, register
|
||||||
|
|
||||||
|
@register("calendar")
|
||||||
|
class Calendar(Component):
|
||||||
|
template = """
|
||||||
|
<div class="calendar-component">
|
||||||
|
<div class="header">
|
||||||
|
{% slot "header" / %}
|
||||||
|
</div>
|
||||||
|
<div class="body">
|
||||||
|
Today's date is <span>{{ date }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
class View:
|
||||||
|
# Handle GET requests
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
# Return HttpResponse with the rendered content
|
||||||
|
return Calendar.render_to_response(
|
||||||
|
request=request,
|
||||||
|
kwargs={
|
||||||
|
"date": request.GET.get("date", "2020-06-06"),
|
||||||
|
},
|
||||||
|
slots={
|
||||||
|
"header": "Calendar header",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
|
||||||
|
The View class supports all the same HTTP methods as Django's [`View`](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.View) class. These are:
|
||||||
|
|
||||||
|
`get()`, `post()`, `put()`, `patch()`, `delete()`, `head()`, `options()`, `trace()`
|
||||||
|
|
||||||
|
Each of these receive the [`HttpRequest`](https://docs.djangoproject.com/en/5.1/ref/request-response/#django.http.HttpRequest) object as the first argument.
|
||||||
|
|
||||||
|
|
||||||
|
<!-- TODO_V1 REMOVE -->
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
**Deprecation warning:**
|
||||||
|
|
||||||
|
Previously, the handler methods such as `get()` and `post()` could be defined directly on the `Component` class:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Calendar(Component):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
return self.render_to_response(
|
||||||
|
kwargs={
|
||||||
|
"date": request.GET.get("date", "2020-06-06"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
This is deprecated from v0.137 onwards, and will be removed in v1.0.
|
||||||
|
|
||||||
|
### Register the URLs manually
|
||||||
|
|
||||||
|
To register the component as a route / endpoint in Django, add an entry to your
|
||||||
|
[`urlpatterns`](https://docs.djangoproject.com/en/5.1/topics/http/urls/).
|
||||||
|
In place of the view function, create a view object with [`Component.as_view()`](../../../reference/api#django_components.Component.as_view):
|
||||||
|
|
||||||
|
```python title="[project root]/urls.py"
|
||||||
|
from django.urls import path
|
||||||
|
from components.calendar.calendar import Calendar
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("calendar/", Calendar.as_view()),
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
[`Component.as_view()`](../../../reference/api#django_components.Component.as_view)
|
||||||
|
internally calls [`View.as_view()`](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.View.as_view), passing the component
|
||||||
|
instance as one of the arguments.
|
||||||
|
|
||||||
|
### Register the URLs automatically
|
||||||
|
|
||||||
|
If you don't care about the exact URL of the component, you can let django-components manage the URLs for you by setting the [`Component.Url.public`](../../../reference/api#django_components.ComponentUrl.public) attribute to `True`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class MyComponent(Component):
|
||||||
|
class Url:
|
||||||
|
public = True
|
||||||
|
|
||||||
|
class View:
|
||||||
|
def get(self, request):
|
||||||
|
return self.component.render_to_response(request=request)
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, to get the URL for the component, use [`get_component_url()`](../../../reference/api#django_components.get_component_url):
|
||||||
|
|
||||||
|
```py
|
||||||
|
from django_components import get_component_url
|
||||||
|
|
||||||
|
url = get_component_url(MyComponent)
|
||||||
|
```
|
||||||
|
|
||||||
|
This way you don't have to mix your app URLs with component URLs.
|
|
@ -1,148 +0,0 @@
|
||||||
_New in version 0.34_
|
|
||||||
|
|
||||||
_Note: Since 0.92, Component no longer subclasses View. To configure the View class, set the nested `Component.View` class_
|
|
||||||
|
|
||||||
Components can now be used as views:
|
|
||||||
|
|
||||||
- Components define the `Component.as_view()` class method that can be used the same as [`View.as_view()`](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.View.as_view).
|
|
||||||
|
|
||||||
- By default, you can define GET, POST or other HTTP handlers directly on the Component, same as you do with [View](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#view). For example, you can override `get` and `post` to handle GET and POST requests, respectively.
|
|
||||||
|
|
||||||
- In addition, `Component` now has a [`render_to_response`](#inputs-of-render-and-render_to_response) method that renders the component template based on the provided context and slots' data and returns an `HttpResponse` object.
|
|
||||||
|
|
||||||
## Component as view example
|
|
||||||
|
|
||||||
Here's an example of a calendar component defined as a view:
|
|
||||||
|
|
||||||
```djc_py
|
|
||||||
# In a file called [project root]/components/calendar.py
|
|
||||||
from django_components import Component, ComponentView, register
|
|
||||||
|
|
||||||
@register("calendar")
|
|
||||||
class Calendar(Component):
|
|
||||||
|
|
||||||
template = """
|
|
||||||
<div class="calendar-component">
|
|
||||||
<div class="header">
|
|
||||||
{% slot "header" / %}
|
|
||||||
</div>
|
|
||||||
<div class="body">
|
|
||||||
Today's date is <span>{{ date }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Handle GET requests
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
context = {
|
|
||||||
"date": request.GET.get("date", "2020-06-06"),
|
|
||||||
}
|
|
||||||
slots = {
|
|
||||||
"header": "Calendar header",
|
|
||||||
}
|
|
||||||
# Return HttpResponse with the rendered content
|
|
||||||
return self.render_to_response(
|
|
||||||
context=context,
|
|
||||||
slots=slots,
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, to use this component as a view, you should create a `urls.py` file in your components directory, and add a path to the component's view:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# In a file called [project root]/components/urls.py
|
|
||||||
from django.urls import path
|
|
||||||
from components.calendar.calendar import Calendar
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path("calendar/", Calendar.as_view()),
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
`Component.as_view()` is a shorthand for calling [`View.as_view()`](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.View.as_view) and passing the component
|
|
||||||
instance as one of the arguments.
|
|
||||||
|
|
||||||
Remember to add `__init__.py` to your components directory, so that Django can find the `urls.py` file.
|
|
||||||
|
|
||||||
Finally, include the component's urls in your project's `urls.py` file:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# In a file called [project root]/urls.py
|
|
||||||
from django.urls import include, path
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path("components/", include("components.urls")),
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: Slots content are automatically escaped by default to prevent XSS attacks. To disable escaping, set `escape_slots_content=False` in the `render_to_response` method. If you do so, you should make sure that any content you pass to the slots is safe, especially if it comes from user input.
|
|
||||||
|
|
||||||
If you're planning on passing an HTML string, check Django's use of [`format_html`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.html.format_html) and [`mark_safe`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.safestring.mark_safe).
|
|
||||||
|
|
||||||
## Modifying the View class
|
|
||||||
|
|
||||||
The View class that handles the requests is defined on `Component.View`.
|
|
||||||
|
|
||||||
When you define a GET or POST handlers on the `Component` class, like so:
|
|
||||||
|
|
||||||
```py
|
|
||||||
class MyComponent(Component):
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
return self.render_to_response(
|
|
||||||
context={
|
|
||||||
"date": request.GET.get("date", "2020-06-06"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs) -> HttpResponse:
|
|
||||||
variable = request.POST.get("variable")
|
|
||||||
return self.render_to_response(
|
|
||||||
kwargs={"variable": variable}
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Then the request is still handled by `Component.View.get()` or `Component.View.post()`
|
|
||||||
methods. However, by default, `Component.View.get()` points to `Component.get()`, and so on.
|
|
||||||
|
|
||||||
```py
|
|
||||||
class ComponentView(View):
|
|
||||||
component: Component = None
|
|
||||||
...
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
return self.component.get(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
return self.component.post(request, *args, **kwargs)
|
|
||||||
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
If you want to define your own `View` class, you need to:
|
|
||||||
|
|
||||||
1. Set the class as `Component.View`
|
|
||||||
2. Subclass from `ComponentView`, so the View instance has access to the component instance.
|
|
||||||
|
|
||||||
In the example below, we added extra logic into `View.setup()`.
|
|
||||||
|
|
||||||
Note that the POST handler is still defined at the top. This is because `View` subclasses `ComponentView`, which defines the `post()` method that calls `Component.post()`.
|
|
||||||
|
|
||||||
If you were to overwrite the `View.post()` method, then `Component.post()` would be ignored.
|
|
||||||
|
|
||||||
```py
|
|
||||||
from django_components import Component, ComponentView
|
|
||||||
|
|
||||||
class MyComponent(Component):
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs) -> HttpResponse:
|
|
||||||
variable = request.POST.get("variable")
|
|
||||||
return self.component.render_to_response(
|
|
||||||
kwargs={"variable": variable}
|
|
||||||
)
|
|
||||||
|
|
||||||
class View(ComponentView):
|
|
||||||
def setup(self, request, *args, **kwargs):
|
|
||||||
super(request, *args, **kwargs)
|
|
||||||
|
|
||||||
do_something_extra(request, *args, **kwargs)
|
|
||||||
```
|
|
|
@ -663,3 +663,42 @@ class MyTable(Component):
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Escaping slots content
|
||||||
|
|
||||||
|
Slots content are automatically escaped by default to prevent XSS attacks.
|
||||||
|
|
||||||
|
In other words, it's as if you would be using Django's [`mark_safe()`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.safestring.mark_safe) function on the slot content:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
class Calendar(Component):
|
||||||
|
template = """
|
||||||
|
<div>
|
||||||
|
{% slot "date" default date=date / %}
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
Calendar.render(
|
||||||
|
slots={
|
||||||
|
"date": mark_safe("<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.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
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!
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
|
||||||
|
If you're planning on passing an HTML string, check Django's use of
|
||||||
|
[`format_html`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.html.format_html)
|
||||||
|
and [`mark_safe`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.safestring.mark_safe).
|
||||||
|
|
|
@ -25,7 +25,7 @@ class Calendar(Component):
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 1. Render the template that contains the `{% component %}` tag
|
### 1. Render the template
|
||||||
|
|
||||||
If you have embedded the component in a Django template using the
|
If you have embedded the component in a Django template using the
|
||||||
[`{% component %}`](../../reference/template_tags#component) tag:
|
[`{% component %}`](../../reference/template_tags#component) tag:
|
||||||
|
@ -74,7 +74,7 @@ template = Template("""
|
||||||
rendered_template = template.render()
|
rendered_template = template.render()
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Render the component directly with [`Component.render()`](../../reference/api#django_components.Component.render)
|
### 2. Render the component
|
||||||
|
|
||||||
You can also render the component directly with [`Component.render()`](../../reference/api#django_components.Component.render), without wrapping the component in a template.
|
You can also render the component directly with [`Component.render()`](../../reference/api#django_components.Component.render), without wrapping the component in a template.
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ rendered_component = calendar.render(
|
||||||
|
|
||||||
The `request` object is required for some of the component's features, like using [Django's context processors](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.RequestContext).
|
The `request` object is required for some of the component's features, like using [Django's context processors](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.RequestContext).
|
||||||
|
|
||||||
### 3. Render the component directly with [`Component.render_to_response()`](../../reference/api#django_components.Component.render_to_response)
|
### 3. Render the component to HttpResponse
|
||||||
|
|
||||||
A common pattern in Django is to render the component and then return the resulting HTML as a response to an HTTP request.
|
A common pattern in Django is to render the component and then return the resulting HTML as a response to an HTTP request.
|
||||||
|
|
||||||
|
@ -159,3 +159,133 @@ def my_view(request):
|
||||||
class SimpleComponent(Component):
|
class SimpleComponent(Component):
|
||||||
response_class = MyCustomResponse
|
response_class = MyCustomResponse
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Rendering slots
|
||||||
|
|
||||||
|
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.0/ref/utils/#django.utils.safestring.mark_safe) function on the slot content:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
class Calendar(Component):
|
||||||
|
template = """
|
||||||
|
<div>
|
||||||
|
{% slot "date" default date=date / %}
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
Calendar.render(
|
||||||
|
slots={
|
||||||
|
"date": mark_safe("<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.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
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!
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
|
||||||
|
If you're planning on passing an HTML string, check Django's use of
|
||||||
|
[`format_html`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.html.format_html)
|
||||||
|
and [`mark_safe`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.safestring.mark_safe).
|
||||||
|
|
||||||
|
### Component views and URLs
|
||||||
|
|
||||||
|
For web applications, it's common to define endpoints that serve HTML content (AKA views).
|
||||||
|
|
||||||
|
If this is your case, you can define the view request handlers directly on your component by using the nested[`Component.View`](../../reference/api#django_components.Component.View) class.
|
||||||
|
|
||||||
|
This is a great place for:
|
||||||
|
|
||||||
|
- Endpoints that render whole pages, if your component
|
||||||
|
is a page component.
|
||||||
|
|
||||||
|
- Endpoints that render the component as HTML fragments, to be used with HTMX or similar libraries.
|
||||||
|
|
||||||
|
Read more on [Component views and URLs](../../concepts/fundamentals/component_views_urls).
|
||||||
|
|
||||||
|
```djc_py title="[project root]/components/calendar.py"
|
||||||
|
from django_components import Component, ComponentView, register
|
||||||
|
|
||||||
|
@register("calendar")
|
||||||
|
class Calendar(Component):
|
||||||
|
template = """
|
||||||
|
<div class="calendar-component">
|
||||||
|
<div class="header">
|
||||||
|
{% slot "header" / %}
|
||||||
|
</div>
|
||||||
|
<div class="body">
|
||||||
|
Today's date is <span>{{ date }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
class View:
|
||||||
|
# Handle GET requests
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
# Return HttpResponse with the rendered content
|
||||||
|
return Calendar.render_to_response(
|
||||||
|
request=request,
|
||||||
|
kwargs={
|
||||||
|
"date": request.GET.get("date", "2020-06-06"),
|
||||||
|
},
|
||||||
|
slots={
|
||||||
|
"header": "Calendar header",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
|
||||||
|
The View class supports all the same HTTP methods as Django's [`View`](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.View) class. These are:
|
||||||
|
|
||||||
|
`get()`, `post()`, `put()`, `patch()`, `delete()`, `head()`, `options()`, `trace()`
|
||||||
|
|
||||||
|
Each of these receive the [`HttpRequest`](https://docs.djangoproject.com/en/5.1/ref/request-response/#django.http.HttpRequest) object as the first argument.
|
||||||
|
|
||||||
|
Next, you need to set the URL for the component.
|
||||||
|
|
||||||
|
You can either:
|
||||||
|
|
||||||
|
1. Automatically assign the URL by setting the [`Component.Url.public`](../../reference/api#django_components.ComponentUrl.public) attribute to `True`.
|
||||||
|
|
||||||
|
In this case, use [`get_component_url()`](../../reference/api#django_components.get_component_url) to get the URL for the component view.
|
||||||
|
|
||||||
|
```djc_py
|
||||||
|
from django_components import Component, get_component_url
|
||||||
|
|
||||||
|
class Calendar(Component):
|
||||||
|
class Url:
|
||||||
|
public = True
|
||||||
|
|
||||||
|
url = get_component_url(Calendar)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Manually assign the URL by setting [`Component.as_view()`](../../reference/api#django_components.Component.as_view) to your `urlpatterns`:
|
||||||
|
|
||||||
|
```djc_py
|
||||||
|
from django.urls import path
|
||||||
|
from components.calendar import Calendar
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("calendar/", Calendar.as_view()),
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
And with that, you're all set! When you visit the URL, the component will be rendered and the content will be returned.
|
||||||
|
|
||||||
|
The `get()`, `post()`, etc methods will receive the [`HttpRequest`](https://docs.djangoproject.com/en/5.1/ref/request-response/#django.http.HttpRequest) object as the first argument. So you can parametrize how the component is rendered for example by passing extra query parameters to the URL:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:8000/calendar/?date=2024-12-13
|
||||||
|
```
|
||||||
|
|
|
@ -249,13 +249,15 @@ Read more about [HTML attributes](../../concepts/fundamentals/html_attributes/).
|
||||||
class Calendar(Component):
|
class Calendar(Component):
|
||||||
template_file = "calendar.html"
|
template_file = "calendar.html"
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
class View:
|
||||||
page = request.GET.get("page", 1)
|
def get(self, request, *args, **kwargs):
|
||||||
return self.render_to_response(
|
page = request.GET.get("page", 1)
|
||||||
kwargs={
|
return self.component.render_to_response(
|
||||||
"page": page,
|
request=request,
|
||||||
}
|
kwargs={
|
||||||
)
|
"page": page,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def get_context_data(self, page):
|
def get_context_data(self, page):
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -35,6 +35,10 @@
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
|
||||||
|
::: django_components.ComponentUrl
|
||||||
|
options:
|
||||||
|
show_if_no_docstring: true
|
||||||
|
|
||||||
::: django_components.ComponentVars
|
::: django_components.ComponentVars
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
@ -123,6 +127,10 @@
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
|
||||||
|
::: django_components.get_component_url
|
||||||
|
options:
|
||||||
|
show_if_no_docstring: true
|
||||||
|
|
||||||
::: django_components.import_libraries
|
::: django_components.import_libraries
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
|
|
@ -462,8 +462,8 @@ ProjectDashboardAction project.components.dashboard_action.ProjectDashboardAc
|
||||||
## `upgradecomponent`
|
## `upgradecomponent`
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
|
usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH]
|
||||||
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color] [--skip-checks]
|
[--traceback] [--no-color] [--force-color] [--skip-checks]
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -507,9 +507,9 @@ Deprecated. Use `components upgrade` instead.
|
||||||
## `startcomponent`
|
## `startcomponent`
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose]
|
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose] [--dry-run]
|
||||||
[--dry-run] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH]
|
[--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback]
|
||||||
[--traceback] [--no-color] [--force-color] [--skip-checks]
|
[--no-color] [--force-color] [--skip-checks]
|
||||||
name
|
name
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -107,6 +107,25 @@ name | type | description
|
||||||
`name` | `str` | The name the component was registered under
|
`name` | `str` | The name the component was registered under
|
||||||
`registry` | [`ComponentRegistry`](../api#django_components.ComponentRegistry) | The registry the component was registered to
|
`registry` | [`ComponentRegistry`](../api#django_components.ComponentRegistry) | The registry the component was registered to
|
||||||
|
|
||||||
|
::: django_components.extension.ComponentExtension.on_component_rendered
|
||||||
|
options:
|
||||||
|
show_root_heading: true
|
||||||
|
show_signature: true
|
||||||
|
separate_signature: true
|
||||||
|
show_symbol_type_heading: false
|
||||||
|
show_symbol_type_toc: false
|
||||||
|
show_if_no_docstring: true
|
||||||
|
show_labels: false
|
||||||
|
|
||||||
|
**Available data:**
|
||||||
|
|
||||||
|
name | type | description
|
||||||
|
--|--|--
|
||||||
|
`component` | [`Component`](../api#django_components.Component) | The Component instance that is being rendered
|
||||||
|
`component_cls` | [`Type[Component]`](../api#django_components.Component) | The Component class
|
||||||
|
`component_id` | `str` | The unique identifier for this component instance
|
||||||
|
`template` | `str` | The rendered template
|
||||||
|
|
||||||
::: django_components.extension.ComponentExtension.on_component_unregistered
|
::: django_components.extension.ComponentExtension.on_component_unregistered
|
||||||
options:
|
options:
|
||||||
heading_level: 3
|
heading_level: 3
|
||||||
|
|
|
@ -18,12 +18,14 @@ class Calendar(Component):
|
||||||
"date": date,
|
"date": date,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
class View:
|
||||||
return self.render_to_response(
|
def get(self, request, *args, **kwargs):
|
||||||
kwargs={
|
return Calendar.render_to_response(
|
||||||
"date": request.GET.get("date", ""),
|
request=request,
|
||||||
},
|
kwargs={
|
||||||
)
|
"date": request.GET.get("date", ""),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@register("calendar_relative")
|
@register("calendar_relative")
|
||||||
|
@ -43,9 +45,11 @@ class CalendarRelative(Component):
|
||||||
"date": date,
|
"date": date,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
class View:
|
||||||
return self.render_to_response(
|
def get(self, request, *args, **kwargs):
|
||||||
kwargs={
|
return CalendarRelative.render_to_response(
|
||||||
"date": request.GET.get("date", ""),
|
request=request,
|
||||||
},
|
kwargs={
|
||||||
)
|
"date": request.GET.get("date", ""),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
|
@ -3,8 +3,9 @@ from django_components import Component, types
|
||||||
|
|
||||||
# HTML into which a fragment will be loaded using vanilla JS
|
# HTML into which a fragment will be loaded using vanilla JS
|
||||||
class FragmentBaseJs(Component):
|
class FragmentBaseJs(Component):
|
||||||
def get(self, request):
|
class View:
|
||||||
return self.render_to_response()
|
def get(self, request):
|
||||||
|
return FragmentBaseJs.render_to_response(request=request)
|
||||||
|
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
|
@ -39,8 +40,9 @@ class FragmentBaseJs(Component):
|
||||||
|
|
||||||
# HTML into which a fragment will be loaded using AlpineJs
|
# HTML into which a fragment will be loaded using AlpineJs
|
||||||
class FragmentBaseAlpine(Component):
|
class FragmentBaseAlpine(Component):
|
||||||
def get(self, request):
|
class View:
|
||||||
return self.render_to_response()
|
def get(self, request):
|
||||||
|
return FragmentBaseAlpine.render_to_response(request=request)
|
||||||
|
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
|
@ -76,8 +78,9 @@ class FragmentBaseAlpine(Component):
|
||||||
|
|
||||||
# HTML into which a fragment will be loaded using HTMX
|
# HTML into which a fragment will be loaded using HTMX
|
||||||
class FragmentBaseHtmx(Component):
|
class FragmentBaseHtmx(Component):
|
||||||
def get(self, request):
|
class View:
|
||||||
return self.render_to_response()
|
def get(self, request):
|
||||||
|
return FragmentBaseHtmx.render_to_response(request=request)
|
||||||
|
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
|
@ -102,8 +105,9 @@ class FragmentBaseHtmx(Component):
|
||||||
|
|
||||||
# Fragment where the JS and CSS are defined on the Component
|
# Fragment where the JS and CSS are defined on the Component
|
||||||
class FragJs(Component):
|
class FragJs(Component):
|
||||||
def get(self, request):
|
class View:
|
||||||
return self.render_to_response(type="fragment")
|
def get(self, request):
|
||||||
|
return FragJs.render_to_response(request=request, type="fragment")
|
||||||
|
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
<div class="frag">
|
<div class="frag">
|
||||||
|
@ -125,8 +129,9 @@ class FragJs(Component):
|
||||||
|
|
||||||
# Fragment that defines an AlpineJS component
|
# Fragment that defines an AlpineJS component
|
||||||
class FragAlpine(Component):
|
class FragAlpine(Component):
|
||||||
def get(self, request):
|
class View:
|
||||||
return self.render_to_response(type="fragment")
|
def get(self, request):
|
||||||
|
return FragAlpine.render_to_response(request=request, type="fragment")
|
||||||
|
|
||||||
# NOTE: We wrap the actual fragment in a template tag with x-if="false" to prevent it
|
# NOTE: We wrap the actual fragment in a template tag with x-if="false" to prevent it
|
||||||
# from being rendered until we have registered the component with AlpineJS.
|
# from being rendered until we have registered the component with AlpineJS.
|
||||||
|
|
|
@ -5,14 +5,16 @@ from django_components import Component, register, types
|
||||||
|
|
||||||
@register("greeting")
|
@register("greeting")
|
||||||
class Greeting(Component):
|
class Greeting(Component):
|
||||||
def get(self, request, *args, **kwargs):
|
class View:
|
||||||
slots = {"message": "Hello, world!"}
|
def get(self, request, *args, **kwargs):
|
||||||
return self.render_to_response(
|
slots = {"message": "Hello, world!"}
|
||||||
slots=slots,
|
return Greeting.render_to_response(
|
||||||
kwargs={
|
request=request,
|
||||||
"name": request.GET.get("name", ""),
|
slots=slots,
|
||||||
},
|
kwargs={
|
||||||
)
|
"name": request.GET.get("name", ""),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def get_context_data(self, name, *args, **kwargs) -> Dict[str, Any]:
|
def get_context_data(self, name, *args, **kwargs) -> Dict[str, Any]:
|
||||||
return {"name": name}
|
return {"name": name}
|
||||||
|
|
|
@ -18,9 +18,11 @@ class CalendarNested(Component):
|
||||||
"date": date,
|
"date": date,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
class View:
|
||||||
return self.render_to_response(
|
def get(self, request, *args, **kwargs):
|
||||||
kwargs={
|
return CalendarNested.render_to_response(
|
||||||
"date": request.GET.get("date", ""),
|
request=request,
|
||||||
},
|
kwargs={
|
||||||
)
|
"date": request.GET.get("date", ""),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import time
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from django_components import Component, register, types
|
from django_components import Component, register, types
|
||||||
|
@ -5,17 +6,18 @@ from django_components import Component, register, types
|
||||||
|
|
||||||
@register("recursive")
|
@register("recursive")
|
||||||
class Recursive(Component):
|
class Recursive(Component):
|
||||||
def get(self, request):
|
class View:
|
||||||
import time
|
def get(self, request):
|
||||||
time_before = time.time()
|
time_before = time.time()
|
||||||
output = self.render_to_response(
|
output = Recursive.render_to_response(
|
||||||
kwargs={
|
request=request,
|
||||||
"depth": 0,
|
kwargs={
|
||||||
},
|
"depth": 0,
|
||||||
)
|
},
|
||||||
time_after = time.time()
|
)
|
||||||
print("TIME: ", time_after - time_before)
|
time_after = time.time()
|
||||||
return output
|
print("TIME: ", time_after - time_before)
|
||||||
|
return output
|
||||||
|
|
||||||
def get_context_data(self, depth: int = 0) -> Dict[str, Any]:
|
def get_context_data(self, depth: int = 0) -> Dict[str, Any]:
|
||||||
return {"depth": depth + 1}
|
return {"depth": depth + 1}
|
||||||
|
|
|
@ -42,6 +42,7 @@ from django_components.extension import (
|
||||||
)
|
)
|
||||||
from django_components.extensions.defaults import Default
|
from django_components.extensions.defaults import Default
|
||||||
from django_components.extensions.view import ComponentView
|
from django_components.extensions.view import ComponentView
|
||||||
|
from django_components.extensions.url import ComponentUrl, get_component_url
|
||||||
from django_components.library import TagProtectedError
|
from django_components.library import TagProtectedError
|
||||||
from django_components.node import BaseNode, template_tag
|
from django_components.node import BaseNode, template_tag
|
||||||
from django_components.slots import SlotContent, Slot, SlotFunc, SlotRef, SlotResult
|
from django_components.slots import SlotContent, Slot, SlotFunc, SlotRef, SlotResult
|
||||||
|
@ -86,6 +87,7 @@ __all__ = [
|
||||||
"ComponentRegistry",
|
"ComponentRegistry",
|
||||||
"ComponentVars",
|
"ComponentVars",
|
||||||
"ComponentView",
|
"ComponentView",
|
||||||
|
"ComponentUrl",
|
||||||
"component_formatter",
|
"component_formatter",
|
||||||
"component_shorthand_formatter",
|
"component_shorthand_formatter",
|
||||||
"ContextBehavior",
|
"ContextBehavior",
|
||||||
|
@ -96,6 +98,7 @@ __all__ = [
|
||||||
"format_attributes",
|
"format_attributes",
|
||||||
"get_component_dirs",
|
"get_component_dirs",
|
||||||
"get_component_files",
|
"get_component_files",
|
||||||
|
"get_component_url",
|
||||||
"import_libraries",
|
"import_libraries",
|
||||||
"merge_attributes",
|
"merge_attributes",
|
||||||
"NotRegistered",
|
"NotRegistered",
|
||||||
|
|
|
@ -751,9 +751,10 @@ class InternalSettings:
|
||||||
|
|
||||||
# Prepend built-in extensions
|
# Prepend built-in extensions
|
||||||
from django_components.extensions.defaults import DefaultsExtension
|
from django_components.extensions.defaults import DefaultsExtension
|
||||||
|
from django_components.extensions.url import UrlExtension
|
||||||
from django_components.extensions.view import ViewExtension
|
from django_components.extensions.view import ViewExtension
|
||||||
|
|
||||||
extensions = [DefaultsExtension, ViewExtension] + list(extensions)
|
extensions = [DefaultsExtension, ViewExtension, UrlExtension] + list(extensions)
|
||||||
|
|
||||||
# Extensions may be passed in either as classes or import strings.
|
# Extensions may be passed in either as classes or import strings.
|
||||||
extension_instances: List["ComponentExtension"] = []
|
extension_instances: List["ComponentExtension"] = []
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from typing import Any, List, Optional, Type
|
from typing import Any, Iterable, List, Optional, Type, Union
|
||||||
|
|
||||||
import django
|
import django
|
||||||
import django.urls as django_urls
|
import django.urls as django_urls
|
||||||
from django.core.management.base import BaseCommand as DjangoCommand
|
from django.core.management.base import BaseCommand as DjangoCommand
|
||||||
from django.urls import URLPattern
|
from django.urls import URLPattern, URLResolver
|
||||||
|
|
||||||
from django_components.util.command import (
|
from django_components.util.command import (
|
||||||
CommandArg,
|
CommandArg,
|
||||||
|
@ -128,7 +128,7 @@ def load_as_django_command(command: Type[ComponentCommand]) -> Type[DjangoComman
|
||||||
################################################
|
################################################
|
||||||
|
|
||||||
|
|
||||||
def routes_to_django(routes: List[URLRoute]) -> List[URLPattern]:
|
def routes_to_django(routes: Iterable[URLRoute]) -> List[Union[URLPattern, URLResolver]]:
|
||||||
"""
|
"""
|
||||||
Convert a list of `URLRoute` objects to a list of `URLPattern` objects.
|
Convert a list of `URLRoute` objects to a list of `URLPattern` objects.
|
||||||
|
|
||||||
|
@ -149,7 +149,7 @@ def routes_to_django(routes: List[URLRoute]) -> List[URLPattern]:
|
||||||
])
|
])
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
django_routes: List[URLPattern] = []
|
django_routes: List[Union[URLPattern, URLResolver]] = []
|
||||||
for route in routes:
|
for route in routes:
|
||||||
# The handler is equivalent to `view` function in Django
|
# The handler is equivalent to `view` function in Django
|
||||||
if route.handler is not None:
|
if route.handler is not None:
|
||||||
|
|
|
@ -60,7 +60,9 @@ from django_components.extension import (
|
||||||
OnComponentInputContext,
|
OnComponentInputContext,
|
||||||
extensions,
|
extensions,
|
||||||
)
|
)
|
||||||
from django_components.extensions.view import ViewFn
|
from django_components.extensions.defaults import ComponentDefaults
|
||||||
|
from django_components.extensions.url import ComponentUrl
|
||||||
|
from django_components.extensions.view import ComponentView, ViewFn
|
||||||
from django_components.node import BaseNode
|
from django_components.node import BaseNode
|
||||||
from django_components.perfutil.component import ComponentRenderer, component_context_cache, component_post_render
|
from django_components.perfutil.component import ComponentRenderer, component_context_cache, component_post_render
|
||||||
from django_components.perfutil.provide import register_provide_reference, unregister_provide_reference
|
from django_components.perfutil.provide import register_provide_reference, unregister_provide_reference
|
||||||
|
@ -570,6 +572,19 @@ class Component(
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# #####################################
|
||||||
|
# EXTENSIONS
|
||||||
|
# #####################################
|
||||||
|
|
||||||
|
# NOTE: These are the classes and instances added by defaults extensions. These fields
|
||||||
|
# are actually set at runtime, and so here they are only marked for typing.
|
||||||
|
Defaults: Type[ComponentDefaults]
|
||||||
|
defaults: ComponentDefaults
|
||||||
|
View: Type[ComponentView]
|
||||||
|
view: ComponentView
|
||||||
|
Url: Type[ComponentUrl]
|
||||||
|
url: ComponentUrl
|
||||||
|
|
||||||
# #####################################
|
# #####################################
|
||||||
# MISC
|
# MISC
|
||||||
# #####################################
|
# #####################################
|
||||||
|
@ -899,6 +914,8 @@ class Component(
|
||||||
def as_view(cls, **initkwargs: Any) -> ViewFn:
|
def as_view(cls, **initkwargs: Any) -> ViewFn:
|
||||||
"""
|
"""
|
||||||
Shortcut for calling `Component.View.as_view` and passing component instance to it.
|
Shortcut for calling `Component.View.as_view` and passing component instance to it.
|
||||||
|
|
||||||
|
Read more on [Component views and URLs](../../concepts/fundamentals/component_views_urls).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# NOTE: `Component.View` may not be available at the time that URLs are being
|
# NOTE: `Component.View` may not be available at the time that URLs are being
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Dict, List, NamedTuple, Tuple, Type, TypeVar
|
from typing import TYPE_CHECKING, Any, Callable, Dict, List, NamedTuple, Tuple, Type, TypeVar, Union
|
||||||
|
|
||||||
import django.urls
|
import django.urls
|
||||||
from django.template import Context
|
from django.template import Context
|
||||||
from django.urls import URLResolver, get_resolver, get_urlconf
|
from django.urls import URLPattern, URLResolver, get_resolver, get_urlconf
|
||||||
|
|
||||||
from django_components.app_settings import app_settings
|
from django_components.app_settings import app_settings
|
||||||
from django_components.compat.django import routes_to_django
|
from django_components.compat.django import routes_to_django
|
||||||
|
@ -504,8 +504,12 @@ class ExtensionManager:
|
||||||
# Internal
|
# Internal
|
||||||
###########################
|
###########################
|
||||||
|
|
||||||
_initialized = False
|
def __init__(self) -> None:
|
||||||
_events: List[Tuple[str, Any]] = []
|
self._initialized = False
|
||||||
|
self._events: List[Tuple[str, Any]] = []
|
||||||
|
self._url_resolvers: Dict[str, URLResolver] = {}
|
||||||
|
# Keep track of which URLRoute (framework-agnostic) maps to which URLPattern (Django-specific)
|
||||||
|
self._route_to_url: Dict[URLRoute, Union[URLPattern, URLResolver]] = {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def extensions(self) -> List[ComponentExtension]:
|
def extensions(self) -> List[ComponentExtension]:
|
||||||
|
@ -636,9 +640,14 @@ class ExtensionManager:
|
||||||
# TODO_V3 - Django-specific logic - replace with hook
|
# TODO_V3 - Django-specific logic - replace with hook
|
||||||
urls: List[URLResolver] = []
|
urls: List[URLResolver] = []
|
||||||
for extension in self.extensions:
|
for extension in self.extensions:
|
||||||
ext_urls = routes_to_django(extension.urls)
|
# NOTE: The empty list is a placeholder for the URLs that will be added later
|
||||||
ext_url_path = django.urls.path(f"{extension.name}/", django.urls.include(ext_urls))
|
curr_ext_url_resolver = django.urls.path(f"{extension.name}/", django.urls.include([]))
|
||||||
urls.append(ext_url_path)
|
urls.append(curr_ext_url_resolver)
|
||||||
|
|
||||||
|
# Remember which extension the URLResolver belongs to
|
||||||
|
self._url_resolvers[extension.name] = curr_ext_url_resolver
|
||||||
|
|
||||||
|
self.add_extension_urls(extension.name, extension.urls)
|
||||||
|
|
||||||
# NOTE: `urlconf_name` is the actual source of truth that holds either a list of URLPatterns
|
# NOTE: `urlconf_name` is the actual source of truth that holds either a list of URLPatterns
|
||||||
# or an import string thereof.
|
# or an import string thereof.
|
||||||
|
@ -647,8 +656,8 @@ class ExtensionManager:
|
||||||
# So we set both:
|
# So we set both:
|
||||||
# - `urlconf_name` to update the source of truth
|
# - `urlconf_name` to update the source of truth
|
||||||
# - `url_patterns` to override the caching
|
# - `url_patterns` to override the caching
|
||||||
ext_url_resolver.urlconf_name = urls
|
extensions_url_resolver.urlconf_name = urls
|
||||||
ext_url_resolver.url_patterns = urls
|
extensions_url_resolver.url_patterns = urls
|
||||||
|
|
||||||
# Rebuild URL resolver cache to be able to resolve the new routes by their names.
|
# Rebuild URL resolver cache to be able to resolve the new routes by their names.
|
||||||
urlconf = get_urlconf()
|
urlconf = get_urlconf()
|
||||||
|
@ -668,6 +677,56 @@ class ExtensionManager:
|
||||||
return command
|
return command
|
||||||
raise ValueError(f"Command {command_name} not found in extension {name}")
|
raise ValueError(f"Command {command_name} not found in extension {name}")
|
||||||
|
|
||||||
|
def add_extension_urls(self, name: str, urls: List[URLRoute]) -> None:
|
||||||
|
if not self._initialized:
|
||||||
|
raise RuntimeError("Cannot add extension URLs before initialization")
|
||||||
|
|
||||||
|
url_resolver = self._url_resolvers[name]
|
||||||
|
all_urls = url_resolver.url_patterns
|
||||||
|
new_urls = routes_to_django(urls)
|
||||||
|
|
||||||
|
did_add_urls = False
|
||||||
|
|
||||||
|
# Allow to add only those routes that are not yet added
|
||||||
|
for route, urlpattern in zip(urls, new_urls):
|
||||||
|
if route in self._route_to_url:
|
||||||
|
raise ValueError(f"URLRoute {route} already exists")
|
||||||
|
self._route_to_url[route] = urlpattern
|
||||||
|
all_urls.append(urlpattern)
|
||||||
|
did_add_urls = True
|
||||||
|
|
||||||
|
# Force Django's URLResolver to update its lookups, so things like `reverse()` work
|
||||||
|
if did_add_urls:
|
||||||
|
# Django's root URLResolver
|
||||||
|
urlconf = get_urlconf()
|
||||||
|
root_resolver = get_resolver(urlconf)
|
||||||
|
root_resolver._populate()
|
||||||
|
|
||||||
|
def remove_extension_urls(self, name: str, urls: List[URLRoute]) -> None:
|
||||||
|
if not self._initialized:
|
||||||
|
raise RuntimeError("Cannot remove extension URLs before initialization")
|
||||||
|
|
||||||
|
url_resolver = self._url_resolvers[name]
|
||||||
|
urls_to_remove = routes_to_django(urls)
|
||||||
|
all_urls = url_resolver.url_patterns
|
||||||
|
|
||||||
|
# Remove the URLs in reverse order, so that we don't have to deal with index shifting
|
||||||
|
for index in reversed(range(len(all_urls))):
|
||||||
|
if not urls_to_remove:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Instead of simply checking if the URL is in the `urls_to_remove` list, we search for
|
||||||
|
# the index of the URL within the `urls_to_remove` list, so we can remove it from there.
|
||||||
|
# That way, in theory, the iteration should be faster as the list gets smaller.
|
||||||
|
try:
|
||||||
|
found_index = urls_to_remove.index(all_urls[index])
|
||||||
|
except ValueError:
|
||||||
|
found_index = -1
|
||||||
|
|
||||||
|
if found_index != -1:
|
||||||
|
all_urls.pop(index)
|
||||||
|
urls_to_remove.pop(found_index)
|
||||||
|
|
||||||
#############################
|
#############################
|
||||||
# Component lifecycle hooks
|
# Component lifecycle hooks
|
||||||
#############################
|
#############################
|
||||||
|
@ -738,4 +797,4 @@ urlpatterns = [
|
||||||
#
|
#
|
||||||
# As such, we lazily set the extensions' routes to the `URLResolver` object. And we use the `include()
|
# As such, we lazily set the extensions' routes to the `URLResolver` object. And we use the `include()
|
||||||
# and `path()` funtions above to ensure that the `URLResolver` object is created correctly.
|
# and `path()` funtions above to ensure that the `URLResolver` object is created correctly.
|
||||||
ext_url_resolver: URLResolver = urlpatterns[0]
|
extensions_url_resolver: URLResolver = urlpatterns[0]
|
||||||
|
|
|
@ -119,6 +119,25 @@ def _apply_defaults(kwargs: Dict, defaults: List[ComponentDefaultField]) -> None
|
||||||
kwargs[default_field.key] = default_value
|
kwargs[default_field.key] = default_value
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentDefaults(ComponentExtension.ExtensionClass): # type: ignore[misc,valid-type]
|
||||||
|
"""
|
||||||
|
The interface for `Component.Defaults`.
|
||||||
|
|
||||||
|
The fields of this class are used to set default values for the component's kwargs.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
from django_components import Component, Default
|
||||||
|
|
||||||
|
class MyComponent(Component):
|
||||||
|
class Defaults:
|
||||||
|
position = "left"
|
||||||
|
selected_items = Default(lambda: [1, 2, 3])
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class DefaultsExtension(ComponentExtension):
|
class DefaultsExtension(ComponentExtension):
|
||||||
"""
|
"""
|
||||||
This extension adds a nested `Defaults` class to each `Component`.
|
This extension adds a nested `Defaults` class to each `Component`.
|
||||||
|
@ -141,6 +160,7 @@ class DefaultsExtension(ComponentExtension):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "defaults"
|
name = "defaults"
|
||||||
|
ExtensionClass = ComponentDefaults
|
||||||
|
|
||||||
# Preprocess the `Component.Defaults` class, if given, so we don't have to do it
|
# Preprocess the `Component.Defaults` class, if given, so we don't have to do it
|
||||||
# each time a component is rendered.
|
# each time a component is rendered.
|
||||||
|
|
181
src/django_components/extensions/url.py
Normal file
181
src/django_components/extensions/url.py
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
import sys
|
||||||
|
from typing import TYPE_CHECKING, Optional, Type, Union
|
||||||
|
from weakref import WeakKeyDictionary
|
||||||
|
|
||||||
|
import django.urls
|
||||||
|
|
||||||
|
from django_components.extension import (
|
||||||
|
ComponentExtension,
|
||||||
|
OnComponentClassCreatedContext,
|
||||||
|
OnComponentClassDeletedContext,
|
||||||
|
URLRoute,
|
||||||
|
extensions,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from django_components.component import Component
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE: `WeakKeyDictionary` is NOT a generic pre-3.9
|
||||||
|
if sys.version_info >= (3, 9):
|
||||||
|
ComponentRouteCache = WeakKeyDictionary[Type["Component"], URLRoute]
|
||||||
|
else:
|
||||||
|
ComponentRouteCache = WeakKeyDictionary
|
||||||
|
|
||||||
|
|
||||||
|
def _get_component_route_name(component: Union[Type["Component"], "Component"]) -> str:
|
||||||
|
return f"__component_url__{component._class_hash}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_component_url(component: Union[Type["Component"], "Component"]) -> str:
|
||||||
|
"""
|
||||||
|
Get the URL for a [`Component`](../api#django_components.Component).
|
||||||
|
|
||||||
|
Raises `RuntimeError` if the component is not public.
|
||||||
|
|
||||||
|
Read more about [Component views and URLs](../../concepts/fundamentals/component_views_urls).
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```py
|
||||||
|
from django_components import Component, get_component_url
|
||||||
|
|
||||||
|
class MyComponent(Component):
|
||||||
|
class Url:
|
||||||
|
public = True
|
||||||
|
|
||||||
|
# Get the URL for the component
|
||||||
|
url = get_component_url(MyComponent)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
url_cls: Optional[Type[ComponentUrl]] = getattr(component, "Url", None)
|
||||||
|
if url_cls is None or not url_cls.public:
|
||||||
|
raise RuntimeError("Component URL is not available - Component is not public")
|
||||||
|
|
||||||
|
route_name = _get_component_route_name(component)
|
||||||
|
return django.urls.reverse(route_name)
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentUrl(ComponentExtension.ExtensionClass): # type: ignore
|
||||||
|
"""
|
||||||
|
The interface for `Component.Url`.
|
||||||
|
|
||||||
|
This class is used to configure whether the component should be available via a URL.
|
||||||
|
|
||||||
|
Read more about [Component views and URLs](../../concepts/fundamentals/component_views_urls).
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
from django_components import Component
|
||||||
|
|
||||||
|
class MyComponent(Component):
|
||||||
|
class Url:
|
||||||
|
public = True
|
||||||
|
|
||||||
|
# Get the URL for the component
|
||||||
|
url = get_component_url(MyComponent)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
public: bool = False
|
||||||
|
"""
|
||||||
|
Whether this [`Component`](../api#django_components.Component) should be available
|
||||||
|
via a URL. Defaults to `False`.
|
||||||
|
|
||||||
|
If `True`, the Component will have its own unique URL path.
|
||||||
|
|
||||||
|
You can use this to write components that will correspond to HTML fragments
|
||||||
|
for HTMX or similar libraries.
|
||||||
|
|
||||||
|
To obtain the component URL, either access the url from
|
||||||
|
[`Component.Url.url`](../api#django_components.ComponentUrl.url) or
|
||||||
|
use the [`get_component_url()`](../api#django_components.get_component_url) function.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```py
|
||||||
|
from django_components import Component, get_component_url
|
||||||
|
|
||||||
|
class MyComponent(Component):
|
||||||
|
class Url:
|
||||||
|
public = True
|
||||||
|
|
||||||
|
# Get the URL for the component
|
||||||
|
url = get_component_url(MyComponent)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
@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__)
|
||||||
|
|
||||||
|
|
||||||
|
class UrlExtension(ComponentExtension):
|
||||||
|
"""
|
||||||
|
This extension adds a nested `Url` class to each [`Component`](../api#django_components.Component).
|
||||||
|
|
||||||
|
This nested `Url` class configures whether the component should be available via a URL.
|
||||||
|
|
||||||
|
Read more about [Component views and URLs](../../concepts/fundamentals/component_views_urls).
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```py
|
||||||
|
from django_components import Component
|
||||||
|
|
||||||
|
class MyComponent(Component):
|
||||||
|
class Url:
|
||||||
|
public = True
|
||||||
|
```
|
||||||
|
|
||||||
|
Will create a URL route like `/components/ext/url/components/a1b2c3/`.
|
||||||
|
|
||||||
|
To get the URL for the component, use `get_component_url`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
url = get_component_url(MyComponent)
|
||||||
|
```
|
||||||
|
|
||||||
|
This extension is automatically added to all [`Component`](../api#django_components.Component)
|
||||||
|
classes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "url"
|
||||||
|
|
||||||
|
ExtensionClass = ComponentUrl
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
# Remember which route belongs to which component
|
||||||
|
self.routes_by_component: ComponentRouteCache = WeakKeyDictionary()
|
||||||
|
|
||||||
|
# Create URL route on creation
|
||||||
|
def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
|
||||||
|
url_cls: Optional[Type[ComponentUrl]] = getattr(ctx.component_cls, "Url", None)
|
||||||
|
if url_cls is None or not url_cls.public:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create a URL route like `components/MyTable_a1b2c3/`
|
||||||
|
# And since this is within the `url` extension, the full URL path will then be:
|
||||||
|
# `/components/ext/url/components/MyTable_a1b2c3/`
|
||||||
|
route_path = f"components/{ctx.component_cls._class_hash}/"
|
||||||
|
route_name = _get_component_route_name(ctx.component_cls)
|
||||||
|
route = URLRoute(
|
||||||
|
path=route_path,
|
||||||
|
handler=ctx.component_cls.as_view(),
|
||||||
|
name=route_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.routes_by_component[ctx.component_cls] = route
|
||||||
|
extensions.add_extension_urls(self.name, [route])
|
||||||
|
|
||||||
|
# Remove URL route on deletion
|
||||||
|
def on_component_class_deleted(self, ctx: OnComponentClassDeletedContext) -> None:
|
||||||
|
route = self.routes_by_component.pop(ctx.component_cls, None)
|
||||||
|
if route is None:
|
||||||
|
return
|
||||||
|
extensions.remove_extension_urls(self.name, [route])
|
|
@ -15,8 +15,19 @@ class ViewFn(Protocol):
|
||||||
|
|
||||||
class ComponentView(ComponentExtension.ExtensionClass, View): # type: ignore
|
class ComponentView(ComponentExtension.ExtensionClass, View): # type: ignore
|
||||||
"""
|
"""
|
||||||
Subclass of `django.views.View` where the `Component` instance is available
|
The interface for `Component.View`.
|
||||||
|
|
||||||
|
Override the methods of this class to define the behavior of the component.
|
||||||
|
|
||||||
|
This class is a subclass of `django.views.View`. The `Component` instance is available
|
||||||
via `self.component`.
|
via `self.component`.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```python
|
||||||
|
class MyComponent(Component):
|
||||||
|
class View:
|
||||||
|
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||||
|
return HttpResponse("Hello, world!")
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# NOTE: This attribute must be declared on the class for `View.as_view()` to allow
|
# NOTE: This attribute must be declared on the class for `View.as_view()` to allow
|
||||||
|
@ -33,42 +44,40 @@ class ComponentView(ComponentExtension.ExtensionClass, View): # type: ignore
|
||||||
# Each method actually delegates to the component's method of the same name.
|
# Each method actually delegates to the component's method of the same name.
|
||||||
# E.g. When `get()` is called, it delegates to `component.get()`.
|
# E.g. When `get()` is called, it delegates to `component.get()`.
|
||||||
|
|
||||||
|
# TODO_V1 - In v1 handlers like `get()` should be defined on the Component.View class,
|
||||||
|
# not the Component class directly. This is to align Views with the extensions API
|
||||||
|
# where each extension should keep its methods in the extension class.
|
||||||
|
# Instead, the defaults for these methods should be something like
|
||||||
|
# `return self.component.render_to_response()` or similar.
|
||||||
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||||
component: "Component" = self.component
|
return getattr(self.component, "get")(request, *args, **kwargs)
|
||||||
return getattr(component, "get")(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||||
component: "Component" = self.component
|
return getattr(self.component, "post")(request, *args, **kwargs)
|
||||||
return getattr(component, "post")(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def put(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
def put(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||||
component: "Component" = self.component
|
return getattr(self.component, "put")(request, *args, **kwargs)
|
||||||
return getattr(component, "put")(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def patch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
def patch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||||
component: "Component" = self.component
|
return getattr(self.component, "patch")(request, *args, **kwargs)
|
||||||
return getattr(component, "patch")(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def delete(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
def delete(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||||
component: "Component" = self.component
|
return getattr(self.component, "delete")(request, *args, **kwargs)
|
||||||
return getattr(component, "delete")(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def head(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
def head(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||||
component: "Component" = self.component
|
return getattr(self.component, "head")(request, *args, **kwargs)
|
||||||
return getattr(component, "head")(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def options(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
def options(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||||
component: "Component" = self.component
|
return getattr(self.component, "options")(request, *args, **kwargs)
|
||||||
return getattr(component, "options")(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def trace(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
def trace(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||||
component: "Component" = self.component
|
return getattr(self.component, "trace")(request, *args, **kwargs)
|
||||||
return getattr(component, "trace")(request, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class ViewExtension(ComponentExtension):
|
class ViewExtension(ComponentExtension):
|
||||||
"""
|
"""
|
||||||
This extension adds a nested `View` class to each `Component`.
|
This extension adds a nested `View` class to each `Component`.
|
||||||
|
|
||||||
This nested class is a subclass of `django.views.View`, and allows the component
|
This nested class is a subclass of `django.views.View`, and allows the component
|
||||||
to be used as a view by calling `ComponentView.as_view()`.
|
to be used as a view by calling `ComponentView.as_view()`.
|
||||||
|
|
||||||
|
|
|
@ -112,10 +112,11 @@ def is_nonempty_str(txt: Optional[str]) -> bool:
|
||||||
return txt is not None and bool(txt.strip())
|
return txt is not None and bool(txt.strip())
|
||||||
|
|
||||||
|
|
||||||
|
# Convert Component class to something like `TableComp_a91d03`
|
||||||
def hash_comp_cls(comp_cls: Type["Component"]) -> str:
|
def hash_comp_cls(comp_cls: Type["Component"]) -> str:
|
||||||
full_name = get_import_path(comp_cls)
|
full_name = get_import_path(comp_cls)
|
||||||
comp_cls_hash = md5(full_name.encode()).hexdigest()[0:6]
|
name_hash = md5(full_name.encode()).hexdigest()[0:6]
|
||||||
return comp_cls.__name__ + "_" + comp_cls_hash
|
return comp_cls.__name__ + "_" + name_hash
|
||||||
|
|
||||||
|
|
||||||
# String is a glob if it contains at least one of `?`, `*`, or `[`
|
# String is a glob if it contains at least one of `?`, `*`, or `[`
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any, Dict, List, Optional, Protocol
|
from typing import Any, Dict, Iterable, Optional, Protocol
|
||||||
|
|
||||||
|
|
||||||
# Mark object as related to extension URLs so we can place these in
|
# Mark object as related to extension URLs so we can place these in
|
||||||
|
@ -62,10 +62,14 @@ class URLRoute:
|
||||||
|
|
||||||
path: str
|
path: str
|
||||||
handler: Optional[URLRouteHandler] = None
|
handler: Optional[URLRouteHandler] = None
|
||||||
children: List["URLRoute"] = field(default_factory=list)
|
children: Iterable["URLRoute"] = field(default_factory=list)
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
extra: Dict[str, Any] = field(default_factory=dict)
|
extra: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if self.handler is not None and self.children:
|
if self.handler is not None and self.children:
|
||||||
raise ValueError("Cannot have both handler and children")
|
raise ValueError("Cannot have both handler and children")
|
||||||
|
|
||||||
|
# Allow to use `URLRoute` objects in sets and dictionaries
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash(self.path)
|
||||||
|
|
|
@ -523,6 +523,9 @@ def _clear_djc_global_state(
|
||||||
sys.modules.pop(mod, None)
|
sys.modules.pop(mod, None)
|
||||||
LOADED_MODULES.clear()
|
LOADED_MODULES.clear()
|
||||||
|
|
||||||
|
# Clear extensions caches
|
||||||
|
extensions._route_to_url.clear()
|
||||||
|
|
||||||
# Force garbage collection, so that any finalizers are run.
|
# Force garbage collection, so that any finalizers are run.
|
||||||
# If garbage collection is skipped, then in some cases the finalizers
|
# If garbage collection is skipped, then in some cases the finalizers
|
||||||
# are run too late, in the context of the next test, causing flaky tests.
|
# are run too late, in the context of the next test, causing flaky tests.
|
||||||
|
|
|
@ -3,10 +3,11 @@ import typing
|
||||||
from typing import Any, Tuple
|
from typing import Any, Tuple
|
||||||
|
|
||||||
# See https://peps.python.org/pep-0655/#usage-in-python-3-11
|
# See https://peps.python.org/pep-0655/#usage-in-python-3-11
|
||||||
if sys.version_info >= (3, 11):
|
# NOTE: Pydantic requires typing_extensions.TypedDict until (not incl) 3.12
|
||||||
|
if sys.version_info >= (3, 12):
|
||||||
from typing import TypedDict
|
from typing import TypedDict
|
||||||
else:
|
else:
|
||||||
from typing_extensions import TypedDict as TypedDict # for Python <3.11 with (Not)Required
|
from typing_extensions import TypedDict as TypedDict
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from typing import Annotated # type: ignore
|
from typing import Annotated # type: ignore
|
||||||
|
|
|
@ -9,12 +9,19 @@ from django_components import Component, register
|
||||||
class MultFileComponent(Component):
|
class MultFileComponent(Component):
|
||||||
template_file = "multi_file/multi_file.html"
|
template_file = "multi_file/multi_file.html"
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs) -> HttpResponse:
|
class View:
|
||||||
variable = request.POST.get("variable")
|
def post(self, request, *args, **kwargs) -> HttpResponse:
|
||||||
return self.render_to_response({"variable": variable})
|
variable = request.POST.get("variable")
|
||||||
|
return MultFileComponent.render_to_response(
|
||||||
|
request=request,
|
||||||
|
kwargs={"variable": variable},
|
||||||
|
)
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs) -> HttpResponse:
|
def get(self, request, *args, **kwargs) -> HttpResponse:
|
||||||
return self.render_to_response({"variable": "GET"})
|
return MultFileComponent.render_to_response(
|
||||||
|
request=request,
|
||||||
|
kwargs={"variable": "GET"},
|
||||||
|
)
|
||||||
|
|
||||||
def get_context_data(self, variable, *args, **kwargs) -> Dict[str, Any]:
|
def get_context_data(self, variable, *args, **kwargs) -> Dict[str, Any]:
|
||||||
return {"variable": variable}
|
return {"variable": variable}
|
||||||
|
|
|
@ -13,12 +13,19 @@ class RelativeFileComponent(Component):
|
||||||
js = "relative_file.js"
|
js = "relative_file.js"
|
||||||
css = "relative_file.css"
|
css = "relative_file.css"
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs) -> HttpResponse:
|
class View:
|
||||||
variable = request.POST.get("variable")
|
def post(self, request, *args, **kwargs) -> HttpResponse:
|
||||||
return self.render_to_response({"variable": variable})
|
variable = request.POST.get("variable")
|
||||||
|
return RelativeFileComponent.render_to_response(
|
||||||
|
request=request,
|
||||||
|
kwargs={"variable": variable},
|
||||||
|
)
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs) -> HttpResponse:
|
def get(self, request, *args, **kwargs) -> HttpResponse:
|
||||||
return self.render_to_response({"variable": "GET"})
|
return RelativeFileComponent.render_to_response(
|
||||||
|
request=request,
|
||||||
|
kwargs={"variable": "GET"},
|
||||||
|
)
|
||||||
|
|
||||||
def get_context_data(self, variable, *args, **kwargs) -> Dict[str, Any]:
|
def get_context_data(self, variable, *args, **kwargs) -> Dict[str, Any]:
|
||||||
return {"variable": variable}
|
return {"variable": variable}
|
||||||
|
|
|
@ -15,12 +15,19 @@ class SingleFileComponent(Component):
|
||||||
</form>
|
</form>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs) -> HttpResponse:
|
class View:
|
||||||
variable = request.POST.get("variable")
|
def post(self, request, *args, **kwargs) -> HttpResponse:
|
||||||
return self.render_to_response({"variable": variable})
|
variable = request.POST.get("variable")
|
||||||
|
return SingleFileComponent.render_to_response(
|
||||||
|
request=request,
|
||||||
|
kwargs={"variable": variable},
|
||||||
|
)
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs) -> HttpResponse:
|
def get(self, request, *args, **kwargs) -> HttpResponse:
|
||||||
return self.render_to_response({"variable": "GET"})
|
return SingleFileComponent.render_to_response(
|
||||||
|
request=request,
|
||||||
|
kwargs={"variable": "GET"},
|
||||||
|
)
|
||||||
|
|
||||||
def get_context_data(self, variable, *args, **kwargs) -> Dict[str, Any]:
|
def get_context_data(self, variable, *args, **kwargs) -> Dict[str, Any]:
|
||||||
return {"variable": variable}
|
return {"variable": variable}
|
||||||
|
|
|
@ -97,7 +97,7 @@ class TestExtensionsListCommand:
|
||||||
call_command("components", "ext", "list")
|
call_command("components", "ext", "list")
|
||||||
output = out.getvalue()
|
output = out.getvalue()
|
||||||
|
|
||||||
assert output.strip() == "name \n========\ndefaults\nview"
|
assert output.strip() == "name \n========\ndefaults\nview \nurl"
|
||||||
|
|
||||||
@djc_test(
|
@djc_test(
|
||||||
components_settings={"extensions": [EmptyExtension, DummyExtension]},
|
components_settings={"extensions": [EmptyExtension, DummyExtension]},
|
||||||
|
@ -108,7 +108,7 @@ class TestExtensionsListCommand:
|
||||||
call_command("components", "ext", "list")
|
call_command("components", "ext", "list")
|
||||||
output = out.getvalue()
|
output = out.getvalue()
|
||||||
|
|
||||||
assert output.strip() == "name \n========\ndefaults\nview \nempty \ndummy"
|
assert output.strip() == "name \n========\ndefaults\nview \nurl \nempty \ndummy"
|
||||||
|
|
||||||
@djc_test(
|
@djc_test(
|
||||||
components_settings={"extensions": [EmptyExtension, DummyExtension]},
|
components_settings={"extensions": [EmptyExtension, DummyExtension]},
|
||||||
|
@ -119,7 +119,7 @@ class TestExtensionsListCommand:
|
||||||
call_command("components", "ext", "list", "--all")
|
call_command("components", "ext", "list", "--all")
|
||||||
output = out.getvalue()
|
output = out.getvalue()
|
||||||
|
|
||||||
assert output.strip() == "name \n========\ndefaults\nview \nempty \ndummy"
|
assert output.strip() == "name \n========\ndefaults\nview \nurl \nempty \ndummy"
|
||||||
|
|
||||||
@djc_test(
|
@djc_test(
|
||||||
components_settings={"extensions": [EmptyExtension, DummyExtension]},
|
components_settings={"extensions": [EmptyExtension, DummyExtension]},
|
||||||
|
@ -130,7 +130,7 @@ class TestExtensionsListCommand:
|
||||||
call_command("components", "ext", "list", "--columns", "name")
|
call_command("components", "ext", "list", "--columns", "name")
|
||||||
output = out.getvalue()
|
output = out.getvalue()
|
||||||
|
|
||||||
assert output.strip() == "name \n========\ndefaults\nview \nempty \ndummy"
|
assert output.strip() == "name \n========\ndefaults\nview \nurl \nempty \ndummy"
|
||||||
|
|
||||||
@djc_test(
|
@djc_test(
|
||||||
components_settings={"extensions": [EmptyExtension, DummyExtension]},
|
components_settings={"extensions": [EmptyExtension, DummyExtension]},
|
||||||
|
@ -141,7 +141,7 @@ class TestExtensionsListCommand:
|
||||||
call_command("components", "ext", "list", "--simple")
|
call_command("components", "ext", "list", "--simple")
|
||||||
output = out.getvalue()
|
output = out.getvalue()
|
||||||
|
|
||||||
assert output.strip() == "defaults\nview \nempty \ndummy"
|
assert output.strip() == "defaults\nview \nurl \nempty \ndummy"
|
||||||
|
|
||||||
|
|
||||||
@djc_test
|
@djc_test
|
||||||
|
@ -159,7 +159,7 @@ class TestExtensionsRunCommand:
|
||||||
output
|
output
|
||||||
== dedent(
|
== dedent(
|
||||||
f"""
|
f"""
|
||||||
usage: components ext run [-h] {{defaults,view,empty,dummy}} ...
|
usage: components ext run [-h] {{defaults,view,url,empty,dummy}} ...
|
||||||
|
|
||||||
Run a command added by an extension.
|
Run a command added by an extension.
|
||||||
|
|
||||||
|
@ -167,9 +167,10 @@ class TestExtensionsRunCommand:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
|
|
||||||
subcommands:
|
subcommands:
|
||||||
{{defaults,view,empty,dummy}}
|
{{defaults,view,url,empty,dummy}}
|
||||||
defaults Run commands added by the 'defaults' extension.
|
defaults Run commands added by the 'defaults' extension.
|
||||||
view Run commands added by the 'view' extension.
|
view Run commands added by the 'view' extension.
|
||||||
|
url Run commands added by the 'url' extension.
|
||||||
empty Run commands added by the 'empty' extension.
|
empty Run commands added by the 'empty' extension.
|
||||||
dummy Run commands added by the 'dummy' extension.
|
dummy Run commands added by the 'dummy' extension.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -739,8 +739,9 @@ class TestComponentRender:
|
||||||
CSRF token: {{ csrf_token|default:"<em>No CSRF token</em>" }}
|
CSRF token: {{ csrf_token|default:"<em>No CSRF token</em>" }}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request):
|
class View:
|
||||||
return self.render_to_response(request=request)
|
def get(self, request):
|
||||||
|
return Thing.render_to_response(request=request)
|
||||||
|
|
||||||
client = CustomClient(urlpatterns=[path("test_thing/", Thing.as_view())])
|
client = CustomClient(urlpatterns=[path("test_thing/", Thing.as_view())])
|
||||||
response = client.get("/test_thing/")
|
response = client.get("/test_thing/")
|
||||||
|
@ -760,8 +761,9 @@ class TestComponentRender:
|
||||||
<p>Existing context: {{ existing_context|default:"<em>No existing context</em>" }}</p>
|
<p>Existing context: {{ existing_context|default:"<em>No existing context</em>" }}</p>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request):
|
class View:
|
||||||
return self.render_to_response(request=request, context={"existing_context": "foo"})
|
def get(self, request):
|
||||||
|
return Thing.render_to_response(request=request, context={"existing_context": "foo"})
|
||||||
|
|
||||||
client = CustomClient(urlpatterns=[path("test_thing/", Thing.as_view())])
|
client = CustomClient(urlpatterns=[path("test_thing/", Thing.as_view())])
|
||||||
response = client.get("/test_thing/")
|
response = client.get("/test_thing/")
|
||||||
|
@ -782,8 +784,12 @@ class TestComponentRender:
|
||||||
<p>Existing context: {{ existing_context|default:"<em>No existing context</em>" }}</p>
|
<p>Existing context: {{ existing_context|default:"<em>No existing context</em>" }}</p>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request):
|
class View:
|
||||||
return self.render_to_response(request=request, context=Context({"existing_context": "foo"}))
|
def get(self, request):
|
||||||
|
return Thing.render_to_response(
|
||||||
|
request=request,
|
||||||
|
context=Context({"existing_context": "foo"}),
|
||||||
|
)
|
||||||
|
|
||||||
client = CustomClient(urlpatterns=[path("test_thing/", Thing.as_view())])
|
client = CustomClient(urlpatterns=[path("test_thing/", Thing.as_view())])
|
||||||
response = client.get("/test_thing/")
|
response = client.get("/test_thing/")
|
||||||
|
|
87
tests/test_component_url.py
Normal file
87
tests/test_component_url.py
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.http import HttpRequest
|
||||||
|
from django.test import Client
|
||||||
|
|
||||||
|
from django_components import Component, get_component_url
|
||||||
|
from django_components.testing import djc_test
|
||||||
|
|
||||||
|
from .testutils import setup_test_config
|
||||||
|
|
||||||
|
setup_test_config({"autodiscover": False})
|
||||||
|
|
||||||
|
|
||||||
|
@djc_test
|
||||||
|
class TestComponentUrl:
|
||||||
|
def test_public_url(self):
|
||||||
|
did_call_get = False
|
||||||
|
did_call_post = False
|
||||||
|
|
||||||
|
class TestComponent(Component):
|
||||||
|
template = "Hello"
|
||||||
|
|
||||||
|
class Url:
|
||||||
|
public = True
|
||||||
|
|
||||||
|
class View:
|
||||||
|
def get(self, request: HttpRequest, **kwargs: Any):
|
||||||
|
nonlocal did_call_get
|
||||||
|
did_call_get = True
|
||||||
|
|
||||||
|
component: Component = self.component # type: ignore[attr-defined]
|
||||||
|
return component.render_to_response()
|
||||||
|
|
||||||
|
def post(self, request: HttpRequest, **kwargs: Any):
|
||||||
|
nonlocal did_call_post
|
||||||
|
did_call_post = True
|
||||||
|
|
||||||
|
component: Component = self.component # type: ignore[attr-defined]
|
||||||
|
return component.render_to_response()
|
||||||
|
|
||||||
|
# Check if the URL is correctly generated
|
||||||
|
component_url = get_component_url(TestComponent)
|
||||||
|
assert component_url == f"/components/ext/url/components/{TestComponent._class_hash}/"
|
||||||
|
|
||||||
|
client = Client()
|
||||||
|
response = client.get(component_url)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.content == b"Hello"
|
||||||
|
assert did_call_get
|
||||||
|
|
||||||
|
response = client.post(component_url)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.content == b"Hello"
|
||||||
|
assert did_call_get
|
||||||
|
|
||||||
|
def test_non_public_url(self):
|
||||||
|
did_call_get = False
|
||||||
|
|
||||||
|
class TestComponent(Component):
|
||||||
|
template = "Hi"
|
||||||
|
|
||||||
|
class Url:
|
||||||
|
public = False
|
||||||
|
|
||||||
|
class View:
|
||||||
|
def get(self, request: HttpRequest, **attrs: Any):
|
||||||
|
nonlocal did_call_get
|
||||||
|
did_call_get = True
|
||||||
|
|
||||||
|
component: Component = self.component # type: ignore[attr-defined]
|
||||||
|
return component.render_to_response()
|
||||||
|
|
||||||
|
# Attempt to get the URL should raise RuntimeError
|
||||||
|
with pytest.raises(
|
||||||
|
RuntimeError,
|
||||||
|
match="Component URL is not available - Component is not public",
|
||||||
|
):
|
||||||
|
get_component_url(TestComponent)
|
||||||
|
|
||||||
|
# Even calling the URL directly should raise an error
|
||||||
|
component_url = f"/components/ext/url/components/{TestComponent._class_hash}/"
|
||||||
|
|
||||||
|
client = Client()
|
||||||
|
response = client.get(component_url)
|
||||||
|
assert response.status_code == 404
|
||||||
|
assert not did_call_get
|
|
@ -22,6 +22,7 @@ from django_components.extension import (
|
||||||
)
|
)
|
||||||
from django_components.extensions.defaults import DefaultsExtension
|
from django_components.extensions.defaults import DefaultsExtension
|
||||||
from django_components.extensions.view import ViewExtension
|
from django_components.extensions.view import ViewExtension
|
||||||
|
from django_components.extensions.url import UrlExtension
|
||||||
|
|
||||||
from django_components.testing import djc_test
|
from django_components.testing import djc_test
|
||||||
from .testutils import setup_test_config
|
from .testutils import setup_test_config
|
||||||
|
@ -126,10 +127,11 @@ def with_registry(on_created: Callable):
|
||||||
class TestExtension:
|
class TestExtension:
|
||||||
@djc_test(components_settings={"extensions": [DummyExtension]})
|
@djc_test(components_settings={"extensions": [DummyExtension]})
|
||||||
def test_extensions_setting(self):
|
def test_extensions_setting(self):
|
||||||
assert len(app_settings.EXTENSIONS) == 3
|
assert len(app_settings.EXTENSIONS) == 4
|
||||||
assert isinstance(app_settings.EXTENSIONS[0], DefaultsExtension)
|
assert isinstance(app_settings.EXTENSIONS[0], DefaultsExtension)
|
||||||
assert isinstance(app_settings.EXTENSIONS[1], ViewExtension)
|
assert isinstance(app_settings.EXTENSIONS[1], ViewExtension)
|
||||||
assert isinstance(app_settings.EXTENSIONS[2], DummyExtension)
|
assert isinstance(app_settings.EXTENSIONS[2], UrlExtension)
|
||||||
|
assert isinstance(app_settings.EXTENSIONS[3], DummyExtension)
|
||||||
|
|
||||||
@djc_test(components_settings={"extensions": [DummyExtension]})
|
@djc_test(components_settings={"extensions": [DummyExtension]})
|
||||||
def test_access_component_from_extension(self):
|
def test_access_component_from_extension(self):
|
||||||
|
@ -152,7 +154,7 @@ class TestExtension:
|
||||||
class TestExtensionHooks:
|
class TestExtensionHooks:
|
||||||
@djc_test(components_settings={"extensions": [DummyExtension]})
|
@djc_test(components_settings={"extensions": [DummyExtension]})
|
||||||
def test_component_class_lifecycle_hooks(self):
|
def test_component_class_lifecycle_hooks(self):
|
||||||
extension = cast(DummyExtension, app_settings.EXTENSIONS[2])
|
extension = cast(DummyExtension, app_settings.EXTENSIONS[3])
|
||||||
|
|
||||||
assert len(extension.calls["on_component_class_created"]) == 0
|
assert len(extension.calls["on_component_class_created"]) == 0
|
||||||
assert len(extension.calls["on_component_class_deleted"]) == 0
|
assert len(extension.calls["on_component_class_deleted"]) == 0
|
||||||
|
@ -184,7 +186,7 @@ class TestExtensionHooks:
|
||||||
|
|
||||||
@djc_test(components_settings={"extensions": [DummyExtension]})
|
@djc_test(components_settings={"extensions": [DummyExtension]})
|
||||||
def test_registry_lifecycle_hooks(self):
|
def test_registry_lifecycle_hooks(self):
|
||||||
extension = cast(DummyExtension, app_settings.EXTENSIONS[2])
|
extension = cast(DummyExtension, app_settings.EXTENSIONS[3])
|
||||||
|
|
||||||
assert len(extension.calls["on_registry_created"]) == 0
|
assert len(extension.calls["on_registry_created"]) == 0
|
||||||
assert len(extension.calls["on_registry_deleted"]) == 0
|
assert len(extension.calls["on_registry_deleted"]) == 0
|
||||||
|
@ -221,7 +223,7 @@ class TestExtensionHooks:
|
||||||
return {"name": name}
|
return {"name": name}
|
||||||
|
|
||||||
registry.register("test_comp", TestComponent)
|
registry.register("test_comp", TestComponent)
|
||||||
extension = cast(DummyExtension, app_settings.EXTENSIONS[2])
|
extension = cast(DummyExtension, app_settings.EXTENSIONS[3])
|
||||||
|
|
||||||
# Verify on_component_registered was called
|
# Verify on_component_registered was called
|
||||||
assert len(extension.calls["on_component_registered"]) == 1
|
assert len(extension.calls["on_component_registered"]) == 1
|
||||||
|
@ -259,7 +261,7 @@ class TestExtensionHooks:
|
||||||
test_slots = {"content": "Some content"}
|
test_slots = {"content": "Some content"}
|
||||||
TestComponent.render(context=test_context, args=("arg1", "arg2"), kwargs={"name": "Test"}, slots=test_slots)
|
TestComponent.render(context=test_context, args=("arg1", "arg2"), kwargs={"name": "Test"}, slots=test_slots)
|
||||||
|
|
||||||
extension = cast(DummyExtension, app_settings.EXTENSIONS[2])
|
extension = cast(DummyExtension, app_settings.EXTENSIONS[3])
|
||||||
|
|
||||||
# Verify on_component_input was called with correct args
|
# Verify on_component_input was called with correct args
|
||||||
assert len(extension.calls["on_component_input"]) == 1
|
assert len(extension.calls["on_component_input"]) == 1
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue