coen.bakker

coen.bakker

LiveView: Is there a way to FIRST delete item from repo and THEN animate out the corresponding UI element?

GOAL
In one of my practice projects, I have a list of items that are stored in a Postgres database and displayed as <li></li> items in the UI. When a user requests an item to be deleted, the item should be deleted from the database and removed from the UI with a certain animation.

CURRENT SOLUTION
My current solution is to first animate out the item from the UI and on completion send a deletion request to the LiveView server. If deletion fails, the item is animated back in.

Specifically:

<!-- HTML page -->
<button class="warning" phx-click={JS.dispatch("delete-task")} data-id={@id}>Delete</button>

// JS Mounted Hook
export default Hooks = {}

Hooks.CustomEventListeners = {
  mounted() {
  
    window.addEventListener('delete-task', e => {
      const task = e.detail.dispatcher
      const taskId = task.dataset.id
      if (confirm('Please confirm you want to delete this task.')) {
        this.animateOut(taskId)
        setTimeout(() => this.pushEvent('delete', {id: taskId}), this.ANIMATION_DURATION)
      } else {
        const target = document.querySelector(`#task-${taskId}`)
        const event = new Event('cancel-menu', {bubbles: true, cancelable: true})
        target.dispatchEvent(event)
      }
    })

  },
  ANIMATION_DURATION: 500,
  animateOut(id) {
    const task = document.querySelector(`#task-${id}`)
    const targetHeight = task.offsetHeight
    task.animation = task.animate(
      [
        {height: `${targetHeight}px`, transform: 'none', margin: '8px 0px'},
        {height: '0px', transform: 'rotateY(-90deg)', margin: '-4px 0px'}
      ],
      {
        duration: this.ANIMATION_DURATION,
        easing: 'cubic-bezier(.36, -0.64, .34, 1.76)',
        fill: 'both'
      }
    )
  }
}

# LiveView

  def mount(_params, _session, socket) do
    if connected?(socket), do: Tasks.subscribe()

    socket =
      socket
      |> assign(...)

    {:ok, socket}
  end

  def handle_info({:task_deleted, task}, socket) do
    tasks =
      socket.assigns[:tasks]
      |> Enum.filter(fn t -> t.id != task.id end)

    socket =
      socket
      |> assign(tasks: tasks)
    {:noreply, socket}
  end

  def handle_event("delete", %{"id" => id}, socket) do
    case Tasks.delete_task_by_id(id) do
      {:ok, _} ->
        socket =
          socket
          |> put_flash(:info, "Task deleted.")
        {:noreply, socket}
      {_, _} ->
        socket =
          socket
          |> put_flash(:error, "Could not delete task.")
          |> push_redirect(to: "/", replace: true)
        {:noreply, socket}
    end
  end

PREFERRED SOLUTION
It bugs me, however, that I haven’t been able to figure out a way to reverse the order: first delete the item from the database, and in case of success, animate the UI element out. I would like to master the control over LiveView, in combination with JavaScript, to be able to solve this, and similar, problems.

In a JS-only application you would be able to separate the database deletion and the removal of the corresponding element from the DOM. I haven’t figured out how to do the equivalent with LiveView. Since there isn’t a thread blocking beforeDestroy hook, I got the impression it might not be in the stars altogether at this point. But I love to be proven wrong.

Curious to your ideas…

Marked As Solved

chrismccord

chrismccord

Creator of Phoenix

you shouldn’t need any of the javascript. Something like this should be it (provided the tasks have their own DOM ids), replace tailwind classes with whatever you have:

~H"""
<%= for task <- @tasks do %>
  <div id={"task-#{task.id}"} phx-remove={fade_out()}>...
<% end %>
"""

def fade_out(%JS{} \\ js) do
  JS.hide(transition:
    {"transition-all transform ease-in duration-300", "opacity-100 scale-100", "opacity-0 scale-95"}
  )
end

The phx-remove will apply when the container is removed from the DOM, so you can simply let the LV remove the task.

Also Liked

coen.bakker

coen.bakker

I got this solution to work… mostly. A list item is deleted and the DOM element of that item first animates out and then gets removed from the DOM. However, the list element jumps to another position in the list at the instance that its item is deleted from the database. If I don’t set a setTimeout on the delete-from-database request, the UI item jump to another position immediately. If I do set a setTimeout the UI element starts the animation in the right list position, but as soon as the timeout is over the item jumps. So, if I set a timeout equal to the animation duration, the user sees the animation as it is intended.

Also, I was wondering whether onBeforeElUpdated (docs) could also be used to animate out the list items. In the docs it is mentioned that onBeforeElUpdated can be used for “For integration with client-side libraries which require a broader access to full DOM management, …”. I am not making a library, but I try to get a feeling for when onBeforeElUpdated is the right tool for the job at hand.

I noticed for example that when I simply console log the from and to of onBeforeElUpdated(from, to), I don’t get logs when I add a new list item. My initial suspicion was that the to would still log a DOM node, since LiveView does update.

I am interested in a possible alternative solution for the one that was posted yesterday for the sake of learning but also because I have JS calculations (e.g. Window.getComputedStyle()) that need to be done before some of my animations.

imkleats

imkleats

It’s not a solution, but it’s a hypothesis of what is happening. It sounds like the DOM updates/patches are being applied sequentially, so the item that replaced the deleted item is being placed at the newly-correct index just before the deletion is applied to the DOM, which then triggers the transition effect. This would result in the swapping of the items.

If that is the case, I don’t know off the top of my head how to solve it.

imkleats

imkleats

As a more elegant CSS option that removes a good chunk of your template code, could you use a calc off of a data attribute off the html tag to set the flex order? Then you could compose a .item-deleted class that does the calculation offset by one, and you simply need to set the attribute in your template and apply the utility class to the deleted item instead.

Where Next?

Popular in Questions Top

greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
tduccuong
Hi, is there any work on GUI with Elixir, that is similar to Electron/Javascript? My idea is to bundle Phoenix and BEAM into a single se...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" =&gt; #BSON.ObjectId&lt;58eb1a7a9ad169198c3dXXXX&gt;, "email" =&gt; ...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New

Other popular topics Top

vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
TunkShif
This post is an instruction guide to help you setup your Neovim for Elixir development from scratch. It includes general information on h...
274 41989 114
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New

We're in Beta

About us Mission Statement