How to populate UI from @form content

132 Views Asked by At

I am new to LiveView, Phoenix and Elixir, and I am struggling to populate the UI from the form content. Here's a simplified version of what I have in my code: I have defined a struct with some simple fields:

defmodule MyApp.MyStruct do
  defstruct some_integer: 3,
            some_string: "",
            some_bool: true
end

Then I have a context:

defmodule MyApp.Context do
  alias MyApp.MyStruct

  def current_struct() do
    %MyStruct{}
  end
end

It simply returns the newly created struct with default values. I will implement modifying later. Now, I need to display a form populated with these values + allow modifying them. This is my _live.ex file:

defmodule MyAppWeb.ViewLive do
  alias MyApp.MyStruct
  use MyAppWeb, :live_view

  def mount(_params, _session, socket) do
    {:ok, assign(socket, form: to_form(Map.from_struct(Context.current_struct())))}
  end

  # Here I have handle_event methods, but they don't do anything at the moment.
end

Finally, this is the .heex file:

<div class="container mx-auto my-4">
  <.form for={@form} phx-change="change_form" phx-submit="submit_form">
    <!-- Number picker -->
    <div class="mb-4">
      <.label>I want to pick a number here:</.label>
    </div>
    <div class="flex items-center space-x-4 mb-4">
      <label class="cursor-pointer">
    <input type="radio" name="some_integer" value="3" class="hidden" checked={Context.current_struct().some_integer == 3}
    />
        <span class={"px-4 py-2 rounded-md #{if Context.current_struct().some_integer == 3, do: "bg-gray-400", else: "bg-gray-200"} hover:bg-gray-400"}>
          3 of something
        </span>
      </label>
      <!-- More of these integer options -->

    <!-- Checkbox -->
    <div class="mb-4">
      <.input
        label="I'd like this to be true or false:"
        type="checkbox"
        value={Context.current_struct().some_bool}
        field={@form[:some_bool]}
      />
    </div>

    <.button>Submit</.button>
  </.form>
</div>

The code above kind of works now for initial values, but it stops working when I select 7 or 10 instead of 3. Note how to get the value or compute background colours I use Context.my_struct() for calculations. I assume, that if I implement handle_event("change_form"...) and modify my current_struct() on each change, then the change would also be applied on UI. But I would like to modify the actual struct only on submit, and in the meantime to get the data from the @form. I tried to do that, but failed.

When I try to do something like

value={@form[:some_bool]}

then it crashes. When I try to use input_value(), then I always get nil, even if inspect(form) shows that both some_integer and some_bool have the values that I expect them to have.

Please advise me how I can achieve this.

1

There are 1 best solutions below

0
On

I assume, that if I implement handle_event("change_form"...) and modify my current_struct() on each change, then the change would also be applied on UI. But I would like to modify the actual struct only on submit, and in the meantime to get the data from the @form

In mount(), you can extract the values from your struct and store the values in the socket, leaving the struct unchanged, then those values can be used to populate the form in render(). After the user changes something in the form, handle_event() will be called, and handle_event() will receive the values that were entered into the form as the second argument.

In addition, inside mount() you can save your original struct in the socket, then in handle_event() you will be able to access any values from the struct that you need. After the user hits Submit, handle_event() will receive the final values entered into the form, which you can use to update the original struct.

Here is an example:

#==========Your struct code ============

defmodule MyApp.MyStruct do
  defstruct some_integer: 3,
            some_string: "",
            some_bool: true
end

defmodule MyApp.Context do
  alias MyApp.MyStruct

  def current_struct() do
    %MyStruct{}
  end
end

#=======================

defmodule DemoWeb.DemoLive do
  use DemoWeb, :live_view

  def mount(_params, _session, socket) do
    orig_struct = MyApp.Context.current_struct()
    o_int = orig_struct.some_integer
    o_string = orig_struct.some_string
    o_bool = orig_struct.some_bool

    form_data = %{
      "input_int" => o_int,
      "input_string" => o_string,
      "input_bool" => o_bool
    }

    socket = assign(socket,
        my_struct: orig_struct,
        current_int: o_int,
        current_string: o_string,
        current_bool: o_bool,
        my_form: to_form(form_data)
      )

    {:ok, socket}
  end

  def render(assigns) do

    # When you write:
    #
    # <.form for={@my_form} ... >
    #   <.input  field={@my_form[:input_int] } ... />
    #
    # that creates an input element with the type atribute set to "text",
    # the name and id attributes set to "input_int", and the value
    # attribute set to the value corresponding to the "input_int" key in the form data
    # given to to_form().  If there's no key for "input_int" in the form data, then there
    # won't be a value attribute--unless you explicitly set the value attribute.  Explicitly
    # setting the value attribute takes precedence over any form data given to to_form().
    #
    ~H"""
    <div>
      <%= "current integer: #{@current_int}, current string: #{@current_string},
           current bool: #{@current_bool}"  %>
    </div>
    <div>
    <%= "original integer: #{@my_struct.some_integer}, original string: #{@my_struct.some_string},
           original bool: #{@my_struct.some_bool}"  %>
    </div>
    <div>-------</div>
    <div>-------</div>


    <.form for={@my_form} phx-change="changed" phx-submit="save">
      <.input label="Some integer:"
              field={@my_form[:input_int] }
              value={@current_int}

      />
      <.input label="Some string:"
              field={@my_form[:input_string]}
              value={@current_string}
      />
      <.input label="Some bool:"
              field={@my_form[:input_bool]}
              value={@current_bool}
      />
      <.button>Submit</.button>
    </.form>
    """
  end

  def handle_event("changed",
                   %{"input_int" => entered_int,
                     "input_string" => entered_string,
                     "input_bool" => entered_bool
                    },
                  socket) do

    #Do something with entered_int, entered_string, entered_bool, e.g. change
    #the values that the <div> uses:
    socket = assign(socket,
                    current_int: entered_int,
                    current_string: entered_string,
                    current_bool: entered_bool
              )

    {:noreply, socket}
  end

  def handle_event("save",
                   %{"input_int" => entered_int,
                     "input_string" => entered_string,
                     "input_bool" => entered_bool
                    },
                   socket) do

    start_struct = socket.assigns.my_struct
    updated_struct = %{start_struct | some_integer: entered_int,
                                      some_string: entered_string,
                                      some_bool: entered_bool}
    #Do something with updated_struct, e.g.:
    IO.inspect(updated_struct, label: "[ME]updated struct")

    {:noreply, socket}
  end


