Skip to content

Instantly share code, notes, and snippets.

@lilactown
Last active December 9, 2019 19:01
Show Gist options
  • Save lilactown/e93a1a0ab25d40df006d77f405c1e535 to your computer and use it in GitHub Desktop.
Save lilactown/e93a1a0ab25d40df006d77f405c1e535 to your computer and use it in GitHub Desktop.
(ns helix-example
(:require
[helix.core :refer [defnc $ <>]]
[helix.hooks :as hooks]
[helix.dom :as d]
["react-dom" :as rdom]))
(defnc Greeting
"A component which greets a user. The user can double click on their name to edit it."
[{:keys [name on-name-change]}]
(let [[editing? set-editing?] (hooks/use-state false)
input-ref (hooks/use-ref nil)
focus-input #(when-let [current (.-current input-ref)]
(.focus current))]
(hooks/use-layout-effect
:auto-deps ;; automatically infer deps array from body; stand in for `[editing?]`
(when editing?
(focus-input)))
(d/div
"Hello, " (if editing?
(d/input {:ref input-ref
:on-change #(on-name-change (.. % -target -value))
:value name
:on-blur #(set-editing? false)})
(d/strong {:on-double-click #(set-editing? true)} name)
"!")))
(defnc App []
(let [[state set-state] (hooks/use-state {:name "Helix User"})
;; annotate with `:callback` metadata to automatically wrap in
;; `use-callback` and infer dependencies from local context
on-name-change ^:callback #(set-name assoc :name %)
;; annotate with `:memo` metadata to wrap in `use-memo` and infer deps as well
name ^:memo (:name state)]
(<> (d/h1 "Welcome!")
($ Greeting {:name name}))))
(rdom/render ($ App) (js/document.getElementById "app"))
@orestis
Copy link

orestis commented Nov 26, 2019

Cool, thanks for the explanation. Another question:

Is there a story on interop where JS components will give you already-in-JS prop maps, which you need to use from CLJS? My current solution is to wrap them in bean so that they can be passed to a an hx component, perhaps doing a merge with some other props too. Would be helpful if $ recognised plain JS maps and didn't try to convert them. A function for prop merging could also help.

An example of how this looks like from a real component, integrating react-beautiful-dnd -- which brings in Draggable:

(defnc DraggableSelectableRow [{:keys [id index value cells-component]}]
  (let [selected? (is-selected? id)]
    [Draggable {:draggableId id
                :key id
                :index index}
     (fn [provided snapshot]
       [:tr (merge {:ref (.-innerRef provided)
                    :class [(when selected? "table--selected")
                            (when (.-isDragging snapshot) "table-row-dragging")]}
                   (bean (.-draggableProps provided)))
        [table/SelectionCell {:id id :is-selected selected?}]
        [cells-component {:value value}]
        [:td (merge {:class "table-row--draggable"}
                    (bean (.-dragHandleProps provided)))
         [:span {:class "icon move"} [:i]]]])]))

@lilactown
Copy link
Author

lilactown commented Nov 26, 2019

Dynamic props are tough.

What I've done so far is introduce an idea of spread props in the $ macro (this includes helix.dom macros too). So you can do something like this:

(let [props {:on-click #(js/alert "clicked!")}]
  ($ MyComponent {:style {:color "red"} & props}))

This way, props are always written as a literal map and you can opt-in to dynamically setting props when needed. This also handles merging; props passed in via the & key will be merged into the JS object generated by the literal props and override them.

So the way that you would handle your case would be like:

(defnc DraggableSelectableRow [{:keys [id index value cells-component]}]
  (let [selected? (is-selected? id)]
    ($ Draggable {:draggableId id
                  :key id
                  :index index}
      (fn [provided snapshot]
        (d/tr {:ref (.-innerRef provided)
               :class [(when selected? "table--selected")
                       (when (.-isDragging snapshot) "table-row-dragging")]
               & (bean (.-draggableProps provided))}
           ($ table/SelectionCell {:id id :is-selected selected?})
           ($ cells-component {:value value})
           (d/td {:class "table-row--draggable" & (bean (.-dragHandleProps provided))}
              (d/span {:class "icon move"} (d/i)))))))

Some additional logic could be added to check if spread props are a map? and if not, treat it like a JS object and merge it. That would remove the need for the bean wrappers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment