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…
Trending in Questions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #blog-post
- #phoenix_html
- #iex
- #graphql
- #ai
- #genstage
- #elixirconf-us
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #security
- #hex










Showing Posts 1 to 9- Show Best Posts
- Show All Posts (oldest first)
- Show All Posts (newest first)
chrismccord
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:
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
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
jsstruct is that if passed to theClient Utility Commands, likeJS.hide.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
setTimeouton the delete-from-database request, the UI item jump to another position immediately. If I do set asetTimeoutthe 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 thatonBeforeElUpdatedcan 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 whenonBeforeElUpdatedis the right tool for the job at hand.I noticed for example that when I simply console log the
fromandtoofonBeforeElUpdated(from, to), I don’t get logs when I add a new list item. My initial suspicion was that thetowould 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
I am having a similar issue.
I have something like
When I remove an element from the list
blablaon 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
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
Thanks for the hypothesis. That would make sense. You’d almost need a new
phx-remove-before-reorderbinding… Without something like this, there’s maybe no way around using a more involved custom JS hook.vanderlindenma
Deleted
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: flexcontainer, you can use theorderCSS-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:
If you’re using Tailwind, you could try using classes instead of relying on the
styleattribute: 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 fromorder-1toorder-12).imkleats
As a more elegant CSS option that removes a good chunk of your template code, could you use a
calcoff of a data attribute off the html tag to set the flex order? Then you could compose a.item-deletedclass 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.