Process.send_after loop is wiping out form in liveview

Hey guys, when inputting a verification code with a count down timer it’s wiping out the form.

Below is reproducable liveview code

defmodule ClimateCollectiveWeb.SmsVerificationLive do
  use ClimateCollectiveWeb, :live_view
  # alias ClimateCollective.Accounts.SMSTokens
  # alias ClimateCollectiveWeb.UserAuth
  # alias ClimateCollective.Accounts.Users

  def render(assigns) do
    ~H"""
    <div class="container">
      <div class="row">
        <div class="col-12">
          <h1>Verify your phone number: <%= @phone %></h1>
          <p>
            If your phone is registered, we sent a verification code to your phone. Please click on the in the sms to continue.
          </p>
          <.simple_form
            id="sms_verification_form"
            phx-change="code_changed"
            for={@form}
            action={~p"/sms_log_in"}
            phx-trigger-action={@trigger_submit}
          >
            <div class="flex flex-row space-x-3 form-group">
              <.label for="code">Verification code</.label>
              <%= @verification_token %>
              <input type="text" name="phone" hidden="true" value={@phone} />
              <div class="w-12">
                <input
                  type="tel"
                  class="form-control code-input"
                  id="code_0"
                  name="code[0]"
                  inputmode="numeric"
                  maxlength="1"
                  pattern="[0-9]*"
                  field="code_0"
                  value=""
                  phx-value-input-id="0"
                  phx-keydown="input_down"
                />
              </div>
              <div class="w-12">
                <input
                  type="tel"
                  class="form-control code-input"
                  id="code_1"
                  name="code[1]"
                  inputmode="numeric"
                  maxlength="1"
                  pattern="[0-9]*"
                  field="code_1"
                  value=""
                  phx-value-input-id="1"
                  phx-keydown="input_down"
                />
              </div>
              <div class="w-12">
                <input
                  type="tel"
                  class="form-control code-input"
                  id="code_2"
                  name="code[2]"
                  inputmode="numeric"
                  maxlength="1"
                  pattern="[0-9]*"
                  field="code_2"
                  value=""
                  phx-value-input-id="2"
                  phx-keydown="input_down"
                />
              </div>
              <div class="w-12">
                <input
                  type="tel"
                  class="form-control code-input"
                  id="code_3"
                  name="code[3]"
                  inputmode="numeric"
                  maxlength="1"
                  pattern="[0-9]*"
                  field="code_3"
                  value=""
                  phx-value-input-id="3"
                  phx-keydown="input_down"
                />
              </div>
              <div class="w-12">
                <input
                  type="tel"
                  class="form-control code-input"
                  id="code_4"
                  name="code[4]"
                  inputmode="numeric"
                  maxlength="1"
                  pattern="[0-9]*"
                  field="code_4"
                  value=""
                  phx-value-input-id="4"
                  phx-keydown="input_down"
                />
              </div>
              <div class="w-12">
                <input
                  type="tel"
                  class="form-control code-input"
                  id="code_5"
                  name="code[5]"
                  inputmode="numeric"
                  maxlength="1"
                  pattern="[0-9]*"
                  field="code_5"
                  value=""
                  phx-value-input-id="5"
                  phx-keydown="input_down"
                />
              </div>
            </div>

            <%= if @invalid do %>
              <p class="text-red-500">Invalid code</p>
            <% end %>

            <:actions>
              <div>
                <.button
                  type="button"
                  class={"btn btn-secondary #{if @resend_in > 0, do: "disabled:opacity-50"}"}
                  phx-click="resend_code"
                  disabled={@resend_in > 0}
                >
                  Resend code <%= if @resend_in > 0, do: @resend_in %>
                </.button>
              </div>
              <.button type="submit" class="btn btn-primary ">
                Submit
              </.button>
            </:actions>
          </.simple_form>
        </div>
      </div>
    </div>
    """
  end

  def mount(_params, session, socket) do
    form = to_form(%{}, as: "sms_verification")
    phone = "+15550123"

    socket =
      socket
      |> assign(:code, "")
      |> assign(:disable_submit, true)
      |> assign(:invalid, false)
      |> assign(:trigger_submit, false)
      |> assign(:resend_in, 30)
      |> assign(:phone, phone)
      |> assign(:verification_token, 654_321)
      |> assign(:form, form)

    if connected?(socket), do: Process.send_after(self(), :update_resend_timer, 1_000)

    {:ok, socket}
  end

  def handle_info(:update_resend_timer, socket) do
    Process.send_after(self(), :update_resend_timer, 1_000)

    if socket.assigns.resend_in >= 1 do
      {:noreply, update(socket, :resend_in, fn resend_in -> resend_in - 1 end)}
    else
      {:noreply, socket}
    end
  end

  def handle_event("input_down", %{"input-id" => id, "key" => "Backspace"}, socket) do
    id = String.to_integer(id)

    cond do
      id > 0 ->
        {:noreply,
         socket
         |> push_event("focus", %{id: "code_#{id - 1}"})
         |> push_event("clear", %{id: "code_#{id - 1}"})}

      true ->
        {:noreply, socket}
    end
  end

  def handle_event("input_down", %{"input-id" => id, "key" => key}, socket) do
    id = String.to_integer(id)

    cond do
      key == "Meta" ->
        {:noreply, socket}

      id < 5 ->
        {:noreply, move_input_field(socket, true, id)}

      id == 5 ->
        {:noreply, assign(socket, trigger_submit: true)}

      true ->
        {:noreply, socket}
    end
  end

  def handle_event("code_changed", %{"code" => code}, socket) do
    code_string = Map.values(code) |> Enum.join()

    if String.length(code_string) == 6 do
      {:noreply, socket |> assign(:code, code) |> assign(trigger_submit: true)}
    else
      {
        :noreply,
        socket
        |> assign(:code, code)
        |> assign(:disable_submit, String.length(code_string) != 6)
      }
    end
  end

  def handle_event("input_pasted", %{"code" => code}, socket) do
    if String.length(code) == 6 do
      {:noreply, socket |> assign(trigger_submit: true)}
    else
      IO.puts("no")
      {:noreply, socket}
    end
  end

  def handle_event("resend_code", _params, socket) do
    {:noreply, socket |> assign(:resend_in, 30) |> assign(:verification_token, "123")}
  end

  defp move_input_field(socket, is_present?, input_id) do
    cond do
      input_id == 5 ->
        socket

      input_id == 0 && !is_present? ->
        socket

      is_present? && input_id < 5 ->
        push_event(socket, "focus", %{id: "code_#{input_id + 1}"})

      !is_present? && input_id > 0 ->
        push_event(socket, "focus", %{id: "code_#{input_id - 1}"})
    end
  end
