I am currently trying to plot multiple markers on a map in a phoenix live view.
One marker is meant to always show the latest known position of an object.
Additionally, the user shall be able to set the timeframe to be displayed and the map will show markers for all known positions during the selected timeframe.
What works?
The Map is rendered as a live component.
I have hooks to add, move or remove markers and pan to other locations on the map.
The marker meant to display the live position works well. It is initially created and then moved whenever a new position update is received.
However, the points listed under 2. seem to only work in some occasions.
What does not work?
The frontend seems to not receive anything if push_event() is called from phoenix update() function
During mount() I can use push_event() to initiate the view (pan to some location) and add initial markers. I can update the initial markers from a handle_info(). This is used to move a devices marker live to the last known position and works well.
To display a timeseres, my idea was to simply pass a list of positions to the map component and clear/add the markers from the components update() function. So the user would select a timeframe and the resulting positions are passed to the map component in the assigns. However, no events arrive at the frontend, even tho the backend seems to do exactly what I want (based on some printouts).
Copying the exact same code from the mount() function to the update() function also does not work (I tested it with a hardcoded list of positions that are pushed to the frontend).
Questions
Does it make a difference if I call push_event() from update()?
What could be reasons that the frontend does not receive the events, if it works from mount() and handle_info()?
can you show us the code that’s causing the issue? I have my suspicion from your description but let’s see what you have there. Just the update function should do.
Sure, I stripped it down to a minimal example. Hope I didn’t forget anything.
I added more than just the update function for people who end up here in future
defmodule LeafletMap do
use Surface.Component
prop positions, :list, default: []
def update(assigns, socket) do
socket = Map.put(socket, :assigns, assigns)
socket =
assigns.positions
|> Enum.reduce(socket, fn {lat, lon}, acc_socket ->
acc_socket
|> push_event("add_marker", %{lat: lat, lon: lon})
end)
IO.inspect socket.private # <-- lists the pushed events as expected
{:ok, socket}
end
def render(assigns) do
~F"""
<div class="ui segment" phx-update="ignore">
<div id="mapid" style={"height: #{@height};"} :hook="Map"></div>
</div>
"""
end
end
end
Map LiveView in plot_positions.ex_:
defmodule PlotPositions do
use Surface.LiveView
def mount(_params, _session, socket) do
positions = #.. read from file or DB
{:ok, socket |> assign(positions: positions)}
end
def render(assigns) do
~F"""
<LeafletMap
positions={@positions}
/>
"""
end
end
I don’t have an idea as to why the client doesn’t receive the pushed events from the server, but I did notice some things.
I believe the hook’s updated callback isn’t called because you use a stateless component, which should be re-rendered each time the parent template changes:
A stateless component is always mounted, updated, and rendered whenever the parent template changes. That’s why they are stateless: no state is kept after the component.
Even if you did used a stateful component instead, you would still see the same behaviour due to the way you assign the assigns and the fact that nothing in the hook’s container actually changes.
I wouldn’t assign the assigns like this as the engine won’t know if any of the assigns changed since you’ve manually changed it. Better use socket = assign(socket, assigns)
I ended up having a custom update_map() function that encapsulates all the push_event() calls and other update logic. And I simply call it from the live view when the map needs to be updated (<- happens only as a result of user input, so that is easy to do).