Form submit on Enter keypress for textarea input type

Hi!

Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a newline to my input field (which is expected behaviour).

Is there a way to make submit work on pressing the Enter key without messing up the way the form is cleared after submitting?

I’m using below code to make the form work. This form works properly when the type of the input element is the default text type rather than a “textarea”.

    <div class="flex items-center py-2">
      <.form for={@form} phx-submit="save" phx-change="update" phx-target={@myself} >
        <.input type="textarea" field={@form[:content]} placeholder="Start typing..." autocomplete="off"/>
        <.button class="text-black" phx-disable-with="Submit">
          Submit
        </.button>
      </.form>
    </div>

Cheers,
Sil

Welcome!

Here’s three distinct things to consider:

  1. listening for user interaction with a generic JS event listener on the textarea input e.g. on key down for the Enter keycode
  2. handling that event by triggering a phx-submit event on the form e.g. dispatching a “submit” event
  3. attaching the JS event listener/handler to the textarea input of a LiveView form e.g. LiveView client hooks via phx-hook

All together that might look something like:

<input type="textarea" ... phx-hook="TextArea" />
let Hooks = {}
Hooks.TextArea = {
  mounted() {
    this.el.addEventListener("keydown", e => {
      if (event.key == "Enter") {
        event.preventDefault();
        this.el.form.dispatchEvent(
          new Event("submit", {bubbles: true, cancelable: true})
        )
      }
    })
  }
}

let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, ...})

Hey, I was just working on this today. very frustrating.
My form wasn’t clearing when a user presses enter. Seems related to: elixir - Reset form input field after submit in Phoenix LiveView - Stack Overflow

This is my pure Elixir w/ a lil bit of Liveview.JS solution. One caveat, if the user presses “shift + enter”, the form will submit

def mount(socket) do
  {:ok,
   socket
   |> assign(:input_message_form, to_form(%{"message" => ""}))
   |> assign(:user_input_value, "")}
end

def handle_event("submit_message", %{"message" => message}, socket) do
  # Do something with the message
  {:noreply, socket |> assign(:user_input_value, "")}
end

def handle_event("user_input_change", %{"message" => changed_input_value}, socket) do
  {:noreply, socket |> assign(:user_input_value, changed_input_value)}
end

def render(assigns) do
  ~H"""
  <.form
    id="input-message-form"
    for={@input_message_form}
    phx-submit="submit_message"
    phx-target={@myself}>
    
    <.input
    field={@input_message_form[:message]}
    type="textarea"
    phx-keydown={JS.dispatch("submit", to: "#input-message-form")}
    phx-key="Enter"
    phx-change="user_input_change"
    phx-target={@myself}
    value={@user_input_value}
    />
    <button type="submit">Submit</button>
  </.form>
  """
end

Thanks for the quick reply!

Works like a charm. I think the way the submit event is triggered from this hooks is a neat solution :slight_smile:

I’ve added a check on the Shift key so that form will not submit on Shift+Enter and instead will add a newline to the textarea:

mounted() {
  this.el.addEventListener("keydown", e => {
    if (e.key == "Enter" && e.shiftKey == false) {
      e.preventDefault();
      this.el.form.dispatchEvent(
        new Event("submit", {bubbles: true, cancelable: true})
      )
    }
  })
}
1 Like

Hi theodore,

I think you don’t need the value={@user_input_value} since you already bind field={@input_message_form[:message]}. If you get rid of the @user_input_value you can use the form itself to update the value. For proper clearing of the form it was necessary to update the form with the phx-change callback function (see How to clear a textbox after submit - #2 by andreaseriksson).

My LiveComponent looks like below, but this should also work for a LiveView:

  def update(assigns, socket) do
    {:ok,
     socket
     |> assign(assigns)
     |> assign_form}
  end

  defp assign_form(socket, content \\ nil) do
    assign(socket, :form, to_form(%{"content" => content}, as: "message"))
  end

  def handle_event("update", %{"message" => %{"content" => content}}, socket) do
    # This function seems to be required for clearing the form after submitting because Phoenix LiveView
    # needs to keep track of the items and can't clean properly otherwise (https://elixirforum.com/t/how-to-clear-a-textbox-after-submit/47002/2)
    {:noreply,
     socket
     |> assign_form(content)}
  end

  def handle_event("save", %{"message" => message_params}, socket) do
    # We don't want to handle messages here. Send them to parent and let parent
    # deal with it.
    send self(), {:new_message, message_params}

    # Set empty form since we propagated the message to parent
    {:noreply,
     socket
     |> assign_form}
  end

  def render(assigns) do
    ~H"""
    <div>
      <.form id="message-form" for={@form} phx-submit="save" phx-change="update" phx-target={@myself}>
        <div class="flex-1 min-w-0">
          <.input
            field={@form[:content]}
            placeholder="Start typing..."
            autocomplete="off"
           />
        </div>
        <.button phx-disable-with="Submit">
          Submit
        </.button>
      </.form>
    </div>
    """
  end

Maybe try making it function properly first with a regular form and then switch to a textarea by using the code suggested by @codeanpeace?

Yea that’s what I thought too, but for some reason the field attribute doesn’t populate the value inside the input. I think I’ll also switch to hooks so I can easily have the shift + enter functionality.

I was running into a similar issue. I ended up pushing an event after creating a new message then clearing the field when I receive the event in the frontent.

in form_component.ex

    case Chats.create_message(message_params) do
      {:ok, _message} ->
        {:noreply,
         socket
         |> push_event("clear-textarea", %{id: "message_content"})
         |> push_patch(to: socket.assigns.patch)}

in app.js

window.addEventListener(`phx:clear-textarea`, (e) => {
  let el = document.getElementById(e.detail.id)
  el.value = ""
})