Phoenix LiveView how to edit content in place?

I am trying to have the server update the database whenever the div on the client is changed. I think the code I have tried does not work because phx-change only works on forms.

I have read through Phoenix.LiveView.JS, and was thinking of using key events events while monitoring whether or not the content of the div has changed. However, that seems overly complex, and I may be overlooking some capabilities of Phoenix and LiveView.

Where else should look to figure out how to accomplish this?

def handle_event("update", _, socket) do
   # update database with new content
    {:noreply, socket}
  end

def render(assigns) do
  ~H"""
  <div contenteditable="true" phx-change="update">
    word
  </td>
  """
end

If you are doing rich text editing it is better to consider solutions such as CKEditor. If you only need plain text you can dress your text area up with CSS. Proper editing and two way synchronisation of contenteditable is very difficult. Hope this helps.

3 Likes

CKEditor looks like it would work if I was creating a professional product. But I am doing this as a personal project to learn how to implement something like that.

andyl/phoenix_live_editable: In-place editing for Phoenix Live View (github.com) is an attempt in this direction. Hope it helps for learning purposes.
Ofcourse, livebook-dev/livebook: Interactive and collaborative code notebooks for Elixir - built with Phoenix LiveView (github.com) has great content editing in place. But, it has too much other functionality, and separating content editable part might be difficult.

1 Like

phoenix_live_editable looks like the perfect place to learn how to get started on this

Taking your requirements at face value, you could create a JS Hook where on mounted you hook up Mutation Observer to your dom and send events to the server as needed.

3 Likes

livebook uses a form for their in-place editing. I have adapted it slightly for my use-case, which looks something like:

              <form phx-change="update">
                <input
                  type="text"
                  id={"list-" <> Integer.to_string(list.id)}
                  name="title"
                  value={list.title}
                  spellcheck="false"
                  autocomplete="off"
                />
                <input type="hidden" name="id" value={list.id} />
              </form>

Careful! Input elements with name="id" in live view forms can cause some very annoying bugs, because that causes the .id attribute of the form element to hold the DOM element of the input with that name rather than id attribute value of the form. See for instance this thread. The solution is to use a name other than id for your input—in this case, perhaps it could be name="list-id".

2 Likes
                <form phx-change="update">
                <input
                  type="text"
                  id={"list-" <> Integer.to_string(list.id)}
                  name="title"
                  value={list.title}
                  spellcheck="false"
                  autocomplete="off"
                />
                <input type="hidden" name="list-id" value={list.id} />
              </form>

I built an inline form for updating one field today and I wanted to toggle between displaying the field and editing the field by simply clicking on it. So, I’ve built this little thing which toggles between the form and a <p>-element when the user clicks on it:

alias Phoenix.LiveView.JS

def render(assigns) do
  ~H"""
    <p
      id={"name-#{@element.id}"}
      # The group element means that the edit icon is shown
      # when a user hovers over the group.
      class="group hover:cursor-pointer flex items-center space-x-2"
      phx-click={toggle_name_divs(@element.id)}
    >
      <span><%= @element.name %></span>
      <.icon name="hero-pencil-solid" class="h-4 w-4 hidden group-hover:block" />
    </p>
    <form
      id={"form-#{@element.id}"}
      phx-submit="update_name"
      phx-target={@myself}
      class="hidden items-center space-x-2"
    >
      <input
        type="text"
        name="name"
        value={@element.name}
        autocomplete="off"
      />
      <.button phx-click={toggle_name_divs(@element.id)}>
        <.icon name="hero-check" />
      </.button>
    </form>
  """
end

def toggle_name_divs(element_id) do
    JS.toggle(to: "#name-#{element_id}", display: "flex")
    |> JS.toggle(to: "#form-#{element_id}", display: "flex")
end

def handle_event("update_name", %{"name" => name}, socket) do
  # Update and assign your element here
end

So, if a user hovers over the name display, an pencil icon shows. If the user clicks on the group, it disappears and the form appears. If the user saves the form, the form disappears and the name display appears again, but it also sends an “update_name”-event to the server. Hope it helps!

17 Likes

My initial implementation looked quite a bit like this but the JS.toggleing would execute before the server had a chance to push the updated values back over the socket and that created a bit of a FOUC for me.

The value would ‘revert’ to what the <p> element held before the update until the server would paste over the new value 1/10th of a second later. At that point I started thinking about optimistic client updates and edge cases etc. - but then I took a step back to reassess.

In the end I moved the toggling bit to the server so the actions are always queued correctly - first the value is updated in the backend and then the UI is updated to reflect the new value