Fork me on GitHub

It's been a while since I did any front-end stuff. I'm currently struggling with getting contentEditable div to act right - the caret keeps moving around, etc. If I remember it right, there was some trick with storing data in a local state (atom) and then queuing for the change in app-db (so the changes in the field are reflected elsewhere). Can someone please point to an example. Thanks.


There are a bunch of links and discussions here and here Also relevant: tl;dr: avoid data roundtrips that re-render the component. Instead, use a stateful component that sends out its state on every change and that's re-rendered only if the external state is different from the internal one. In terms of specific code, re-com has a bunch of components that use that technique.


I know that article about the laggy-input. But it doesn't solve the problem with the caret. I'm gonna look through the other things you shared. Thank you!


also, I think there's no :default-input for contentEditable divs


I've gotten very close by storing the position of the caret and then restoring it after the input change, but the code looks a bit convoluted and I decided to abandon this approach. I think it makes sense to solve that by using local state. re-com seems to be doing it that way.


There shouldn't be any need on storing the caret position. Although I've never actually had to deal with editable divs. Let my try something out...


How are you handling the change of the text - via :on-input?


I've tried different handlers: on-input, on-key-down/up. Then I've found 'react-contenteditable' package on npm, but it still has the same problem. Like something simple like this:

[:> ContentEditable
 {:html text
  :on-change (fn [ev]
               (let [new-text (-> (.. ev -currentTarget -innerHTML))]
                 (rf/dispatch-sync [:set-text new-text])))}]


doesn't work properly, caret often moves around


Hmm, interesting. So it seems that the technique with external+internal model tracking that works with input fields doesn't actually work with editiable divs. Let me try that NPM package you mentioned.


Aha, a combination of the two seems to work:

(defn editable-div [{:keys [value on-change]}]
  (r/with-let [external-value (r/atom value)
               internal-value (r/atom (or value ""))]
    (when (not= @external-value value)
      (reset! external-value value)
      (reset! internal-value value))
    [:> ContentEditable {:html      @internal-value
                         :on-change (fn [^js event]
                                      (let [txt (.. event -target -value)]
                                        (reset! internal-value txt)
                                        (on-change txt)))}]))


Oh. Let me try that. Thank you!


Hmm. There's something that I'm missing. It's weird. When it's a single line of text - everything works fine. As soon I add new lines - the caret starts jumping to the end again. But I think I'm on the right track. Thank you for your awesome help and guidance.

👍 2

(defn editable-field [data key]
  (let [val (get data key)
        external-val (r/atom val)
        internal-val (r/atom (if (nil? @external-val) "" @external-val))]
    (when (not= @external-val val)
      (reset! external-val val)
      (reset! internal-val val))
    [:div {:class '[p-2]}
     [:h2 {:class '[font-medium mb-2]} (titleize key)]
     [:> ContentEditable
      {:class '[w-full border p-2 bg-white]
       :html @internal-val
       :on-change (fn [^js ev]
                    (let [new-text (-> (.. ev -currentTarget -innerHTML))]
                      (reset! internal-val new-text)
                      (rf/dispatch-sync [::module-types/update-draft
                                         {:key key
                                          :text new-text}])))}]]))

;; and to render it, from parent component

(for [k [:former_title :short_description :description :tips]]
     ^{:key k} [editable-field data k])
Everything seems to be working. Once again, thank you Eugene for saving me hours of struggle!


NP! Glad you got it working.