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…