4

I have an text input element that uses component state and application state.

In the example shown in React: More About Refs, the goal is to call focus on the element after re-rendering. Here is the key part, done with JS in React.

clearAndFocusInput: function() {
  // Clear the input
  this.setState({userInput: ''}, function() {
    // This code executes after the component is re-rendered
    this.refs.theInput.getDOMNode().focus();   // Boom! Focused!
  });
},

I want to do something similar with Om. I've noticed neither

  1. Om's set-state! (for changing component state; see the docs and source), nor

  2. Om's update! (for changing application state; see the docs and source)

provides ways to specify a callback. So I'm looking for other ways to cause something to happen later, after a re-render.

Here's my example:

(defn input-component
  [{:keys [app-state-key class-name]}]
  (fn [data owner]
    (reify
      om/IInitState
      (init-state
        [this]
        {:text (data app-state-key)})

      om/IRenderState
      (render-state
        [this state]
          (let [handle-change (handle-change-fn data app-state-key)]
            (dom/input
              #js {:ref (name app-state-key)
                   :type "text"
                   :className class-name
                   :value (:text state)
                   :onChange #(handle-change % owner state)}))))))

(defn handle-change-fn
  [app-state app-state-key]
  (fn [e owner state]
    (let [element (.-target e)
          value (.-value element)]
      (om/set-state! owner :text value)
      (if-let [value' (parse-int value)]
        (om/update! app-state app-state-key value')))))

(Note: parse-int, not shown, "cleans up" the component state so that it is suitable for the application state.)

Changing the text input's component state doesn't cause it to lose focus, but mutating the application state does.

I've tried using core.async channels, but that does not seem to help because I only want the callback to happen after the re-render has completed.

I also tried using IDidUpdate, like this:

(defn input-component
  [{:keys [app-state-key class-name]}]
  (fn [data owner]
    (reify
      ; ...
      om/IDidUpdate
      (did-update
        [this prev-props prev-state]
        (let [e (om/get-node (.-owner this))]
          (.log js/console "did-update" e)
          (.focus e)))
      ; ...
      )))

Update: The IDidUpdate lifecycle event does fire if only the component state is updated. However, it does not fire if the application state changes (due to om/update!, above).

David J.
  • 31,569
  • 22
  • 122
  • 174
  • I'm curious about why `IDidUpdate` didn't fire. It won't fire after the initial render of course, so I'm assuming that you mean it doesn't fire after the input element has been modified. – skillet-thief Sep 21 '14 at 19:23
  • I just updated the last paragraph: I do see `IDidUpdate` firing after changes to component state. However, in my case, it does not after changes to application state; this makes me think that if a component is destroyed and recreated, `IDidUpdate` does not fire. – David J. Sep 21 '14 at 20:21
  • That is probably what is happening. If a component is destroyed, then a new component is being mounted. In this case, you would need `IDidMount` instead. – skillet-thief Sep 22 '14 at 01:21
  • I just looked over the React docs for `ref` again. `IDidUpdate`/`IDidMount` probably won't quite do the same thing they are doing in the React example with ref. To follow their example, you would have to use ref in your `handle-change` function, accessing the ref property of `owner` and then setting the focus through the owner object. If I understand the docs correctly, `ref` basically just gives you access to the React component in a context that is outside of the usual lifecycle. Interesting stuff! – skillet-thief Sep 22 '14 at 08:54

1 Answers1

0

One potential solution off the top of my head is to use core.async with the :tx-listen in your om/root call.

The :tx-listen option allows you to specify a function to be called whenever the provided app-state changes - so we provide a function that writes to a channel that has a 'pub' attached to it.

Then when your component mounts, you subscribe to events on that channel using IWillMount, and clean it up on IWillUnmount.

Obviously there's some room for cleanup here (particularly, you may wish to narrow down the type of events listened to by your component) - but it demonstrates the basic idea.

....
(defonce changes (chan))
(defonce changes-pub (pub changes :msg-type))
....
(om/root
   (input-component {:app-state-key :input-state
                     :class-name "theClass"})
   app-state
   {:target (. js/document (getElementById "app"))
    :tx-listen (fn [tx-data root-cursor]
                 (put! changes {:msg-type :state-updated}))})

Then - in your component code you can subscribe to the changes on mount, and close the channel on unmount

   ...
   (reify    
      om/IWillMount
      (will-mount [_]
        (sub changes-pub :state-updated (:sub-chan (om/get-state owner)))
        (go-loop []
          (if-let [update (<! changes)]
            (let [node (om/get-node owner (name app-state-key))]
              (println "Resetting focus")
              (.focus node)
              (recur)))))  
      om/IWillUnmount
      (will-unmount [_]
        (close! (:sub-chan (om/get-state owner))))
      om/IInitState
      (init-state 
        [this]
        {:text (data app-state-key)
         :sub-chan (chan)})
      ...
      )))
Sandipan
  • 48
  • 5