Saving and retrieving string with Jason in a LiveView form

I am saving a string that contains an array of maps

[
  {
    "url": "https://test.com",
    "id": "abc"
  }
]

using javascript JSON.stringify, and then showing it again in the live view (in a custom way), so for example let’s say I will display the url in a p element, so I did it like this:

    <%= case Jason.decode(input_value(f, :my_string)) do %>
	    <% {:ok, l} -> %>
	        <%= if is_list(l) do %>
	            <%= for m <- l do %>
		            <%= if is_map(m) do %>
		                <p><%= m["url"] %></p>
		            <%= end %>
	            <% end %>
	        <% end %>
	    <% {:error, reason} ->%>
	        <b>there is an error</b>
    <% end %>

I am looking for a better way to save and retrieve Jason data that are stored as string in LiveView

What is the structure backing the form? Is it a changeset? If so, is there a particular reason you need to save this as a string instead of an {:array, :map}?

Here’s what I’m getting at: I think the parsing probably shouldn’t happen in your LiveView, and definitely not inline in your HEEx. It should happen in a separate step that either sets the value to something valid (list of maps) or returns an error because it’s invalid.

1 Like

the changeset has only the url, the id is for the UI to be able to remove and add new urls, (each url has an id) so for example
if the user removes the element with id abc the corresponding url is removed.

I can also use {:array, :string}, but how would you add and remove from the array using the LiveView form,

Do I also need to parse the array in the schema module?

Since the input is different from the persisted format, one option would be to use a separate schema just for that input. Ecto supports this with embedded_schema (Embedded Schemas — Ecto v3.9.0), which is a bit of a misnomer in this case. All it really means is that it’s not backed by a database table. So what I’d do personally is make an embedded schema for this form, and may actually embed a schema itself to specify the inner values:

defmodule UrlInput do

#…

embedded_schema do
  embeds_many :url_specs do
    field :id, :string
    field :url, :string
  end
end

def changeset(input, url_specs) when is_binary(url_specs) do
  case Jason.decode(url_specs) do
    {:ok, url_specs} ->
      changeset(input, url_specs)

    {:error, _} ->
      input
      |> cast([], %{})
      |> add_error(:url_specs, “invalid json”)
  end
end

def changeset(input, url_specs) do
  input
  |> cast([], %{url_specs: url_specs})
  |> cast_embed(:url_specs, required: true, with: &url_spec_changeset/2)
end

def url_spec_changeset(url_spec, attrs) do
  url_spec
  |> cast([:url, :id], attrs)
  |> validate_required([:url, :id])
end

Above is a sort of sketch of what you could do. This is obviously more code and I’ve left things out, but IMO it’s also easier to follow and more robust, and makes your view logic much simpler. This would also put you on the path to giving live feedback on whether the json is valid/etc.

1 Like