How to store form data in tools like Local Storage, IndexedDB and cookies? Do I need a JS hook?

I am making a small “In house” app and it doesn’t require authentication. However, it does require users to enter their name multiple times throughout the day and I want a simple light solution to fix this. I looked into phx,gen,auth and it seems like overkill.

The approach I am exploring is to simply capture the users first instance of filling out a form and storing their name data in the browser, when they revisit the form they don’t need to re enter it. They just press enter.

In JS accessing local storage and other browser API’s is easy. In phoenix I’ve read that it can be done with hooks but before I go that path, I want to ask:

What ways are developer typically getting access to tools like local storage, indexedDB and cookies from phoenix ?

In my head I imagine that when a user submits a form, the response function captures the input, code runs that communicates with the storage API and stores the data in IndexedDB (or whatever).
In Phoenix, I don’t know if it’s that easy.

Is there a module that I can import with methods that let me store data in IndexedDB (or other browser storage tools) and that I can run in my Phoenix event handler function?

Hey @wktdev you have basically two options for Phoenix here:

Option 1: Stick with good old fashioned cookies

You can use Form bindings — Phoenix LiveView v0.19.1 to submit the form to a normal phoenix HTTP controller, which lets you then set cookies via conn |> put_session. This session information is available to your liveviews, and you can use it to store the user name in your case.

Option 2: Localstorage / indexdb / whatever

In these cases I would have the form do a normal live view submit, but then in the def handle_event("submit". ...) clause do a push_event to a hook you have on the page to stash the state in local storage. When the page loads, you can push_event up a request to fetch that information and use its reply to populate the form. There isn’t anything baked in for this though, so you’d need to write the hook.

3 Likes

Thanks for this, great direct answer.

1 Like

I think Ben’s Option 1 is the way to go in your case, but if you’re still wondering how to access Session/Local storages from within LiveView I can provide you an example to achieve that globally in every liveview without a JS hook.

Define a helper module:

defmodule MyAppWeb.Utils do
  import Phoenix.LiveView

  def push_session_storage(socket, method, params \\ []) do
    push_event(socket, "session-storage", %{
      method: method,
      params: List.wrap(params)
    })
  end
end

Catch this event on the client’s side in assets/js/app.js

window.addEventListener("phx:session-storage", (event) => {
  const { method, params } = event.detail;
  const result = sessionStorage[method](...params);
  if (method === "getItem") {
    liveSocket.main.channel.push("item_from_session_storage", {
      key: params.at(0),
      value: result,
    });
  }
});

Now in your liveviews you can import the helper module and simply call:

push_session_storage(socket, :setItem, ["foo", "bar"])
push_session_storage(socket, :removeItem, "baz")
push_session_storage(socket, :clear)
push_session_storage(socket, :getItem, "foo")

The last step is to receive getItem result in liveviews:

def handle_info(%{event: "item_from_session_storage", payload: payload}, socket) do
  IO.inspect(payload["key"])
  IO.inspect(payload["value"])

  {:noreply, socket}
end

My main concern with this approach is whether you can store sensitive data in there and trust it. Hey @voltone (nice to see you here :hugs:), what do you think about security and trustworthiness of session/local storages?

5 Likes

Sensitive data in my scenario doesn’t matter. As far as I am concerned your code is a useful example.

Hey there @ibarch :slight_smile:

Whether or not storing data in local/session storage is safe depends on who needs to trust whom. If this is just a ‘cache’ for user input in a form, then the fact that the user can tamper with the data in the store should not be an issue, as they could just as easily submit a new value in the form itself.

2 Likes

I copied your code with no errors.

When I submit a form and run:

Utils.push_session_storage(socket, :setItem, ["foo", "bar"])

Should I immediately be able to look in my browser and see [“foo”, “bar”] in the session or cookie storage?

If so, it isn’t appearing.

My entire event handler looks like this:

   def handle_event("create-status", params, socket)  do  
       Utils.push_session_storage(socket, :setItem, ["foo", "bar"])
       {:noreply, redirect(socket, to: "/")}

   end
``

Try this version:

def handle_event("create-status", params, socket)  do
  socket =
    socket
    |> Utils.push_session_storage(:setItem, ["foo", "bar"])
    |> redirect(to: "/")
    
   {:noreply, socket}
end

Nothing in session storage. Your code doesn’t break the app, it just doesn’t fix the problem.

I decided to give JavaScript hooks a try.

I have transmitted input data from the form to local storage

let liveSocket = new LiveSocket("/live", Socket, {

	params: {_csrf_token: csrfToken},
	hooks:{
		Box:{
		 mounted(){
                    this.el.developer.addEventListener("input",(event)=>{
                       localStorage.setItem("developer", event.target.value)
                    })
      
			}
		}
	}
})

Now I need to learn how to get the data from local storage to the value property of the input element.

I followed this and it worked:

1 Like

One thing I should mention for anyone that stumbles onto this thread, the tutorial I linked to above leaves out one piece of information. The variable referenced in the heex form needs to be set in the mount function or it doesn’t work ( see example below).

Description

The code below creates an object in local storage that has a single key titled developer and a value assigned to developer. The value of developer is set when a user types into the form. When a user revisits the form the previous data they entered prepopulates the field.

Here is the full code for the next person:

assets/app.js

let liveSocket = new LiveSocket("/live", Socket, {

	params: {
		_csrf_token: csrfToken
	},
	hooks: {
		Box: {
			mounted() {

				this.el.developer.addEventListener("input", (event) => {
					localStorage.setItem("developer", event.target.value)
				})

					this.pushEvent("get_localstorage", {
					developer: localStorage.getItem("developer"),
				})


			}
		},



	}
})

Liveview File

  def mount(_params, _session, socket) do  
     {:ok, assign(socket, developer: "None")}  
  end
def handle_event("get_localstorage", %{"developer" => developer}, socket) do
    {:noreply, assign(socket, developer: developer)}
  end

<form phx-submit="create-status" phx-hook="Box" id="some_id_23cb927rc97" >
 <input  value={@developer} name="developer"  type="text" />
</form>
2 Likes