LiveView Form adds decimals to inputs only inside inputs_for when you change the next value

Using the core_components provided with a fresh phoenix app. If you create a form with an inputs_for, which has contain float fields using ‘number’ inputs, decimals will be amended to its value when you change the next field. It doesn’t do this for float inputs not inside the inputs_for.

ezgif-5-f5540488fc

For example, my schema looks like this

  use Ecto.Schema
  import Ecto.Changeset

  defmodule InnerPlace do
    use Ecto.Schema
    import Ecto.Changeset

    schema "inner_place" do
      field(:float_1, :float)
      field(:float_2, :float)
    end

    def changeset(user, params \\ %{}) do
      user
      |> cast(params, [:float_1, :float_2])
    end
  end

  schema "place" do
    field(:float_1, :float)
    field(:float_2, :float)
    has_one(:inner_place, InnerPlace, on_replace: :update)
  end

  def changeset(place, params \\ %{}) do
    place
    |> cast(params, [:float_1, :float_2])
    |> cast_assoc(:inner_place)
  end

The form looks like this

      <.simple_form for={@form} phx-change="validate" phx-submit="save">
        <.input field={@form[:float_1]} label="Float 1" type="number"/>
        <.input field={@form[:float_2]} label="Float 2" type="number"/>
        <.inputs_for :let={ip} field={@form[:inner_place]}>
          <.input field={ip[:float_1]} label="inputs_for Float 1" type="number"/>
          <.input field={ip[:float_2]} label="inputs_for Float 2" type="number"/>
        </.inputs_for>
      </.simple_form>

The validate does nothing special either

  def handle_event("validate", %{"place" => params}, socket) do
    changeset =
      socket.assigns.place
      |> Place.changeset(params)
      |> Map.put(:action, :validate)

    {:noreply, socket |> assign(form: to_form(changeset))}
  end

Does anyone know why this might be happening, and how to prevent this?

From what I can see, it has to do with how Phoenix does change tracking and updates on the page. I’m not an expert so take what I say with a grain of salt, but it should be more or less ok.

When you update one of the floats, Phoenix will return a diff of this change so that the browser can update. Among other things, it sets the value to 1.0. But LiveView will never update an input value when it is in focus so the value is not reflected on screen.

Now, why the inner fields change… The floats for the main Place are easy for LiveView to do change tracking. So when an other field change, it can know they have not been updated and doesn’t need to send anything to the client. But for the fields in :inner_place, I guess LiveView can’t really do change tracking on the individual floats, so when 1 of them changes, it sends the changes for both to the client (where the input in focus isn’t updated but the one that’s not in focus is).

Now, as for what you can do… Working with floats is never really fun. There can always be problems with how to display or with rounding. Try putting 1 in the :inner_place float and see it turn to 1.0e3, which would confuse many users.

You could for example normalize the value you pass to the inputs:

<.input field={ip[:float_1]} label="inputs_for Float 1" type="number" value={normalize_float(ip[:float_1].value)} />

def normalize_float(n) when is_float(n), do: Decimal.from_float(n) |> Decimal.normalize() |> Decimal.to_string(:normal)
def normalize_float(n), do: n

This would make sure scientific notation isn’t used and remove unnecessary zeroes. But then if someone entered 1.0 in the input, it would be converted to 1. You could modify core_component’s input to do this automatically when the value is a float, possibly controlled by some options if you want to tailor the behaviour.

1 Like

The reason here is that inputs_for does use for, which rerenders all items if any changes.

1 Like