Skip to content

Commit

Permalink
Merge branch 'main' of github.com:hotwire-django/django-turbo-response
Browse files Browse the repository at this point in the history
  • Loading branch information
michael-yin committed Nov 7, 2023
2 parents be09481 + e0c7786 commit 45d921e
Show file tree
Hide file tree
Showing 17 changed files with 990 additions and 619 deletions.
58 changes: 58 additions & 0 deletions docs/source/channels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Django-Channels

This library can also be used with [django-channels](https://channels.readthedocs.io/en/stable/). As with multiple streams, you can use the **TurboStream** class to broadcast turbo-stream content from your consumers.

```python
from turbo_response import render_turbo_stream, render_turbo_stream_template
from channels.generic.websocket import AsyncJsonWebsocketConsumer

class ChatConsumer(AsyncJsonWebsocketConsumer):

async def chat_message(self, event):

# DB methods omitted for brevity
message = await self.get_message(event["message"]["id"])
num_unread_messages = await self.get_num_unread_messages()

if message:
await self.send(
TurboStream("unread_message_counter")
.replace.render(str(num_unread_messages))
)

await self.send(
TurboStream("messages").append.template(
"chat/_message.html",
{"message": message, "user": self.scope['user']},
).render()
)
```

See the django-channels documentation for more details on setting up ASGI and channels. Note that you will need to set up your WebSockets in the client, for example in a Stimulus controller:

```javascript
import { Controller } from 'stimulus';
import { connectStreamSource, disconnectStreamSource } from '@hotwired/turbo';

export default class extends Controller {
static values = {
socketUrl: String,
};

connect() {
this.source = new WebSocket(this.socketUrlValue);
connectStreamSource(this.source);
}

disconnect() {
if (this.source) {
disconnectStreamSource(this.source);
this.source.close();
this.source = null;
}
}
}
```

**Note** if you want to add reactivity directly to your models, so that model changes broadcast turbo-streams automatically, we recommend the [turbo-django](https://github.com/hotwire-django/turbo-django) package.

200 changes: 200 additions & 0 deletions docs/source/form.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
# Form Validation

The most common pattern for server-side validation in a Django view consists of:

1. Render the initial form
2. Validate on POST
3. If any validation errors, re-render the form with errors and user input
4. If no validation errors, save to the database (and/or any other actions) and redirect

In order to make this work with Turbo you can do one of two things (**Note**: requires minimum **@hotwired/turbo 7.0.0-beta.3**):

1. When the form is invalid, return with a 4** status response.
2. Add *data-turbo="false"* to your `<form>` tag.

If neither of these options are set, Turbo will throw an error if your view returns any response that isn't a redirect.

Note that if you set *data-turbo="false"* on your form like so:

```html
<form method="post" action="..." data-turbo="false">
```

Turbo will force a full-page refresh, just as the same attribute does to link behavior. This might be acceptable however when working with views and forms e.g. in 3rd party packages where you don't want to change the default workflow.

If you want to continue using forms with Turbo just change the response status to a 4**, e.g. 422:

```python
import http

from django.shortcuts import redirect
from django.template.response import TemplateResponse

from myapp import MyForm

def my_view(request):
if request.method == "POST":
form = MyForm(request.POST)
if form.is_valid():
# save data etc...
return redirect("/")
status = http.HTTPStatus.UNPROCESSABLE_ENTITY
else:
form = MyForm()
status = http.HTTPStatus.OK
return TemplateResponse(request, "my_form.html", {"form": my_form}, status=status)
```

As this is such a common pattern, we provide for convenience the **turbo_response.render_form_response** shortcut function which automatically sets the correct status depending on the form state (and adds "form" to the template context):

```python
from django.shortcuts import redirect

from turbo_response import render_form_response

from myapp import MyForm

def my_view(request):
if request.method == "POST":
form = MyForm(request.POST)
if form.is_valid():
# save data etc...
return redirect("/")
else:
form = MyForm()
return render_form_response(request, form, "my_form.html")
```

If you are using CBVs, this package has a mixin class, **turbo_response.mixins.TurboFormMixin** that sets the correct status automatically to 422 for an invalid form:

```python
from django.views.generic import FormView

from turbo_response import redirect_303
from turbo_response.mixins import TurboFormMixin

from myapp import MyForm

class MyView(TurboFormMixin, FormView):
template_name = "my_form.html"

def form_valid(self, form):
return redirect_303("/")
```

In addition you can just subclass these views for common cases:

- **turbo_response.views.TurboFormView**
- **turbo_response.views.TurboCreateView**
- **turbo_response.views.TurboUpdateView**

In some cases you may wish to return a turbo-stream response containing just the form when the form is invalid instead of a full page visit. In this case just return a stream rendering the form partial in the usual manner. For example:

```python
from django.shortcuts import redirect_303
from django.template.response import TemplateResponse
from django.views.generic import FormView

from turbo_response import TurboStream

from myapp import MyForm

def my_view(request):
if request.method == "POST":
form = MyForm(request.POST)
if form.is_valid():
# save data etc...
return redirect_303("/")
return TurboStream("form-target").replace.template("_my_form.html").render(request=request)
else:
form = MyForm()
return TemplateResponse(request, "my_form.html", {"form": my_form})
```

Or CBV:

```python
class MyView(TurboFormMixin, FormView):
template_name = "my_form.html"

def form_valid(self, form):
return redirect_303("/")

def form_invalid(self, form):
return TurboStream("form-target").replace.template("_my_form.html").render(request=request)
```

And your templates would look like this:

*my_form.html*

```html
{% extends "base.html" %}

{% block content %}
<h1>my form goes here..</h1>
{% include "_my_form.html" %}
{% endblock content %}
```

*_my_form.html*

```html
<form method="POST" id="form-target" action="/my-form">
{% csrf_token %}
{{ form.as_p }}
</form>
```

As this is a useful pattern in many situations, for example when handling forms inside modals, this package provides a mixin class **turbo_response.mixins.TurboStreamFormMixin**:

```python
from django.views.generic import FormView
from turbo_response.mixins import TurboStreamFormMixin

class MyView(TurboStreamFormMixin, FormView):
turbo_stream_target = "form-target"
template_name = "my_form.html"
# action = Action.REPLACE
```

This mixin will automatically add the target name to the template context as *turbo_stream_target*. The partial template will be automatically resolved as the template name prefixed with an underscore: in this example, *_my_form.html*. You can also set it explicitly with the *turbo_stream_template_name* class attribute. The default action is "replace".

As with the form mixin above, the package includes a number of view classes using this mixin:

- **turbo_response.views.TurboStreamFormView**
- **turbo_response.views.TurboStreamCreateView**
- **turbo_response.views.TurboStreamUpdateView**

So the above example could be rewritten as:

```python
from turbo_response.views import TurboStreamFormView

class MyView(TurboStreamFormView):
turbo_stream_target = "form-target"
template_name = "my_form.html"
```

The model-based classes automatically set the target DOM ID based on the model. The pattern for **TurboStreamCreateView** is *form-<model_name>* and for **TurboStreamUpdateView** *form-<model-name>-<pk>*. You can override this by setting the *target* attribute explicitly or overriding the *get_turbo_stream_target* method.

A final point re: forms: Turbo processes forms using the FormData API and only includes inputs with a value. This means all buttons, inputs etc. must have a value. For example suppose you have a button like this:

```html
<button name="send_action">Do this</button>
```

If your view code checks for this value:

```python
if "send_action" in request.POST:
...
```

it will consistently fail. You should have something like:

```html
<button name="send_action" value="true">Do this</button>
```

to ensure the FormData object includes the button value.
25 changes: 15 additions & 10 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
.. django-turbo-response documentation master file, created by
sphinx-quickstart on Tue Dec 29 02:19:26 2020.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
django-turbo-response
==========================================
=====================

This package provides helpers for server-side rendering of *Turbo Frame* and *Turbo Stream*, which aim to work with the `Hotwire <https://hotwire.dev/>`_ project.

.. _topics:

.. include:: ./main.md
:parser: myst
Topics
------

.. toctree::
:hidden:
:maxdepth: 2

self
install.md
template-tags.md
original.md
form.md
redirect.md
channels.md
test.md
51 changes: 51 additions & 0 deletions docs/source/install.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Installation

## Requirements

This library requires Python 3.8+ and Django 3.2+.

## Getting Started

```shell
pip install django-turbo-response
```

Next update **INSTALLED_APPS**:

```python
INSTALLED_APPS = [
"turbo_response",
...
]
```

**Note**: This install does not include any client libraries (e.g. Turbo or Stimulus). You may wish to add these yourself using your preferred Javascript build tool, or use a CDN. Please refer to the Hotwire documentation on installing these libraries.

## Middleware

You can optionally install `turbo_response.middleware.TurboMiddleware`. This adds the attribute `turbo` to your `request` if the Turbo client adds `Accept: text/vnd.turbo-stream.html;` to the header:

```python
MIDDLEWARE = [
...
"turbo_response.middleware.TurboMiddleware",
"django.middleware.common.CommonMiddleware",
...
]
```

This is useful if you want to check if a stream is requested, so you can optionally return a stream or a normal response:

```python
if request.turbo:
# return Turbo Stream
else:
# return normal response
```

If the request originates from a turbo-frame it will also set the `frame` property:

```python
if request.turbo.frame == "my-playlist":
pass
```
Loading

0 comments on commit 45d921e

Please sign in to comment.