3

Say that you have a text field which is the input of a chat program written in cljs with reagent. It could look something like this:

(defn chat-input []
  (let [written-text (atom "")]
    (fn []
      [:textarea
       {:value     @written-text
        :on-change #(reset! written-text (-> % .-target .-value))}])))

Now the easy way to implement sending a message is to add a send button. But there's one interaction that's so integral to chat that you can't really be without it: That enter or shift-enter sends the message. But I can't figure out how to implement it.

My first try was to simply add a :on-key-press event handler to send the message and reset the state to "". This solution was inspired by How to detect enter key press in reagent.

(defn chat-input []
  (let [written-text (atom "")]
    (fn []
      [:textarea
       {:value        @written-text
        :on-change    #(reset! written-text (-> % .-target .-value))
        :on-key-press (fn [e]
                        (let [enter 13]
                          (println "Key press" (.-charCode e))
                          (if (= (.-charCode e) enter)
                            (reset! written-text "")
                            (println "Not enter."))))}])))

The problem being that the call to (reset! written-text "") in :on-key-press has no effect, probably because it's overridden by the :on-change event handler.

So do you have any ideas on how to implement this functionality? If so, please do share!

Community
  • 1
  • 1
Rovanion
  • 4,382
  • 3
  • 29
  • 49

2 Answers2

3

you were on the right track, but forgot about the js event model: in your case both onChange and onKeyPress are triggered, because the target is a textarea where enter key changes the input. So in js onKeyPress is triggered first, and then it triggers onChange if the key would change something. What you need is to disable this default behavior of keyPress with preventDefault:

(defn chat-input []
  (let [written-text (atom "")]
    (fn []
      [:textarea
       {:value        @written-text
        :on-change    #(reset! written-text (.. % -target -value))
        :on-key-press (fn [e]
                        (when (= (.-charCode e) 13)
                          (.preventDefault e)
                          (reset! written-text "")))}])))

that should fix the problem.

leetwinski
  • 17,408
  • 2
  • 18
  • 42
2

Here's lot more advanced solution that mccraigmccraig on the clojurians slack so kindly allowed me to share with you. It expands the height of the textarea as the contents of the input gets larger which emulates how the chat input works in slack.

But the important part for this question is that it's :on-key-press contains a (.preventDefault e).

(defn update-rows
  [row-count-atom max-rows dom-node value]
  (let [field-height   (.-clientHeight dom-node)
        content-height (.-scrollHeight dom-node)]
    (cond
      (and (not-empty value)
           (> content-height field-height)
           (< @row-count-atom max-rows))
      (swap! row-count-atom inc)

      (empty? value)
      (reset! row-count-atom 1))))

(defn expanding-textarea
  "a textarea which expands up to max-rows as it's content expands"
  [{:keys [max-rows] :as opts}]
  (let [dom-node      (atom nil)
        row-count     (atom 1)
        written-text  (atom "")
        enter-keycode 13]
    (reagent/create-class
     {:display-name "expanding-textarea"

      :component-did-mount
      (fn [ref]
        (reset! dom-node (reagent/dom-node ref))
        (update-rows row-count max-rows @dom-node @written-text))

      :component-did-update
      (fn []
        (update-rows row-count max-rows @dom-node @written-text))

      :reagent-render
      (fn [{:keys [on-change-fn] :as opts}]
        (let [opts (dissoc opts :max-rows)]
          [:textarea
           (merge opts
                  {:rows        @row-count
                   :value       @written-text
                   :on-change   (fn [e]
                                  (reset! written-text (-> e .-target .-value)))
                   :on-key-down (fn [e]
                                  (let [key-code (.-keyCode e)]
                                    (when (and (= enter-keycode key-code)
                                               (not (.-shiftKey e))
                                               (not (.-altKey e))
                                               (not (.-ctrlKey e))
                                               (not (.-metaKey e)))
                                      (do
                                        (.preventDefault e)
                                        (send-chat! @written-text)
                                        (reset! written-text "")))))})]))})))
Rovanion
  • 4,382
  • 3
  • 29
  • 49
  • Thank you for the example of a complet component. It helps a lot! – Rozar Fabien Nov 29 '19 at 00:40
  • 1
    In my case, to get the behavior I was expected, I had to use `reagent/atom` instead of `atom`. Maybe it's the case in the provided solution but as the header is not involved, I had a doubt. – Rozar Fabien Nov 29 '19 at 00:47