Reagent wrappers for @headlessui/react, bringing accessible, keyboard-friendly, style-agnostic UI components to Reagent and re-frame projects.
Install as a Clojure dependency. Assuming you run your project with
shadow-cljs, @headlessui/react
will be installed as a JS dependency.
Otherwise, you may have to install it yourself with npm/yarn.
Since v1.4.0.32
, headlessui-reagent
tracks @headlessui/react
's versioning.
That is, the first three segments of the version (1.4.0
) indicate that this
library was built with @headlessui/react
version 1.4.0
. The last segment
(32
) distinguishes between releases of this library that were all built with
the same version of @headlessui/react
.
If for some reason you need an earlier version, both v1.2.0
and v1.2.1
of
headlessui-reagent
were built with @headlessui/react
1.2.0
. Earlier
releases were built with @headlessui/react
1.0.0
.
Usage follows the Headless UI API. For example, to use a Disclosure in Reagent, with Tailwind CSS (though any styling system will work):
(require '[headlessui-reagent.core :as ui])
[ui/disclosure
[ui/disclosure-button {:class [:w-full :px-4 :py-2 :text-sm :font-medium :text-purple-900 :bg-purple-100 :rounded-lg]}
"Explain"]
[ui/disclosure-panel {:class [:px-4 :pt-4 :pb-2 :text-sm :text-gray-500]}
[:p "Some explanation."]]]
To see each of the components in action, check out the examples.
To conditionally apply markup or styles based on the component's state, you have a few choices.
First, if you're using Headless UI with Tailwind CSS, as many people do, consider using the @headlessui/tailwindcss plugin. That'll let you target specific UI states with plain CSS classes.
[ui/menu
[ui/menu-button "More"]
[ui/menu-items
[ui/menu-item
[:a.ui-active:bg-blue-500.ui-active:text-white.ui-not-active:bg-white.ui-not-active:text-black
{:href "/account-settings"}
"Account Settings"]]
,,,]]
For a more programmatic approach, Headless UI provides "render
props". If the Reagent component is given a single function as a
child, the function is called with a hash map of render props (e.g. :open
for
a Disclosure). The return value of the function, which should be a single
(hiccup-style) component, will be rendered.
[ui/disclosure
(fn [{:keys [open]}]
[:<>
[ui/disclosure-button (if open "Hide" "Show")]
[ui/disclosure-panel ,,,]])]
If you need to control the CSS classes only, not the component's contents,
:class
can be a function which will receive the render props:
[ui/disclosure-button {:class (fn [{:keys [open]}]
[:border (when open :bg-blue-200)])}
"Show more"]
Many Headless UI components accept an "as"
prop, which controls how they are
rendered into the dom. If the corresponding Reagent component is given an :as
prop, it can be any hiccup-style component: a string, a keyword or a function
which returns hiccup.
If :as
is a full-fledged Reagent component (i.e. a function which returns
hiccup), then that component must accept two arguments, its properties and its
children:
(defn panel-ul [props children]
(into [:ul.bg-red-500 props] children))
[ui/disclosure-panel {:as panel-ul}
[:li "Note this."]
[:li "This too."]]
The props will contain ARIA attributes, event handlers and other attributes
necessary for the :as
component to work correctly, so you must use them.
The above example is so simple it would be more easily written as:
[ui/disclosure-panel {:as :ul.bg-red-500}
[:li "Note this."]
[:li "This too."]]
Or, closest to the Headless UI style, as:
[ui/disclosure-panel {:as "ul", :class [:bg-red-500]}
[:li "Note this."]
[:li "This too."]]
The Listbox, RadioGroup and Combobox components are designed to assist in
picking an item from a list of items. Headless UI coordinates this by having a
root element and several child elements. The child elements each have a value
,
to identify themselves. The root element also has a value
for the currently
selected item and an onChange
handler that is called with a different value
when another item is selected.
In this library, these correspond to the :value
and :on-change
attributes.
The :value
must be a JavaScript object, and similarly the argument to the
:on-change
callback will be a JavaScript object. The library does not
automatically convert between JavaScript and ClojureScript.
This begs the question, what's the best way to use these components when the
items are ClojureScript objects? The trick is to ensure that the items each have
a unique identifier that is a JavaScript primitive (numbers and strings are
preferred because they are primitives in both ClojureScript and JavaScript). We
use this unique identifier as the item's :value
. When a new item is selected,
:on-change
will be called with the primitive identifier. At that point, we're
back in ClojureScript code, so we can use the identifier to lookup the full
item.
Here's an example. Notice that :id
, an integer, is used as the :value
in
both the ui/listbox
and ui/listbox-option
. That is, to Headless UI, it's
assisting in picking an integer. That integer is converted back into a full
person in :on-change
.
(def people [{:id 1, :name "Wade Cooper"}
{:id 2, :name "Arlene Mccoy"}
{:id 3, :name "Devon Webb"}
{:id 4, :name "Tom Cook"}
{:id 5, :name "Tanya Fox"}
{:id 6, :name "Hellen Schmidt"}])
(def person-by-id (zipmap (map :id people) people))
(reagent.core/with-let [!selected (reagent.core/atom (first people))]
(let [selected @!selected]
[ui/listbox
{:value (:id selected)
:on-change #(reset! !selected (get person-by-id %1))}
[ui/listbox-button (:name selected)]
[ui/listbox-options
(for [person people]
^{:key (:id person)}
[ui/listbox-option
{:value (:id person)}
(:name person)])]]))
There are some known limitations to the interop between Reagent and Headless UI. Bug fixes welcome!
In some cases where Headless UI would usually render a wrapper element, it
permits rendering the children directly instead, by passing React.Fragment
as
"as"
. This is not supported because Headless UI and Reagent fail to convey
props between them.
;; DON'T do this
[ui/menu-button {:as :<>}
[:button.block {:type "button"} "Open"]]
If you'd like to support Tailwind Labs, the creators of Headless UI, consider signing up for Tailwind UI. You'll get access to hundreds of UI components, many of which use Headless UI, and which can be adapted to work with headlessui-reagent.
headlessui-reagent isn't an official part of Headless UI, and I don't get anything when you sign up for Tailwind UI. I just believe that Tailwind CSS and Headless UI are a natural fit for Reagent/Hiccup projects, and want to see them thrive.
Copyright © 2022 Jacob Maine
Distributed under the MIT License.