Django: combine ListView and DeleteView to use with HTMX?

Question:

I’m using Django with HTMX to manage a CRUD table in which I want to list and delete objects.

For this, I have a ListView which displays a table (using django-tables) with pagination, sorting, and text search features. It works as expected with HTMX: for example, if you go to the next page, the whole page is not reloaded: HTMX gets the table and upload a specific part of the DOM.

It looks like this:

enter image description here

The code of the ListView looks like:

class Proposals(SingleTableMixin, SearchableMixin, ListView):
    table_class = ProposalTable # for django-tables2
    ordering = "id"
    model = Proposal
    paginate_by = 5
    search_filters = ["title__icontains"] # custom text search

    def get_template_names(self):
        # Trick to chose between loading the whole page (first time user goes to the page) or just the table via HTMX (when user uses pagination or sorting, it reloads the table only in the DOM)
        if self.request.htmx:
            return "fragments/proposals_fragment.html"
        return "proposals.html"

Now, I’m trying to add a delete feature, with the best UX. I have explored multiple ways until now:

  1. Just by removing the row from the DOM once the object is actually removed from the DB → bad: while it’s fast, it makes the pagination wrong/inconsistent, and with less objects in the table page.

  2. Telling HTMX to redirect to the current url (response["HX-Redirect"] = request.htmx.current_url) → bad: while the final result is ok, it’s slow, and the user can use the UI and make unwanted actions until the redirection has actually occurred. And of course, I don’t benefit from HTMX partial DOM update features here.

So, I was thinking of a 3rd method which sounds better:

  1. When the user deletes the object, it should delete the object in DB, then act exactly like the ListView in HTMX mode (ie: return the table page). That way I would be able to update the DOM locally with the new table page, without a full page reload.

The thing is I have no idea about how to do this in Django. I’m pretty sure I don’t want to mess with FBVs, but I need some guidance. Here is the things I’m thinking of:

  • using two different urls ("proposals" (GET) and "proposals/int:pk/delete" (DELETE)) pointing to the same view
  • This view could be a custom one named “ListDeleteView” combining lower level Django mixins together like MultipleObjectTemplateResponseMixin + BaseListView + DeletionMixin. Or maybe others?

Am I on the right path? Can you provide some guidance, especially about how to make combined generic views, if it’s a good idea to you?

Thanks a lot.

Asked By: David Dahan

||

Answers:

What you want to do is to implement delete method on the Proposals list view, DeleteMixin implementation is very simple so you can use it and have something like this:

class Proposals(SingleTableMixin, SearchableMixin, DeletionMixin, ListView):
    table_class = ProposalTable # for django-tables2
    ordering = "id"
    model = Proposal
    paginate_by = 5
    search_filters = ["title__icontains"] # custom text search

    def get_template_names(self):
        if self.request.htmx and not self.request.htmx.history_restore_request:
            return "fragments/proposals_fragment.html"
        return "proposals.html"

    def delete(self, request, *args, **kwargs):
        super().delete(request, *args, **kwargs)
        return super().get(request, *args, **kwargs)

Here you override the delete, let it play and then return result as it was normal get instead of the redirect. And you want also want to handle htmx.history_restore_request I added that as well.

Answered By: preator

Alternatively you can use a function view that returns nothing to replace the deleted row. Following your example

@require_http_methods(['DELETE'])
def delete_proposal(request, pk):
    Proposals.objects.filter(id=pk).delete()
    return render(request, "proposal/partial/proposal_delete.html")

Where proposal/partial/proposal_delete.html would be an empty file. You would need to feed this view into your hx-delete but that should do the job.

Answered By: bedevere