jsx-to-clojurescript

Command and library to convert JSX snippets to Om/Reagent or other Clojurescript-style format.


Keywords
clojurescript, jsx

Documentation

JSX to Clojurescript     jsx   arrow   clojurescript

Moving old ReactJS codebase to Clojurescript? Tired of manually typing ReactJS examples as Clojurescript?

Search no more!

This is command utility and library to convert JSX snippets to Om/Reagent/Rum Clojurescript-style format. Note, this is by no means to be perfect JS->Cljs compiler, output will often still need touch of your loving hand, but, hey, most of dirty work will be done 😎

This library uses acorn-jsx to parse JSX into nice AST. So big kudos there. Since it's Node.js library, you can use this library only in Clojurescript targeted to Node.js

Installation

As command:

npm install -g jsx-to-clojurescript

As library:

[jsx-to-clojurescript "0.1.9"]

As Alfred workflow (Mac only):

I also made workflow, you can download it here Following things are needed:

  • You installed via npm install -g jsx-to-clojurescript
  • Workflow assumes following paths /usr/local/bin/node /usr/local/bin/jsx-to-clojurescript. If you have different, you can change it in Alfred preferences when you open this workflow's run script.

Use it with keyword jsxcljs and paste JSX. To change command line arguments for all following queries use jsxcljs set and type arguments. Don't put arguments into jsxcljs, only JSX string.

Build your own:

lein cljsbuild once

Usage

jsx-to-clojurescript -h

  Usage: jsx-to-clojurescript [options] <string>

  Converts JSX string into selected Clojurescript React library format

  Options:

    -h, --help            output usage information
    -V, --version         output the version number
    -t --target [target]  Target library (om/reagent/rum). Default om
    --ns [string]         Namespace for compoments. Default ui
    --dom-ns [ns]         Namespace for DOM compoments. Default dom
    --lib-ns [ns]         Target library ns. Default for Om: 'om'. Default for reagent & rum: 'r'
    --kebab-tags          Convert tags to kebab-case?
    --kebab-attrs         Convert attributes to kebab-case?
    --camel-styles        Keep style keys as camelCase
    --remove-attr-vals    Remove attribute values?
    --omit-empty-attrs    Omit empty attributes?
    --styles-as-vector    Keep multiple styles as vector instead of merge

Okay let's start with something simple :bowtie:

<div>
    <RaisedButton label="Secondary" secondary={true} style={style} />
    <RaisedButton label="Disabled" disabled={true} style={style} />
</div>
jsx-to-clojurescript --kebab-tags "$(pbpaste)"
(dom/div
 {}
 (ui/raised-button {:label "Secondary", :secondary true, :style style})
 (ui/raised-button {:label "Disabled", :disabled true, :style style}))

Now something more nested... 😉

<AppBar
    title="Title"
    iconElementLeft={<IconButton><NavigationClose /></IconButton>}
    iconElementRight={
      <IconMenu
        iconButtonElement={
          <IconButton><MoreVertIcon /></IconButton>
        }
        targetOrigin={{horizontal: 'right', vertical: 'top'}}
        anchorOrigin={{horizontal: 'right', vertical: 'top'}}
      >
        <MenuItem primaryText="Refresh" />
        <MenuItem primaryText="Help" />
        <MenuItem primaryText="Sign out" />
      </IconMenu>
    }
  />
 jsx-to-clojurescript --kebab-tags --kebab-attrs --ns "u" --target reagent --omit-empty-attrs "$(pbpaste)"
[u/app-bar
 {:title "Title",
  :icon-element-left [u/icon-button [u/navigation-close]],
  :icon-element-right
  [u/icon-menu
   {:icon-button-element [u/icon-button [u/more-vert-icon]],
    :target-origin {:horizontal "right", :vertical "top"},
    :anchor-origin {:horizontal "right", :vertical "top"}}
   [u/menu-item {:primary-text "Refresh"}]
   [u/menu-item {:primary-text "Help"}]
   [u/menu-item {:primary-text "Sign out"}]]}]

Conditions and anonymous functions are okay too! 😃

<div style={{width: '100%', maxWidth: 700, margin: 'auto'}}>
        <Stepper activeStep={stepIndex}>
          <Step>
            <StepLabel>Select campaign settings</StepLabel>
          </Step>
          <Step>
            <StepLabel>Create an ad group</StepLabel>
          </Step>
          <Step>
            <StepLabel>Create an ad</StepLabel>
          </Step>
        </Stepper>
        <div style={contentStyle}>
          {finished ? (
            <p>
              <a href="#"
                onClick={(event) => {
                  event.preventDefault();
                  this.setState({stepIndex: 0, finished: false});
                }}
              >
                Click here
              </a> to reset the example.
            </p>
          ) : (
            <div>
              <p>{this.getStepContent(stepIndex)}</p>
              <div style={{marginTop: 12}}>
                <FlatButton
                  label="Back"
                  disabled={stepIndex === 0}
                  onTouchTap={this.handlePrev}
                  style={{marginRight: 12}}
                />
                <RaisedButton
                  label={stepIndex === 2 ? 'Finish' : 'Next'}
                  primary={true}
                  onTouchTap={this.handleNext}
                />
              </div>
            </div>
          )}
        </div>
      </div>
