Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save bhb/7f4c92e17c65e505df0b762c15a296ab to your computer and use it in GitHub Desktop.
Save bhb/7f4c92e17c65e505df0b762c15a296ab to your computer and use it in GitHub Desktop.

Revisions

  1. bhb revised this gist Jun 13, 2017. 1 changed file with 0 additions and 1 deletion.
    1 change: 0 additions & 1 deletion reagent-state-bidi-input.cljs
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,3 @@
    ;loaded from gist: https://gist.github.com/pesterhazy/bc309afa0883f29a07131685cc1087da
    (ns klipse-like.core
    (:require [reagent.core :as r])
    (:import [goog.async Delay]))
  2. bhb revised this gist Jun 13, 2017. 1 changed file with 111 additions and 61 deletions.
    172 changes: 111 additions & 61 deletions reagent-state-bidi-input.cljs
    Original 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)))))])})))
    {: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)))))])})))
    {: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."]
    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."]
    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]
    [:div
    [:button {:on-click refresh} "Remount"]]])}))
    {: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 [])
    (defn on-js-reload [])
  3. @pesterhazy pesterhazy revised this gist Jun 13, 2017. 1 changed file with 0 additions and 3 deletions.
    3 changes: 0 additions & 3 deletions reagent-state-bidi-input.cljs
    Original file line number Diff line number Diff line change
    @@ -161,9 +161,6 @@
    [demo-2]
    [demo-3]
    [demo-4]
    [:div
    [:h3 "State"]
    [:pre {} (pr-str@!state)]]
    [:div
    [:button {:on-click refresh} "Remount"]]])}))

  4. @pesterhazy pesterhazy revised this gist Jun 13, 2017. 1 changed file with 18 additions and 14 deletions.
    32 changes: 18 additions & 14 deletions reagent-state-bidi-input.cljs
    Original file line number Diff line number Diff line change
    @@ -1,13 +1,10 @@
    (ns klipse.playground
    (ns klipse-like.core
    (: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"}))
    (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
    [:h1 "demo-1: instant updates"]
    [: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"]]])
    "Set from the outside"]]
    [state-ui]])

    (defn delayed [f]
    (fn [& args]
    (js/setTimeout #(apply f args) 500)))

    (defn demo-2 []
    [:div
    [:h1 "demo-2: asynchronous delayed-swap"]
    [: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"]]])
    "Set from the outside"]]
    [state-ui]])

    (defn demo-3 []
    [:div
    [:h1 "demo-3: uncontrolled input"]
    [: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"]]])
    "Set from the outside"]]
    [state-ui]])

    (defn demo-4 []
    [:div
    [:h1 "demo-4: bidirectional input field"]
    [: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"]]])
    "Set from the outside"]]
    [state-ui]])


    (defonce !refresh-count (r/atom 0))
    @@ -158,7 +162,7 @@
    [demo-3]
    [demo-4]
    [:div
    [:h1 "State"]
    [:h3 "State"]
    [:pre {} (pr-str@!state)]]
    [:div
    [:button {:on-click refresh} "Remount"]]])}))
  5. @pesterhazy pesterhazy revised this gist Jun 13, 2017. 1 changed file with 175 additions and 1 deletion.
    176 changes: 175 additions & 1 deletion reagent-state-bidi-input.cljs
    Original file line number Diff line number Diff line change
    @@ -1 +1,175 @@
    ;; init
    (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 [])
  6. @pesterhazy pesterhazy created this gist Jun 13, 2017.
    1 change: 1 addition & 0 deletions reagent-state-bidi-input.cljs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    ;; init