Fork me on GitHub
#fulcro
<
2022-07-04
>
stagmoose04:07:51

I was using rich-text editor (slate.js https://docs.slatejs.org/walkthroughs/02-adding-event-handlers) in fulcro and want to implement a nested structure of rich text textarea using Slate react component. I tried to follow the fulcro book to use react-interop to use Slate react components. The problem I encountered is how I can make different textarea pulling data from the same place of client database in sync. (you can see that at the end of my video, the task 5 textarea is not updated) I have two questions: 1. Is it reasonable to use (ui-editor {:content content :id id :this this}) like I did in my code to pass this object around. Or there's a better way? 2. How could I fix the problem of different textareas with same data not syncing together. The code I wrote look like this:

(ns com.example.ui
  (:require
   ["react" :as react]
   ["react-dom" :as react-dom]
   ["slate" :refer (createEditor)]
   ["slate-react" :refer (Slate Editable withReact)]
   [com.fulcrologic.fulcro.algorithms.react-interop :as interop]
   [com.example.mutations :as mut]
   [com.fulcrologic.fulcro.algorithms.merge :as merge]
   [com.fulcrologic.fulcro.algorithms.tempid :as tempid]
   [com.fulcrologic.fulcro.algorithms.data-targeting :as targeting]
   [com.fulcrologic.fulcro.algorithms.normalized-state :as norm]
   [com.fulcrologic.fulcro.components :as comp :refer [defsc transact!]]
   [com.fulcrologic.fulcro.raw.components :as rc]
   [com.fulcrologic.fulcro.data-fetch :as df]
   [com.fulcrologic.fulcro.mutations :refer [defmutation]]
   [com.fulcrologic.fulcro.dom :as dom :refer [button div form h1 h2 h3 input label li ol p ul]]))

(declare ui-todo)
(declare ui-editor)

(defmutation change-context [{id :id data :new-data}]
  (action [{:keys [state]}]       
          (swap! state assoc-in [:todo/id id :todo/content] data)))

(defn get-text [data props]
  (let [this (goog.object/get props "this")
        id (goog.object/get props "id")
        children (.-children (first data))
        all-data (apply str (map #(.-text %) children))]
    all-data
    (comp/transact! this [(change-context {:id id :new-data all-data})])))

(defn Editor [props]
  (let [[editor] (react/useState #(-> (createEditor)
                                      (withReact))) ;; bind this?
        init-val (clj->js [{"type" "paragraph"
                            "children" [{"text" (.-content props)}]}])
        editable-comp (react/createElement Editable #js {} nil)
        slate-comp (react/createElement Slate
                                        #js {"editor" editor
                                             "value" init-val
                                             "onChange" #(js/console.log (get-text % props))}
                                        editable-comp)]
    slate-comp))


(def ui-editor (interop/react-factory Editor))

(defsc Todo [this {:todo/keys [id content child]}]
  {:query (fn [] [:todo/id :todo/content {:todo/child '...}])
   :ident :todo/id}
  (div (p {:style {:background-color "red"}} content)
       (ui-editor {:content content :id id :this this})
       (when (seq child)
         (dom/ul
          (dom/div
           ;; (js/console.log "this ->" content id)
           ;; (js/console.log child)
           (map (fn [p] (ui-todo p)) child))))))

(def ui-todo (comp/factory Todo {:keyfn :todo/id}))


(defsc Root [this {all-todo :all-todo :as props}]
  {:query [{:all-todo (comp/get-query Todo)}]}
  (div
   (div "todos: "
        (map ui-todo all-todo))))

stagmoose04:07:45

The mock data used:

(def todo-data
    [{:todo/id 1
      :todo/content "task"
      :todo/child [[:todo/id 3] [:todo/id 4]]}
     {:todo/id 2
      :todo/content "task 2"
      :todo/child []}
     {:todo/id 3
      :todo/content "task 3"
      :todo/child [[:todo/id 5]]}
     {:todo/id 4
      :todo/content "task 4"
      :todo/child []}
     {:todo/id 5
      :todo/content "task 5"
      :todo/child []}])

  (merge/merge-component! app ui/Todo todo-data
                          :replace [:all-todo])

tony.kay05:07:02

when you merge data you should NOT use normalized data. You should have {:todo/id 3} in place of [:todo/id 3] in your child ref. for example. If todo 3 isn’t staying in sync it is because you don’t have it really normalized, which this initialization might be the cause of.

tony.kay05:07:12

Yes, in a plain react component like you’ve build, you can send this through props. NOTE: There is a hooks ns in Fulcro that has wrappers for the hook functions.

stagmoose07:07:34

@U0CKQ19AQ Hi, Tony. Thanks for your help! I tried your method and it doesn't seem to work. Like the video below, the second "task 5 textarea" is not synced with the edited text.

Björn Ebbinghaus15:07:08

This is a slate.js thing, not fulcro. The value prop of the Slate component only sets the initial state.

Björn Ebbinghaus15:07:13

(def ui-slate (interop/react-factory Slate))
(def ui-editable (interop/react-factory Editable))


(defmutation change-context [{id :id data :new-data}]
  (action [{:keys [state]}]
    (swap! state assoc-in [:todo/id id :todo/content] data)))

(defsc Editor [_ {:keys [content]} {:keys [onChange]}]
  {:use-hooks? true}
  (let [[editor] (hooks/use-state #(-> (createEditor) (withReact)))
        init-val [{"type" "paragraph"
                   "children" [{"text" content}]}]]
    (set! (.-children editor) (clj->js init-val))
    (ui-slate
      {:editor editor
       :value init-val
       :onChange onChange}
      (ui-editable {}))))

(def ui-editor (comp/computed-factory Editor))

(defsc Todo [this {:todo/keys [id content child]}]
  {:query [:todo/id :todo/content {:todo/child '...}]
   :ident :todo/id
   :initial-state
   (fn [{:todo/keys [id content child]}]
     #:todo{:id id
            :content content
            :child (mapv #(comp/get-initial-state Todo %) child)})}
  (div (p {:style {:background-color "red"}} content)
    (ui-editor {:content content :id id}
      {:onChange (fn [data]
                   (let [children (.-children (first data))
                         all-data (apply str (map #(.-text %) children))]
                     (comp/transact! this [(change-context {:id id :new-data all-data})])))})
    (when (seq child)
      (dom/ul
        (dom/div
        ;; (js/console.log "this ->" content id)
        ;; (js/console.log child)
          (map (fn [p] (ui-todo p)) child))))))

(def ui-todo (comp/factory Todo {:keyfn :todo/id}))

Björn Ebbinghaus15:07:14

You could call (set! (.-children editor) (clj->js init-val)) in the render function of your editor.

stagmoose16:07:23

@U4VT24ZM3 Thanks for the help! I have to test this tomorrow. I also want to ask if I use defsc for the editor to make a fulcro component. Is it still possible to call Editor.end(editor, []) like https://stackoverflow.com/a/69605303/3737707 ? My concern is that the editor in the code above is a react component instance instead of a fulcro component instance. So do I need to take extra steps to make this work?