jsx-to-clojurescript --kebab-tags --kebab-attrs --ns "u" --target reagent --omit-empty-attrs "$(pbpaste)"
[:div
 {:style {:width "100%", :max-width 700, :margin "auto"}}
 [u/stepper
  {:active-step step-index}
  [u/step [u/step-label "Select campaign settings"]]
  [u/step [u/step-label "Create an ad group"]]
  [u/step [u/step-label "Create an ad"]]]
 [:div
  {:style content-style}
  (if finished
    [:p
     [:a
      {:href "#",
       :on-click
             (fn [event]
               (prevent-default event)
               (r/set-state this {:step-index 0, :finished false}))}
      "Click here"]
     " to reset the example."]
    [:div
     [:p (get-step-content step-index)]
     [:div
      {:style {:margin-top 12}}
      [u/flat-button
       {:label        "Back",
        :disabled     (= step-index 0),
        :on-touch-tap handle-prev,
        :style        {:margin-right 12}}]
      [u/raised-button
       {:label        (if (= step-index 2) "Finish" "Next"),
        :primary      true,
        :on-touch-tap handle-next}]]])]]

Mapping? No problem! Notice how map doesn't require any more editing ☺️

<ul>
    {this.props.results.map(function(result) {
        return <ListItemWrapper data={result}/>;
    })}
</ul>
jsx-to-clojurescript --ns "" --target om "$(pbpaste)"
(dom/ul
 {}
 (map
    (fn [result]
        (ListItemWrapper {:data result}))
    (:results props)))

Still not impressed? We can do spread attributes too! 😀

<Animated.View
   {...this.state.panResponder.panHandlers}
   style={this.state.pan.getLayout()}>
   {this.props.children}
 </Animated.View>
jsx-to-clojurescript --ns "" --target om "$(pbpaste)"
(AnimatedView
 (merge
  (:pan-handlers (:pan-responder state))
  {:style (get-layout (:pan state))})
 (:children props))

Array of styles as a nice merge 😌

<View style={styles.container}>
   <View style={[styles.box, {width: this.state.w, height: this.state.h}]} />
</View>
jsx-to-clojurescript --target reagent "$(pbpaste)"
[ui/View
 {:style (:container styles)}
 [ui/View
  {:style
   (merge (:box styles) {:width (:w state), :height (:h state)})}]]

Reagent can do neat trick with ids and classes 😗

<div id="my-id" className="some-class some-other">
    <span className={styles.span}>
        <b className={"home"}>Home</b>
    </span>
</div>
jsx-to-clojurescript --kebab-attrs --target reagent "$(pbpaste)"
[:div#my-id.some-class.some-other
 {}
 [:span {:class-name (:span styles)} [:b.home {} "Home"]]]

LOL variable declarations and conditions?! 😂

< Navigator initialRoute = {
    {
      name: 'My First Scene',
      index: 0
    }
  }
  renderScene = {
    (route, navigator) =>
    < MySceneComponent
    name = {
      route.name
    }
    onForward = {
      () => {
        var nextIndex = route.index + 1,
          myOtherIndex = nextIndex + 10;

        navigator.push({
          name: 'Scene ' + nextIndex,
          index: nextIndex,
        });

        var yetAnotherIndex = myOtherIndex - 1;
      }
    }
    onBack = {
      () => {
        if (route.index > 0) {
          navigator.pop();
        } else if (route.index == 0) {
          someFuction();
          namingIsHardFun();
        } else {
        	var myGreatParam = 5;
          someOtherFunction(myGreatParam);
        }
      }
    }
    />
  }
  />
jsx-to-clojurescript --kebab-attrs --kebab-tags "$(pbpaste)"
(ui/navigator
  {:initial-route {:name "My First Scene", :index 0},
   :render-scene  (fn [route navigator]
                    (ui/my-scene-component
                      {:name (:name route),
                       :on-forward
                             (fn []
                               (let [next-index (+ (:index route) 1)
                                     my-other-index (+ next-index 10)
                                     yet-another-index (- my-other-index 1)]
                                 (push navigator {:name (+ "Scene " next-index), :index next-index}))),
                       :on-back
                             (fn []
                               (if (> (:index route) 0)
                                 (pop navigator)
                                 (if (= (:index route) 0)
                                   (do
                                     (some-fuction)
                                     (naming-is-hard-fun))
                                   (let [my-great-param 5]
                                     (some-other-function my-great-param)))))}))})

No problem with regular JS 😉

const myConst = {some: 34};

function explode(size) {
	var earth = "planet";
	var foo = "bar";
	return boom(earth) * size + myConst;
}

explode(42);
jsx-to-clojurescript "$(pbpaste)"
(do
  (def my-const {:some 34})
  (defn explode [size]
    (let [earth "planet"
          foo "bar"]
      (+ (* (boom earth) size) my-const)))
  (explode 42))

Alright folks, that's enough of examples, I guess you get the picture 😉. If you saw error like ERROR: Don't know how to handle node type <something> it means I haven't implmented some JS syntax yet. Open issue or PR :)

Library API

I won't write API here for 2 reasons:

  1. Not sure if this has many use cases as a library

  2. Core codebase is just ~200 very straightforward lines of code. You will get it very quickly, when you see it. (gotta love Clojure 💜)

If interested, library is extendable, you can easily add other targets other than Om/Reagent (with a single function!)