end

Here’s what happens:

  1. Land on page
  2. Input code
  3. Once the button refreshes form data dissappears

ezgif-3-4bb3f9dcc5

Would love some help! I think I’m missing some sort of concept

Thank you in advance!

Hey @Morzaram , the live view is the definitive state of the page. This means that you need to map the input values to an assign, and set that assign when you get a value. This way when the page re-renders it re-renders with the value set.

1 Like

Hey Ben,

Thanks for the quick response!

What would be the best approach to this since it’s not tied to a schema?

Sorry for not knowing directly from your feedback what you mean!

1 Like

For each input, set the value like so:

                <input
                  type="tel"
                  class="form-control code-input"
                  id="code_0"
                  name="code[0]"
                  inputmode="numeric"
                  maxlength="1"
                  pattern="[0-9]*"
                  field="code_0"
                  value={@values[0]}
                  phx-value-input-id="0"
                  phx-keydown="input_down"
                />

Instead of doing this 6 times I’d consider using a loop in the template.

Anyway in the handle_event clause you have to handle the change, I would then

update(socket, :values, fn values -> Map.put(values, index, value) end)

Basically the idea is that you set value= in the input off of an assign, and then you make sure that when you get an input, you use that input to set the assign.

1 Like

I’ve never implemented one of these types of interfaces before (what do you even call it, “multiple, single-character input fields?”), so take this with a grain of salt…

I’ve always thought that if I did need to do this, I would use a single hidden input field that was actually submitted with the form (as in only it would have a name attribute), and it would be a client-side presentation concern to display the values across multiple fields. When it comes time to handle things like paste events, I think having a separation between the data and the presentation/event-handling would make things cleaner and easier.

1 Like

Thank you! I was definitely over thinking this!

I would love to go with your approach but I legit have no idea how to implement it.
What approach is the way that you have? Could you expand on how you’d wire up the hidden field with this?