end

Here is what the LiveView page looks like:


enter image description here

This function definition:

 def handle_event("save",
                   %{"input_int" => entered_int,
                     "input_string" => entered_string,
                     "input_bool" => entered_bool
                    },
                   socket) do

pattern matches on the second argument to extract the values entered into the form. Pattern matching a map works like this:

iex(2)> function_argument = %{"a" => 10, "b" => 20}
%{"a" => 10, "b" => 20}

iex(3)> %{"a" => a_val} = function_argument
%{"a" => 10, "b" => 20}

iex(4)> a_val
10

iex(5)> %{"b" => b_val, "a" => a_val} = function_argument
%{"a" => 10, "b" => 20}

iex(6)> b_val
20

iex(7)> a_val
10

You could also do the pattern match like this:

 def handle_event("save", payload, socket) do

   %{"input_int" => entered_int,
     "input_string" => entered_string,
     "input_bool" => entered_bool} = payload
               

This code:

updated_struct = %{start_struct | some_integer: entered_int,
                                  some_string: entered_string,
                                  some_bool: entered_bool}

uses a special syntax for updating a map or struct (which looks like the syntax used for lists). The special syntax ensures that the keys are already in the map so that mispellings don't add new keys to a map.

On the web page, watch the values above the form change as you type in the input fields. Then hit Submit and examine the output in the terminal window.

When I try to use input_value(), then I always get nil, even if inspect(form) shows that both some_integer and some_bool have the values that I expect them to have.

The only way I can get nil when I call:

Phoenix.HTML.Form.input_value(my_form, :input_int)

is if the key does not exist in my_form, e.g.:

Phoenix.HTML.Form.input_value(my_form, :non_exist)

For instance, I can add the following code to the "save" handle_event() function:

my_form = socket.assigns.my_form
IO.inspect(my_form, label: "[ME]my_form")
IO.inspect(Phoenix.HTML.Form.input_value(my_form, :input_int), label: "[ME]input_value()" )

then when I hit Submit, I see the following output in the terminal window:

[ME]my_form: %Phoenix.HTML.Form{
  source: %{"input_bool" => true, "input_int" => 3, "input_string" => ""},
  impl: Phoenix.HTML.FormData.Map,
  id: nil,
  name: nil,
  data: %{},
  hidden: [],
  params: %{"input_bool" => true, "input_int" => 3, "input_string" => ""},
  errors: [],
  options: [],
  index: nil,
  action: nil
}
[ME]input_value(): 3

Notice that the value returned was the value which was used to create the form in mount().

When I try to do something like

value={@form[:some_bool]}

then it crashes

When I try writing:

<.input label="Some integer:"
        field={@my_form[:input_int] }
        value={@my_form[:input_int] } />

it crashes for me too:

Phoenix.HTML.Safe not implemented for %Phoenix.HTML.FormField

====

I just read the following in the docs (Form Bindings)

Form Events
To handle form changes and submissions, use the phx-change and phx-submit events. In general, it is preferred to handle input changes at the form level ....

def handle_event("validate", %{"user" => params}, socket) do
  form =
    %User{}
    |> Accounts.change_user(params)
    |> Map.put(:action, :insert)
    |> to_form()

  {:noreply, assign(socket, form: form)}
end

I think that is saying that instead of dynamically changing the value attribute of an input element, you should use the entered values in the form to create a new form with to_form(), then assign the new form to the socket. Like this:

...
...
<.form for={@my_form} phx-change="changed" phx-submit="save">
  <.input label="Some integer:"
          field={@my_form[:input_int] }  
                <======= No value attribute
  />
  <.input label="Some string:"
          field={@my_form[:input_string]}
                <======== No value attribute
  />
  <.input label="Some bool:"
          field={@my_form[:input_bool]}
                <======== No value attribute
  />
  <.button>Submit</.button>
</.form>

def handle_event("changed",
                   payload =
                   %{"input_int" => entered_int,
                     "input_string" => entered_string,
                     "input_bool" => entered_bool
                    },
                  socket) do

    #Do something with entered_int, entered_string, entered_bool, e.g. change
    #the values that the <div> uses:
    socket = assign(socket,
                    current_int: entered_int,
                    current_string: entered_string,
                    current_bool: entered_bool,
                    my_form: to_form(payload)   <======== 
              )

    {:noreply, socket}
  end