Skip to content

Commit

Permalink
Work around form-1 components as render-prop return values
Browse files Browse the repository at this point in the history
  • Loading branch information
mainej committed Feb 22, 2022
1 parent f406b4b commit f09c777
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 49 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ change log follows the conventions of
### Added
- Created example project which demonstrates all the headlessui-reagent
components.
### Fixed
- Worked around a problem where form-1 components couldn't be used as the return
value of a render-props function. form-2 and form-3 components still won't
work, though they'll be rare.

## [1.5.0.47]
### Changed
Expand Down
26 changes: 0 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,32 +160,6 @@ person in `:on-change`.
There are some known limitations to the interop between Reagent and Headless UI.
Bug fixes welcome!

### Reagent components and render props

When using the render props style (passing a function as the only argument to a
component), if Headless UI needs to pass props (ARIA attributes, event handlers,
etc.) to the returned component, which it often does, the component must be a
hiccup keyword, not a Reagent component function:

```clojure
;; DON'T do this
(defn my-component [{:keys [active]} copy]
[:a.block {:href "#" :class (when active :bg-blue-500)} copy])

[ui/menu-item
(fn [props]
[my-component props "A menu item"])]

;; Instead, do this
[ui/menu-item
(fn [{:keys [active]}]
[:a.block {:href "#" :class (when active :bg-blue-500)} "A menu item"])]
```

This can be annoying, but is necessary because Headless UI can't seem to forward
props to Reagent component function elements in the same way that it can for
hiccup keywords.

### Rendering children directly

In some cases where Headless UI would usually render a wrapper element, it
Expand Down
86 changes: 63 additions & 23 deletions src/headlessui_reagent/utils.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,72 @@
(string/join "-")
keyword))

(defn pass-through-props
"We need props appropriate for a reagent component. We receive React style
(defn js->clj-more
"We need props appropriate for a Reagent component. We receive React style
props that have been shallowly converted to a CLJ hashmap. Finishes the
conversion to reagent props."
conversion to Reagent props."
[props]
(set/rename-keys (reduce (fn [result [k v]]
;; Is js->clj safe?
(assoc result (dashify k) (js->clj v)))
{}
(dissoc props :children))
props)
{:class-name :class}))

(defn adapt-render-props-fn
"Adapts a ClojureScript function to work as a Headless UI render props function.
The ClojureScript function will receive a keyword-ized version of the render
props and should return a Reagent-style component vector. The first element of
the vector, the component, can be a hiccup-style keyword or a form-1 Reagent
component."
[f]
(fn [js-slot]
;; Call the ClojureScript function, getting back a Reagent vector. Then
;; convert that to a React element so that Headless UI can use it.
(r/as-element
(let [[comp & args :as comp-vector] (f (js->clj js-slot :keywordize-keys true))]
;; HACK: comp is either a hiccup keyword or a Reagent component function.
;; Not sure why, but with r/as-element, the keyword works as expected
;; while the Reagent component function doesn't propogate changes to the
;; js-slot, nor are the id, ARIA attributes or event handlers attached to
;; it. To get around this, we call the function, effectively converting
;; from the non-working to the working style. Doesn't work when comp is a
;; form-2 or form-3 component.
;;
;; Render props break a lot of React caching, even when used without any
;; Reagent interop, so I'm not worried we're making that worse by calling
;; the form-1 function.
(if (fn? comp)
(let [comp-vector (apply comp args)]
(assert (vector? comp-vector)
"\nThe render props function cannot return a form-2 or form-3 component.\n\nSuggestion: return a hiccup-style keyword or a form-1 component.")
comp-vector)
comp-vector)))))

(defn adapt-class-fn
"Adapts a ClojureScript function to work as a Headless UI class function.
The ClojureScript function will receive a keyword-ized version of the render
props and should return whatever usually works as a Reagent :class -- a
string, keyword, or vector of keywords."
[f]
(fn [js-slot]
(r/class-names (f (js->clj js-slot :keywordize-keys true)))))

(defn adapt-as-component
"Adapts a ClojureScript :as key to work as a Headless UI 'as' component.
The :as key can be a hiccup-style keyword or a form-1 Reagent component
function."
[as]
;; TODO: can this be made to work for react/Fragment or :<> ?
;; If so, can it also work for ui/transition, and other components which
;; don't pass through headlessui->reagent?
(r/reactify-component
(fn [{:keys [children] :as props}]
[as (js->clj-more (dissoc props :children)) children])))

(defn headlessui->reagent
"Convert a @headlessui/react component into a reagent component."
[component]
Expand All @@ -36,31 +90,17 @@
props
(cond-> props
(and (contains? props :as)
;; this interop code would handle strings, but better to avoid it
;; this interop code would handle strings, but better
;; to let them pass straight through to Headless UI
(not (string? (:as props))))
(update :as (fn [as]
(r/reactify-component
(fn [{:keys [children] :as inner-props}]
[as (pass-through-props inner-props) children]))))
(update :as adapt-as-component)

(and (contains? props :class)
(fn? (:class props)))
(update :class (fn [f]
(fn [js-slot]
(r/class-names (f (js->clj js-slot :keywordize-keys true))))))))
(update :class adapt-class-fn)))

children (if (and (= 1 (count children))
(fn? (first children)))
(let [f (first children)]
[(fn [js-slot]
(let [[comp :as comp-and-args] (f (js->clj js-slot :keywordize-keys true))]
(assert (or
;; we are certain a different component will receive the props
(and (map props)
(contains? props :as))
;; we are rendering a hiccup keyword which, for whatever reagent-internal reason, can receive props
(keyword? comp))
"headlessui can't pass props to reagent components; suggestion: return a hiccup keyword-style component")
(r/as-element comp-and-args)))])
[(adapt-render-props-fn (first children))]
children)]
(into [:> component props] children))))

0 comments on commit f09c777

Please sign in to comment.