JS-based rich text editor disappers as soon as page loaded

I am integrating this editor into my Liveview app. I observed that the editor shows up nicely but only shortly while page being loaded/refreshed and just disappears right after that! Here is how I did it:

Within <head> in main layout:

<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/jodit/3.1.39/jodit.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/jodit/3.1.39/jodit.min.js"></script>

Hook in app.js:

Hooks.RichTxtEditor = {
  mounted() {
    console.log('Start rich text editor on: ' + this.el.id)
    const editor = new Jodit('#' + this.el.id)
  }
}

Within the form:

<%= textarea f, :feedback, 
           phx_hook: "RichTxtEditor",
           class: "form-control", rows: 5,
           required: true, value: @feedback %>

The werid thing is, it is not about a particular editor, I tried with CKEdtior, Quill, and several others. I observed same behavior!

Could that be something with Liveview?

Thanks a lot!

1 Like

Perhaps try adding phx-update="ignore"
My setup for quill seems to work like this:

<div 
    id="quillEditor"
    phx-hook="quillEditor"
    phx-update="ignore"
    class="quillEditor"
  >
  </div>
<%= text_input f, :quill, [value: Jason.encode!(@changeset.data.quill), type: "hidden", id: "quillHiddenInput", phx_update: "ignore"] %>

Then my hook looks like this:

export const quillEditor = {
  mounted() {
    const input = document.getElementById('quillHiddenInput');
    const editorInstance = new Quill('#quillEditor', {});
    let initContent = JSON.parse(input.value || "{}")
    editorInstance.setContents(initContent)
    editorInstance.on('text-change', function (delta, oldDelta, source) {
      const contents = editorInstance.getContents();
      input.value = JSON.stringify(contents);
    });
  }
}
1 Like

@gdub01 answer is correct, but let me save you a headache down the line, I JUST finished fixing a weird bug with textareas and JS editors you might want to be aware of:

On slower PCs, the textarea contents might not be loaded immediately and you will get truncated data, because document.readyState when you initialise your JS wrapper (CodeMirror, Quill, etc.) is not loaded yet.

The correct way of initialising a textarea wrapper is:

initQuill() {
  const editorInstance = new Quill("...");
}

