fceruti
Double dom event handler using Alpine.js & LiveView
In a nutshell the issue I’m facing is that every time I register alpine’s @ or x-on event handler, two are registered and called when the action is performed.
The issue is solved if I comment liveSocket.connect(), which leads me to believe the problem is in the interoperation of liveSocket and Alpine.
I’ve circled the internet without success. Has anyone had this issue and solved it?
Packages:
Phoenix: 1.6.2
Phoenix Live View: 0.16.4
Surface: 0.6.1
Alpinejs: 3.3.5 & 3.4.2
Surface component:
defmodule TimetaskApp.WeekCalendar do
use Surface.LiveComponent
prop hours, :list, required: true
prop dates, :list, required: true
@impl true
def render(assigns) do
~F"""
<div class="w-full h-full flex flex-col" style="height: calc(100vh - 100px)" id="week-calendar"> {!-- Calendar --}
<div class="h-full flex-1 flex flex-row relative">
<div
class="flex-1 flex"
id="week-calendar-columns"
:hook="WeekCalendar"
x-init="fromHour = +$el.dataset.fromHour; toHour = +$el.dataset.toHour;"
x-data="week_calendar_data"
data-from-hour={List.first(@hours)}
data-to-hour={List.last(@hours)}
>
<div
class="flex-1 flex"
id="week-calendar-days-container"
x-ref="days_container"
>
{#for date <- @dates}
<div
id={"day_col_#{format_date(date)}"}
data-date={format_date(date)}
class="flex-1 border-r border-tt-border-calendar relative"
@mousedown.stop="startNewTimebox($event, $el.dataset.date)"
>
</div>
{/for}
</div>
</div>
</div>
</div>
"""
end
end
app.js:
import { Socket } from "phoenix"
import { LiveSocket } from "phoenix_live_view"
import topbar from "topbar"
import Alpine from "alpinejs"
import Hooks from "./_hooks"
import "./app/week_calendar.js"
window.Alpine = Alpine
Alpine.start()
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
params: { _csrf_token: csrfToken },
hooks: Hooks,
dom: {
onBeforeElUpdated(from, to) {
if (from._x_dataStack) {
window.Alpine.clone(from, to)
}
},
},
});
topbar.config({ barColors: { 0: "#03ad8c" }, shadowColor: "rgba(0, 0, 0, .3)" })
window.addEventListener("phx:page-loading-start", info => topbar.show())
window.addEventListener("phx:page-loading-stop", info => topbar.hide())
liveSocket.connect()
window.liveSocket = liveSocket
./app/week_calendar.js
document.addEventListener("alpine:init", () => {
Alpine.data("week_calendar_data", () => ({
fromHour: 0,
toHour: 0,
newTimebox: null,
startNewTimebox(event, date) {
const fromTime = this.getTimeFromEvent(event)
this.newTimebox = {
date: date,
fromTime: fromTime,
toTime: null
}
console.log(`Start new timebox ${date} ${fromTime.hour}:${fromTime.minute}`)
},
}))
});
Chrome dev tools:
Chrome console:
![]()
Marked As Solved
cmo
Here is a cut down version. Note that I’m using Surface, hence the <#Raw> tags (I assume there is a parallel in plain old liveview) and different syntax. The hook reads attributes from the hidden spans and sends them to Apline. Alpine creates the html for the options. I had a bit of trouble getting this to work and when it worked I stopped touching it and moved on. I hope to revist if/when I get some time ![]()
<div
id={"column-filter-#{@field}"}
:hook="ColumnOptionsFilter"
x-data="{
...
options: [],
...
}"
@options-loaded="options = $event.detail.options"
...
>
<div id={"column-filter-#{@field}-data"} hidden>
{#for option <- @all_options}
<span data-option={option} />
{/for}
</div>
...
<div id={"column-filter-options-#{@field}"} phx-update="ignore">
<#Raw>
<template x-for="(option, index) in options" :key="index">
</#Raw>
<div class="flex items-center py-1">
<input
x-model="selected"
:value="option"
:id="option"
:name="option"
type="checkbox"
:class="{ 'opacity-50': !available[index]}"
class="h-4 w-4 text-blue-900 focus:ring-blue-900 border-gray-300"
/>
<label
:for="option"
x-text="option"
:class="{ 'text-gray-400': !available[index], 'text-gray-900': available[index] }"
class="ml-2 block text-sm"
/>
</div>
<#Raw>
</template>
</#Raw>
</div>
...
</div>
Hook:
const ColumnOptionsFilter = {
mounted() {
const options = Array.from(
document.getElementById(this.el.id + "-data").children || []
).map(({ dataset: { option, available, selected } }) => {
return option === undefined ? "[Blanks]" : option;
});
...
setTimeout(() => {
if (options.length > 0) {
var event = new CustomEvent("options-loaded", {
detail: { options: options, available: available },
});
this.el.dispatchEvent(event);
this.optionsLoaded = true;
}
}, 0);
},
Also Liked
fceruti
Solved it!
Solution summary:
Don’t generate LiveView controlled tags inside x-data scope. Use alpine’s templates to render and pass data to x-data.
Do this:
<div x-data={"{elements: #{render_elements(elements)}}"}>
<#Raw><template x-for="ele in elements"></#Raw>
<span x-text="ele" @click="console.log('click')"/>
<#Raw></template></#Raw>
</div>
Not this:
<div x-data>
{#for ele in elements}
<span @click="console.log('click')">{ele}</span>
{/for}
</div>
Popular in Questions
Other popular 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
- #javascript
- #code-sync
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance










