Forked from pesterhazy/00-reagent-state-bidi-input.md
Last active
June 13, 2017 22:51
-
-
Save bhb/7f4c92e17c65e505df0b762c15a296ab to your computer and use it in GitHub Desktop.
Revisions
-
bhb revised this gist
Jun 13, 2017 . 1 changed file with 0 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,4 +1,3 @@ (ns klipse-like.core (:require [reagent.core :as r]) (:import [goog.async Delay])) -
bhb revised this gist
Jun 13, 2017 . 1 changed file with 111 additions and 61 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,3 +1,4 @@ ;loaded from gist: https://gist.github.com/pesterhazy/bc309afa0883f29a07131685cc1087da (ns klipse-like.core (:require [reagent.core :as r]) (:import [goog.async Delay])) @@ -20,21 +21,21 @@ [{initial-value :value}] (let [!local-value (atom initial-value)] (r/create-class {:display-name "buffered-input-ui" :component-will-receive-props (fn [this [_ {new-value :value}]] (reset! !local-value new-value)) :render (fn [this] [input* (-> (r/props this) (assoc :value @!local-value) (update :on-change-text (fn [original-on-change-text] (fn [v] (reset! !local-value v) (r/force-update this) (original-on-change-text v)))))])}))) (defn bidi-input-ui [{initial-value :value}] @@ -45,34 +46,34 @@ (reset! !local-value @!next-value)) 500)] (r/create-class {:display-name "bidi-input-ui" :component-will-receive-props (fn [this [_ {new-value :value}]] (let [prev-value (:value (r/props this))] (when (not= prev-value new-value) (let [now (.getTime (js/Date.)) ;; if no update happend recently, fast-track update fast-track? (> (- now @!last-inner-update) 2000)] (reset! !next-value new-value) (if fast-track? (.fire delay-update) (.start delay-update)) (reset! !last-inner-update (.getTime (js/Date.))))))) :component-will-unmount (fn [] (.dispose delay-update)) :render (fn [this] [input* (-> (r/props this) (assoc :value @!local-value) (update :on-change-text (fn [original-on-change-text] (fn [v] (reset! !next-value v) (.fire delay-update) (reset! !last-inner-update (.getTime (js/Date.))) (r/force-update this) (original-on-change-text v)))))])}))) (defn state-ui [] [:pre {} (pr-str @!state)]) @@ -113,9 +114,9 @@ [:div [:h3 "demo-3: uncontrolled input"] [:p "One attempt to solve this problem is to use an uncontrolled input. But this means that you can't change the value from the outside, by resetting the state. You can refresh the input field by clicking on Remount below, but that doesn't solve the problem."] [:p [input* {:default-value (:demo-3 @!state) :on-change-text (fn [v] @@ -131,12 +132,12 @@ [:div [:h3 "demo-4: bidirectional input field"] [:p "A better solution is to establish bidirectional synchronisation between local state and the (delayed) global state. With bidi binding, explicit user input - typing into the input element - always takes precendence over new incoming global state. Furthermore, incoming global updates are debounce, i.e. delayed until no new updates are received for 500ms. Finally, incoming state updates are fast-tracked if no activity was received for the last 2000ms."] [:p [bidi-input-ui {:value (:demo-4 @!state) :on-change-text (delayed #(swap! !state assoc :demo-4 %))}]] @@ -147,6 +148,54 @@ "Set from the outside"]] [state-ui]]) (defn dec-to-zero [x] (if (and x (pos? x)) (dec x) 0) ) (defn bidi-input-ui2 [{initial-value :value}] (let [!expected-values (atom {}) !local-value (r/atom initial-value)] (r/create-class {:display-name "bidi-input-ui2" :component-will-receive-props (fn [this [_ {new-value :value}]] (let [prev-value (:value (r/props this))] (when (neg? (dec (get @!expected-values new-value))) (reset! !local-value new-value)) (swap! !expected-values update new-value dec-to-zero))) :render (fn [this] [input* (-> (r/props this) (assoc :value @!local-value) (update :on-change-text (fn [original-on-change-text] (fn [v] (swap! !expected-values update v inc) (reset! !local-value v) (r/force-update this) (original-on-change-text v)))))])}))) (defn demo-5 [] [:div [:h3 "demo-5: bidirectional input field 2"] [:p "Can we simplify the state by exploiting the fact that the UI expects the backend to echo all changes to the 'value'? Here is an example with two text fields mirroring the same input and also a working 'set from the outside' button. I have not noticed any lag." ] [:p [bidi-input-ui2 {:value (:demo-5 @!state) :on-change-text (delayed #(swap! !state assoc :demo-5 %))}]] [:p [bidi-input-ui2 {:value (:demo-5 @!state) :on-change-text (delayed #(swap! !state assoc :demo-5 %))}]] [:p [:button {:on-click (fn [] (swap! !state assoc :demo-5 "from-the-outside"))} "Set from the outside"]] [state-ui]]) (defonce !refresh-count (r/atom 0)) @@ -155,14 +204,15 @@ (defn root* [] (r/create-class {:render (fn [] [:div {:style {:max-width 600}} [demo-1] [demo-2] [demo-3] [demo-4] [demo-5] [:div [:button {:on-click refresh} "Remount"]]])})) (defn root [] (prn [:render :root]) @@ -173,4 +223,4 @@ (remount) (defn on-js-reload []) -
pesterhazy revised this gist
Jun 13, 2017 . 1 changed file with 0 additions and 3 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -161,9 +161,6 @@ [demo-2] [demo-3] [demo-4] [:div [:button {:on-click refresh} "Remount"]]])})) -
pesterhazy revised this gist
Jun 13, 2017 . 1 changed file with 18 additions and 14 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,13 +1,10 @@ (ns klipse-like.core (:require [reagent.core :as r]) (:import [goog.async Delay])) (enable-console-print!) (defonce !state (r/atom nil)) (defn input* "Like :input, but support on-change-text prop. Avoids mutability pitfalls of @@ -77,9 +74,12 @@ (r/force-update this) (original-on-change-text v)))))])}))) (defn state-ui [] [:pre {} (pr-str @!state)]) (defn demo-1 [] [:div [:h3 "demo-1: instant updates"] [:p "With instant synchronous updates, no problems are visible"] [:p [buffered-input-ui {:value (:demo-1 @!state) @@ -88,15 +88,16 @@ [:button {:on-click (fn [] (swap! !state assoc :demo-1 "from-the-outside"))} "Set from the outside"]] [state-ui]]) (defn delayed [f] (fn [& args] (js/setTimeout #(apply f args) 500))) (defn demo-2 [] [:div [:h3 "demo-2: asynchronous delayed-swap"] [:p "When adding a 500ms artifical delay, we see laggy typing behavior"] [:p [buffered-input-ui {:value (:demo-2 @!state) @@ -105,11 +106,12 @@ [:button {:on-click (fn [] (swap! !state assoc :demo-2 "from-the-outside"))} "Set from the outside"]] [state-ui]]) (defn demo-3 [] [:div [:h3 "demo-3: uncontrolled input"] [:p "One attempt to solve this problem is to use an uncontrolled input. But this means that you can't change the value from the outside, by resetting the state. You can refresh the input field by clicking on Remount below, but that @@ -122,11 +124,12 @@ [:button {:on-click (fn [] (swap! !state assoc :demo-3 "from-the-outside"))} "Set from the outside"]] [state-ui]]) (defn demo-4 [] [:div [:h3 "demo-4: bidirectional input field"] [:p "A better solution is to establish bidirectional synchronisation between local state and the (delayed) global state. With bidi binding, explicit user input - typing into the input element - always takes precendence over new @@ -141,7 +144,8 @@ [:button {:on-click (fn [] (swap! !state assoc :demo-4 "from-the-outside"))} "Set from the outside"]] [state-ui]]) (defonce !refresh-count (r/atom 0)) @@ -158,7 +162,7 @@ [demo-3] [demo-4] [:div [:h3 "State"] [:pre {} (pr-str@!state)]] [:div [:button {:on-click refresh} "Remount"]]])})) -
pesterhazy revised this gist
Jun 13, 2017 . 1 changed file with 175 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1 +1,175 @@ (ns klipse.playground (:require [reagent.core :as r]) (:import [goog.async Delay])) (enable-console-print!) (defonce !state (r/atom {:demo-1 "demo-1" :demo-2 "demo-2" :demo-3 "demo-3" :demo-4 "demo-4"})) (defn input* "Like :input, but support on-change-text prop. Avoids mutability pitfalls of on-change events" [{:keys [on-change on-change-text] :as props}] (assert (fn? on-change-text) "Must have on-change-text prop") (assert (nil? on-change) "Must not have on-change prop") [:input (-> props (assoc :on-change (fn [e] (on-change-text (-> e .-target .-value)))) (dissoc :on-change-text))]) (defn buffered-input-ui [{initial-value :value}] (let [!local-value (atom initial-value)] (r/create-class {:display-name "buffered-input-ui" :component-will-receive-props (fn [this [_ {new-value :value}]] (reset! !local-value new-value)) :render (fn [this] [input* (-> (r/props this) (assoc :value @!local-value) (update :on-change-text (fn [original-on-change-text] (fn [v] (reset! !local-value v) (r/force-update this) (original-on-change-text v)))))])}))) (defn bidi-input-ui [{initial-value :value}] (let [!local-value (r/atom initial-value) !next-value (atom nil) !last-inner-update (atom 0) delay-update (Delay. #(when (not= @!next-value @!local-value) (reset! !local-value @!next-value)) 500)] (r/create-class {:display-name "bidi-input-ui" :component-will-receive-props (fn [this [_ {new-value :value}]] (let [prev-value (:value (r/props this))] (when (not= prev-value new-value) (let [now (.getTime (js/Date.)) ;; if no update happend recently, fast-track update fast-track? (> (- now @!last-inner-update) 2000)] (reset! !next-value new-value) (if fast-track? (.fire delay-update) (.start delay-update)) (reset! !last-inner-update (.getTime (js/Date.))))))) :component-will-unmount (fn [] (.dispose delay-update)) :render (fn [this] [input* (-> (r/props this) (assoc :value @!local-value) (update :on-change-text (fn [original-on-change-text] (fn [v] (reset! !next-value v) (.fire delay-update) (reset! !last-inner-update (.getTime (js/Date.))) (r/force-update this) (original-on-change-text v)))))])}))) (defn demo-1 [] [:div [:h1 "demo-1: instant updates"] [:p "With instant synchronous updates, no problems are visible"] [:p [buffered-input-ui {:value (:demo-1 @!state) :on-change-text #(swap! !state assoc :demo-1 %)}]] [:p [:button {:on-click (fn [] (swap! !state assoc :demo-1 "from-the-outside"))} "Set from the outside"]]]) (defn delayed [f] (fn [& args] (js/setTimeout #(apply f args) 500))) (defn demo-2 [] [:div [:h1 "demo-2: asynchronous delayed-swap"] [:p "When adding a 500ms artifical delay, we see laggy typing behavior"] [:p [buffered-input-ui {:value (:demo-2 @!state) :on-change-text (delayed #(swap! !state assoc :demo-2 %))}]] [:p [:button {:on-click (fn [] (swap! !state assoc :demo-2 "from-the-outside"))} "Set from the outside"]]]) (defn demo-3 [] [:div [:h1 "demo-3: uncontrolled input"] [:p "One attempt to solve this problem is to use an uncontrolled input. But this means that you can't change the value from the outside, by resetting the state. You can refresh the input field by clicking on Remount below, but that doesn't solve the problem."] [:p [input* {:default-value (:demo-3 @!state) :on-change-text (fn [v] (swap! !state assoc :demo-3 v))}]] [:p [:button {:on-click (fn [] (swap! !state assoc :demo-3 "from-the-outside"))} "Set from the outside"]]]) (defn demo-4 [] [:div [:h1 "demo-4: bidirectional input field"] [:p "A better solution is to establish bidirectional synchronisation between local state and the (delayed) global state. With bidi binding, explicit user input - typing into the input element - always takes precendence over new incoming global state. Furthermore, incoming global updates are debounce, i.e. delayed until no new updates are received for 500ms. Finally, incoming state updates are fast-tracked if no activity was received for the last 2000ms."] [:p [bidi-input-ui {:value (:demo-4 @!state) :on-change-text (delayed #(swap! !state assoc :demo-4 %))}]] [:p [:button {:on-click (fn [] (swap! !state assoc :demo-4 "from-the-outside"))} "Set from the outside"]]]) (defonce !refresh-count (r/atom 0)) (defn refresh [] (swap! !refresh-count inc)) (defn root* [] (r/create-class {:render (fn [] [:div {:style {:max-width 600}} [demo-1] [demo-2] [demo-3] [demo-4] [:div [:h1 "State"] [:pre {} (pr-str@!state)]] [:div [:button {:on-click refresh} "Remount"]]])})) (defn root [] (prn [:render :root]) [root* {:key @!refresh-count}]) (defn remount [] (r/render-component [root] js/window.klipse-container)) (remount) (defn on-js-reload []) -
pesterhazy created this gist
Jun 13, 2017 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1 @@ ;; init