LiveView toggle between views by a cookie setting

Scenario
• I have an interface where I want to toggle between list view and table view.
• I also want to keep track on whether the user is in list vs table view in a cookie/session as a preference. This way when they come back I can load up the their default_view

The Setup
• From the Controller, I am able to set the initial default_view by using put_session(conn, :default_view, "list")
• From LiveView, I have a <button phx-click="toggle_view"> that fires def handle_event("toggle_view", _value, socket) which then toggles between the values list_view and table_view and renders the correct template.

Where I’m stuck
From LiveView, I cannot figure out how to bubble up or delegate from the def handle_event("toggle_view", _value, socket) do to my controller so that it can then update the session with the new default_view.

Here is my current solution:

# Controller

defmodule LiveFooWeb.PageController do
  use LiveFooWeb, :controller

  def index(conn, _params) do
    users = [1,2,3,4,5]
    conn = put_session(conn, :default_view, "list") # This is the default setting
    default_view = get_session(conn, :default_view)
    render(conn, "index.html", users: users, default_view: default_view)
  end

  def show(conn, _params) do
    users = [6,7,8,9,10]
    default_view = get_session(conn, :default_view)
    render(conn, "show.html", users: users, default_view: default_view)
  end

end

Here is the tableview

defmodule LiveFooWeb.TableViewLive do
    use Phoenix.LiveView

    def render(assigns) do
      case assigns.type_of_view do
          "list"  -> list_view(assigns)
          "table" -> table_view(assigns)
      end
    end

    def table_view(assigns) do
        ~L"""
        <%= Phoenix.View.render(LiveFooWeb.ComponentsView, "table.html", users: assigns.users, type_of_view: assigns.type_of_view) %>
        <button phx-click="toggle_view"><%= @btn_label %></button>
        """
    end

    def list_view(assigns) do
        ~L"""
        <%= Phoenix.View.render(LiveFooWeb.ComponentsView, "list.html", users: assigns.users, type_of_view: assigns.type_of_view) %>
        <button phx-click="toggle_view"><%= @btn_label %></button>
        """
    end

  
    def mount(_params, %{"users" => users, "type_of_view" => type}, socket) do
      {:ok, assign(socket, users: users, type_of_view: type, btn_label: "change to table")}
    end

    def handle_event("toggle_view", _value, socket) do
        case socket.assigns.type_of_view do
            "list" ->  
               # Need to tell the controller to update the session with the new :default_view
               # I know this won't work because no access to conn
               # Plug.Conn.put_session(conn, :default_view, "table")
               #
              {:noreply, assign(socket, type_of_view: "table", btn_label: "change to list")}
            "table" ->  
               # Need to tell the controller to update the session with the new :default_view
               # I know this won't work because no access to conn
               # Plug.Conn.put_session(conn, :default_view, "table")
               #
              {:noreply, assign(socket, type_of_view: "list", btn_label: "change to table")}
        end
      end

  end

Research
I was searching the forum and I found this article. It sounds relatable but I don’t follow on how the solution works.

Research
I found this github issue and comment that states you cannot put session data via web sockets.

I might have to use Javascript to update the session but I’m still not sure how that would work.

You could also consider changing this ^ to be a value stored in a user preferences table instead of as part of their session.

as an option, you can rely solely on LiveView and cookie/localstorage to determine what are user selected settings, but it has downsides of course.

What you can do is to send the data from localstorage or cookie from javascript, during LiveView initialization.

let liveSocket = new LiveSocket('/live', Socket, {
  params: {
    _csrf_token: csrfToken,
    saved: getFromLocalstorage('saved', []), // this is additional thing that you are sending  to your LiveView
  },
  hooks: hooks,
});

You can later access this value from mount function in LiveView with get_connect_params(socket) (I actually had similar question not long time ago How to send data from localstorage to LiveView on initial load)

There is a downside however, you will not have access to the data from localstorage on server on the inital load.
What will happen is that client will make a request to your website. It will get back initial render of the page (together with all assets, js etc). Since, the connection to LiveView happens only after js code is loaded, your initial data from localstorage will be sent to server only at that point.

For your case, it will mean that, initially, page with table(let’s say that’s default) will be loaded, then page will send settings from localstorage/cookie to server and LiveView will rerender. It happens quite fast, but you will see flicker for sure. so if user configured it to be a list, there will be table and then switch to list in a course of few seconds of initial load. To avoid that, you can of course have some kind of spinner(which partialy defeats the purpose, of not going with SPA). Not sure, if it will suite your use case, for me it was perfect, because showing loading on initial load was not something bad for my use case. Basically, It works really well, if it is not the first page of your application

2 Likes

Is this commit what you need?

2 Likes

