Skip to content

Commit

Permalink
[workaround] Use uncontrolled inputs
Browse files Browse the repository at this point in the history
Reagent's built-in workaround for controlled inputs doesn't work when
we use the new `react.dom.client` API to render to a shadow root.

This makes the cursor skip to the end whenever you try to insert a
character into a text input field.

This article describes the bug in some detail:

https://github.com/reagent-project/reagent/blob/master/doc/ControlledInputs.md

It also offers a workaround. Basically, for our :input
components, instead of the :value prop (a controlled input), we use
:default-value (uncontrolled). Then, we have to force the :input
component to remount when we dispatch an event which would update the
:default-value. That allows us to "control" the value via a
subscription, while keeping the healthy cursor behavior of an
"uncontrolled" input.

So far, the only input we actually need to control is the new app-db
editor.

This is not a good smell, and could be a pitfall for future
development. I think it's tolerable for now, though. I hope reagent
can fix this issue - it seems really complicated.

reagent-project/reagent#597
  • Loading branch information
kimo-k committed Nov 25, 2023
1 parent 7ff06f6 commit 0596b68
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 151 deletions.
28 changes: 14 additions & 14 deletions src/day8/re_frame_10x/components/inputs.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -95,17 +95,17 @@
:align :center
:children
[[material/search]
[:input {:type "text"
:value @val
:auto-focus true
:placeholder placeholder
:size (if (> 20 (count (str @val)))
25
(count (str @val)))
:on-change #(do (reset! val (-> % .-target .-value))
(on-change %))
:on-key-down #(case (.-which %)
13 (do
(save)
(reset! val ""))
nil)}]]])))
[:input {:type "text"
:default-value @val
:auto-focus true
:placeholder placeholder
:size (if (> 20 (count (str @val)))
25
(count (str @val)))
:on-change #(do (reset! val (-> % .-target .-value))
(on-change %))
:on-key-down #(case (.-which %)
13 (do
(save)
(reset! val ""))
nil)}]]])))
4 changes: 3 additions & 1 deletion src/day8/re_frame_10x/components/re_com.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@
internal-model (reagent/atom (if (nil? @external-model) "" @external-model))] ;; Create a new atom from the model to be used internally (avoid nil)
(fn
[& {:keys [model on-change on-submit status status-icon? placeholder width height rows change-on-blur? validation-regex disabled? class style attr]
{react-key :key} :args
:or {change-on-blur? true}}]
(let [latest-ext-model (deref-or-value model)
disabled? (deref-or-value disabled?)
Expand Down Expand Up @@ -357,7 +358,8 @@
:padding-right "12px"} ;; override for when icon exists
style)
:placeholder placeholder
:value @internal-model
:default-value @internal-model
:key react-key
:disabled disabled?
:on-change (fn [event]
(let [new-val (-> event .-target .-value)]
Expand Down
2 changes: 1 addition & 1 deletion src/day8/re_frame_10x/navigation/views.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@
:align :center
:children [[material/search]
[:input {:type "text"
:value @filter-str
:default-value @filter-str
:auto-focus true
:placeholder "filter event history"
:size (if (> 20 (count (str @filter-str)))
Expand Down
277 changes: 142 additions & 135 deletions src/day8/re_frame_10x/panels/app_db/views.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
[clojure.data]
[devtools.prefs]
[devtools.formatters.core]
[day8.re-frame-10x.inlined-deps.reagent.v1v2v0.reagent.core :as reagent]
[day8.re-frame-10x.inlined-deps.garden.v1v3v10.garden.units :refer [px]]
[day8.re-frame-10x.inlined-deps.spade.git-sha-93ef290.core :refer [defclass]]
[day8.re-frame-10x.inlined-deps.re-frame.v1v3v0.re-frame.core :as rf]
Expand Down Expand Up @@ -88,147 +89,153 @@
:children children]))

(defn pod-header [{:keys [id path path-str open? diff? sort? editing? edit-str]} data]
(let [ambiance @(rf/subscribe [::settings.subs/ambiance])
expand-all? @(rf/subscribe [::app-db.subs/expand-all? id])
log-any? @(rf/subscribe [::settings.subs/any-log-outputs?])]
[rc/v-box
:children
[[rc/h-box
:class (styles/section-header ambiance)
:align :center
:height styles/gs-31s
:children
[[pod-header-section
(let [editor-key (reagent/atom (gensym))
refresh-input! #(reset! editor-key (gensym))]
(fn [{:keys [id path path-str open? diff? sort? editing? edit-str]} data]
(let [ambiance @(rf/subscribe [::settings.subs/ambiance])
expand-all? @(rf/subscribe [::app-db.subs/expand-all? id])
log-any? @(rf/subscribe [::settings.subs/any-log-outputs?])]
[rc/v-box
:children
[[rc/box
:width "30px"
:height styles/gs-31s
:justify :center
:align :center
:class (styles/no-select)
:style {:cursor "pointer"}
:attr {:title (str (if open? "Close" "Open") " the pod bay doors, HAL")
:on-click (handler-fn (rf/dispatch [::app-db.events/set-path-visibility id (not open?)]))}
:child [buttons/expansion {:open? open?
:size styles/gs-31s}]]]]
[[rc/h-box
:class (styles/section-header ambiance)
:align :center
:height styles/gs-31s
:children
[[pod-header-section
:children
[[rc/box
:width "30px"
:height styles/gs-31s
:justify :center
:align :center
:class (styles/no-select)
:style {:cursor "pointer"}
:attr {:title (str (if open? "Close" "Open") " the pod bay doors, HAL")
:on-click (handler-fn (rf/dispatch [::app-db.events/set-path-visibility id (not open?)]))}
:child [buttons/expansion {:open? open?
:size styles/gs-31s}]]]]

[rc/h-box
:class (styles/path-header-style ambiance)
:size "auto"
:style {:height styles/gs-31s
:border-right pod-border-edge}
:align :center
:children
[[rc/input-text
:class (styles/path-text-input-style ambiance)
:attr {:on-blur #(rf/dispatch [::app-db.events/update-path-blur id])}
:width "100%"
:model path-str
:on-change #(rf/dispatch [::app-db.events/update-path id %]) ;;(fn [input-string] (rf/dispatch [:app-db/search-string input-string]))
:on-submit #() ;; #(rf/dispatch [::app-db.events/add-path %])
:change-on-blur? false
:placeholder "enter an app-db path like [:todos 1]"]]]
[rc/h-box
:class (styles/path-header-style ambiance)
:size "auto"
:style {:height styles/gs-31s
:border-right pod-border-edge}
:align :center
:children
[[rc/input-text
:class (styles/path-text-input-style ambiance)
:attr {:on-blur #(rf/dispatch [::app-db.events/update-path-blur id])}
:width "100%"
:model path-str
:on-change #(rf/dispatch [::app-db.events/update-path id %]) ;;(fn [input-string] (rf/dispatch [:app-db/search-string input-string]))
:on-submit #() ;; #(rf/dispatch [::app-db.events/add-path %])
:change-on-blur? false
:placeholder "enter an app-db path like [:todos 1]"]]]

(when (> (count path) 0)
[buttons/icon
{:icon [material/clear]
:title "Clear path in current inspector"
:on-click #(rf/dispatch [::app-db.events/update-path id ""])}])
(when (> (count path) 0)
[buttons/icon
{:icon [material/clear]
:title "Clear path in current inspector"
:on-click #(rf/dispatch [::app-db.events/update-path id ""])}])

(when (> (count path) 0)
[rc/gap-f :size styles/gs-7s])
(when (> (count path) 0)
[rc/gap-f :size styles/gs-7s])

(when (> (count path) 0)
[buttons/icon
{:icon [material/arrow-drop-up]
:title "Open parent path in current inspector"
:on-click #(rf/dispatch [::app-db.events/update-path id (str (if (> (count path) 1) (pop path) ""))])}])
(when (> (count path) 0)
[buttons/icon
{:icon [material/arrow-drop-up]
:title "Open parent path in current inspector"
:on-click #(rf/dispatch [::app-db.events/update-path id (str (if (> (count path) 1) (pop path) ""))])}])

[pod-header-section
:width "49px"
:justify :center
:align :center
:attr {:on-click (handler-fn (rf/dispatch [::app-db.events/set-diff-visibility id (not diff?)]))}
:children
[[rc/checkbox
:model diff?
:label ""
#_#_:style {:margin-left "6px"
:margin-top "1px"}
:on-change #(rf/dispatch [::app-db.events/set-diff-visibility id (not diff?)])]]]
[pod-header-section
:width "49px"
:justify :center
:align :center
:attr {:on-click (handler-fn (rf/dispatch [::app-db.events/set-sort-form? id (not sort?)]))}
:children
[[rc/checkbox
:model sort?
:label ""
:on-change #(rf/dispatch [::app-db.events/set-sort-form? id (not sort?)])]]]
[pod-header-section
:width "49px"
:justify :center
:align :center
:attr {:on-click (handler-fn (rf/dispatch [::app-db.events/set-expand-all? id (not expand-all?)]))}
:children
[[buttons/icon {:icon [(if expand-all? material/unfold-less material/unfold-more)]
:title (str (if expand-all? "Close" "Expand") " all nodes in this inspector")
:on-click #(rf/dispatch [::app-db.events/set-expand-all? id (not expand-all?)])}]]]
[pod-header-section
:width styles/gs-50s
:justify :center
:children
[[buttons/icon
{:icon [material/close]
:title "Remove this inspector"
:on-click #(rf/dispatch [::app-db.events/remove-path id])}]]]
[pod-header-section
:width styles/gs-50s
:justify :center
:children
[[buttons/icon {:icon [(if editing? material/unfold-less material/unfold-more)]
:title (str (if expand-all? "Close" "Expand") " the node editor")
:on-click (if editing?
#(rf/dispatch [::app-db.events/finish-edit id])
#(rf/dispatch [::app-db.events/start-edit id]))}]]]
[pod-header-section
:width styles/gs-31s
:justify :center
:last? true
:children
[[rc/box
:style {:margin "auto"}
:child
(when log-any?
[buttons/icon {:icon [material/print]
:title "Dump inspector data into DevTools"
:on-click #(rf/dispatch [:global/log data])}])]]]]]
(when editing?
[rc/h-box
:width "100%"
:children
[[rc/input-textarea
:change-on-blur? false
:model edit-str
:on-change #(rf/dispatch [::app-db.events/set-edit-str id %])]
[:input {:type "file"
:on-change #(rf/dispatch [::app-db.events/open-file
(first (.-files (.-target %)))
[::app-db.events/set-edit-str id]])}]
[buttons/icon
{:icon [material/arrow-drop-down]
:title "Save data to EDN"
:on-click #(rf/dispatch [::app-db.events/save-to-file (serialize-special-types data)])}]
[buttons/icon
{:icon [material/refresh]
:title "Copy data"
:on-click #(rf/dispatch [::app-db.events/set-edit-str id (serialize-special-types data)])}]
[buttons/icon
{:icon [material/check-circle-outline]
:title "Edit data"
:on-click #(re-frame.core/dispatch
[::app-db.events/edit path edit-str])}]]])]]))
[pod-header-section
:width "49px"
:justify :center
:align :center
:attr {:on-click (handler-fn (rf/dispatch [::app-db.events/set-diff-visibility id (not diff?)]))}
:children
[[rc/checkbox
:model diff?
:label ""
#_#_:style {:margin-left "6px"
:margin-top "1px"}
:on-change #(rf/dispatch [::app-db.events/set-diff-visibility id (not diff?)])]]]
[pod-header-section
:width "49px"
:justify :center
:align :center
:attr {:on-click (handler-fn (rf/dispatch [::app-db.events/set-sort-form? id (not sort?)]))}
:children
[[rc/checkbox
:model sort?
:label ""
:on-change #(rf/dispatch [::app-db.events/set-sort-form? id (not sort?)])]]]
[pod-header-section
:width "49px"
:justify :center
:align :center
:attr {:on-click (handler-fn (rf/dispatch [::app-db.events/set-expand-all? id (not expand-all?)]))}
:children
[[buttons/icon {:icon [(if expand-all? material/unfold-less material/unfold-more)]
:title (str (if expand-all? "Close" "Expand") " all nodes in this inspector")
:on-click #(rf/dispatch [::app-db.events/set-expand-all? id (not expand-all?)])}]]]
[pod-header-section
:width styles/gs-50s
:justify :center
:children
[[buttons/icon
{:icon [material/close]
:title "Remove this inspector"
:on-click #(rf/dispatch [::app-db.events/remove-path id])}]]]
[pod-header-section
:width styles/gs-50s
:justify :center
:children
[[buttons/icon {:icon [(if editing? material/unfold-less material/unfold-more)]
:title (str (if expand-all? "Close" "Expand") " the node editor")
:on-click (if editing?
#(rf/dispatch [::app-db.events/finish-edit id])
#(rf/dispatch [::app-db.events/start-edit id]))}]]]
[pod-header-section
:width styles/gs-31s
:justify :center
:last? true
:children
[[rc/box
:style {:margin "auto"}
:child
(when log-any?
[buttons/icon {:icon [material/print]
:title "Dump inspector data into DevTools"
:on-click #(rf/dispatch [:global/log data])}])]]]]]
(when editing?
[rc/h-box
:width "100%"
:children
[[rc/input-textarea
:change-on-blur? false
:model edit-str
:args {:key @editor-key}
:on-change #(rf/dispatch [::app-db.events/set-edit-str id %])]
[:input {:type "file"
:on-change #(do (refresh-input!)
(rf/dispatch [::app-db.events/open-file
(first (.-files (.-target %)))
[::app-db.events/set-edit-str id]]))}]
[buttons/icon
{:icon [material/arrow-drop-down]
:title "Save data to EDN"
:on-click #(rf/dispatch [::app-db.events/save-to-file (serialize-special-types data)])}]
[buttons/icon
{:icon [material/refresh]
:title "Copy data"
:on-click #(do (refresh-input!)
(rf/dispatch [::app-db.events/set-edit-str id (serialize-special-types data)]))}]
[buttons/icon
{:icon [material/check-circle-outline]
:title "Edit data"
:on-click #(re-frame.core/dispatch
[::app-db.events/edit path edit-str])}]]])]]))))

(def diff-url "https://github.com/day8/re-frame-10x/blob/master/docs/HyperlinkedInformation/Diffs.md")

Expand Down

0 comments on commit 0596b68

Please sign in to comment.