Inline Editing With Turbo Frames
Objective
- Learn how to do inline editing with Turbo Frames
Detail
Template
Update hotwire_django_app/templates/turbo_frame/task_detail.html
<turbo-frame id="task-detail-{{ instance.pk }}" class="flex-1"> <! --- new --->
<a class="btn-blue mr-3" href="{% url 'turbo-frame:task-update' instance.pk %}">
Edit
</a>
<a class="btn-red mr-3" href="{% url 'turbo-frame:task-delete' instance.pk %}">
Delete
</a>
{{ instance.due_date }}: {{ instance.title }}
</turbo-frame>
Now, in the <turbo-frame id="task-list">
, every li
element contains <turbo-frame id="task-detail-{{ instance.pk }}"
It should look like this
<ul id="task-list-ul">
<li>
<turbo-frame id="task-detail-1">
Task Detail 1
</turbo-frame>
</li>
<li>
<turbo-frame id="task-detail-2">
Task Detail 2
</turbo-frame>
</li>
</ul>
- If we click button inside the
turbo-frame id="task-detail-{{ instance.pk }}"
, the task detail would be updated within theli
element. - On the server side, we can return Turbo Frame to only update specific
turbo-frame id="task-detail-{{ instance.pk }}"
Edit
View
Update hotwire_django_app/turbo_frame/views.py
def update_view(request, pk):
instance = get_object_or_404(Task, pk=pk)
if request.method == 'POST':
form = TaskForm(request.POST, instance=instance)
if form.is_valid():
form.save()
if request.turbo.frame:
# if request come from Turbo Frame
return redirect(reverse('turbo-frame:task-detail', kwargs={'pk': instance.pk}))
else:
# if request come from standard page
messages.success(request, 'Task update successfully')
return redirect(reverse('turbo-frame:task-detail', kwargs={'pk': instance.pk}))
status = http.HTTPStatus.UNPROCESSABLE_ENTITY
else:
status = http.HTTPStatus.OK
form = TaskForm(instance=instance)
return render(request, 'turbo_frame/update_page.html', {'form': form}, status=status)
Template
Create hotwire_django_app/templates/turbo_frame/form/update.html
{% load crispy_forms_tags %}
<form method="post" action="{% url 'turbo-frame:task-update' form.instance.pk %}">
{% csrf_token %}
{{ form|crispy }}
<button type="submit" class="btn-blue">Submit</button>
{% if request.turbo.frame %}
<a href="{% url 'turbo-frame:task-detail' form.instance.pk %}" class="btn-red">Cancel</a>
{% else %}
<a href="{% url 'turbo-frame:task-list' %}" class="btn-red">Cancel</a>
{% endif %}
</form>
If the request has Turbo Frame header, the Cancel
link points to the task detail URL.
Update hotwire_django_app/templates/turbo_frame/update_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">Edit Task</h1>
{% if request.turbo.frame %}
<turbo-frame id="{{ request.turbo.frame }}">
{% include 'turbo_frame/form/update.html' %}
</turbo-frame>
{% else %}
{% include 'turbo_frame/form/update.html' %}
{% endif %}
</div>
{% endblock %}
Delete
View
Update hotwire_django_app/turbo_frame/views.py
def delete_view(request, pk):
instance = get_object_or_404(Task, pk=pk)
if request.method == 'POST':
instance.delete()
if request.turbo.frame:
# if request come from Turbo Frame
response = TurboFrame(f"task-detail-{pk}").response('') # new
return response
else:
# if request come from standard page
messages.success(request, 'Task deleted successfully')
return redirect('turbo-frame:task-list')
return render(request, 'turbo_frame/delete_page.html', {'instance': instance})
Here we return empty HTML for Turbo Frame to replace the content of the frame.
Template
Create hotwire_django_app/templates/turbo_frame/form/delete.html
{% load crispy_forms_tags %}
<form method="post" action="{% url 'turbo-frame:task-delete' instance.pk %}">
{% csrf_token %}
<div class="p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-800" role="alert">
Are you sure you want to delete "{{ instance.title }}"?
</div>
{{ form|crispy }}
<button type="submit" class="btn-blue">Submit</button>
{% if request.turbo.frame %}
<a href="{% url 'turbo-frame:task-detail' instance.pk %}" class="btn-red">Cancel</a>
{% else %}
<a href="{% url 'turbo-frame:task-list' %}" class="btn-red">Cancel</a>
{% endif %}
</form>
Update hotwire_django_app/templates/turbo_frame/delete_page.html
{% 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">Delete Task</h1>
{% if request.turbo.frame %}
<turbo-frame id="{{ request.turbo.frame }}">
{% include 'turbo_frame/form/delete.html' %}
</turbo-frame>
{% else %}
{% include 'turbo_frame/form/delete.html' %}
{% endif %}
</div>
{% endblock %}
Manual Test
Test 1
- Now visit http://127.0.0.1:8000/turbo-frame/
- We pick one task and click
Edit
button to edit the task - Turbo sent request to
http://127.0.0.1:8000/turbo-frame/{pk}/update/
, since the button was in<turbo-frame>
element, the request hasTurbo Frame
header. - Django server return response which contains
<turbo-frame id="task-detail-{pk}">
- The specific
turbo-frame
will be updated to display theedit form
, so we can do inline editing. - We edit the task and then click
submit
button to submit the form. - If the form validation succeed, Django server will return
redirect
response. - Turbo send request to
http://127.0.0.1:8000/turbo-frame/{pk}/
, and the<turbo-frame id="task-detail-{pk}">
in the response will be used to update the content of the specificturbo-frame
.
Test 2
- Now visit http://127.0.0.1:8000/turbo-frame/
- We pick one task and click
Delete
button to delete the task - Turbo sent request to
http://127.0.0.1:8000/turbo-frame/{pk}/delete/
, since the button was in<turbo-frame>
element, the request hasTurbo Frame
header. - Django server return response which contains
<turbo-frame id="task-detail-{pk}">
but the content is empty. - So the
turbo-frame
element will be updated to display the empty content - We should find a way to remove the whole
li
element from theul
, I will talk about the solutionTurbo Stream
in later chapter.
Conclusion
In this chapter, we have learned how to use Turbo Frame to do inline editing, please use browser devtool to check the request and response and make sure you understand how it works.
We will keep improving the total workflow in the next chapters.