mirror of
https://github.com/django-components/django-components.git
synced 2025-08-04 14:28:18 +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
|
@ -65,10 +65,10 @@ to configure the extensions on a per-component basis.
|
|||
|
||||
### Example: Component as View
|
||||
|
||||
The [Components as Views](../../fundamentals/components_as_views) feature is actually implemented as an extension
|
||||
The [Components as Views](../../fundamentals/component_views_urls) feature is actually implemented as an extension
|
||||
that is configured by a `View` nested class.
|
||||
|
||||
You can override the `get`, `post`, etc methods to customize the behavior of the component as a view:
|
||||
You can override the `get()`, `post()`, etc methods to customize the behavior of the component as a view:
|
||||
|
||||
```python
|
||||
class MyTable(Component):
|
||||
|
|
|
@ -106,8 +106,9 @@ from django_components import Component, types
|
|||
|
||||
# HTML into which a fragment will be loaded using HTMX
|
||||
class MyPage(Component):
|
||||
def get(self, request):
|
||||
return self.render_to_response()
|
||||
Class View:
|
||||
def get(self, request):
|
||||
return self.component.render_to_response(request=request)
|
||||
|
||||
template = """
|
||||
{% load component_tags %}
|
||||
|
@ -138,11 +139,13 @@ class MyPage(Component):
|
|||
|
||||
```djc_py title="[root]/components/demo.py"
|
||||
class Frag(Component):
|
||||
def get(self, request):
|
||||
return self.render_to_response(
|
||||
# IMPORTANT: Don't forget `type="fragment"`
|
||||
type="fragment",
|
||||
)
|
||||
class View:
|
||||
def get(self, request):
|
||||
return self.component.render_to_response(
|
||||
request=request,
|
||||
# IMPORTANT: Don't forget `type="fragment"`
|
||||
type="fragment",
|
||||
)
|
||||
|
||||
template = """
|
||||
<div class="frag">
|
||||
|
@ -184,8 +187,9 @@ from django_components import Component, types
|
|||
|
||||
# HTML into which a fragment will be loaded using AlpineJS
|
||||
class MyPage(Component):
|
||||
def get(self, request):
|
||||
return self.render_to_response()
|
||||
class View:
|
||||
def get(self, request):
|
||||
return self.component.render_to_response(request=request)
|
||||
|
||||
template = """
|
||||
{% load component_tags %}
|
||||
|
@ -222,11 +226,13 @@ class MyPage(Component):
|
|||
|
||||
```djc_py title="[root]/components/demo.py"
|
||||
class Frag(Component):
|
||||
def get(self, request):
|
||||
# IMPORTANT: Don't forget `type="fragment"`
|
||||
return self.render_to_response(
|
||||
type="fragment",
|
||||
)
|
||||
class View:
|
||||
def get(self, request):
|
||||
return self.component.render_to_response(
|
||||
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
|
||||
# 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
|
||||
class MyPage(Component):
|
||||
def get(self, request):
|
||||
return self.render_to_response()
|
||||
class View:
|
||||
def get(self, request):
|
||||
return self.component.render_to_response(request=request)
|
||||
|
||||
template = """
|
||||
{% load component_tags %}
|
||||
|
@ -318,11 +325,13 @@ class MyPage(Component):
|
|||
|
||||
```djc_py title="[root]/components/demo.py"
|
||||
class Frag(Component):
|
||||
def get(self, request):
|
||||
return self.render_to_response(
|
||||
# IMPORTANT: Don't forget `type="fragment"`
|
||||
type="fragment",
|
||||
)
|
||||
class View:
|
||||
def get(self, request):
|
||||
return self.component.render_to_response(
|
||||
request=request,
|
||||
# IMPORTANT: Don't forget `type="fragment"`
|
||||
type="fragment",
|
||||
)
|
||||
|
||||
template = """
|
||||
<div class="frag">
|
||||
|
|
|
@ -10,6 +10,6 @@ nav:
|
|||
- HTML attributes: html_attributes.md
|
||||
- Defining HTML / JS / CSS files: defining_js_css_html_files.md
|
||||
- Autodiscovery: autodiscovery.md
|
||||
- Components as views: components_as_views.md
|
||||
- Component views and URLs: component_views_urls.md
|
||||
- HTTP Request: http_request.md
|
||||
- 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>
|
||||
"""
|
||||
```
|
||||
|
||||
### 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).
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue