Search code examples
elixirphoenix-frameworkphoenix-live-view

Update the value of a form field after phx-change


I have a LiveView application which searches for airport codes. When a user enters ham it should replace the content of that form field with HAM (String.upcase/1) but it doesn't. But according to my understanding of my code it should. What do I have to change to replace all inputs in that field with an upcased version?

BTW: It works if I add a button to it and use phx-submit instead of phx-change. But I'd like to have it working for phx-change.

enter image description here

Setup

$ mix phx.new travelagent --live --no-ecto
$ cd travelagent

lib/travelagent_web/router.ex

[...]
scope "/", TravelagentWeb do
  pipe_through :browser

  live "/", PageLive, :index
  live "/search", SearchLive
end
[...]

lib/travelagent/airports.ex

defmodule Travelagent.Airports do
  def search_by_code(""), do: []

  def search_by_code(code) do
    list_airports()
    |> Enum.filter(&String.starts_with?(&1.code, code))
  end

  def list_airports do
    [
      %{name: "Berlin Brandenburg", code: "BER"},
      %{name: "Berlin Schönefeld", code: "SXF"},
      %{name: "Berlin Tegel", code: "TXL"},
      %{name: "Bremen", code: "BRE"},
      %{name: "Köln/Bonn", code: "CGN"},
      %{name: "Dortmund", code: "DTM"},
      %{name: "Dresden", code: "DRS"},
      %{name: "Düsseldorf", code: "DUS"},
      %{name: "Frankfurt", code: "FRA"},
      %{name: "Frankfurt-Hahn", code: "HHN"},
      %{name: "Hamburg", code: "HAM"},
      %{name: "Hannover", code: "HAJ"},
      %{name: "Leipzig Halle", code: "LEJ"},
      %{name: "München", code: "MUC"},
      %{name: "Münster Osnabrück", code: "FMO"},
      %{name: "Nürnberg", code: "NUE"},
      %{name: "Paderborn Lippstadt", code: "PAD"},
      %{name: "Stuttgart", code: "STR"}
    ]
  end
end

lib/travelagent_web/live/search_live.ex

defmodule TravelagentWeb.SearchLive do
  use TravelagentWeb, :live_view
  alias Travelagent.Airports

  def mount(_params, _session, socket) do
    socket =
      socket
      |> assign(:airport_code, "")
      |> assign(:airports, [])

    {:ok, socket}
  end

  def handle_event(
        "airport_code_search",
        %{"airport_code" => airport_code},
        socket
      ) do
    airport_code = String.upcase(airport_code)

    socket =
      socket
      |> assign(:airport_code, airport_code)
      |> assign(:airports, Airports.search_by_code(airport_code))

    {:noreply, socket}
  end
end

lib/travelagent_web/live/search_live.html.leex

<form phx-change="airport_code_search">
  <fieldset>
    <label for="nameField">Airport Code</label>
    <input type="text" name="airport_code" value="<%= @airport_code %>"
    placeholder="e.g. FRA" 
    autofocus autocomplete="off" />
  </fieldset>
</form>

<%= unless @airports == [] do %>
  <h2>Search Results</h2>
  <table>
    <thead>
      <tr>
        <th>Airport Code</th>
        <th>Name</th>
      </tr>
    </thead>
    <tbody>
      <%= for airport <- @airports do %>
      <tr>
        <td><%= airport.code %></td>
        <td><%= airport.name %></td>
      </tr>
      <% end %>
    </tbody>
  </table>
<% end %>

JavaScript Console Output

JavaScript Console Output

The phx-submit version

If I use this form:

<form phx-submit="airport_code_search">
  <fieldset>
    <label for="nameField">Airport Code</label>
    <input type="text" name="airport_code" value="<%= @airport_code %>"
    placeholder="e.g. FRA" 
    autofocus autocomplete="off" />
    <input class="button-primary" type="submit" value="Search Airport">
  </fieldset>
</form>

I get this working result (after entering fra and clicking the button):

enter image description here

PS: There are a million of possible solutions for this problem with JavaScript or CSS. But I'd like to know how to properly solve this with LiveView.


Solution

  • According to the documentation,

    The JavaScript client is always the source of truth for current input values. For any given input with focus, LiveView will never overwrite the input's current value, even if it deviates from the server's rendered updates.

    A solution in this case is to use JS hooks.