Tutorial

In place edit with LiveView

Phoenix 1.7

A very common practice in web applications these days is to allow for inline editing of content. This is especially true when the value is a text field and contains like a name or title. You can see this pattern if you use something like Trello or Linear.

Instead of using a form input, you can use the HTML attribute contenteditable. That can make any element that has text editable. The upside is that you dont break your normal styling and the page will look like the content that you edit, which can reduce the visual clutter of traditional form elements.

The downside is that inputs has a data validations built in so if you need data validation, you probably will be better of with plain forms.

In this tutorial, I will go though how to use contenteditable in Phoenix LiveView to achieve the pretty inline editing.

Creating a reusable LiveView LiveComponent

Our journey begins with the construction of a reusable in-place edit component, designed as a Phoenix LiveComponent. This section will detail the component's architecture, emphasizing its flexibility for use in various application contexts. We aim to construct a versatile component that transforms standard elements into editable fields upon user interaction, thereby enhancing the overall user experience.

defmodule TutorialWeb.InPlaceEditComponent do
  use Phoenix.LiveComponent
  use Phoenix.Component

  attr :id, :string, required: true
  attr :content, :string, required: true
  attr :on_save, :any, default: nil
  attr :rest, :global
  def editable(assigns) do
    ~H"""
    <.live_component module={__MODULE__} id={@id} content={@content} on_save={@on_save} rest={@rest} />
    """
  end

  def render(assigns) do
    ~H"""
    <div
      id={@id}
      phx-hook="InPlaceEdit"
      phx-keydown="cancel"
      phx-key="escape"
      phx-target={@myself}
      contenteditable="true"
      {@rest}
    >
      <%= @content %>
    </div>
    """
  end

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

  def handle_event("update", %{"content" => content}, socket) do
    case socket.assigns.on_save do
      {%{} = record, attr} -> send(self(), {record, %{attr => content}})
      _ -> nil
    end

    {:noreply, socket}
  end

  def handle_event("cancel", _, socket) do
    {:noreply, push_event(socket, "cancel", %{content: socket.assigns.content})}
  end
end

Implementing JavaScript Hooks for LiveView Interactions

There are some thing that the LiveView can do by itself. Or at least as fair as I know now. It does not track the changes of the content that you are editing. That means, when saving it, we need to check what the element contains, and push that to the LiveView. For this purpose, I am setting up a LiveView javascript hook so I can easily communicate between.

phx-hook="InLineEdit"

The hook is pretty simple. First of all, It will listen to blur-events. Blur means that I am done with the element and click away. That will then push the content to the LiveComponent. Note that I used this.pushEventTo so I can send it back to the LiveComponent instead of the LiveView.

// Add this amongst the other LiveView Hooks
Hooks.InPlaceEdit = {
  mounted() {
    this.el.addEventListener('blur', event => {
      this.pushEventTo(this.el, 'update', {content: this.el.innerText})
    })

    this.handleEvent('cancel', data => {
      this.el.innerText = data.content
      this.el.blur()
    })
  }
}

Start using the LiveComponent in your application

With our in-place edit component and JavaScript hooks prepared, we're set to integrate them into a Phoenix application. This section provides a detailed walkthrough for embedding the LiveComponent into a LiveView, such as a user profile or company details page. We'll guide you through transforming static content into dynamic, editable fields, effectively bringing your application to vibrant life.

# Add this LiveView callback
def handle_info({%Companies.Company{} = company, attrs}, socket) do
  case Companies.update_company(company, attrs) do
    {:ok, company} ->
      {:noreply, assign(socket, company: company)}
    {:error, _} ->
      {:noreply, socket}
  end
end
<!-- Replace -->
<:item title="Name"><%= @company.address %></:item>

<!-- With -->
<:item title="Name">
  <.editable id="company-name" content={@company.name} on_save={{@company, :name}} />
</:item>

Ensuring Smooth Operation: Testing and Debugging

The successful integration of the in-place edit feature sets the stage for rigorous testing and debugging. This section sheds light on effective strategies to test your LiveView components, ensuring they operate flawlessly. We'll also delve into debugging techniques to identify and resolve any issues, ensuring a polished and reliable in-place editing feature.

Conclusion

This tutorial has navigated the intricacies of creating a dynamic, in-place edit feature using Phoenix LiveView. By embracing the power of LiveComponents and JavaScript hooks, we've unlocked new possibilities for enhancing user experiences in Phoenix applications. As you continue to explore and innovate with Phoenix LiveView, consider the vast potential of in-place editing and other real-time features to elevate your applications to new heights of interactivity and engagement.