Fl4m3Ph03n1x

Fl4m3Ph03n1x

How to get app.html.heex to use assigns properly with LiveView

Background

I have a Phoenix application, where all pages (expect the login page) have a menu at the top.
This menu will therefore only appear if the user has already logged in.

I am trying to replicate this behaviour, by incorporating said menu in the app.html.heex so I don’t have to repeat it constantly.

However, no matter what I do, the menu is never displayed.

Code

I am trying to change my app.html and verify if the user logged in using assigns[:user].

app.html.heex:

<header>

 <%= if assigns[:user] do %>
   <h1> Cool Menu here </h1>
 <% end %>

</header>

<main class="">
  <div class="mx-auto max-w-2xl">
    <.flash_group flash={@flash} />
    <%= @inner_content %>
  </div>
</main>

The login process is async, as shown here. Basically I send a login request to a Manager, and when it feels like replying back, I update the socket with assigns[:user] and redirect to a cool page.

user_login_live.ex:

defmodule WebInterface.MyAppLive do
  use WebInterface, :live_view

  @impl true
  def mount(_params, _session, socket), do: {:ok, socket}

  @impl true
  def handle_event("login", params, socket) do
    IO.puts("Seding async request")
    :ok = Manager.login(params)
    {:noreply, socket}
  end

  @impl true
  def handle_info({:login,  user, :done}, socket) do
    IO.puts("Authentication succeeded for user #{inspect(user)}")

    updated_socket =
      socket
      |> assign(:user, user)
      |> redirect(to: ~p"/cool_page")

    {:noreply, updated_socket}
  end

end

I would expect the cool page to have the <h1> menu, but that is not the case.

Questions

  • What am I doing wrong here?
  • Isn’t the assigns updated automatically?
  • Does app.html.heex work differently from other files?

Marked As Solved

Fl4m3Ph03n1x

Fl4m3Ph03n1x

For the sake of completeness, I have decided to post my final answer. This curated answer is mostly a resume of this huge thread that focuses on the most relevant issues and explores other options I also found. I hope future readers find it interesting.


Why this happens

So, after a lot of investigation and help from the community I found out what is happening.

