Turbo Frame and Django Form
Objective
- Learn to make Django form work with Turbo Frame
Load Django Form in Turbo Frame
Update hotwire_django_app/templates/turbo_frame/index.html
{% extends "turbo_frame/base.html" %}
{% block content %}
<div class="w-full max-w-7xl mx-auto px-4">
<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Turbo Frame Index Page</h1>
<div class="mb-4">
<turbo-frame id="task-create" src="{% url 'turbo-frame:task-create' %}"> <!-- new -->
Loading...
</turbo-frame>
</div>
<turbo-frame id="task-list" src="{% url 'turbo-frame:task-list' %}"> <!-- update -->
Loading...
</turbo-frame>
</div>
{% endblock %}
Notes:
- We set
src
to the turbo frame to doeager loading
, so it would load content and fill theturbo-frame
element automatically. - We add
task-create
frame to load the form from theturbo-frame:task-create
Update hotwire_django_app/templates/turbo_frame/create_page.html to add the <turbo-frame id="task-create">
{% extends "turbo_frame/base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="w-full max-w-7xl mx-auto px-4">
<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Create Task</h1>
<turbo-frame id="task-create"> <! --- new --->
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<button type="submit" class="btn-blue">Submit</button>
</form>
</turbo-frame>
</div>
{% endblock %}
Now if we check http://127.0.0.1:8000/turbo-frame/, we should see the task create form on the top of the page.
Form Action
If we try to submit the form on the index page, it would fail.
Let's check the network, the POST request is sent to http://127.0.0.1:8000/turbo-frame/
, which is wrong. Because the action
of the form is not set yet.
Let's update hotwire_django_app/templates/turbo_frame/create_page.html to explicitly set action
to the form element.
{% extends "turbo_frame/base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="w-full max-w-7xl mx-auto px-4">
<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Create Task</h1>
<turbo-frame id="task-create">
<form method="post" action="{% url 'turbo-frame:task-create' %}"> <!-- update -->
{% csrf_token %}
{{ form|crispy }}
<button type="submit" class="btn-blue">Submit</button>
</form>
</turbo-frame>
</div>
{% endblock %}
- If we type one letter in the
title
field and submit the form. - The form validation would fail, and we can see the form error on the index page immediately, without page reload.
- If the form validation succeed, Django server would return
302
response, and Turbo will visit the list page, since no Turbo frametask-create
is found, we will seeContent missing
on the page. We will fix it in the next section.
Form Response
Any request triggered by an interaction within the Turbo Frame will include a "Turbo-Frame" header
So in Django view, we can check the request header, and then decide to return normal HTTP response or Turbo Frame response.
If the form is valid and from Turbo Frame, we return HTML which contains turbo-frame
tag.
If the form is valid but not from Turbo Frame, we redirect to the detail page.
Update hotwire_django_app/turbo_frame/views.py
from django.http import HttpResponse
def create_view(request):
if request.method == 'POST':
form = TaskForm(request.POST)
if form.is_valid():
instance = form.save()
if 'Turbo-Frame' in request.headers: # new
# if the request comes within Turbo Frame
text = """
<turbo-frame id="task-create">
Task created successfully
</turbo-frame>
"""
return HttpResponse(text)
else:
messages.success(request, 'Task created successfully')
return redirect(reverse('turbo-frame:task-detail', kwargs={'pk': instance.pk}))
status = http.HTTPStatus.UNPROCESSABLE_ENTITY
else:
status = http.HTTPStatus.OK
form = TaskForm()
return render(request, 'turbo_frame/create_page.html', {'form': form}, status=status)
Let's submit the form on the index page, we can see the Task created successfully
message.
As you can see, if the request was sent from Turbo Frame, we can return <turbo-frame>
element to update the page content without refresh, without writing any JavaScript code.
Install django-turbo-response
It is tedious to write raw HTML in Django view, let's use some tool to help us.
Next, let's use django-turbo-response to help us better render Turbo Frame.
Add django-turbo-response==0.0.52
to the requirements.txt
django-turbo-response==0.0.52
(venv)$ pip install -r requirements.txt
Update hotwire_django_app/settings.py
INSTALLED_APPS = [
'turbo_response', # new
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'turbo_response.middleware.TurboMiddleware', # new
...
]
Notes:
- Add
turbo_response
to theINSTALLED_APPS
- Add
turbo_response.middleware.TurboMiddleware
to theMIDDLEWARE
Now django-turbo-response
has been installed, let's use it in our project.
Return Turbo Response Using django-turbo-response
Update hotwire_django_app/turbo_frame/views.py
from turbo_response import TurboFrame
def create_view(request):
if request.method == 'POST':
form = TaskForm(request.POST)
if form.is_valid():
instance = form.save()
messages.success(request, 'Task created successfully')
if request.turbo.frame:
# if the request comes within Turbo Frame
response = TurboFrame(
request.turbo.frame
).template('turbo_frame/messages.html', {}).response(request) # new
return response
else:
return redirect(reverse('turbo-frame:task-detail', kwargs={'pk': instance.pk}))
status = http.HTTPStatus.UNPROCESSABLE_ENTITY
else:
status = http.HTTPStatus.OK
form = TaskForm()
return render(request, 'turbo_frame/create_page.html', {'form': form}, status=status)
request.turbo.frame
containsTurbo-Frame
header from the request, it is set byturbo_response.middleware.TurboMiddleware
- We use
TurboFrame
to return a template response, which contains the successful message.
Fix Form On The Standard Page
Visit http://127.0.0.1:8000/turbo-frame/create/, and try to create a new Task.
We will see the successful message, but not get redirected to the task detail page, let's fix it.
Template
Create hotwire_django_app/templates/turbo_frame/form/create.html
{% load crispy_forms_tags %}
<form method="post" action="{% url 'turbo-frame:task-create' %}">
{% csrf_token %}
{{ form|crispy }}
<button type="submit" class="btn-blue">Submit</button>
</form>
We extract the form code from create_page.html
to form/create.html
Update hotwire_django_app/templates/turbo_frame/create_page.html
{% extends "turbo_frame/base.html" %}
{% block content %}
<div class="w-full max-w-7xl mx-auto px-4">
<h1 class="text-4xl sm:text-6xl lg:text-7xl mb-6">Create Task</h1>
{% if request.turbo.frame == "task-create" %}
<turbo-frame id="task-create">
{% include 'turbo_frame/form/create.html' %}
</turbo-frame>
{% else %}
{% include 'turbo_frame/form/create.html' %}
{% endif %}
</div>
{% endblock %}
The logic is simple, we only render turbo-frame
element, if the HTTP request has Turbo-Frame
header.
- If we create task on http://127.0.0.1:8000/turbo-frame/, the Django view will return Turbo response.
- If we create task on http://127.0.0.1:8000/turbo-frame/create/ the Django view will return 302 response.
The benefit of this approach is, we reused the template code, and make the Django view works for both Turbo Frame and standard pages.
In real projects, this can also help us migrate the project to Turbo Frame step by step, without bringing too much changes to the existing code.