Prepare to Learn Turbo Frame
Objective
- Prepare Django app for us to learn
Turbo Frame
Django App
Let's create turbo_frame
app, we will learn how Turbo Frame
works with this Django app.
(venv)$ mkdir -p ./hotwire_django_app/turbo_frame
(venv)$ python manage.py startapp turbo_frame ./hotwire_django_app/turbo_frame
./hotwire_django_app
├── __init__.py
├── asgi.py
├── settings.py
├── tasks
├── templates
├── turbo_drive
├── turbo_frame # new
├── urls.py
└── wsgi.py
Update hotwire_django_app/turbo_frame/apps.py to change the name to hotwire_django_app.turbo_frame
from django.apps import AppConfig
class TurboFrameConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'hotwire_django_app.turbo_frame' # update
Add hotwire_django_app.turbo_frame
to the INSTALLED_APPS
in hotwire_django_app/settings.py
INSTALLED_APPS = [
...
'hotwire_django_app.turbo_frame', # new
]
# check if there is any error
$ ./manage.py check
System check identified no issues (0 silenced).
View
Edit hotwire_django_app/turbo_frame/views.py
import http
from django.shortcuts import render, redirect, get_object_or_404
from django.urls import reverse
from django.contrib import messages
from hotwire_django_app.tasks.models import Task
from hotwire_django_app.tasks.forms import TaskForm
def list_view(request):
object_list = Task.objects.all().order_by('-pk')
context = {
"object_list": object_list,
}
return render(request, 'turbo_frame/list_page.html', context)
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')
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)
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()
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)
def delete_view(request, pk):
instance = get_object_or_404(Task, pk=pk)
if request.method == 'POST':
instance.delete()
messages.success(request, 'Task deleted successfully')
return redirect('turbo-frame:task-list')
return render(request, 'turbo_frame/delete_page.html', {'instance': instance})
def detail_view(request, pk):
instance = get_object_or_404(Task, pk=pk)
return render(request, 'turbo_frame/detail_page.html', {'instance': instance})
Notes:
- Here we create 4 FBV, which are for CRUD work.
- If task is created or updated successfully, user would be redirected to the task detail page.
- If task is deleted successfully, user would be redirected to the task list page.
Template
base.html
create hotwire_django_app/templates/turbo_frame/base.html
{% load webpack_loader static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% stylesheet_pack 'turbo_frame' attrs='data-turbo-track="reload"' %}
{% javascript_pack 'turbo_frame' attrs='data-turbo-track="reload" defer' %}
</head>
<body>
{% include 'turbo_frame/navbar.html' %}
{% include 'turbo_frame/messages.html' %}
{% block content %}
{% endblock content %}
</body>
</html>
Notes:
- Please note in this Django app, we will use
turbo_frame
entry file ({% javascript_pack 'turbo_frame'
). we will create it in a bit.
Create hotwire_django_app/templates/turbo_frame/messages.html
{% for message in messages %}
{# data-turbo-cache="false" will tell Turbo to not cache the element #}
<div data-turbo-cache="false" class="p-4 mb-4 text-sm text-green-700 bg-green-100 rounded-lg dark:bg-green-200 dark:text-green-800" role="alert">
{{ message|safe }}
</div>
{% endfor %}
Create hotwire_django_app/templates/turbo_frame/navbar.html
<nav class="flex items-center justify-between flex-wrap bg-teal-500 p-6 mb-4">
<div class="w-full">
<a href="{% url 'turbo-frame:task-list' %}" class="inline-block mt-0 text-teal-200 hover:text-white mr-4">
List
</a>
<a href="{% url 'turbo-frame:task-create' %}" class="inline-block mt-0 text-teal-200 hover:text-white mr-4">
Create
</a>
</div>
</nav>
Please note the URL namespace is turbo-frame
now.
create_page.html
Create hotwire_django_app/templates/turbo_frame/create_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">Create Task</h1>
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<button type="submit" class="btn-blue">Submit</button>
</form>
</div>
{% endblock %}
update_page.html
create hotwire_django_app/templates/turbo_frame/update_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">Edit Task</h1>
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<button type="submit" class="btn-blue">Submit</button>
<a href="{% url 'turbo-frame:task-list' %}" class="btn-red">Cancel</a>
</form>
</div>
{% endblock %}
delete_page.html
Create 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>
<form method="post">
{% 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>
<a href="{% url 'turbo-frame:task-list' %}" class="btn-red">Cancel</a>
</form>
</div>
{% endblock %}
list_page.html
Create hotwire_django_app/templates/turbo_frame/list_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">Task List</h1>
<div class="md:w-2/3 bg-white rounded-lg border mb-4">
<ul class="divide-y-2 divide-gray-100" id="task-list-ul">
{% for instance in object_list %}
<li class="p-3 flex items-center">
{% include 'turbo_frame/task_detail.html' with instance=instance only %}
</li>
{% endfor %}
</ul>
</div>
</div>
{% endblock %}
detail_page.html
Create hotwire_django_app/templates/turbo_frame/detail_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">Task Detail</h1>
<div class="mb-3 p-3 border">
{% include 'turbo_frame/task_detail.html' with instance=instance only %}
</div>
<div>
<a href="{% url 'turbo-frame:task-list'%}" class="btn-blue">Go to list</a>
</div>
</div>
{% endblock %}
task_detail.html
Create hotwire_django_app/templates/turbo_frame/task_detail.html
<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 }}
This template is used in both list_page.html
and detail_page.html
Now we have file structure like this
./turbo_frame
├── base.html
├── create_page.html
├── delete_page.html
├── detail_page.html
├── list_page.html
├── messages.html
├── navbar.html
├── task_detail.html
└── update_page.html
Frontend
Create frontend/src/styles/turbo_frame.scss
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
.btn-blue {
@apply inline-flex items-center;
@apply px-4 py-2;
@apply font-semibold rounded-lg shadow-md;
@apply text-white bg-blue-500;
@apply hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75;
}
.btn-red {
@apply inline-flex items-center;
@apply px-4 py-2;
@apply font-semibold rounded-lg shadow-md;
@apply text-white bg-red-500;
@apply hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-400 focus:ring-opacity-75;
}
Create frontend/src/application/turbo_frame.js
// This is the scss entry file
import "../styles/turbo_frame.scss";
import "@hotwired/turbo";
Notes:
- We import
turbo_frame.scss
we just created - And this JS entry file would be imported in the above
turbo_frame/base.html
URL
Create hotwire_django_app/turbo_frame/urls.py
from django.urls import path
from .views import list_view, create_view, update_view, delete_view, detail_view
app_name = 'turbo-frame'
urlpatterns = [
path('list/', list_view, name='task-list'),
path('create/', create_view, name='task-create'),
path('<int:pk>/', detail_view, name='task-detail'),
path('<int:pk>/update/', update_view, name='task-update'),
path('<int:pk>/delete/', delete_view, name='task-delete'),
]
Notes:
- Here we use
app_name
to set thenamespace
of the Django app.
Update hotwire_django_app/urls.py
from django.contrib import admin
from django.urls import path, include
from django.views.generic import TemplateView
urlpatterns = [
path('', TemplateView.as_view(template_name="index.html")),
path('turbo-drive/', include('hotwire_django_app.turbo_drive.urls')),
path('turbo-frame/', include('hotwire_django_app.turbo_frame.urls')), # new
path('admin/', admin.site.urls),
]
Manual Test
Restart webpack
$ npm run start
(venv)$ python manage.py runserver
- Visit http://127.0.0.1:8000/turbo-frame/list/, you can see task list.
- Try to click the top
Create
link and create a new Task. - Try to edit the existing Task.
- Try to delete the existing Task.
Conclusion
In this chapter, we created a basic CRUD app, next, we will use Turbo Frame to make it more interactive.