// In your hook
if (document.readyState == "complete") {
  initQuill();
} else {
  document.addEventListener("readystatechange", () => {
    if (document.readyState == "complete") {
      initQuill();
    }
  }
}

Reference: https://stackoverflow.com/questions/29431252/codemirror-not-finding-a-textarea-because-dom-is-not-completely-loaded

This is completely off-topic, but as I’ve just experienced this problem I hope I’ve saved you a future headache :slight_smile:

8 Likes

Thanks you guys for the answers, but, unfortunately it did not work for me, despite of with phx-update="ignore"!

Even weirder, I extracted the relevant part into a dummy app and pushed it on Github. When I run it, the textarea shows up first, then after ~2 minutes (yes it was 2 minutes on my HP Core i5 laptop), the liveview websocket is established and the RTF editor shows up. This time it won’t disappear.

I suspect something with liveview + latency + DOM state, but don’t know what (sorry for my skills in both JS and Phoenix, hence this question!)

Could you please take a look at the Github repo?
Thanks a lot!

Trying your code out locally works as it should, but I only copied the JS and LiveView. It looks like you are on an old version of LiveView, so my guess is that is your problem. Your mix file has phoenix 1.4.13, and phoenix_live_view 0.8. Please try jumping to LV 0.12.1 . While you’re at it, you should also use phoenix 1.5 :slight_smile: If you run mix archive.install hex phx_new and mix phx.new my_app --live it will set everything up.

3 Likes

Out if curiosity are you using Firefox?

@chrismccord thanks, will try that and give feedback.
@ityonemo yes, but I also tried Chrome and Midori --> same behavior

I’m sporadically getting really late (sometimes a minute) socket mounts, in FF, but not chrome, but I’m waiting to bump to latest liveview to try and suss it out (it’s also an admin panel, so, sadly low priority for me)

I traced to the root cause, in same .html.leex file I have two hooks:

<div hidden="true" phx-hook="SomeAction"></div>

and the RTF editor:

<%= textarea f, :feedback, 
           phx_hook: "RichTxtEditor", phx_update: "ignore" %>

Now it went like this: the RichTxtEditor hook runs first --> the RTF editor shows up correctly. Then comes the SomeAction hook which pushes an event to backend to update some assign --> page updated, this time the RichTxtEditor hook does not get run --> the RTF edtior disappears.

Disabling the SomeAction hooks gives what I want.

My question is, how to always run the RichTxtEditor hook after each page update, without having to disable the other hook? I tried the updated callback in the hook but did not work.

PS: I upgraded the app to Liveview 0.12.0 and Phoenix 1.5.

The problem is that you are placing phx_update: "ignore" on the textarea element, but once the text editor is initialized that’s no longer used and the tag isn’t copied over to the div created by the editor library.

Add phx-update="ignore" to a div that contains your editor and it will work as supposed:

<div class="col-12 my-2" phx-update="ignore">
    <%= label f, :feedback, do: "feedback" %>
    <%= textarea f, :feedback, phx_hook: "RichTxtEditor", class: "form-control", rows: 5, value: @feedback %>
</div>
2 Likes

thanks @sfusato, it works!

but at the cost of not updating the value of the text editor when it is changed from the server side. My usecase is the content of text editor is multi-lang, when I change the language, its content should be changed accordingly.

Can I achieve that with some kind of hook?

Remove the phx-update="ignore" attribute and simply reinitialize the editor in the updated method of the same hook (basically, duplicate the logic you already have in mounted). But, now you also need to save any changes since an update will reset the editors to their initial values (a changeset + phx_change on the form are perfect for this).

Not really on topic…but fwiw I’ve noticed this in FF as well with LV 0.12.1

@sfusato that would not work for my case, because I have base64 encoded photo in the RTF content. Sending that to backend for every changes would not scale…

I resorted to the OK solution:

  • use keyUp event to record changes for all other input fields of the form
  • use the phx-update="ignore" for the field attached to the RTF editor
  • on updating RTF content from server, I do a push_redirect to the same current URL
  • use phx-submit to update new RTF content to server

Not sure if there is a better alternative…

1 Like

Hi @gdub01 where do you place you hook? it goes into app.js? i cant figure out how to make it work

I’m doing it like this in app.js:

import { quillEditor, quillEditorReadOnly } from './quillSetup.js'

let Hooks = {
  quillEditor: quillEditor,
  quillEditorReadOnly: quillEditorReadOnly,
}

let liveSocket = new LiveSocket("/live", Socket, { 
  hooks: Hooks, 
  params: { 
    _csrf_token: csrfToken, 
    width: window.innerWidth
  } 
})

then in quillSetup looks (in part) like this:

import Quill from "quill";
import 'quill/dist/quill.snow.css';


export const quillEditorReadOnly = {
  mounted() {
    const quillInputId = this.el.dataset.quillInputId
    const quillInput = document.getElementById(quillInputId);

    const editorInstance = new Quill(this.el, {
      placeholder: '',
      readOnly: true,
    });

    let initContent = JSON.parse(quillInput.value || "{}")
    editorInstance.setContents(initContent)
  }
}

and I call it like this:

      <div phx-update="ignore">
        <input value="<%= Jason.encode!(@content_item.quill) %>"
          type="hidden"
          id="read-only-quill"
        />
        <div
          id="quillEditor"
          phx-hook="quillEditorReadOnly"
          data-quill-input-id="read-only-quill"
          class="content is-large">
        </div>
      </div>

Not sure that that’s the best way… but just what I’m currently doing…

1 Like

(post withdrawn by author, will be automatically deleted in 24 hours unless flagged)