I put together a solution that works but relies on javascript on setting cookies.

The goal
• Navigate the content of a website in either table_view or list_view
• User clicks a button to toggle between the views.
• Save that preference using cookies.
• Use that cookie to render the correct layout.
• Can’t store that preference in a table as the app has no login / account details.

Step 1 Controller

I check to see if the cookie “foo” exists. I have an index and show to navigate to two pages.

defmodule LiveFooWeb.PageController do
  use LiveFooWeb, :controller

  def index(conn, _params) do    
    users = [1,2,3,4,5]
    default_view = get_cookie_list_preference(conn)
    render(conn, "index.html", users: users, type_of_view: default_view)
  end

  def show(conn, _params) do
    users = [6,7,8,9,10]
    default_view = get_cookie_list_preference(conn)
    render(conn, "show.html", users: users, type_of_view: default_view)
  end

  defp get_cookie_list_preference(conn) do
    result = 
      Plug.Conn.fetch_cookies(conn) 
      |> Map.get(:cookies) 
      |> Map.get("foo") # This is the name of the key
    case result do
      nil -> 
        "list"
      _   -> 
        result
    end
  end

end

Step 2 Templates

index.html.eex and show.html.eex
These two files have the same code and layout in that they include both the LiveView and _script.html

<%= live_render(@conn, LiveFooWeb.TableViewLive, session: %{"users" => @users, "type_of_view" => @type_of_view}) %>
<%= render "_script.html" %>

Step 3 LiveFooWeb.TableViewLive

• This file references the HTML for the table.html and list.html
• Each html file has it’s own tags that have the following DOM attributes:

    <section data-user-view="table" id="my_table" />
    <section data-user-view="list" id="my_table" />

• Each has a button that toggle between the views and an ID “my_btn”

  <phx-click="toggle_view" id="my_btn">....

LiveFooWeb.TableViewLive

defmodule LiveFooWeb.TableViewLive do
    use Phoenix.LiveView

    def render(assigns) do
      case assigns.type_of_view do
          "list"  -> list_view(assigns)
          "table" -> table_view(assigns)
      end
    end

    def table_view(assigns) do
        ~L"""
        <%= Phoenix.View.render(LiveFooWeb.ComponentsView, "table.html", users: assigns.users, type_of_view: "table") %>
        <button phx-click="toggle_view" id="my_btn">change to list</button>
        """
    end

    def list_view(assigns) do
        ~L"""
        <%= Phoenix.View.render(LiveFooWeb.ComponentsView, "list.html", users: assigns.users, type_of_view: "list") %>
        <button phx-click="toggle_view" id="my_btn">change to table</button>
        """
    end

  
    def mount(_params, %{"users" => users, "type_of_view" => type} = session, socket) do
      {:ok, assign(socket, users: users, type_of_view: type)}
    end


    def handle_event("toggle_view", _value, socket) do
        case socket.assigns.type_of_view do
            "list" ->  
              {:noreply, assign(socket, type_of_view: "table")}
            "table" -> 
              {:noreply, assign(socket, type_of_view: "list")}
        end
      end


  end

Step 4 - The Javascript

This js is included in Step 2.
This is where javascript listens for the button to be clicked on and then updates the cookie:

_script.html


<script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>

<script>
// 1. Get the table that we're going to track
let field = document.getElementById("my_table");
 
 // 2. Listen for clicks on button
document.getElementById("my_btn").addEventListener("click", function(){

  let table = "table"
  let list = "list"

// 3. We remove the current cookie before setting a new one
  Cookies.remove('foo') 

// Access Data attributes
// https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes
// field is referencing elements by id = "my_table"
// field.dataset.userView is referencing --> <section data-user-view="list" .... id="my_table">

  if (table == field.dataset.userView) {
    Cookies.set('foo', list)
  } else {
    Cookies.set('foo', table)
  }

});

</script>

Solution

This works for now. But this feels brittle.
I have been spending a lot of time reading up on cookies, sessions, con, LiveView so I have been try to understand everything while solving this problem.

From the docs
I read a few lines about LiveView controller and LiveView router. Maybe that can help.
Maybe I could create a Plug that does that type of cookie checking? Need to investigate.

Open to critiques.

1 Like

Agree. If I set up a user account system I will do this. Excellent suggestion.

Didn’t know you could pass it using JS like that. Need to further investigate. Thanks

This is interesting.
You can render a Phoenix.LiveView.Controller and pass in the cookie to the LiveView without even needing a template. Cool.

What does :toggle atom key mean in the router?

# router.ex
get "/toggle", PageController, :toggle

I posted a solution that works but it’s not the best idea.

Atom :toggle in the router is name of function in DemoWeb.PageController handling GET /toggle request.