coen.bakker

coen.bakker

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…

Showing Posts 1 to 9

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.

coen.bakker

coen.bakker OP

That’s great. Happily proven wrong. Thank you.

Think I’ll dive into LiveView’s source code also to get a detailed look at what happens in LiveView.JS. Curious what that js struct is that if passed to the Client Utility Commands, like JS.hide.

coen.bakker

coen.bakker OP

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.

vanderlindenma

vanderlindenma

I am having a similar issue.

I have something like

<%= for bla <- blabla do %>
     <div
        id={"#{bla}"}
        phx-remove={
          JS.transition({"ease-out duration-1000", "opacity-100", "opacity-0"},
            time: 1000
          )
        }
      >
        <%= bla %>
      </div >
 <% end %>

When I remove an element from the list blabla on the server, the new list of divs is re-render and the remove element is nicely faded-out except that before the element starts fading out, it first drops one level in the list. As if it was moved lower on the list of divs (I think that’s actually what’s happening in the DOM from what I can see in my inspector).

Any trick to avoid that “bump before remove animation” would be much appreciated :folded_hands:

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.

vanderlindenma

vanderlindenma

Thanks for the hypothesis. That would make sense. You’d almost need a new phx-remove-before-reorder binding… Without something like this, there’s maybe no way around using a more involved custom JS hook.

vanderlindenma

vanderlindenma

Deleted

vanderlindenma

vanderlindenma

Ok, here is a workaround. Much hackier than one would like. And only works if the container’s display is flex. But does the job.

If you’re in a display: flex container, you can use the order CSS-attribute to control the order in which elements appear independent of their order in the DOM.

So you can quickly update the order of the soon-to-be-deleted element before the remove side-effect takes over to make sure it does not visually shifts (despite having been demoted one rank in the DOM order).

Updating my example above, it would look something like this:

<div class="flex">
  <%= for {bla, index} <- Enum.with_index(blabla) do %>
       <div
          style={"order: #{2 * index};"}
          phx-remove={
            JS.set_attribute({"style", "order: #{2 * index - 1}"})
            |> JS.transition({"ease-out duration-1000", "opacity-100", "opacity-0"},
              time: 1000
            )
          }
        >
          <%= bla %>
        </div >
   <% end %>
</div>

If you’re using Tailwind, you could try using classes instead of relying on the style attribute: order - Flexbox & Grid - Tailwind CSS. But I couldn’t make my example work with classes and preset TW classes will limit the number of elements you can include in your list (preset TW order classes currently go from order-1 to order-12).

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.

— All posts loaded —

Where Next? Top

Trending in Questions Top

stjefim
Hello! Suppose you are building workflow (order / task / payment) processing system with the following requirements: Each workflow con...
New
jonnycharles
I’m in search of an Elixir library that offers PDF generation capabilities similar to Ruby’s Prawn. While there have been discussions abo...
New
spammy
I’m looking to build a personal workflow to quickly deploy web applications written in elixir/phoenix, for local consumption (ie not on t...
New
dli
Before I dive in myself, did anyone successfully sprinkle Hologram into their existing LiveView app? Looking for hints regarding: Addi...
New
roeland
Kia ora, We have been using elixir-google-api to connect to Google Drive. However, with the updates to Tesla due to CVEs this is now bro...
New
bottlenecked
Hi all, I wanted to ask how the community is dealing with post-release steps. Today we have Ecto migrations, which make sure that the db...
New
rahultumpala
Hello, I have an Elixir backend that implements a custom protocol over TCP. I want to load test the backend and assess the performance o...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews