How to add JS loaded textarea with LiveView form?

I’m trying to use this WYSIWYG Markdown editor in my live view form https://github.com/sparksuite/simplemde-markdown-editor (Does anyone know which editor Elixir Forum uses, I want something like this).

Here’s my form

<div class="message_form" style="height: 100px;">
        <%= form_for :message, "#", [phx_change: :typing, phx_submit: :save], fn f -> %>
          <%= textarea f, :text, id: "wysiwyg", phx_hook: "messageTextarea" %>
          <%= submit "Save" %>
        <% end %>
      </div>

Here’s my JS

Hooks.messageTextarea = {
  mounted() {
    new SimpleMDE({ element: document.getElementById("wysiwyg") });
  },
  updated() {
    new SimpleMDE({ element: document.getElementById("wysiwyg") });
  },

The editor appears when the page loads, but when the first character is typed, there is a reload, the editor appears again, but there’s no text in it. If I log the events, I seem to get a bunch of beforeUpdate and updated events firing. How can I keep my events, but stop it from reloading the text area on each character typed?

You likely want to set the textarea to phx-update="ignore" and not reinitialize SimpleMDE. Also instead of document.getElementById("wysiwyg") you could just use this.el.

Putting phx_update: "ignore" on the textarea didn’t do anything. It did work if I put phx_update: "ignore" on the form itself. However then the form doesn’t reset after submitting.

You can try putting the textarea in a wrapping element like a div and only make this one be phx_update: "ignore".

If I do that, it still doesn’t reset the text area to blank after submitting. I don’t mind that, as long as I can trigger something after saving to clear the textarea.

Yeah, this is the place where you’ll notice the problem of having to places trying to be the source of truth. The only “trigger” your liveview will get it that the updated() part of your hook is called.

Presumably after each “change” event is fired, LiveView does something and reloads the text area. Is there anyway to hook into that update and only update the value of the text area, instead of completely reloading the element?

Nope, the server doesn’t send structured updates. It just sends optimized template diffs.

I see. I guess I can either have the WYSIWYG editor, or the user_is_typing indicator, but not both. Hmm. Maybe i can hack around it with some JS.

Is there a way to manually send a phx event from JS? If I put phx-change="typing" on something, that doesn’t work if my JS lib replaces the element. Is there a way I can listen to changes via the 3rd party lib (I know how to do this part), and then trigger events to get sent via phx? Kinda like in channels. channel.push("typing")

pushEvent is what you are looking for I think - documented here: https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#module-js-interop-and-client-controlled-dom

What a coincidence, I just visited the forum to post something else and saw your post. I’m also using this editor in my app.

A question: Where is your form and how are you loading the editor? Mine is in a live_modal. The only way I could make the editor (kinda) work was by placing a <script> tag inside a phx-ignore element. It doesn’t always load, though, and I don’t know why.

And a tip: SimpleMDE hasn’t been maintained in a long time. Use EasyMDE instead!

hey @wowlixir, were you able to make it work? how did you export the textarea in your template? i can´t get it to work

Hopefully this helps out anyone in the future. Based on the tip found here. For my use case, what i did was:

  1. Set up a form in a live component.
def render(assigns) do
  ~L"""
  <div class="form_container">
    <%= f = form for @changeset, 
       #,
       phx_target: @myself,
       phx_change: "validate_form" ,
       id: "richtext_form"
    %>
      <div 
        phx-hook="RichText"
        phx-update="ignore"
        phx-target="<%= @myself %>"
        id="richtext_container"
      >
        <%= textarea f, 
          :the_content, 
          value: @clientside_richtext_input 
        %>
      </div>
    </form>
  </div>
  """
end 

Reason being is i still want to validate the form input on the server side. But the pushing the params after a validation function in the returning {:noreply, socket |> ...} kept on failing and the data being put into to the rich text wasn’t stored anywhere. Which led to:

  1. Create two handle events. One for the form validation, the other to handle the clientside hook and pass the data into an assign that will be set as the value for the textarea.
def handle_event(
  "validate_form", 
  %{"richtext_form" => params}, 
  socket) do 
  {:noreply, socket |> validate_form(params)}
end

def handle_event(
  "handle_clientside_richtext", 
  %{"richtext_data" => richtext_data},
  socket) do 
  {:noreply, 
    socket 
    |> assign_form(:clientside_richtext_input, richtext_data)
    |> push_event("richtext_event", %{richtext_data: richtext_data})}
end

This is so that the richtext data can be stored by cycling back and forth between the client and backend for when the live component is updated. Now for the same thing inside the JS code.

  1. In a hook object do
const RichText = {
  mounted(){
    init(this.el.querySelector("#richtext_form_the_content"))
  },
  updated(){
    const textArea = init(this.el.querySelector("#richtext_form_the_content"));
    textArea.codemirror.on("change", ( ) => {
      this.pushEventTo(
        this.el, 
        "handle_clientside_richtext",
        {richtext_data: textArea.value()}
      );
      this.handleEvent(
        "richtext_event",
        (richtext_data) => textArea.value(richtext_data)
      )
    })
  }
}

The logic is in the updated function since mounted rich text area gets erased right away.

  1. And finally to have easyMDE be usable do
import easyMDE from "easyMDE";

const init = (element) => new easyMDE({
  element: element,
  forceSync: true
})

The forceSync: true is for the data to be set as the textarea value on the clientside and play well with the backend.

It took a few days of looking back and forth at different examples, reading the forum, rereading the docs, looking at source code, and feeling dumb. This is the simplest solution that i can think of once it all clicked.

*edit typo fix: phx-update: "TextEditor" to phx-hook: "RichText"

1 Like