Skip to content

Instantly share code, notes, and snippets.

@mhuebert
Last active March 1, 2024 16:55
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mhuebert/4e3af2fbc33b40e74539487e50110a14 to your computer and use it in GitHub Desktop.
Save mhuebert/4e3af2fbc33b40e74539487e50110a14 to your computer and use it in GitHub Desktop.
material-ui's CSS-in-JS with Reagent. (see https://material-ui.com/customization/css-in-js/)

material-ui allows for customizing CSS via a higher-order component called withStyles. Like many higher-order components which expect render props, the purpose of withStyles is to accept some parameters, and then introduce a new variable into the scope of the component tree.

One succinct and simple way to translate the concept of scope into Clojure is via a custom let macro. Usage of such a macro is demonstrated here:

(:require [material-ui.styles :as m])

(m/let [{:keys [leftPad]} {:leftPad 
                           {:paddingLeft 8}}]
  ;; `leftPad` is now the _className_ associated with {:paddingLeft 8}                             
  [my-component {:class leftPad} ...])

The let macro is built on top of the with-styles higher-order component, which is material-ui's withStyles component wrapped to handle conversion between JS and Clojure data structures, and allows returning plain Reagent components in the body function.

Two more features to demonstrate:

  1. material-ui components accept a classes key to override the injected styles for a component.
  2. Instead of a plain map of className->styles, you can pass a function which will be passed the current material-ui theme.
(m/let [classes (fn [^js theme]
                 {:root {:color (.. theme -palette -primary -dark)}})]
  [some-component {:classes classes} ...])
(ns material-ui.styles
#?(:clj (:refer-clojure :exclude [let]))
#?(:cljs (:require ["@material-ui/core/styles" :refer [withStyles]]))
#?(:cljs (:require-macros material-ui.styles)))
#?(:cljs
(defn with-styles
"Given a map of {classKey, styles} (or a function which takes a theme & returns such a map),
returns the same map of {classKey, className}"
[styles-or-fn body-fn]
(let [hoc (withStyles
(if (fn? styles-or-fn)
(comp clj->js styles-or-fn)
(clj->js styles-or-fn)))
body-component (r/reactify-component
(fn [{:keys [classes]}]
[body-fn (-> classes
(js->clj :keywordize-keys true))]))]
(react/createElement
(hoc body-component)))))
#?(:clj
(defmacro let [bindings & body]
(loop [[var-name style-expr :as bindings] bindings
out `(r/as-element
(do ~@body))]
(if (empty? bindings)
out
(recur (drop 2 bindings)
`[~'material-ui.styles/with-styles
~style-expr
(fn [~var-name] ~out)])))))
@suseek
Copy link

suseek commented Jul 7, 2018

Hey, is it possible with this code to pass this.props.style to a composition?
Like it is here, when I wanted to place it in a separate component instead of placing it directly underneath the GridList

<GridList className={classes.gridList} cols={2.5}>
        {tileData.map(tile => (     
      <GridListTile cols={1} style={{...this.props.style, background: 'green'}} onClick={this.handleClick}>
        {this.props.id}
      </GridListTile>
))}
</GridList>

For CLJS - here I want to move GridListTile to a separate place with props.styles from GridList that I can use.

[:> GridList {:cell-height 180 :cols 3}
          (for [image @images]
            (let [image-key (get-in image [:details :id])]
              ^{:key image-key} 
                [:> GridListTile ...? ]))]

@beders
Copy link

beders commented Mar 1, 2024

this is a bit outdated. The whole point of creating a HOC with withStyles is that it can be re-used.
This is not the case here as (react/createElement (hoc body-component)) is called.

Using styles is probably better.

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