Help me with a clojurescript macro

clojurescript
macros
#1

We have some boiler-plate Cljs code that does something like this:

(defn mui->reagent [mui-name]
  (r/adapt-react-class (gobj/get js/MaterialUI mui-name)))

(def app-bar (mui->reagent "Button"))
(def card (mui->reagent "Card"))
.... 

Basically it introduces a def for every js/MaterialUI component and there are lots of them. I thought that could be cleaned up using a macro and wrote something like this in a .cljc file:

(defn react-import [tag]
  `(def ~(symbol (str "my-"(->kebab-case tag)))
    (reagent.core/adapt-react-class (goog.object/get js/MaterialUI ~tag))))
    
(defmacro import-mui [tags]
  `(do [email protected](map react-import tags)))

And it works if called like this:

(import-mui ["Button" "Card" "Checkbox"])

That will introduce 3 defs: my-button, my-card, my-checkbox. So, you can see that it works but only half way. I thought: “What if I retrieve the list of the components from MaterialUI object”? Something like:

(require '[my-mui.components :refer-macros [import-ui]])

(import-mui (filterv capitalized? (js/Object.keys js/MaterialUI))
;; you need to use only capitalized keys, 
;; how `capitalized?` function works it's not important here

But that doesn’t work for some reason, it simply doesn’t want to feed the list in there. If I take the same list produced by (filterv capitalized? (js/Object.keys js/MaterialUI)), put it in a vector and feed to the macro - it works. But it doesn’t work when done like above.

So I thought what if I do it inside the macro, but I can’t make that work either.

For the sake of simplicity, let’s forget about MaterialUI and React and simplify things a bit:

Let’s create two files, my_ns.cljc and my_ns.cljs, in .cljc file:

(defn new-def [t]
  `(def ~(symbol (str "my-" t))
    ~t))

(defmacro try-me [tags]
  `(do [email protected](map new-def tags)))

in .cljs file:

(requre '[my-ns :refer-macros [try-me]])

(set! js/window.GlobalList #js {"One" {} "Two" {} "Three" {}})

Now, if I eval (try-me ["One" "Two" "Three"]) - it works. It creates 3 defs. But If I try:

(try-me (js/Object.keys js/GlobalList))

It wouldn’t. It throws an exception with the following message:

Can't def ns-qualified name in namespace my-(js at line 1 <cljs repl>

So why is that? Why this doesn’t work?

What’s the fundamental difference between passing a literal vector vs. passing a computed vector into a macro? What am I missing?

Can someone help me out? Thanks!

#2

Remember that macros rewrite Clojure code at compilation time. They just take the raw form that was passed into the call and rewrite it to something else. They never eval their arguments.

On top of that the macros for ClojureScript are expanded at compilation time which happens in Clojure on the JVM. So they don’t have access to any JS at all.

So fundamentally what you are trying to do isn’t possible in CLJS.

3 Likes