Re-initializing wysiwyg editor on liveview updated hook, hook runs but editor doesn't actually initialize

Hey folks, wondering if anyone has any insight as to why a wysiwyg editor (summernote) would initialize okay on a hook mount , but doesn’t re-initialize on a hook updated event?

I could phx-update=“ignore” this, but I need to be able to re-order a list of these with liveview.

This is for liveview 15.7. The JS code is here:

Hooks.InitSummernote = {
  mounted() {
    init_summernote(
      this.el,
      this.el.dataset.target,
      this.el.dataset.input,
      this.el.dataset.updateEvent,
      this.el.dataset.dbid,
      this
    );
  },
  updated() {
    /* Todo: debounce this */
    init_summernote(
      this.el,
      this.el.dataset.target,
      this.el.dataset.input,
      this.el.dataset.updateEvent,
      this.el.dataset.dbid,
      this
    );
  }
}

function init_summernote(el, targetComponent, targetInput, updateEvent, databaseID, lv) {
  $(el).summernote({
    minHeight: 200,
    toolbar: [
      ['font', ['bold', 'italic', 'underline', 'clear']],
      ['para', ['ul', 'ol', 'paragraph']],
      ['view', ['codeview']]
    ],
    tooltip: false,
    cleaner: {
      action: 'paste',
      keepHtml: false,
    },
    callbacks: {
      onChange: function (contents, $editable) {
        $(targetInput).html(contents);

        if (targetInput != undefined && updateEvent != undefined && databaseID != undefined) {
          lv.pushEventTo(targetComponent, updateEvent, { content: contents, target: targetInput, id: databaseID })
        }
      }
    }
  })
  console.log("init summernote")
}

And the liveview template:

<input
   id="<%= @form_names_map.consent %>[<%= @repeater_index %>][content]"
   name="<%= @form_names_map.consent %>[<%= @repeater_index %>][content]"
    type="hidden" name="content" value="<%= @content %>">
<div class="max-w-full">
     <textarea class="summernote" phx-hook="InitSummernote"
         id="<%= "consents-#{@id}-content" %>"
         data-dbid="<%= @id %>"
         data-target="<%= @myself %>"
         data-update-event="consent_change"
         data-input="<%= @form_names_map.consent %>[<%= @repeater_index %>][content]">
           <%= @content %>
     </textarea>
</div>

Any ideas why this wouldn’t work? Or a better solution to handle a list of re-orderable wysiwyg editors while staying in sync with the liveview?

Have you tried keeping a reference to the WYSIWYG editor? In mounted you do something like this.editor = init_summernote() and then you can edit or remove and reinitialise it in updated.

Thanks for the help. That sounds like it might be a good idea here, although I’m not sure I fully understand.

If I’m understanding right, I’d be persisting the summernote element after it’s initialized in this and on the updated event I would try to reinitialize it somehow by reading that object back out?

Do you think this would have a different outcome from just trying to initialize it on the updated textarea (what I’m trying now)?

Right now the summernote initialization seems to work on the updated event - if I console log the result it’s got the summernote stuff - but it’s just never reflected in the dom.

Are you using phx-update="ignore" on the hook element?

Whether you need to kill the existing and recreate another one, or just update the existing one depends on the library and situation. I would expect that most of the time we update the existing one. There are a few ways to do so.

Under certain circumstances I have to destroy and recreate a slider, like so.

import noUiSlider from "nouislider";

const TimeSlider = {
  mounted() {
    ...
    this.createSlider();
  },
  updated() {
    ...
    this.slider.destroy();
    this.createSlider();
  },
  createSlider() { this.slider = noUiSlider.create(this.elem, {...}); }

For a charting library, I define an event handler in mounted() and send it an event so that it can update itself.

  def update(assigns, socket) do
    socket = assign(socket, assigns)
    {:ok, push_event(socket, "echarts:#{socket.assigns.id}:init", %{"option" => assigns.option})}
  end
const Echart = {
  mounted() {
    ...
    this.handleEvent(`echarts:${this.props.id}:init`, ({ option }) => { ... });
  },

Other times you don’t set phx-update="ignore" and simply update a data-something attribute on the hook and handle it all in the hook. There are a thousand ways to skin a cat.

Have you tried with phx-update="ignore"? I only ask because my understanding is that phx-update is meant to apply to the content within the div (you can still change attributes), so maybe moving it around would still preserve the inner HTML.

Can you show how your setting your assigns in your LiveView’s mount function?