GithubHelp home page GithubHelp logo

Comments (77)

Deraen avatar Deraen commented on May 14, 2024 5

This is now documented in Reagent repo, with example project. Some additional logic is required for :select and :multiline TextFields, and autosize textarea can't be fixed currently.

https://github.com/reagent-project/reagent/blob/master/doc/examples/material-ui.md
https://github.com/reagent-project/reagent/blob/master/examples/material-ui/src/example/core.cljs

from cljs-react-material-ui.

superstructor avatar superstructor commented on May 14, 2024 4

I think the problem is that a Material UI textfield is a not a reagent.impl.template/input-component? so it bypasses all of the special handling in reagent-input, input-spec, input-handle-change, input-set-value (which does manual caret repositioning), input-render-setup etc. The condition is based on the name of the component, which in the case of Material UI is not input or textarea.

from cljs-react-material-ui.

radhikalism avatar radhikalism commented on May 14, 2024 4

Added support for overriding on-change as well, now... reagent-project/reagent@master...arbscht:synthetic-input

(def text-field (r/adapt-react-class (aget js/MaterialUI "TextField")
                  ;; Optional...
                  {:synthetic-input
                   ;; A valid map value for `synthetic-input` does two things:
                   ;; 1) It implicitly marks this component class as an input type so that interactive
                   ;;    updates will work without cursor jumping.
                   ;; 2) Reagent defers to its functions when it goes to set a value for the input component,
                   ;;    or signal a change, providing enough data for us to decide which DOM node is our input
                   ;;    node to target and continue processing with that (or any arbitrary behaviour...); and
                   ;;    to handle onChange events arbitrarily.
                   ;;
                   ;;    Note: We can also use an extra hook `on-write` to execute more custom behaviour
                   ;;    when Reagent actually writes a new value to the input node, from within `on-update`.
                   ;;
                   ;;    Note: Both functions receive a `next` argument which represents the next fn to
                   ;;    execute in Reagent's processing chain.
                   {:on-update (fn [next root-node rendered-value dom-value component]
                                 (let [input-node (.querySelector root-node "input")
                                       textarea-nodes (array-seq (.querySelectorAll root-node "textarea"))
                                       textarea-node (when (= 2 (count textarea-nodes))
                                                       ;; We are dealing with EnhancedTextarea (i.e.
                                                       ;; multi-line TextField)
                                                       ;; so our target node is the second <textarea>...
                                                       (second textarea-nodes))
                                       target-node (or input-node textarea-node)]
                                   (when target-node
                                     ;; Call Reagent's input node value setter fn (extracted from input-set-value)
                                     ;; which handles updating of a given <input> element,
                                     ;; now that we have targeted the correct <input> within our component...
                                     (next target-node rendered-value dom-value component
                                       ;; Also hook into the actual value-writing step,
                                       ;; since `input-node-set-value doesn't necessarily update values
                                       ;; (i.e. not dirty).
                                       {:on-write
                                        (fn [new-value]
                                          ;; `blank?` is effectively the same conditional as Material-UI uses
                                          ;; to update its `hasValue` and `isClean` properties, which are
                                          ;; required for correct rendering of hint text etc.
                                          (if (clojure.string/blank? new-value)
                                            (.setState component #js {:hasValue false :isClean false})
                                            (.setState component #js {:hasValue true :isClean false})))}))))
                    :on-change (fn [next event]
                                 ;; All we do here is continue processing but with the event target value
                                 ;; extracted into a second argument, to match Material-UI's existing API.
                                 (next event (-> event .-target .-value)))}}))

from cljs-react-material-ui.

radhikalism avatar radhikalism commented on May 14, 2024 3

Quick update: still working on a more correct solution. My earlier snippet does update <input> element values, but then input state does not always match the TextField component's state. In particular, Material-UI TextFields keep a state flag hasValue to determine if the value is empty/null/undefined (and show hint text etc). If it was previously :hasValue false, and we directly write to the inner <input> element, it will render overlapping hint text and value text. To solve this, we can call setState from within input-set-value to sync the TextField component, so this will be another component-specific function for cljs-react-material-ui to supply to Reagent.

from cljs-react-material-ui.

metametadata avatar metametadata commented on May 14, 2024 3

I stumbled upon the very similar problem while using cljsjs.react-bootstrap and managed to fix it by implementing the wrapper component which just needs to intercept on-change and value props in order to sync DOM with the virtual DOM at the right time. Theoretically, this approach should work for any text-input-like React components.

Unfortunately, I didn't have time to test it with cljs-react-material-ui. So I'm just throwing it in here in hope that maybe it can help someone solve the issue without patching Reagent:

https://gist.github.com/metametadata/3b4e9d5d767dfdfe85ad7f3773696a60

from cljs-react-material-ui.

vharmain avatar vharmain commented on May 14, 2024 3

In case someone is looking for a simple fix to caret problem with MaterialUI 1.0 and re-frame, providing following InputProps to TextField component seems to do the trick:

{:InputProps
  {:inputComponent
    (r/reactify-component
      (fn [props]
        [:input (dissoc props :inputRef)]))}}

For some reason replacing the default implementation with [:input ...] makes reagent recognize it as normal input and apply the "caret positioning fix".

Tested with [cljsjs/material-ui "1.0.0-beta.40-0"] and [reagent "0.8.1"].

Hope this helps someone.

from cljs-react-material-ui.

madvas avatar madvas commented on May 14, 2024 2

@superstructor have you actually tried that, because it doesn't really work that way for me. Could you provide example with atom? I assume you made a typo in :display-name and meant to write there either input or textarea.

Anyways here's my piece of code which doesn't work:

(defn text-field []
  (r/create-class
    {:display-name "input"
     :reagent-render (fn [props]
                       [rui/text-field props])}))

(defn simple-text-field [text]
  (let [text-state (r/atom text)]
    (fn []
      [text-field
       {:display-name "input"
        :id "my-textfield"
        :value @text-state
        :on-change (fn [e] (reset! text-state (.. e -target -value)))}])))

But it works when I override checking function in following way:

(set! reagent.impl.template/input-component?
      (fn [x]
        (or (= x "input")
            (= x "textarea")
            (= (reagent.interop/$ x :name) "TextField"))))

from cljs-react-material-ui.

madvas avatar madvas commented on May 14, 2024 2

Ok pushed changes, this should be resolved now

from cljs-react-material-ui.

madvas avatar madvas commented on May 14, 2024 1

Try it out like this

(defn simple-text-field [text]
  (let [text-state (r/atom text)]
    (fn []
      [rui/text-field
       {:id "example"
        :default-value @text-state
        :on-change (fn [e] (reset! text-state (.. e -target -value)))}])))

from cljs-react-material-ui.

radhikalism avatar radhikalism commented on May 14, 2024 1

@madvas I agree, this snippet is just a rough proof of the bug.

I don't think cljs-react-material-ui should patch Reagent. But I also don't think Reagent should be specifically aware of Material quirks for cljs-react-material-ui.

My current view is that Reagent should allow adapt-react-class to take some extra parameters that tag the class with a flag or predicate that would let the component pass the input-component? check, and also associate an optional fn that "locates" the node for a component in input-set-value (default being to just find-dom-node (> 0.6.0) or get :cljsInputElement (0.6.0)).

Then cljs-react-material-ui can modify its text-field definition to set those optional parameters when it invokes r/adapt-react-class.

What do you think? I'm open to other approaches.

Also, how would you like to co-ordinate with Reagent releases? Target 0.6.0 and re-visit with the next release, or just target the next release?

from cljs-react-material-ui.

skuteli avatar skuteli commented on May 14, 2024 1

@metametadata thanks a lot, this idea to use normal atom instead of r/atom was what I couldn't figure out myself.
Do you guys know why they postponed the PR from @arbscht to 0.8? This is important as more and more people use material-ui with reagent, either via this project or not.

from cljs-react-material-ui.

madvas avatar madvas commented on May 14, 2024 1

As far as I see it's not. It's still pending PR at reagent: reagent-project/reagent#282 . Don't know what's Rum's status on this

from cljs-react-material-ui.

p-himik avatar p-himik commented on May 14, 2024 1

I've just stumbled upon this issue after trying to switch from re-com to Material UI.
One of the proposed fixes I've seen on Clojurians Slack is to call reagent/flush in the :onChange handler. And it appears to be working.
It's a big discussion and a bit hard to follow given the number of the links and the amount of knowledge of all of the internals. Could you please help me understand why reagent/flush has never been even mentioned? Is it somehow a fundamentally flawed solution?

An update: the solution with reagent/flush works only in tandem with storing an internal model, like it's done here: https://github.com/Day8/re-com/blob/master/src/re_com/misc.cljs#L87-L89

from cljs-react-material-ui.

euccastro avatar euccastro commented on May 14, 2024

:default-value fixes it! I have no idea why.

I'll close this since I imagine this is an upstream issue and the workaround is simple and painless. But unless this is already documented in a prominent place I've missed, I think this deserves a big warning in the README. The workaround isn't obvious at all to me, and without that the issue is really crippling.

Thank you very much!

from cljs-react-material-ui.

edannenberg avatar edannenberg commented on May 14, 2024

This is really just a work around, and one that will sooner or later lead to problems as the input becomes uncontrolled. For any serious work controlled inputs are a must IMHO. I remember Reagent had the same issue before this fix. By the looks of it this should also work with Material-UI inputs as the fix just checks for the type property which is correctly set. Did you already investigate why this is not the case?

How do you solve the simple use case of form fields rendering data that arrives over the network, i.e. shortly after the view is already rendered for the first time? Seems like a lot of hoops with uncontrolled inputs.

from cljs-react-material-ui.

madvas avatar madvas commented on May 14, 2024

I haven't really investigated what's situation like in latest reagent with this. But you're right, it's a workaround. I remember having some issues with this back in a day, probably in a scenario you described (when wanted to pre-fill form with delayed data from network). Honestly, right now, I pretty much have no clue what this library could do in order to solve this in any reasonable way (no crazy workarounds).

from cljs-react-material-ui.

edannenberg avatar edannenberg commented on May 14, 2024

From what I gathered a clean fix is not possible due to the async rendering used by Clojure React wrappers like Om or Reagent. Reagent (after much discussion) resorted to manual caret repositioning. No idea though if this is doable from your library. Maybe some thin wrapper around input components that takes care of this?

Also agreeing with @euccastro that this should be mentioned in the docs.

from cljs-react-material-ui.

madvas avatar madvas commented on May 14, 2024

Okay, thanks for a info. I updated readme. I'll try to look at this in more depth when I find more time.

from cljs-react-material-ui.

superstructor avatar superstructor commented on May 14, 2024

This (extremely ugly) workaround works:

(defn textfield []
  (reagent/create-class
    {:display-name   "textfield"
     :reagent-render (fn [props]
                       [rui/text-field props])}))

from cljs-react-material-ui.

madvas avatar madvas commented on May 14, 2024

Oh, really? Thank you a lot for this!!

from cljs-react-material-ui.

superstructor avatar superstructor commented on May 14, 2024

Yes sorry meant to type textarea. Overriding the checking function is probably a better solution ?

from cljs-react-material-ui.

edannenberg avatar edannenberg commented on May 14, 2024

Fix works, but seems to have some side effects. After switching to 0.2.23 text-fields don't update anymore unless I'm actually typing in the field:

[:input {:value (:foo @state)}]
[rui/text-field {:value (:foo @state)}]

With 0.2.22 both fields update if state changes by external means after mounting, with 0.2.23 this is only true for [:input].

from cljs-react-material-ui.

superstructor avatar superstructor commented on May 14, 2024

This fix usually works, except when (reagent.interop/$ x :name) is, for example, t instead of TextField with :simple optimisations. Not 100% verified, will comment further when I have verified.

from cljs-react-material-ui.

madvas avatar madvas commented on May 14, 2024

Okay guys, you were both right.
@superstructor I resolved issue with optimisations in recently pushed 0.2.24
Hoveever, @edannenberg your issue is far more serious. Yes value can't be changed once set. So basically we're back where we were except we can set :value now instead of :default-value.

I tried to hack around reagent a bit, but couldn't get around this. So I opened issue at reagent here explaining issue and asking for a help.

from cljs-react-material-ui.

superstructor avatar superstructor commented on May 14, 2024

Thanks for the fix with optimisations! Looks like the latest release on clojars is still 0.2.23 ?

from cljs-react-material-ui.

radhikalism avatar radhikalism commented on May 14, 2024

So there are 3 aspects to this bug.

  1. The text-field component isn't detected by Reagent as an input component. (The input-component? patch solves this.)
  2. When there are updates, Reagent targets the wrong DOM node to set :value.
  3. Both of these are caused by the Material text-field's DOM structure being wrapped in a div, whereas Reagent expects find-dom-node to return an input element.

If we were to hack Reagent to workaround 1 and 2 directly, it might look like this:

diff --git a/src/reagent/impl/template.cljs b/src/reagent/impl/template.cljs
index 17e3a9f..1040acc 100644
--- a/src/reagent/impl/template.cljs
+++ b/src/reagent/impl/template.cljs
@@ -116,7 +116,10 @@
     ($! this :cljsInputDirty false)
     (let [rendered-value ($ this :cljsRenderedValue)
           dom-value ($ this :cljsDOMValue)
-          node (find-dom-node this)]
+          node (find-dom-node this)
+          node (if-not (= "INPUT" ($ node :tagName)) ;; Is this a wrapped component?
+                 (.querySelector node "input") ;; Locate a child input inside the wrapper
+                 node)]
       (when (not= rendered-value dom-value)
         (if-not (and (identical? node ($ js/document :activeElement))
                      (has-selection-api? ($ node :type))
@@ -195,9 +198,10 @@
   ($! this :cljsInputLive nil))

 (defn ^boolean input-component? [x]
-  (case x
-    ("input" "textarea") true
-    false))
+  (boolean (or (= x "input")
+               (= x "textarea")
+               (when-let [prop-types ($ x :propTypes)] ;; Is this a wrapped Material input?
+                 (aget prop-types "inputStyle")))))

 (def reagent-input-class nil)

However, this exact patch is probably not the right way to structure a generic library.

What if Reagent provided opportunities through adapt-react-class for users to override how input components are detected and located?

from cljs-react-material-ui.

madvas avatar madvas commented on May 14, 2024

@arbscht thank you very much for your reply. Your patch is against master version of reagent, how would it look like against version 0.6.0? couldn't figure it out somehow

from cljs-react-material-ui.

radhikalism avatar radhikalism commented on May 14, 2024

@madvas Reagent master has b65afde4 which uses find-dom-node. Not sure if targeting 0.6.0 without find-dom-node will be buggy. Nevertheless, something like this might work:

diff --git a/src/reagent/impl/template.cljs b/src/reagent/impl/template.cljs
index fc6b0c2..a9c13b7 100644
--- a/src/reagent/impl/template.cljs
+++ b/src/reagent/impl/template.cljs
@@ -108,8 +108,16 @@
   [input-type]
   (contains? these-inputs-have-selection-api input-type))

+(defn get-input-node [this]
+  (let [outer-node ($ this :cljsInputElement)
+        inner-node ($ outer-node :input)]
+    (if (and inner-node
+             (= "INPUT" ($ inner-node :tagName)))
+      inner-node
+      outer-node)))
+
 (defn input-set-value [this]
-  (when-some [node ($ this :cljsInputElement)]
+  (when-some [node (get-input-node this)]
     ($! this :cljsInputDirty false)
     (let [rendered-value ($ this :cljsRenderedValue)
           dom-value ($ this :cljsDOMValue)]
@@ -186,9 +194,10 @@
         ($! :ref #($! this :cljsInputElement %1))))))

 (defn ^boolean input-component? [x]
-  (case x
-    ("input" "textarea") true
-    false))
+  (boolean (or (= x "input")
+               (= x "textarea")
+               (when-let [prop-types ($ x :propTypes)] ;; Is this a wrapped Material input?
+                 (aget prop-types "inputStyle")))))

 (def reagent-input-class nil)

I'm not actually sure where :input comes from, but it's there in 0.6.0 and apparently not in master.

from cljs-react-material-ui.

madvas avatar madvas commented on May 14, 2024

Thanks! But it seems like it's not complete solution. I've tried to patch reagent like this:

(set! reagent.impl.template/input-component?
      (fn [x]
        (boolean (or (= x "input")
                     (= x "textarea")
                     (when-let [prop-types ($ x :propTypes)] ;; Is this a wrapped Material input?
                       (aget prop-types "inputStyle"))))))

(defn get-input-node [this]
  (let [outer-node ($ this :cljsInputElement)
        inner-node ($ outer-node :input)]
    (if (and inner-node
             (= "INPUT" ($ inner-node :tagName)))
      inner-node
      outer-node)))

(set! reagent.impl.template/input-set-value
      (fn [this]
        (when-some [node (get-input-node this)]
          ($! this :cljsInputDirty false)
          (let [rendered-value ($ this :cljsRenderedValue)
                dom-value ($ this :cljsDOMValue)]
            (when (not= rendered-value dom-value)
              (if-not (and (identical? node ($ js/document :activeElement))
                           (reagent.impl.template/has-selection-api? ($ node :type))
                           (string? rendered-value)
                           (string? dom-value))
                ;; just set the value, no need to worry about a cursor
                (do
                  ($! this :cljsDOMValue rendered-value)
                  ($! node :value rendered-value))

                ;; Setting "value" (below) moves the cursor position to the
                ;; end which gives the user a jarring experience.
                ;;
                ;; But repositioning the cursor within the text, turns out to
                ;; be quite a challenge because changes in the text can be
                ;; triggered by various events like:
                ;; - a validation function rejecting a user inputted char
                ;; - the user enters a lower case char, but is transformed to
                ;;   upper.
                ;; - the user selects multiple chars and deletes text
                ;; - the user pastes in multiple chars, and some of them are
                ;;   rejected by a validator.
                ;; - the user selects multiple chars and then types in a
                ;;   single new char to repalce them all.
                ;; Coming up with a sane cursor repositioning strategy hasn't
                ;; been easy ALTHOUGH in the end, it kinda fell out nicely,
                ;; and it appears to sanely handle all the cases we could
                ;; think of.
                ;; So this is just a warning. The code below is simple
                ;; enough, but if you are tempted to change it, be aware of
                ;; all the scenarios you have handle.
                (let [node-value ($ node :value)]
                  (if (not= node-value dom-value)
                    ;; IE has not notified us of the change yet, so check again later
                    (batch/do-after-render #(reagent.impl.template/input-set-value this))
                    (let [existing-offset-from-end (- (count node-value)
                                                      ($ node :selectionStart))
                          new-cursor-offset (- (count rendered-value)
                                               existing-offset-from-end)]
                      ($! this :cljsDOMValue rendered-value)
                      ($! node :value rendered-value)
                      ($! node :selectionStart new-cursor-offset)
                      ($! node :selectionEnd new-cursor-offset))))))))))

And when you try 2 inputs like this:

(defn simple-text-field [text]
  (let [text-state (r/atom text)]
    (js/setInterval (fn []
                      (reset! text-state (str (rand-int 99999))))
                    1000)
    (fn []
      [:div
       [rui/text-field
        {:id "my-textfield"
         :value @text-state
         :on-change (fn [e] (reset! text-state (.. e -target -value)))}]
       [:input
        {:id "my-input"
         :value @text-state
         :on-change (fn [e] (reset! text-state (.. e -target -value)))}]])))

you'll see :input value is being correctly changed in intervals, but rui/text-field value doesn't change

from cljs-react-material-ui.

radhikalism avatar radhikalism commented on May 14, 2024

Be careful with set! — I think it doesn't update all references to the function from callers in closures that were already defined (as the old version of the bound fn appears to be captured/cached/inlined?). To solve, either:

  • set! all Vars in the chain that depends on input-set-value (recursively), just so that the correct versions are called all the way in reagent.impl.template.
  • Or literally patch template.cljs in a clone of Reagent and depend on that build.

Otherwise it will misbehave as you see.

(Indeed there are other incomplete aspects to this patch. It incorrectly detects radio/check inputs in input-component?. And get-input-node isn't sufficiently nil-safe. But this should be enough to get your test project to work.)

from cljs-react-material-ui.

radhikalism avatar radhikalism commented on May 14, 2024

@madvas Try this, paste and invoke set-overrides!:

;; A better but idiosyncratic way for reagent to locate the real
;; input element of a component. It may be that the component's
;; top-level (outer) element is an <input> already. Or it may be that
;; the outer element is a wrapper, and the real <input> is somewhere
;; within it (as is the case with Material TextFields).
;; Somehow the `:input` property seems to be pre-populated (in version
;; 0.6.0 of Reagent) with a reference to the real <input>, so we use
;; that if available.
;; At the time of writing, future versions of Reagent will likely
;; change this behavior, and a totally different patch will be
;; required for identifying the real <input> element.
(defn get-input-node [this]
  (let [outer-node ($ this :cljsInputElement)
        inner-node (when (and outer-node (.hasOwnProperty outer-node "input"))
                     ($ outer-node :input))]
    (if (and inner-node
             (= "INPUT" ($ inner-node :tagName)))
      inner-node
      outer-node)))

;; This is the same as reagent.impl.template/input-set-value except
;; that the `node` binding uses our `get-input-node` function. Even
;; the original comments are reproduced below.
(defn input-set-value
  [this]
  (when-some [node (get-input-node this)]
    ($! this :cljsInputDirty false)
    (let [rendered-value ($ this :cljsRenderedValue)
          dom-value      ($ this :cljsDOMValue)]
      (when (not= rendered-value dom-value)
        (if-not (and (identical? node ($ js/document :activeElement))
                  (reagent.impl.template/has-selection-api? ($ node :type))
                  (string? rendered-value)
                  (string? dom-value))
          ;; just set the value, no need to worry about a cursor
          (do
            ($! this :cljsDOMValue rendered-value)
            ($! node :value rendered-value))

          ;; Setting "value" (below) moves the cursor position to the
          ;; end which gives the user a jarring experience.
          ;;
          ;; But repositioning the cursor within the text, turns out to
          ;; be quite a challenge because changes in the text can be
          ;; triggered by various events like:
          ;; - a validation function rejecting a user inputted char
          ;; - the user enters a lower case char, but is transformed to
          ;;   upper.
          ;; - the user selects multiple chars and deletes text
          ;; - the user pastes in multiple chars, and some of them are
          ;;   rejected by a validator.
          ;; - the user selects multiple chars and then types in a
          ;;   single new char to repalce them all.
          ;; Coming up with a sane cursor repositioning strategy hasn't
          ;; been easy ALTHOUGH in the end, it kinda fell out nicely,
          ;; and it appears to sanely handle all the cases we could
          ;; think of.
          ;; So this is just a warning. The code below is simple
          ;; enough, but if you are tempted to change it, be aware of
          ;; all the scenarios you have handle.
          (let [node-value ($ node :value)]
            (if (not= node-value dom-value)
              ;; IE has not notified us of the change yet, so check again later
              (reagent.impl.batching/do-after-render #(input-set-value this))
              (let [existing-offset-from-end (- (count node-value)
                                               ($ node :selectionStart))
                    new-cursor-offset        (- (count rendered-value)
                                               existing-offset-from-end)]
                ($! this :cljsDOMValue rendered-value)
                ($! node :value rendered-value)
                ($! node :selectionStart new-cursor-offset)
                ($! node :selectionEnd new-cursor-offset)))))))))


;; This is the same as `reagent.impl.template/input-handle-change`
;; except that the reference to `input-set-value` points to our fn.
(defn input-handle-change
  [this on-change e]
  ($! this :cljsDOMValue (-> e .-target .-value))
  ;; Make sure the input is re-rendered, in case on-change
  ;; wants to keep the value unchanged
  (when-not ($ this :cljsInputDirty)
    ($! this :cljsInputDirty true)
    (reagent.impl.batching/do-after-render #(input-set-value this)))
  (on-change e))


;; This is the same as `reagent.impl.template/input-render-setup`
;; except that the reference to `input-handle-change` points to our fn.
(defn input-render-setup
  [this jsprops]
  ;; Don't rely on React for updating "controlled inputs", since it
  ;; doesn't play well with async rendering (misses keystrokes).
  (when (and (some? jsprops)
          (.hasOwnProperty jsprops "onChange")
          (.hasOwnProperty jsprops "value"))
    (let [v         ($ jsprops :value)
          value     (if (nil? v) "" v)
          on-change ($ jsprops :onChange)]
      (when (nil? ($ this :cljsInputElement))
        ;; set initial value
        ($! this :cljsDOMValue value))
      ($! this :cljsRenderedValue value)
      (js-delete jsprops "value")
      (doto jsprops
        ($! :defaultValue value)
        ($! :onChange #(input-handle-change this on-change %))
        ($! :ref #($! this :cljsInputElement %1))))))


;; This version of `reagent.impl.template/input-component?` is
;; effectively the same as before except that it also detects Material's
;; wrapped components as input components. It does this by looking for
;; a property called "inputStyle" as an indicator. (Perhaps not a
;; robust test...)
;;
;; By identifying input components more liberally, Material textfields
;; are permitted into the code path that manages caret positioning
;; and selection state awareness, in reaction to updates. This alone
;; is necessary but insufficient.
(defn input-component?
  [x]
  (or (= x "input")
    (= x "textarea")
    (when-let [prop-types ($ x :propTypes)]
      ;; Material inputs all have "inputStyle" prop
      (and (aget prop-types "inputStyle")
        ;; But we only want text-fields, so let's exclude radio/check inputs
        (not (aget prop-types "checked"))
        ;; TODO: ... and other non-text-field inputs?
        ))))


;; This is the same as `reagent.impl.template/input-spec` except that
;; the reference to `input-render-setup` points to our fn.
(def input-spec
  {:display-name "ReagentInput"
   :component-did-update input-set-value
   :reagent-render
   (fn [argv comp jsprops first-child]
     (let [this reagent.impl.component/*current-component*]
       (input-render-setup this jsprops)
       (reagent.impl.template/make-element argv comp jsprops first-child)))})


;; This is the same as `reagent.impl.template/reagent-input` except
;; that the reference to `input-spec` points to our definition.
(defn reagent-input []
  (when (nil? reagent.impl.template/reagent-input-class)
    (set! reagent.impl.template/reagent-input-class (reagent.impl.component/create-class input-spec)))
  reagent.impl.template/reagent-input-class)


;; Now we override the existing functions in `reagent.impl.template`
;; with our own definitions.
(defn set-overrides!
  []
  (set! reagent.impl.template/input-component? input-component?)
  (set! reagent.impl.template/input-handle-change input-handle-change)
  (set! reagent.impl.template/input-set-value input-set-value)
  (set! reagent.impl.template/input-render-setup input-render-setup)
  (set! reagent.impl.template/input-spec input-spec)
  (set! reagent.impl.template/reagent-input reagent-input))

from cljs-react-material-ui.

madvas avatar madvas commented on May 14, 2024

This works great, thanks! But I'm not sure if we can include it like this in library. If people will use different versions of reagent, it will easily break.

from cljs-react-material-ui.

madvas avatar madvas commented on May 14, 2024

Great, I think extra params to adapt-react-class is fairly elegant approach. We'll see what reagent guys will think. If you feel like doing pull request to reagent, go for it, since you seem to understand problem very well. If not, I can do it. I think just targeting next release is good enough. We'll leave it as it is for 0.6.0.

from cljs-react-material-ui.

euccastro avatar euccastro commented on May 14, 2024

I tried @arbscht's set-overrides! snippet with reagent 0.6.0, and the caret works fine when I type in text. Alas, I can't set the value of my TextField programmatically (ie., in a reaction). Is this expected?

from cljs-react-material-ui.

madvas avatar madvas commented on May 14, 2024

@arbscht What's status on this, have you made any progress?

from cljs-react-material-ui.

radhikalism avatar radhikalism commented on May 14, 2024

Nope, only got as far as a very specific monkey-patch that suits the application I'm working on, which would probably break Reagent for non-crmui users if factored out.

Haven't made a working proof-of-concept of my proposed enhanced adapt-react-class yet. It's a little tricky to properly expose the API I sketched out and pass the params all the way down the chain in reagent.impl.template. Either involves mutable js values or more significant code changes (and regression risk) than I'd like...

I can't think of a better solution, just needs some care and time to test Reagent thoroughly, which is beyond me for the moment! Might have a go at it over the holidays.

from cljs-react-material-ui.

radhikalism avatar radhikalism commented on May 14, 2024

FYI something else I was looking at last time: maybe we can reshape the component being exposed to Reagent so that its <input> is findable by the existing code that tries to locate it ((find-dom-node this)). I don't know how feasible this is but it would have the benefit of leaving Reagent unchanged and containing any custom glue hacks to crmui only, which may be appropriate.

from cljs-react-material-ui.

madvas avatar madvas commented on May 14, 2024

Yea, I recently needed to solve this, so I came with such workaround:

(defn text-field* [{:keys [:default-value]}]
  (let [prev-value (r/atom default-value)]
    (fn [{:keys [:rows :on-change :default-value] :as props}]
      (if (= default-value @prev-value)
        [ui/text-field
         (merge
           props
           {:default-value default-value
            :on-change (fn [e val]
                         (reset! prev-value val)
                         (when on-change
                           (on-change e val)))})]
        (do
          (reset! prev-value default-value)
          [:div {:style {:min-height (+ 72 (* (dec (or rows 1)) 24))}}])))))

In which, by storing previous value in local state I can detect if it was changed programatically or by user. If it was programatically it renders empty div of the same height, and then immediately it renders text-field back, so it makes 1 little flickering when value is changed programatically. Not ideal, but better than nothing when you desperately need something.

from cljs-react-material-ui.

radhikalism avatar radhikalism commented on May 14, 2024

@madvas See if this patched Reagent works better for you: reagent-project/reagent@master...arbscht:synthetic-input

You'll have to configure TextField in crmui itself like this:

(def text-field (r/adapt-react-class (aget js/MaterialUI "TextField")
                  {:synthetic-input? true
                   :input-selector "input"})) ;; default selector, use querySelector syntax

It has a crude implementation of an API that allows locating the correct <input> element in a synthetic input component (one whose root node is not necessarily <input>). Still doesn't support Material's quirk with hasValue etc that I mentioned in #17 (comment) but that can be added next. I'm reasonably satisfied it won't impact performance of any existing code paths so far.

Let me refine this patch a little before posting for discussion at reagent-project. The pitch is that Reagent should support synthetic inputs for any use case involving a React component that handles input without a literal <input> tag at its root — not just crmui.

from cljs-react-material-ui.

radhikalism avatar radhikalism commented on May 14, 2024

In which, by storing previous value in local state I can detect if it was changed programatically or by user. If it was programatically it renders empty div of the same height, and then immediately it renders text-field back, so it makes 1 little flickering when value is changed programatically. Not ideal, but better than nothing when you desperately need something.

Sidenote: one of my earlier workarounds was like this too. As a slight improvement, instead of a zero-height div, all you need is to uniquely refresh :key for the text-field on non-interactive programmatic updates. Makes the hiccup cleaner. But of course, it has drawbacks like losing cursor position and focus on such updates and may even reanimate (if I remember correctly).

from cljs-react-material-ui.

radhikalism avatar radhikalism commented on May 14, 2024

Update: now fleshed out the proposed Reagent synthetic input API so that it takes a couple of optional functions to influence how it behaves when setting values (programmatic updates). Also configuring a component class to have such a function implicitly makes it detectable as an input component (interactive updates). This helps with maintaining Material-UI component state as well as handling selections/cursor correctly.

reagent-project/reagent@master...arbscht:synthetic-input

Use from crmui like so:

(def text-field (r/adapt-react-class (aget js/MaterialUI "TextField")
                  ;; Optional...
                  {:synthetic-input-setter
                   ;; A fn value for `synthetic-input-setter` does two things:
                   ;; 1) It implicitly marks this component class as an input type so that interactive
                   ;;    updates will work without cursor jumping.
                   ;; 2) Reagent defers to this fn when it goes to set a value for the input component,
                   ;;    providing enough data for us to decide which DOM node is our input node and
                   ;;    continue processing with that (or any arbitrary behaviour...). We can also
                   ;;    supply an extra hook `on-write` to execute more custom behaviour when Reagent
                   ;;    actually writes a new value to the input node.
                   (fn [input-node-set-value root-node rendered-value dom-value component]
                     (when-let [input-node (.querySelector root-node "input")] ;; Choose our specific inner input node
                       ;; Call Reagent's input node value setter fn (extracted from input-set-value)
                       ;; which handles updating of a given <input> element, now that we have targeted
                       ;; the correct <input> within our component...
                       (input-node-set-value input-node rendered-value dom-value component
                         ;; Also hook into the actual value-writing step, since `input-node-set-value` doesn't
                         ;; necessarily update values (i.e. not dirty).
                         {:on-write
                          (fn [new-value]
                            ;; `blank?` is effectively the same conditional as Material-UI uses to update its
                            ;; `hasValue` and `isClean` properties, which are required for correct
                            ;; rendering of hint text etc.
                            (if (clojure.string/blank? new-value)
                              (.setState component #js {:hasValue false :isClean false})
                              (.setState component #js {:hasValue true :isClean false})))})))}))

Thoughts @madvas?

from cljs-react-material-ui.

madvas avatar madvas commented on May 14, 2024

Simply amazing! Elegantly solved all pain points on my project. Can't be more thankful! 👏👏👏

from cljs-react-material-ui.

madvas avatar madvas commented on May 14, 2024

There's one tiny detail, new value is not passed to :on-change as a second argument, must be gotten from first argument as event.target.value

from cljs-react-material-ui.

madvas avatar madvas commented on May 14, 2024

Also I've noticed it doesn't work for textarea. I thought I can solve it by adding

(or (.querySelector root-node "input")
    (.querySelector root-node "textarea"))

but it didn't help

from cljs-react-material-ui.

radhikalism avatar radhikalism commented on May 14, 2024

Also I've noticed it doesn't work for textarea.

Yeah, multi-line/textarea is implemented quite differently. You can select the "right" node with (->> "textarea" (.querySelectorAll root-node) (nth 2)) or something like that. That will fix cursor jumping but won't help with programmatic updates, since there seems to be a shadow textarea node to take care of. I haven't explored this yet but it should be possible with a more sophisticated setter function.

There's one tiny detail, new value is not passed to :on-change as a second argument, must be gotten from first argument as event.target.value

Just checking, this comment in reference to the snippet you pasted, not a regression, right?

from cljs-react-material-ui.

radhikalism avatar radhikalism commented on May 14, 2024

I think this works with :multi-line true TextFields:

(def text-field (r/adapt-react-class (aget js/MaterialUI "TextField")
                  ;; Optional...
                  {:synthetic-input-setter
                   ;; A fn value for `synthetic-input-setter` does two things:
                   ;; 1) It implicitly marks this component class as an input type so that interactive
                   ;;    updates will work without cursor jumping.
                   ;; 2) Reagent defers to this fn when it goes to set a value for the input component,
                   ;;    providing enough data for us to decide which DOM node is our input node and
                   ;;    continue processing with that (or any arbitrary behaviour...). We can also
                   ;;    supply an extra hook `on-write` to execute more custom behaviour when Reagent
                   ;;    actually writes a new value to the input node.
                   (fn [input-node-set-value root-node rendered-value dom-value component]
                     (let [input-node (.querySelector root-node "input")
                           textarea-nodes (array-seq (.querySelectorAll root-node "textarea"))
                           textarea-node (when (= 2 (count textarea-nodes))
                                           ;; We are dealing with EnhancedTextarea (i.e. multi-line TextField)
                                           ;; so our target node is the second <textarea>...
                                           (second textarea-nodes))
                           target-node (or input-node textarea-node)]
                       (when target-node
                         ;; Call Reagent's input node value setter fn (extracted from input-set-value)
                         ;; which handles updating of a given <input> element, now that we have targeted
                         ;; the correct <input> within our component...
                         (input-node-set-value target-node rendered-value dom-value component
                           ;; Also hook into the actual value-writing step, since `input-node-set-value`
                           ;; doesn't necessarily update values (i.e. not dirty).
                           {:on-write
                            (fn [new-value]
                              ;; `blank?` is effectively the same conditional as Material-UI uses to update
                              ;; its `hasValue` and `isClean` properties, which are required for correct
                              ;; rendering of hint text etc.
                              (if (clojure.string/blank? new-value)
                                (.setState component #js {:hasValue false :isClean false})
                                (.setState component #js {:hasValue true :isClean false})))}))))}))

from cljs-react-material-ui.

madvas avatar madvas commented on May 14, 2024

heh, I was just typing comment that this works for me

(or (.querySelector root-node "input")
    (.item (.querySelectorAll root-node "textarea") 1))

from cljs-react-material-ui.

madvas avatar madvas commented on May 14, 2024

Just checking, this comment in reference to the snippet you pasted, not a regression, right?

I don't mean my snippet. I mean when I applied your patch :on-change stopped getting second argument

from cljs-react-material-ui.

radhikalism avatar radhikalism commented on May 14, 2024

I don't mean my snippet. I mean when I applied your patch :on-change stopped getting second argument

Interesting, looking at Reagent's code I can't see where a second argument comes from. I guess it's a pre-existing difference with Material-UI, which seems to be revealed now that Reagent is controlling the component state rather than React?

Reagent:

(defn input-handle-change [this on-change e]
  ($! this :cljsDOMValue (-> e .-target .-value))
  ;; Make sure the input is re-rendered, in case on-change
  ;; wants to keep the value unchanged
  (when-not ($ this :cljsInputDirty)
    ($! this :cljsInputDirty true)
    (batch/do-after-render #(input-component-set-value this)))
  (on-change e))

Material-UI:

  handleInputChange = (event) => {
    this.setState({hasValue: isValid(event.target.value)});
    if (this.props.onChange) {
      this.props.onChange(event, event.target.value);
    }
  };

(Reagent overwrites onChange with its own handler...)

from cljs-react-material-ui.

madvas avatar madvas commented on May 14, 2024

lovely, thanks many times!

from cljs-react-material-ui.

alvatar avatar alvatar commented on May 14, 2024

@madvas is this fixed currently in the latest release? I'm using it with Rum and I keep experiencing the same issue.

from cljs-react-material-ui.

alvatar avatar alvatar commented on May 14, 2024

Ping @madvas :)

from cljs-react-material-ui.

alvatar avatar alvatar commented on May 14, 2024

Thanks @madvas. I thought this could be solved here.

from cljs-react-material-ui.

Deraen avatar Deraen commented on May 14, 2024

The fix in Reagent 0.8-alpha can be used like this: https://github.com/Deraen/problems/blob/reagent-material-ui-text-field/src/hello_world/core.cljs#L11-L15 (not sure why those options have to be provided, as these "defaults" probably work usually.)

from cljs-react-material-ui.

madvas avatar madvas commented on May 14, 2024

Great, thanks for this example @Deraen!

from cljs-react-material-ui.

Deraen avatar Deraen commented on May 14, 2024

After further testing, I noticed the code didn't update the input value correctly. This is now fixed, by selecting the real DOM input node from the TextField Div node. But this solution still has some problems, for cases where TextField has some logic based on if the value is set (hintText hidden, floatingLabel). Seems like hintText and floatingLabel have to be also updated manually on :on-update function, because the :value property is not sent to TextField component.

https://github.com/Deraen/problems/blob/reagent-material-ui-text-field/src/hello_world/core.cljs#L11-L25

from cljs-react-material-ui.

radhikalism avatar radhikalism commented on May 14, 2024

@Deraen In my earlier draft I dealt with the value-based logic differently. See: #17 (comment) for the full snippet. I can't test Reagent 0.8-alpha right now but it looks like it should still work.

My approach was to update hasValue and isClean states on the component, which influences how hint text and labels behave. This is executed at the correct moment via the :on-write hook on next which is invoked inside on-update:

(next ... {:on-write (fn [new-value]      
                       ;; `blank?` is effectively the same conditional as Material-UI uses
                       ;; to update its `hasValue` and `isClean` properties, which are
                       ;; required for correct rendering of hint text etc.
                       (if (clojure.string/blank? new-value)
                         (.setState component #js {:hasValue false :isClean false})
                         (.setState component #js {:hasValue true :isClean false})))

from cljs-react-material-ui.

Deraen avatar Deraen commented on May 14, 2024

@arbscht Calling setState on component doesn't work (if the component is the last param on on-update) because that is the NativeWrapper Reagent component, not React-native component. Or am I doing something wrong?

from cljs-react-material-ui.

Deraen avatar Deraen commented on May 14, 2024

@arbscht Do you remember how this should work? My current understanding is that it might be possible to get access to the real component by passing make-element result to input-render-setup / on-update at syntheric-input-spec: https://github.com/reagent-project/reagent/blob/master/src/reagent/impl/template.cljs#L292

That would make it possible to call setState (or other methods provided by TextField).

from cljs-react-material-ui.

metametadata avatar metametadata commented on May 14, 2024

@Deraen @arbscht
I did a PoC of the solution I pitched for previously: https://github.com/metametadata/problems/blob/919abde1997dafef26e1dc8388bb58645ac6f79b/src/hello_world/core.cljs#L45

Tested in Edge, Chrome, Firefox.

It doesn't require patching Reagent and taking in account MaterialUI implementation details (i.e. :floating-label-text and :hint-text don't break after applying a fix). So I'd vote for removing synthetic-input from Reagent altogether.

from cljs-react-material-ui.

radhikalism avatar radhikalism commented on May 14, 2024

@Deraen Upon closer examination, I think you're right that setState on component isn't correct. However, it seems to accidentally work because updating state on the NativeWrapper forces a re-render. So it should work if we simply call force-update during on-write?

from cljs-react-material-ui.

radhikalism avatar radhikalism commented on May 14, 2024

@metametadata I like your solution, it's easier to understand. I used a similar approach as a workaround to this problem in a project. However, it had some tradeoffs:

  • Reagent's behaviour with synthetic inputs was poorly defined and quirky, so it wasn't clear whether this kind of workaround would be robust with future versions. If we choose to go with this approach instead of synthetic input handling, I still think Reagent should nail down its own behaviour — even if it is just to explicitly fail fast on complex/wrapped inputs rather than partially processing.
  • Wrapping the original on-change handler to always update local-value makes it tricky to filter user input. If the original on-change handler rejects or fails an update, local-value can go out of sync. We could try to update local-value after the original handler, maybe introduce a return-value protocol or forward a callback or something?
  • Relying on shouldComponentUpdate is probably harmless in this case but given its future behaviour may be a hint rather than a strict directive, I wanted to find a different solution path.

from cljs-react-material-ui.

metametadata avatar metametadata commented on May 14, 2024

@arbscht great that we're on the same page.

  • Maybe Reagent could simply console.warn about the rendering problems if it detects wrapped inputs? Deserves a separate feature issue in reagent-project I guess. But then there must be way to turn off these warnings, at least for the input components which are already "fixed" by the user.
  • Agree, out of sync values can be a problem. I haven't encountered anything like this situation yet though so can't tell how to go forward about it.
  • Yes, I agree about shouldComponentUpdate.

I should say at this stage my main concern is to revert the MR about synthethic-input hooks because we know that solution can be external to Reagent, even though the provided PoC is not bulletproof. I would be interested in publishing a universal "fixer" lib myself but I don't have enough free time at the moment to accomplish it 🙂

from cljs-react-material-ui.

Deraen avatar Deraen commented on May 14, 2024

Reagent can't detect wrapped inputs.

I fear wrapping the components, like in @metametadata's example, is not going to be bulletproof no matter how much it is improved. There are just too many corner cases and differences between components. Only fixing TextField would require much more code than the example, and this code wouldn't work for different input components.

The reason for the problem is Reagent implementation, so it makes sense that the Reagent has the fix. I'll test my idea for fixing synthetic input.

from cljs-react-material-ui.

metametadata avatar metametadata commented on May 14, 2024

Reagent can't detect wrapped inputs.

Personally I don't care much about Reagent not being able to detect the wrapped inputs and warn users. Because incorrect rendering of inputs in async React wrappers is a common problem and non-beginners know about it. Beginners will bump into it themselves pretty quickly.

I fear wrapping the components, like in @metametadata's example, is not going to be bulletproof no matter how much it is improved. There are just too many corner cases and differences between components.

But we can say the same thing about the solutions based on new hooks.
The main selling point of my solution (wrapping and re-rendering the component at correct time) is that it doesn't care about the internal stucture of the wrapped input and doesn't require any changes in Reagent source.

Only fixing TextField would require much more code than the example, and this code wouldn't work for different input components.

But isn't it already fully fixed in my example? As you can see, wrapping MaterialUI's TextField requires no special code, :floating-label-text and :hint-text work as before fixing.

fixed-async-input wrapper is more or less universal, I used the similar code to fix the react- bootstrap components in my project without any additional code to account for corner cases. It is also used in reagent-toolbox. The provided wrapper should work as long as original-component behaves as a typical input.

And even if there are corner cases then they can still be dealt with without any hooks exposed by Reagent lib itself.

The reason for the problem is Reagent implementation, so it makes sense that the Reagent has the fix.

Yes, the root reason of the problem is async rendering. The solution can be a part of Reagent project of course. But I don't think that the hooks are better because:

  1. they bloat the core of the lib,
  2. if I understand everything correctly, using them to fix TextField requires knowledge of the internal structure of the component in order to not break :floating-label-text and :hint-text.

Tl/dr: let's not touch Reagent core because:

  1. There's a sane Reagent-agnostic solution: wrap a component and re-render it synchronously when needed.
  2. The universal wrapper from my example already works for 90% (?) of broken components out of the box. It can be refined and put into a separate lib or be part of Reagent org repo (but this is not very important as long as users know where to find it).

from cljs-react-material-ui.

radhikalism avatar radhikalism commented on May 14, 2024

@Deraen Here's what I've got working, based on your finding about needing to reference the inner component rather than the NativeWrapper:

In reagent template.cljs:

diff --git a/src/reagent/impl/template.cljs b/src/reagent/impl/template.cljs
index dc04813..a3169cf 100644
--- a/src/reagent/impl/template.cljs
+++ b/src/reagent/impl/template.cljs
@@ -254,6 +254,7 @@
    :reagent-render
    (fn [on-update on-change argv comp jsprops first-child]
      (let [this comp/*current-component*]
+       (oset jsprops "ref" (fn [inner] (oset this "innerComponent" inner)))
        (input-render-setup this jsprops {:synthetic-on-update on-update
                                          :synthetic-on-change on-change})
        (make-element argv comp jsprops first-child)))})

Usage:

(def text-field
  (r/adapt-react-class
   (.-TextField js/MaterialUI)
   {:synthetic-input
    {:on-update (fn [input-node-set-value node rendered-value dom-value this]
                  ;; node is the element rendered by TextField, i.e. div.
                  ;; Update input dom node value
                  (let [node (aget (.getElementsByTagName node "input") 0)]
                    (input-node-set-value node rendered-value dom-value this
                                          {:on-write (fn [new-value]
                                                       (let [inner-component (aget this "innerComponent")]
                                                         (if (clojure.string/blank? new-value)
                                                           (.setState inner-component #js {:hasValue false :isClean false})
                                                           (.setState inner-component #js {:hasValue true :isClean false}))))})))
     :on-change (fn [on-change e]
                  (on-change e (.. e -target -value)))}}))

What do you think of using refs for this purpose?

from cljs-react-material-ui.

radhikalism avatar radhikalism commented on May 14, 2024

@metametadata On further examination, I would still like a simpler solution (and the hooks code could certainly be improved) but I'm not sure that the local-value atom workaround is preferable.

Personally I don't care much about Reagent not being able to detect the wrapped inputs and warn users.

doesn't require any changes in Reagent source.

Yes, the root reason of the problem is async rendering.

[the hooks] bloat the core of the lib,

Tl/dr: let's not touch Reagent core

Async rendering is the root problem, but a complicating factor is that Reagent makes brittle assumptions about the shape of input components. Whatever solution we choose for controlling async rendering, we still ought to clean up Reagent core's buggy assumptions and partially failing code paths. Whether we warn users or silently abort or add documentation or something else, I don't know. But we can't revert to how it was and just build more workaround layers upon a broken foundation. So we'll have to touch Reagent either way.

I would be interested in publishing a universal "fixer" lib

The universal wrapper from my example already works 90% (?) of broken components out of the box. It can be refined and put into a separate lib or be part of Reagent (but this is not very important as long as users know where to find it).

Actually I think this is important for users. Having to use a "fixer" library just to make text fields work is kind of a bad smell. Also, users may not easily know when to use Reagent normally and when to switch to this "fixer" API or pattern. It would be better to make Reagent work in the general case even if it adds some complexity via discoverable optional parameters at the use site.

The main selling point of my solution (re-rendering the component at correct time) is that it doesn't care about the internal stucture of the wrapped input

if I understand everything correctly, using them to fix TextField requires knowledge of the internal structure of the component in order to not break :floating-label-text and :hint-text.

In the hooks-based solution, all requirements for special knowledge are clearly separated.

  • Reagent only needs to know that inputs can be simple (default) or complex (synthetic).
  • The integrating UI library or user code (in this case cljs-react-material-ui) needs to know a little about the widgets that it is supplying to Reagent via adapt-react-class, and encapsulate it using optional callbacks. I think this is a reasonable expectation.

Agree, out of sync values can be a problem. I haven't encountered anything like this situation yet though so can't tell how to go forward about it.

I have encountered this use case, which is what led me to the approach involving hooks in Reagent in the first place. Filtering user input for validation or triggering help etc. turned out to be tricky when generically wrapping on-change on the input widget. As mentioned, there may be ways to workaround this but AFAIK it involves breaking the contract/signature of the typical on-change callback (which end users are exposed to). Whereas the hooks-based approach only changes the adapt-react-class API that is more rarely used. And adding that kind of support will make the external-state option increasingly complex too.

from cljs-react-material-ui.

metametadata avatar metametadata commented on May 14, 2024

Whatever solution we choose for controlling async rendering, we still ought to clean up Reagent core's buggy assumptions and partially failing code paths. Whether we warn users or silently abort or add documentation or something else, I don't know. But we can't revert to how it was and just build more workaround layers upon a broken foundation. So we'll have to touch Reagent either way.

I'm not sure I understand what are the partially failing code paths and what is currently broken in the foundation. Reagent only promises to render "raw" :inputs correctly and it seems to work fine at the moment. If there's some cleaning up needed in that part of codebase then it should be a separate issue. But it's not an argument for adding new public API.

Having to use a "fixer" library just to make text fields work is kind of a bad smell. Also, users may not easily know when to use Reagent normally and when to switch to this "fixer" API or pattern. It would be better to make Reagent work in the general case even if it adds some complexity via discoverable optional parameters at the use site.

The bad smell will stay anyways as the problem is inherent to async rendering. I.e. I can as well say that using hooks to make text fields work is a bad smell :) It will still have to be explained when to use hooks and how. And anyways not many people read docs that thoroughly until they see bugs in their apps. And if they do read then we can just put a link to "fixer" lib or any other non-hook solution in the docs for them. So IMHO adding optional public API to give users a hint about potential problem is a weak argument for hooks.

The integrating UI library or user code (in this case cljs-react-material-ui) needs to know a little about the widgets that it is supplying to Reagent via adapt-react-class, and encapsulate it using optional callbacks. I think this is a reasonable expectation.

I think that's the worst part of the hook-based solution. As a user of the thrid-party lib I don't want ever to depend on its implementation details. In the next version they decide to rename isClean to something else in MaterialUI.js and the CLJS apps which use this flag in hooks will break. It's just too much to test and understand. Why users would spend time on testing and figuring out the internals of MaterialUI's text fields if a universal "fixer wrapper" just always works out of the box without dependency on original component implementation?

Filtering user input for validation or triggering help etc. turned out to be tricky when generically wrapping on-change on the input widget. As mentioned, there may be ways to workaround this but AFAIK it involves breaking the contract/signature of the typical on-change callback (which end users are exposed to).

OK, it's hard to reason about it for me without looking at some code. I did have some validation/filtering in custom inputs but maybe my use-case was different. Can you maybe provide a simple use case so that I could think how wrapper fix could be accomodated for such scenario? I will try to create an example demo.

But let's imagine it will require changing the contract of on-change callback. Then I guess I would still prefer such "non-standard-API" wrapper to hook-based solution which preserves the callback API but depends on internals of the original components. I use react-bootstrap JS lib in my project directly without any CLJS wrapper libs (well, I use cljsjs version but it doesn't count as wrapper) and would like to upgrade to new versions without re-testing all my fixed inputs.

from cljs-react-material-ui.

radhikalism avatar radhikalism commented on May 14, 2024

Reagent only promises to render "raw" :inputs correctly and it seems to work fine at the moment.

It's not clear to me this is the case. Maybe I missed something in the docs, but the behaviour seems to be undefined/quirky rather than well-defined as only supporting "raw" inputs. This bug in c-r-m-ui arose in the first place because it tried to use adapt-react-class with the expectation that Reagent would support complex TextFields. So I don't think it's obvious. For this reason, I suggested that if Reagent is going to commit to only supporting "raw" inputs, it should explicitly assert/warn/fail/document as such.

The mess in Reagent is that certain code paths make an effort to support complex input components by attempting to render, set state, trigger events, etc. Some of these behaviours succeed and others do not. It can be misleading and confusing since a basic use of a complex input field seems to work at first, and then certain features don't, but there is no explanation why. I don't have time to walk through all the code that tries to adapt complex inputs only to fail slowly. If you dig into reagent-project/reagent#282 you can see the different code paths described (which I'm suggesting should either execute correctly, or not execute at all).

The bad smell will stay anyways as the problem is inherent to async rendering. I.e. I can as well say that using hooks to make text fields work is a bad smell :)

Yes, I agree it's unpleasant and can be improved. But the question for me is which option is worse.

It will still have to be explained when to use hooks and how.

There is a key difference though:

  • If we extend adapt-react-class, the Reagent user only has to define overrides once. In fact, if the actual user is already depending on a glue library like c-r-m-ui or similar, the override can be defined there for free.
  • If we alter the behaviour of on-change on the adapted component, the user is exposed to this quirk in all use sites, each of which is an opportunity for bugs or incidental complexity. Even if the user depends on a "fixer" library, that remains true. The only way to avoid this that I can think of is to only support the 80% case, which is probably acceptable for one-off hacks as a user, but defeats the purpose of a general UI library toolkit.

And anyways not many people read docs that thoroughly until they see bugs in their apps.

I think this is a good point and we should probably add a hint or assertion that the adapted class is not functioning correctly along the code paths I mentioned above. But I think it's a nice-to-have if there are documented hooks provided, and more important if the user is supposed to rely on an external workaround or pattern.

OK, it's hard to reason about it for me without looking at some code. I did have some validation/filtering in custom inputs but maybe my use-case was different. Can you maybe provide a simple use case so that I could think how wrapper fix could be accomodated for such scenario? I will try to create an example demo.

In on-change just make it conditional whether text-state gets updated or not. e.g. try to enforce a length limit such as 8 chars -- (when (<= len 8) (swap! text-state ...)). Without any further workarounds, the PoC will update local-value eagerly but text-state may or may not remain the same.

I use react-bootstrap JS lib in my project directly without any CLJS wrapper libs (well, I use cljsjs version but it doesn't count as wrapper) and would like to upgrade to new versions without re-testing all my fixed inputs.

One thing I've realized in this discussion is that in the universe where hooks are the better option, it shows that certain aspects of Material-UI's internals to do with DOM structure as well as hasValue or isClean is an interesting concern for its users such as c-r-m-ui. I think a case could be made that Material-UI should expose those necessary attributes with a stable public API (if not already, I haven't checked). A PR there would be a good idea to settle that risk, if there isn't a cleaner way to communicate with the original component.

Regardless of that point, I think we still need to consider which is the worse option between the two before us if upstream did nothing. One option is to depend on particular supported combinations of Material-UI+c-r-m-ui in order to use Reagent's hooks. The other option is to depend on a particular supported combination of the "fixer" library (or the local-value pattern used directly) plus Reagent (since its behaviour is buggy, undefined or quirky). At best these are equally bad or good options, I don't think it's convincing either way.

This is a pretty complex issue because of integration across multiple projects and stuff falling through the cracks, so there are lots of combinations for solutions. I don't know if adding hooks is really the best answer but I'm increasingly sure an external workaround wrapper which will alter the contract of an important callback (just for this one weird upstream bug or limitation)... is not obviously a better one.

from cljs-react-material-ui.

metametadata avatar metametadata commented on May 14, 2024

This bug in c-r-m-ui arose in the first place because it tried to use adapt-react-class with the expectation that Reagent would support complex TextFields. So I don't think it's obvious.
I suggested that if Reagent is going to commit to only supporting "raw" inputs, it should explicitly assert/warn/fail/document as such.

+1. I had to read issues in Reagent and other projects to get that Reagent won't try to fix complex inputs but only its "own"/"raw" inputs. Specifically, this is the issue from Reagent: reagent-project/reagent#79.

The mess in Reagent is that certain code paths make an effort to support complex input components by attempting to render, set state, trigger events, etc.

Now I understand, thanks.

In fact, if the actual user is already depending on a glue library like c-r-m-ui or similar, the override can be defined there for free.

So in this case c-r-m-u-i will have to depend on the way TextField is implemented in MaterialUI.js. It's bad because no code should depend on implementation details of some other code. To me it's a very pretty strong argument against the "hooks" solution. "Hooks" are dangerous because the resulting code will be error-prone by design. On the other hand, "wrapper" solution is always safe by design. Introducing "hooks" in Reagent would contribute to spreading bad habits amongst programmers for the sake of supporting masking/filtering/etc. in inputs.

The only way to avoid this that I can think of is to only support the 80% case, which is probably acceptable for one-off hacks as a user, but defeats the purpose of a general UI library toolkit.
... e.g. try to enforce a length limit such as 8 chars -- (when (<= len 8) (swap! text-state ...)). Without any further workarounds, the PoC will update local-value eagerly but text-state may or may not remain the same.

It looks like React itself doesn't support the filtering/masking scenario you'd like to have, see facebook/react#955:

"this is not a bug because React cannot guess the cursor position if you manipulate the input value in some way"

Thus it's an overambitious task to make Reagent fix inputs and complex inputs to behave differently from what React does by default. I'm totally fine if Reagent wouldn't help with this problem at all and (even more so) I'm fine if wrapper libs such as c-r-m-u-i wouldn't try to solve it either.
The PoC "wrapper" pattern strives only to make the complex controlled input behave in the same as the usual React controlled input, i.e. prevent cursor jumps in a normal scenario where user sees the results of the key presses, without any masking, filtering, etc in place.

... Material-UI's internals to do with DOM structure as well as hasValue or isClean is an interesting concern for its users such as c-r-m-ui. I think a case could be made that Material-UI should expose those necessary attributes with a stable public API (if not already, I haven't checked). A PR there would be a good idea to settle that risk, if there isn't a cleaner way to communicate with the original component.

Sounds unlikely that the authors of MaterialUI or any other complex inputs would ever bother with providing such public API only because Reagent decided to fix the problem not solved in React itself :)

TL/DR:

  • The current c-r-m-u-i issue is about cursor jumps on normal typing. Such cursor jumps happen because of async rendering. It's a common problem, e.g. see similar problem in Om: omcljs/om#295 (interestingly, as far as I understood, it was closed by David Nolen as not something that needs fixing in Om itself).
  • Cursors jumps introduced by "naive" filtering/masking is an edge case which is not supported even in React.js (based on facebook/react#955).
  • Thus I think Reagent/c-r-m-u-i/etc. should not even try to fix such cursor jumps (because React itself doesn't support it). It's too ambitious and also is hard to implement.
  • Reagent does fix cursor jumps during normal typing in "raw" inputs but not in complex inputs (see reagent-project/reagent#79).
  • But it seems like the existing code in Reagent which fixes cursor jumps needs some cleaning up.
  • All these input and complex input behavior in Reagent must be documented better. It should cover raw inputs, why there're cursor jumps possible in complex inputs, how to adapt complex inputs, cursor jump on "naive" filtering/masking, etc.
  • Maybe, if possible, warnings must be shown to user to remind that his inputs will experience cursor jumps.
  • I think Reagent should not provide an API which encourages writing dangerous code. And "hooks", although the seem to allow "naive" filtering/masking pattern, are dangerous because they will always require knowledge of the internals of the adapted component.
  • I don't like "wrapper which will alter the contract of an important callback" solution either.
  • I'm more than fine with having something like the PoC "wrapper" which adapts the complex input to behave similarly to raw React inputs and nothing else.

from cljs-react-material-ui.

metametadata avatar metametadata commented on May 14, 2024

Actually it looks like Om provides a "wrapper" solution officially:

https://github.com/omcljs/om/blob/7ab33e8dee1133cf031df93a08787531a2f1985d/src/main/om/dom.cljs#L20

from cljs-react-material-ui.

radhikalism avatar radhikalism commented on May 14, 2024

+1. I had to read issues in Reagent and other projects to get that Reagent won't try to fix complex inputs but only its "own"/"raw" inputs. Specifically, this is the issue from Reagent: reagent-project/reagent#79.

There are couple of important nuances in the discussion around reagent-project/reagent#79 and reagent-project/reagent#126 and our current topic:

  • The meaning of "complex" vs "raw" inputs here is not "custom DOM element" (which was discussed and avoided) vs input. "Complex" or "synthetic" here is rather ordinary/raw input etc that happen to be children within other typical DOM elements (which are usually stylistic/structural noise).
  • The purpose of these hooks is to allow Reagent to unwrap any such kind of "packaging" and reach the correct input elements that it already supports as a result of 79 and 126, and to callback to the original component to sync/reset.

It looks like React itself doesn't support the filtering/masking scenario you'd like to have, see facebook/react#955:

I'm not sure this holds, given that Reagent actually does make an effort to manage caret positions because of 79 and 126.

Also, quoting from 126:

[This patch] is for situations where the programmer is using that on-change for transform or validation purposes. For example, if the programmer wishes to enforce a certain pattern, like "99.9". If the user enters an "a" char, then the programmer may wish to reject it immediately. So on-change provides a general transformation and verification mechanism useful in cases where you don't want to wait for on-blur or form submission.

So I figure this scenario is intended to be supported after all. I don't think it is overambitious to make the behaviour pluggable so Reagent's existing caret-managing code paths can successfully complete executing with respect to regular HTML input etc, which happen to appear in the component's DOM as a child of other container elements.

Sounds unlikely that the authors of MaterialUI or any other complex inputs would ever bother with providing such public API

The particular implementation details which are interesting are:

  • The DOM structure, or more particularly a well-known reference or a reliable selector query, in order to locate the native correct input element (the thing which Reagent already supports).
  • Some way to signal a change affecting dirty state (hasValue / isClean) which Material-UI could have documented or exposed as is, or via some indirection. This kind of thing is not unusual for a UI widget to expose publicly. By analogy, AWT, Swing, Tk, Windows Forms, etc. So I don't think it's unlikely to be supported if necessary.

only because Reagent decided to fix the problem not solved in React itself :)

To be clear, it is a problem that Reagent has decided to fix already, since 79 and 126, including apparently the filtering/validation use case (if I understand the issue log correctly). The hooks extension is not about expanding the scope of this behaviour to custom HTML tags or diverging any further from original React. It is to allow already-supported HTML components to be correctly located within any arbitrary wrapping of a third-party component, and to callback to the original component in order to sync/reset using a common UI pattern, using the code paths that Reagent is already providing for caret management.

Actually it looks like Om provides a "wrapper" solution officially:
https://github.com/omcljs/om/blob/7ab33e8dee1133cf031df93a08787531a2f1985d/src/main/om/dom.cljs#L20

That's interesting. I need to review that code more closely when I have time, but so far it looks to me like Om would have the same limitations as the wrapper solution here, which diverges from the direction Reagent has gone towards since 79 and 126.

from cljs-react-material-ui.

metametadata avatar metametadata commented on May 14, 2024

Got it. So Reagent tries to fix cursor jumps when one applies what I called a "naive" filtering/masking pattern and it cannot do the same with adapted components (which I erroneously called "complex inputs") unless "hooks" API is exposed.

from cljs-react-material-ui.

Deraen avatar Deraen commented on May 14, 2024

I implemented change to use ref to get reference to the real component which can be used by on-update function: reagent-project/reagent#351

from cljs-react-material-ui.

Deraen avatar Deraen commented on May 14, 2024

Some new developments on reagent-project/reagent#351. The fix I found won't work with MaterialUI 1.0-beta.

from cljs-react-material-ui.

Deraen avatar Deraen commented on May 14, 2024

@vharmain Nice! "For some reason", the reason being that in this case the input element is created through Reagent logic which will wrap the component in Reagent input component with the fixes. Reagent doesn't have any control if the component is created directly using React. This seems like very good solution to this problem, no hacks required anywhere, just use of proper option in Material-UI Input component: https://material-ui.com/api/input/

from cljs-react-material-ui.

vharmain avatar vharmain commented on May 14, 2024

Cool, that makes sense. Thanks for the explanation @Deraen.

from cljs-react-material-ui.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.