Validate (to uppercase) and reset (when manually modified) not working as expected

To begin: I’m fairly new at web-frontend and trying to learn by cloning a popular word puzzle game.

I have an input field where a guess can be entered, in a manually crafted form in a liveview. It’s possible to enter text via real-keyboard or an onscreen rendered keyboard.

At first I was using <.input... but was having trouble styling it (not respecting class, is this expected?) so I changed to just a raw <input... so the form code is basically:

<.form id="game-guess" for={@form} phx-change="validate" phx-submit="save">
  <input type="text" id="guess-id" class="guess-box" value={@guess} field="guess" name="guess" />
  <button type="reset" class="delete-button">X</button>
</.form>

I’m attempting to convert all guesses to uppercase only, and limit to 5 characters, so my validate looks like:

def handle_event("validate", payload, socket) do
  newguess = String.upcase(payload["guess"])
  |> String.slice(0, 5)

  socket =
    assign(socket,
      guess: newguess,
      form: to_form(%{
        "guess" => newguess
      })
    )
  {:noreply, socket}
end

newguess looks correct when I inspect it, but the form doesn’t respect that / validate doesn’t work at all. I’m certain I shouldn’t be using the value={@guess} directly in the assigns (which is easy enough to remove by using value={"#{@form.params["guess"]}"} but that feels like basically the same hack) and that I am doing something wrong in the form creation, but haven’t been able to figure out what I should do. For a while I thought it was an issue with web browsers respecting capitalization changes, but the truncation also doesn’t work.

If I manually set the assigns.guess from my onscreen keyboard, then the reset button doesn’t work. I suspect they’re related.

I think my main question is: How do I update a form’s input field value in a validate so it is reflected in the view immediately?

You don’t need the interpolation, value={@form.params["guess"]} will do just fine, maybe value={@form["guess"].value} is more idiomatic, Phoenix.HTML.FormField — Phoenix.HTML v3.3.0

1 Like

The value is not being overwritten because LiveView does not overwrite the contents of inputs that have focus. See the docs here:

https://hexdocs.pm/phoenix_live_view/form-bindings.html#javascript-client-specifics

If it worked that way, you would run into all sorts of wonky behavior due to latency, because the server’s state would always be behind the client, so every time an update came back it would delete some of your text as you type.

For this sort of thing (modifying input as a user types), you can use a JS hook and some standard JavaScript to listen to the input event and uppercase the string synchronously on the client. You can find the docs for hooks here:

https://hexdocs.pm/phoenix_live_view/js-interop.html#client-hooks-via-phx-hook

Then if for some reason you need to validate that the field is uppercase on the server, you can just check it on submit and throw an error (since only a malicious user would send a lowercase string to the server).

2 Likes

Oh, and also, the reason you can’t style the input component is because it comes pre-styled with tailwind classes and so it doesn’t pass the @class attribute in by default. But these components are literally generated into your project (they’re in core_components.ex), so you can change them to whatever you want.

What you did (styling a one-off input) is also fine - it just depends on what you’re doing. If you want to re-use that input elsewhere in the project you can turn it into another component.

1 Like

Thanks for all the answers everyone! I updated to not use interpolation and it does look cleaner, but no behavior change.

The explanation about not modifying fields with focus makes complete sense, thank you for all the docs pointers! (Will try the JS Hook approach soon)

Also the styling being baked into core components is nice, too. I figured these were functions defined somewhere but thought it was at the framework level and they weren’t opening in my editor automagically so I stopped hunting for them after a bit. (Though I had read that “daunting” file a bit in the past, just forgot about it.) Would it be a bad practice to customize this version to allow passing in class definitions to append?

Finally, for the reset button: Is it possible to wire things up in a way that the default <button type="reset"... works for manually entered values? Or should I just hook that to another action and handle it directly (this seems simple and correct enough).

It’s fine - it depends on context. Obviously the more customization you add the harder the component will be to re-use. Especially with Tailwind, since you have to pass in the universe in order to style a component. You can also accept variables as assigns and then use them to dynamically choose the relevant classes (e.g. for color or size). I use that pattern a lot (though I don’t use Tailwind).

Buttons with type="reset" are pretty rare and generally not recommended, for various reasons. I think in this case the problem is that Phoenix is injecting the current value of the input into the value="..." attr, which is used to reset the input (so it just gets reset to the server value). You would have the same problem, I think, with a React controlled input.

Implementing the reset functionality in code should fix that. It should work server-side because clicking the button will steal focus from the input allowing it to be reset.

1 Like