Is this proper way to handle all CRUD operations in a single view (endpoint) using HTMX?
I'm new to using HTMX with Django and I'm seeking guidance. I'm in the process of redesigning a large CRUD application, aiming to integrate HTMX for improved interactions. Given the complexity of the app and its interactions with the database across various models, I want to ensure I start off on the right foot by adopting HTMX best practices early on.
I've seen in many tutorials that everyone follows the common practice of creating separate views and URL paths for each database transaction (such as create, edit, delete, update). As I think that this could result in a significant amount of additional code and time investment, I would prefer to put all of the application logic in just one view (or endpoint), for each model.
Can you please suggest me if this is the right flow and right way to do it:
def movie(request, template_name="movies.html"):
movies = Movie.objects.all()
movie_form = MovieForm(request.POST or None)
if request.method == 'POST':
action = request.POST.get('action')
# 1) CREATE
if action == 'add_movie':
if movie_form.is_valid():
movie_form.save()
content = render(request, 'partials/movie-table.html', {'movies': movies})
return HttpResponse(content, status=201,
headers={
'HX-Trigger': json.dumps({'create': movie_form.cleaned_data['name']})}
)
else:
content = render(request, 'error.html', {'error': movie_form.errors})
return HttpResponse(content, status=202)
# 2) DELETE
elif action == 'delete_movie':
Movie.objects.get(id=request.POST.get('movie_id')).delete()
content = render(request, 'movie.html', {'movies': movies})
return HttpResponse(content, status=201,
headers={
'HX-Trigger': json.dumps({'delete: movie_form.cleaned_data['name']})}
)
# 3) UPDATE (two steps are necessary)
# first step is used to get the necessary movie data that will populate the form
elif action == 'load_movie':
movie_id = request.POST.get('movie_id')
movie_form = MovieForm(instance = Movie.objects.get(id=request.POST.get('movie_id')))
movie_form.id = movie_id
return render(request, 'edit-movie.html', {'movie_form': movie_form})
# UPDATE (second step, actual edit)
elif action == 'edit_movie':
movie_form = MovieForm(request.POST, instance = Movie.objects.get(id=request.POST.get('movie_id')))
if movie_form.is_valid():
movie_form.save()
content = render(request, 'movie.html', {'movies': movies})
return HttpResponse(content, status=201,
headers={
'HX-Trigger': json.dumps({'update': movie_form.cleaned_data['name']})}
)
else:
return HttpResponse(status=400)
return render(request, template_name, {'movies' : movies, 'movie_form' : movie_form})
Here is the explanation of the code above - the user interface comprises three distinct buttons: Add, Edit, and Delete. Each button triggers a different action, communicated to the backend using the hx-vals argument:
<div class="ui button" id="add_movie__button" name="add_movie__button" type="submit" hx-post="{% url 'movie' %}" hx-vals='{"action": "add_movie"}' hx-target="#table_movies">
Based on the value of hx-vals (action), the backend determines how to handle the posted parameters. Once the addition, editing, or deletion process is complete, the backend sends an appropriate HX-Trigger (e.g., 'update') to JavaScript. This trigger assists in determining the subsequent actions to take, such as updating the modal and displaying relevant messages to the user.
While using a single endpoint has its advantages, such as streamlining development, it also presents challenges, particularly with the Update procedure. In this scenario, the backend initially needs to receive the ID of the movie the user wishes to edit. Then, it retrieves the corresponding movie from the database, sends it back to populate the form, and awaits further user input. Upon submission, another action is sent to the backend to update the movie's data, which is then saved to the database.
Just in case someone wants to check, I have included relevant parts of the templates:
modals.html
<!-- Delete button -->
...
<div class="button" id="delete_movie__button" name="delete_movie__button" type="button" hx-post="{% url 'movie' %}" hx-target="#table_movies">Delete<div>
...
<!-- Edit button, that is part of the edit form, that is part of the modal -->
...
<div class="button" id="edit_movie__button" name="edit_movie__button" type="submit" hx-post="{% url 'movie' %}" hx-target="#table_movies" hx-vals='{"action": "edit_movie", "movie_id": "{{ movie_form.id }}"}'>
<i class="add icon"></i> Edit
...
main_template.html
...
function deleteMovieModal(){
// opens delete movie modal
$('#delete_movie__modal').modal('show');
}
function editMovieModal(){
// opens edit movie modal
$('#edit_movie__modal').modal('show');
}
function deleteMovieId(movie_id){
// adds hx-vals argument to the delete button in delete movie modal
document.getElementById("delete_movie__button").setAttribute("hx-vals",`{"action": "delete_movie", "movie_id": "${movie_id}"}`);
}
...
partials/table_movies.html
...
<td>
<!-- clicking this button opens delete movie modal and calls the function that passes an movie id argument to the delete button in delete movie modal -->
<div class="button" onclick="deleteMovieModal();deleteMovieId({{ movie.id }});">Delete</div>
</td>
<td>
<!-- clicking this button opens edit movie modal and sends htmx request that populates form data with choosen movie, in the edit movie modal -->
<div class="button" onclick="editMovieModal();editMovieId({{ movie.id }});" hx-post="{% url 'movie' %}" hx-vals='{"action": "load_movie", "movie_id": {{ movie.id }} }' hx-target="#edit_movie__modal_content" >Edit </div>
</td>
...
Can you share your thoughts about this approach? Any thoughts/ideas/suggestions are appreciated.