Turns out that session data (data that needs to travel between multiple pages) cannot be shared over websockets. LiveViews use Websockets and therefore suffer from this limitation (How to get app.html.heex to use assigns properly with LiveView - #18 by sodapopcan - Questions / Help - Elixir Programming Language Forum):

Yes, if you are logging in a user you have to store a successful login id in a session. The thing is, you can’t set session data over a websocket. This is a websocket limitation, not a LiveView one ( … )

This also means you cant use cookies/alter them (Persisting data across liveview navigation - #4 by soup - Questions / Help - Elixir Programming Language Forum):

( … ) because LV is all over a websocket you cant alter the cookies so your stuck with local storage or another system.

Possible solutions

However, this is not the end. There are other options if you need to share data between websockets. I was able to identify 4:

  1. Add the session data to the URL of the target page. An example of this is using GET HTTTP method with session data as parameters (or perhaps using post): elixir - How to get app.html.heex to use assigns properly with LiveView - Stack Overflow
  2. Save the data inside a global ETS table in the server: Persisting data across liveview navigation - #4 by soup - Questions / Help - Elixir Programming Language Forum
  3. Save the data in an Agent. You could have an Agent per websocket, thus avoiding a global state and improving efficiency: Persisting data across liveview navigation - #4 by soup - Questions / Help - Elixir Programming Language Forum
  4. Store the data in a Phoenix Session: How to get app.html.heex to use assigns properly with LiveView - #18 by sodapopcan - Questions / Help - Elixir Programming Language Forum

Here you have to weight the risks/benefits of every solution. If you have your own website, solution 4 would probably be the most secure, while solution 1 would also be OK provided you encrypt the data before putting it in the url parameters or post body request.

However, in my specific case, a Windows desktop application for a single user, these considerations are not important. I have therefore opted for solution 2, since it is simpler than solution 3 and the data there is mostly read only.

Final code

So now I am using a temporal persistence layer to store session information:

defmodule WebInterface.Persistence do
  @moduledoc """
  Responsible for temporary persistence in WebInterface. Uses ETS beneath the scenes.
  """

  alias ETS
  alias Shared.Data.User

  @table_name :data

  @spec init :: :ok | {:error, any}
  def init do
    with {:ok, _table} <- ETS.KeyValueSet.new(name: @table_name, protection: :public) do
      :ok
    end
  end

  @spec set_user(User.t) :: :ok | {:error, any}
  def set_user(%User{} = user) do
    with {:ok, table} <- ETS.KeyValueSet.wrap_existing(@table_name),
      {:ok, _updated_table} <- ETS.KeyValueSet.put(table, :user, user) do
        :ok
      end
  end

  @spec get_user :: {:ok, User.t} | {:error, any}
  def get_user do
    with {:ok, table} <- ETS.KeyValueSet.wrap_existing(@table_name) do
      ETS.KeyValueSet.get(table, :user)
    end
  end

  @spec has_user? :: boolean
  def has_user? do
    case get_user() do
      {:ok, nil} -> false
      {:ok, _user} -> true
      _ -> false
    end
  end
end

Now in my app file, I simply ask the persistence module if I have the data I want. This is quite transparent:

app.html.heex:

<header>
 <%= if WebInterface.Persistence.has_user?() do %>
  <h1> <%= @user.name %> is awesome !</h1>
  <% end %>

</header>
<main class="">
  <div class="mx-auto max-w-2xl">
    <.flash_group flash={@flash} />
    <%= @inner_content %>
  </div>
</main>

And I set/get the data I want via the Persistence API.

user_login_live.ex:

defmodule WebInterface.UserLoginLive do
  use WebInterface, :live_view

  alias Manager
  alias Shared.Data.{Credentials, User}
  alias WebInterface.Persistence

  @impl true
  def mount(_params, _session, socket), do: {:ok, socket}

  @impl true
  def handle_event("login", %{"email" => email, "password" => password} = params, socket) do
   # this is an async request for authentication
   # here we simply start it 
   :ok =
      email
      |> Credentials.new(password)
      |> Manager.login(Map.has_key?(params, "remember-me"))

      {:noreply, socket}
  end


  @impl true
  def handle_info({:login, %User{} = user, :done}, socket) do
    # when we receive this message, we know authentication is done.
     
    # and in other places we use get_user/0 to retrieve session data
    :ok = Persistence.set_user(user)

    {:noreply,  socket |> redirect(to: ~p"/other_page")}
  end
end

For now this solution works wonderfully.

Where Next?

Popular Frontend topics Top

pillaiindu
Some days ago I came across a video teaching the internals of git. It had some nice diagrams and animations. The diagrams looked like han...
New
davearonson
I’m unit-testing some JS, with Jasmine, and I’d like to check our coverage. We’re not using any front-end framework, nor much JS, so no ...
New
Zuber
How to make a website like webnovel and wattpadd where subscribers and logged in user post stories like their own Novel in a website and ...
New
david-j-m
Hi, have a svelte spa gallery site - repl here. Have a couple of category buttons - Oil on Canvas and WaterColor… Sidebar contains all im...
New
AlessandroDsgroup
Hi to everyone, we are experiencing a 401 error related to the connection of the websocket (in reference to our web app); we are unable ...
New
sona11
I have a 1D array of numbers and need help splitting them into groups using a jagged array so that I can perform a series of computations...
New
harwind
First have a look at the code: function mainfunc(func, par3, par2){ window[func](par3, par2); } function calledfunc(par3, par2){ ...
/js
New
Fl4m3Ph03n1x
Background I have a fresh umbrella app and I am trying to create a Phoenix app inside it. However, even though I can create the Phoenix a...
New
wilsoc31
First time building an app. It will run for Android and also for iPhone 15 but will not for iPod. Not sure why or where to even begin. ...
New
Clintonsuck
Hello, I am new and trying to build my first app. So far, everything was going okay, but now I’m stuck and don’t know how to proceed. May...
New

Other popular topics Top

Devtalk
Reading something? Working on something? Planning something? Changing jobs even!? If you’re up for sharing, please let us know what you’...
1033 17470 383
New
dasdom
No chair. I have a standing desk. This post was split into a dedicated thread from our thread about chairs :slight_smile:
New
AstonJ
I have seen the keycaps I want - they are due for a group-buy this week but won’t be delivered until October next year!!! :rofl: The Ser...
New
Margaret
Hello content creators! Happy new year. What tech topics do you think will be the focus of 2021? My vote for one topic is ethics in tech...
New
Rainer
Not sure if following fits exactly this thread, or if we should have a hobby thread… For many years I’m designing and building model air...
New
PragmaticBookshelf
Create efficient, elegant software tests in pytest, Python's most powerful testing framework. Brian Okken @brianokken Edited by Kat...
New
rustkas
Intensively researching Erlang books and additional resources on it, I have found that the topic of using Regular Expressions is either c...
New
PragmaticBookshelf
Author Spotlight: Karl Stolley @karlstolley Logic! Rhetoric! Prag! Wow, what a combination. In this spotlight, we sit down with Karl ...
New
CommunityNews
A Brief Review of the Minisforum V3 AMD Tablet. Update: I have created an awesome-minisforum-v3 GitHub repository to list information fo...
New
Fl4m3Ph03n1x
Background Lately I am in a quest to find a good quality TTS ai generation tool to run locally in order to create audio for some videos I...
New