Form component as Phoenix.Component

Instead of writing inputs like <%= input ... %>, I understand we should use a Phoenix.Component function.
I have a form that has 2 dates, one select and one datalist, the latter is populated via a search query when typing.
I did not find components ready to use so I guess we should write them. I believe something like the code below can be used, but it does not work as it should.
When I fill the form, it is captured by the “on-change” binding and the payload is correct. However, the input can disappear from the cell. For example, if I set the “datalist” and then the “select”, then the data list input is emptied. Same thing if I set a date, not persisted on the screen, only.
When I use the form <%= …%>, this does not happen. Furthermore, I did not change the date inputs for the moment because I have validations on them with an error-tag and I did not succeed to change them to functions till now.
Anyway, I am looking for guidance on where I can find or get inspired for these standard components.

<.form  :let={f}  for={@changeset} id="query_picker"
      phx-submit="send"
      phx-change="change"
      phx-target={@myself}   
>
   <.datalist users={@users} phx_change={@myself}></.datalist>
   <.select status={@status}></.select>
   <%= date_input(f, :start_date, id: "start_date" ) %>
      <%= error_tag(f, :start_date) %>
   <%= date_input(f, :end_date, id: "end_date") %>
      <%= error_tag(f, :end_date) %>
  <button form="query_picker" >Send</button>
</.form>
def datalist(assigns) do
    ~H"""
      <input list="datalist" id="datalist-input" name="query_picker[user]"
        phx_change="search-email"
        phx_target={@phx_change}
        phx_debounce="500"
        placeholder="enter an email"
        />
      <datalist id="datalist">
        <%= for user <- @users do %>
          <option value={user} id={"#{user}"} />
        <% end %>
      </datalist>
    """
end
def select(assigns) do
    ~H"""
      <select id="select" name="query_picker[status]">
        <%= for status <- @status do %>
          <option value={status}><%= status %></option>
        <% end %>
      </select>
    """
end
slot(:inner_block, required: true)
attr(:users, :list)
attr(:user, :string)
attr(:phx_change, :string)
# form "phx-change" handler
def handle_event("change", %{"query_picker" => params}, socket) do
    changeset =
      %QueryPicker{}
      |> QueryPicker.changeset(params)
      |> Map.put(:action, :validate)
    {:noreply, assign(socket, :changeset, changeset)}
end

# fill in a datalist for users when typing
def handle_event("search-email", %{"query_picker" => %{"user" => email}}, socket) do
    datalist = LiveMap.User.search(email) |> Enum.map(& &1.email)
    {:noreply, assign(socket, users: datalist)}
end

# LiveMap.User.search
def search(string) do
    like = "%#{string}%"
    from(u in User,
      where: ilike(u.email, ^like)
    )
    |> Repo.all()
end

For the `<%= %> version, I used a helper ( a copy of the Phoenix.HTML):

defmodule LiveMapWeb.InputHelpers do
  use Phoenix.HTML

  def datalist_input(opts, [do: _] = block_options) do
    content_tag(:datalist, opts, block_options)
  end

  def option_input(user) do
    content_tag(:option, "", value: user)
  end
end

and use in the form:

<%= datalist_input(id: "datalist") do %>
     <%= for user <- @users do %>
          <%= option_input(user) %>
     <% end %>
<% end %>
1 Like

Phoenix 1.7 will ship with new form markup abstractions like components for inputs.

3 Likes

ok.
What is strange is that the generated HTML is identical.
When I update one input, say a date, this triggers a phx-change and the values of these function components (datalist and select) are set back to their default values, so the next time I make a change, the default value is picked. Chrome and Firefox behave the same. Aynway, std version works.

I set the select:

Screenshot 2022-10-12 at 11.58.27

Then I set one date:

Screenshot 2022-10-12 at 11.58.46

Screenshot 2022-10-12 at 11.59.07

Nb: Firstly the code above is wrong. Below is ok.

<.datalist users={@users} target={@myself}></.datalist>

and

def datalist(assigns) do
    ~H"""
      <input list="datalist" id="datalist-input" name="query_picker[user]"
        phx-change="search-email"
        phx-target={@target}
        phx_debounce="500"
        placeholder="enter an email"
        />
      <datalist id="datalist">
          <option :for={user <- @users} value={user}  />
      </datalist>
    """
  end
attr(:users, :list)
  attr(:user, :string)
  attr(:phx_change, :string)
  attr(:target, :string)
```

Any implicit state the browser holds onto will be wiped. With LV the server is driving the content, which means you need to make sure to set the input value for it to not be lost. It doesn’t seem like you’re doing that.

Yes, indeed, Ibut there is no default value for a select, it picks the first in the list. Perhaps with the attribute “selected”. For the datalist, you can set the attribute “value” to the input but this fills the placeholder on first mount. To be continued

To confirm what you said, it works now:

  • for the datalist when you add the attribute “value” to the input field

  • for the select by putting the “selected” attribute to true, although I had to add an extra parameter:

<select id="select" name="query_picker[status]">
    <option :for={status <- @status} selected={status == @select}><%= status %></option>
</select>

using <.select status={@status} select={@select}><./select> where @select is set in the “change” handler. This is way too complicated, there must be something more clever. The form <%= select is much easier to use and works.

1 Like

I mean the select helper does all this for you under the hood as well. There’s a reason those helpers were created in the first place instead of telling people to use plain html. If you write your own html then you’ll need to care for the same things.