netto
I need to persist a value to a user session navigating between liveviews. I have found similar use cases describing some “cart” functionality but no solution fit. Most of the info is pre phoenix 1.7 without sigil_p, avoiding Phoenix.Router.Helpers and other goodies.
So I hacked a solution to explain the problem and maybe someone could help me understand what I missed to have this so complicated.
https://github.com/nettinho/live_global_var
Here is the plain mix phx.new --no-ecto project, and I will comment the only changes I made.
In the app.html.heex I have a select:
<header class="px-4 sm:px-6 lg:px-8">
<ul>
<li><.link href={~p"/"}>Home</.link></li>
<li><.link href={~p"/view1"}>View1</.link></li>
<li><.link href={~p"/view2"}>View2</.link></li>
</ul>
<form phx-change="global-var-change">
<label>Select option</label>
<select name="global_var_select">
<option
:for={{label, value} <- @options}
selected={value == @global_var}
value={value}>
<%= label %>
</option>
</select>
</form>
<div>Current value: <%= @global_var %></div>
</header>
<main class="px-4 py-20 sm:px-6 lg:px-8">
<div class="mx-auto max-w-2xl">
<%= @inner_content %>
</div>
</main>
On the app Web module LiveGlobalVarWeb I implement handle_event/3 to handle global-var-change on all liveviews:
Note hardcoded_get_path/2. I wasn’t able to build the current URL from the socket. I tried enabling the Phoenix.Router.Helpers but wasn’t able.
I also realised that the socket has a :host_uri but for some reason the :path attribute is nil.
defmodule LiveGlobalVarWeb do
...
def live_view do
quote do
use Phoenix.LiveView,
layout: {LiveGlobalVarWeb.Layouts, :app}
unquote(html_helpers())
defp hardcoded_get_path(LiveGlobalVarWeb.View1Live, params), do: ~p"/view1?#{params}"
defp hardcoded_get_path(LiveGlobalVarWeb.View2Live, params), do: ~p"/view2?#{params}"
defp hardcoded_get_path(_, params), do: ~p"/?#{params}"
def handle_event("global-var-change", %{"global_var_select" => value}, socket) do
path = hardcoded_get_path(socket.view, %{"global_var" => value})
#push_patch/2 and push_navigate/2 requests don't go through the plug pipelines
{:noreply, redirect(socket, to: path)}
end
end
end
...
end
And this is the Router that put it all together:
- Puts the value in the session if it comes in the
params on_mountto assign the select options and the “global value” from the session
defmodule LiveGlobalVarWeb.Router do
use LiveGlobalVarWeb, :router
@example_options [{"", nil}, {"Opt1", "1"}, {"Opt2", "2"}, {"Opt3", "3"}]
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :put_root_layout, {LiveGlobalVarWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
plug :put_global_var
end
scope "/", LiveGlobalVarWeb do
pipe_through :browser
live_session :global_var, on_mount: [{__MODULE__, :mount_global_var}] do
live "/", HomeLive
live "/view1", View1Live
live "/view2", View2Live
end
end
# Plug to persist the value from them params if exists
def put_global_var(%{params: %{"global_var" => value}} = conn, _) do
put_session(conn, :global_var, value)
end
def put_global_var(conn, _), do: conn
# `on_mount` to assign the select options and the "global value" from the session
def on_mount(:mount_global_var, _params, session, socket) do
global_var = Map.get(session, "global_var", nil)
socket =
socket
|> Phoenix.Component.assign_new(:options, fn -> @example_options end)
|> Phoenix.Component.assign_new(:global_var, fn -> global_var end)
{:cont, socket}
end
end
Is there a way to do this, or achieve this behaviour without having to navigate passing the URL params?
Thanks in advance!
Trending in Questions
Other Trending Topics
Latest Phoenix Threads
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
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #blog-post
- #ai
- #phoenix_html
- #elixirconf-us
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming










Showing Posts 1 to 7- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
soup
I think you have two problems,
Re 1,
Each liveview socket has an
idfield which can be used, and is persistent across navigation between live view processes in the samelive_session, but refreshes, crashes or inter-session navigation will generate a new id, so its probably not persistent enough for what you want.Your best bet is generating a cookie/local storage token and using that to look up data server side (or just dump it all in the cookie/ls). There is a good fly.io elixir-files post on using local storage.
(Note you cannot adjust cookies from a LV websocket, only on a true HTTP request. You can adjust local storage values from any JS event, so one may fit your needs better than the other (or both)).
(This is assuming your users are not reliably “logged in”, otherwise you can use your auth session id.)
Re 2,
Once you know a LV is attached to a specific user, you have a bazillion options for storing the data.
ETS is super simple (but actively frowned upon in the docs for session data as it doesn’t expire, you can write something to purge it), or you could use a
Registry+GenServer(orAgent?) if it makes sense (perhaps there are associatedTasks etc). Both these options lose data when the server goes down!. These are also single-node, so any setup that may have users hit multiple servers will have issues.Or just stick it in a db, or stick changes in a db and cache to ets/etc.
Loading the data into the socket can be done as you are already with the mount hook.
There were a few things in your example, where each link was
hrefnotnavigate, unsure if that was intentional. I wouldn’t say this is your end goal, but maybe this will get you moving along.Not sure if that’s helpful.
netto
Thank you for the reply! I didn’t know about ETS and it looks pretty nice!
I really don’t care that this information is lost when the server go down. It is basically for the user to switch domains inside the application.
My other option when I was thinking of this was to spawn an
Agentto store the state (using themix phx.gen.authuser_tokenas key) but I wondered if it was overkill.I just felt that this belonged next to the
user_tokenin the session, but I can’t find a way toput_sessionit inside the liveview lifecycle.If it is not possible, I definitely like your ETS approach better than the example.
Thanks again
soup
Yeah, because LV is all over a websocket you cant alter the cookies so your stuck with local storage or another system.
From what I recall, you can do hacks like trigger a HTTP request from JS to adjust the cookies from LV, but it wont propagate into the LV, so you end up double handling everything.
In this case your agent would probably become a bottleneck where every view will have to wait for the agent to sequentially handle each “get_var” call as messages are processed sequentially,
Instead you probably want an Agent/GenServer per LV, so each handles its own “persistent state”
Previously I have done similar to what you describe, using the
live_session_idfromphx.gen.authas a way to look up the correct genserver.phx.gen.authalso generates some broadcast messages on logout that you can use to tear down the servers (but I still have an automatic “no messages in N time, kill” callback (Seesend_after,Registry).The ETS table has concurrent reads and a simpler & existing interface, so its probably the best “out of the box” option where you can just pair the records against your auth token or whatever, just be aware you need manage eviction or the memory will continue to grow.
I would preference ETS over separate genservers unless there is clear behaviour to associate with the data (eg: they can set the value and it should revert after n minutes or something or you’re defining workflows between liveviews that are dependent on other services, etc).
ibarch
This hacky approach to put something into session from within Liveview may work for you.
In one of your liveviews:
Add this line to the bottom of
yourapp_web/components/layouts/app.html.heexThe following rule might be unnecessary, but I recommend to add it to
assets/app.cssjust in case:In
yourapp_web/router.exadd the following line outside of any scopeCreate
yourapp_web/controllers/plug_session_controller.exwithnetto
It makes sense. I wasn’t thinking that the session is set in the cookies, and that Liveview communication goes below it. Now wonder if this shouldn’t just be a JS hook that updates de session. I’m not sure it is possible, as it is encoded in the cookie. I’ll look into it.
Thanks for taking your time and sharing. Even though it avoids the url params stuff, I am not a fan of embedding a hidden iframe.
soup
Just FYI, as with most of Phoenix’s underbelly, session is provided by Plug, which can make tracking the docs down a bit of a egg hunt.
There used to be an explicit guide but it seems to have dropped out of the later docs:
Old guide, briefly reading it, it doesn’t appear particularly wrong or misleading but it might have quirks: Sessions – Phoenix v1.3.0-rc.0
ctrl-f session: Plug — Phoenix v1.8.8
Plug.Session (cookie & ets): Plug.Session.COOKIE — Plug v1.20.2
ibarch
This solution is flawed and insecure. After giving the problem some thought, I came up with something more suitable for production:
Define a helper function for Liveviews.
Catch this event on the client side in
assets/js/app.jsand send a fetch request back to the server (to a regular controller).Add a route for this request:
Define a controller.
The approach above can be easily extended to accommodate the
push_delete_cookie/2functionality.