react-jade-transformer

Really clean transforms from Jade to React in your Javascript

Usage no npm install needed!

<script type="module">
  import reactJadeTransformer from 'https://cdn.skypack.dev/react-jade-transformer';
</script>

README

React Jade Transformer

Yeah, another one... This library is a no frills transformer from Jade Markup to React components, because I wanted one that works the way I want it to. It uses the Jade Parser, Esprima, and escodegen under the hood, so parsing and results should be very true to their initial implementation.

WARNING

This is an early implementation. Not all features of Jade are transformed into something sensible in React yet.

Currently Supported:

  • Elements
  • Mixins as Components
  • Buffered Output
  • Each/For
  • If/Else/Unless/Else If
  • Switch
  • Classes

Transformation Reference

All Jade Transformations would be written inside rj tagged with backticks, for example rjspan.spanClass. This reference is written with the assumption that you know Jade and React syntax.

Basic Structure

All standard DOM elements will transform to a React.DOM call.

span.spanClass
React.DOM.span({ className: 'spanClass ' });

Attributes render as properties.

.component#someId(data-foo="bar")
React.DOM.div({
    'id': 'someId',
    'data-foo': 'bar',
    className: 'component '
});

Jade Classes will get mixed with a class property, so you can use shorthand classes in conjunction with classSet or equivalent.

.component(class=React.addons.classSet(classesObj))
React.DOM.div({ className: 'component ' + React.addons.classSet(classesObj) });

Object Literals can be passed in for properties such as style.

.component(style={width: 234})
React.DOM.div({
    'style': { width: 234 },
    className: 'component '
});

Jade does NOT support objects being passed to standard DOM elements (only mixins), so this will not work as you expect:

.component(propsObj)
React.DOM.div({
    'propsObj': true,
    className: 'component '
});

Nesting Children and Strings works as you would expect:

.component
  h1.component__title This is the title
React.DOM.div({ className: 'component ' },
  React.DOM.h1({ className: 'component__title ' }, 'This is the title')
);

Variables

Variables can be output using the standard Jade syntax for buffered output.

.component
  h1.component__title= this.props.title
React.DOM.div({ className: 'component ' },
  React.DOM.h1({ className: 'component__title ' }, this.props.title)
);

This would also work for breaking up components as variables, ie:

var title = rj`h1.component__title= this.props.title`

rj`
.component
  = title
`
var title = React.DOM.h1({ className: 'component__title ' }, this.props.title);

React.DOM.div({ className: 'component ' }, title);

If Conditionals

If/Unless Conditionals are rendered as ternary statements.

.component
  if this.props.title
    h1.component__title= this.props.title
  else
    h2 No title Provided
React.DOM.div({ className: 'component ' },
  this.props.title
    ? React.DOM.h1({ className: 'component__title ' }, this.props.title)
    : React.DOM.h2({}, 'No title Provided')
);

Be aware that you can ONLY return a single element from an If or Else Conditional:

.component
  if this.props.title
    h1.component__title= this.props.title
    h2 This won't render
  else
    div
      h1 These will both render
      h2 So Will This
React.DOM.div({ className: 'component ' },
  this.props.title
    ? React.DOM.h1({ className: 'component__title ' }, this.props.title)
    : React.DOM.div({},
        React.DOM.h1({}, 'These will both render'),
        React.DOM.h2({}, 'So Will This')
      )
);

Case Conditionals

Cases create self calling functions that run a switch statement, so you can do longer cases within your components. The function is bound to this so you can still access component methods.

case someVar.length
  when 0
    h1 Nothing Here
  when 1
    h1 One Thing Here
  default
    h1 Lots of things here
    h2 This will never be reached.
(function () {
    switch (someVar.length) {
    case 0:
        return React.DOM.h1({}, 'Nothing Here');
    case 1:
        return React.DOM.h1({}, 'One Thing Here');
    default:
        return React.DOM.h1({}, 'Lots of things here');
    }
}.bind(this));

Loops

each is supported as a standard map function. while is not supported. The this variable is bound to the map function.

ul.items
  each thing, ix in things
    li.item(key=ix)= thing
React.DOM.ul({ className: 'items ' },
  things.map(function (thing, ix) {
    return React.DOM.li({
        'key': ix,
        className: 'item '
    }, thing);
  }, this)
);

Calling Components

Just like JSX, you can call React Component Classes without a factory, by making use of Jade's mixin syntax.

+Component(
  onClick=this.onClick
)
React.createElement(Component, { 'onClick': this.onClick });

Unlike DOM elements, you can provide a single argument as the properties, allowing you to pass along props without writing out every single property.

+Component(_.assign(this.props, {
  "foo": "bar"
}))
React.createElement(Component, _.assign(this.props, { 'foo': 'bar' }));

Components take children same as elements.

+Component(onClick=this.onClick)
  p Some Children
  p Some More Children
React.createElement(Component, { 'onClick': this.onClick },
  React.DOM.p({}, 'Some Children'),
  React.DOM.p({}, 'Some More Children')
);

&attributes

One of the limitations of Jade is that you can't provide single arguments to DOM elements, for example:

div(propertiesObject)

...does not work, as the parser does not recognise it. The transformer supports &attributes to allow you to pass an extra property object to a DOM element or a component mixin:

+Component(onClick=this.onClick)&attributes(this.props)
  div&attributes({className: "blue")
  p Some More Children
React.createElement(Component, Object.assign({ 'onClick': this.onClick }, this.props),
  React.DOM.div({className: "blue"}),
  React.DOM.p({}, 'Some More Children')
);

This transformation makes use of the Object.assign functionality, so the assumption is there that you'll shim Object.assign until Harmony is supported. This feels like a safe shim to work with, unlikely to drift in functionality from the specification.

Unescaped HTML

Unescaped variables will be transformed into the standard property used for React's unescaped HTML.

article.article
  section.article__content!= this.renderedHTML

 -- or --

article.article
  section.article__content
    != this.renderedHTML
React.DOM.article({
  className: "article"
}, React.DOM.section({
  dangerouslySetInnerHTML: {__html: this.renderedHTML}
});

Extra children can not be provided when using unescaped HTML.

On the Roadmap

This is a pretty early implementation so I'm not sure when I'll consider it 'done'. Currently in use on some projects, I'm implementing things as I hit the use case, so file an issue if you feel like something can clearly be added to the transform. Things I can see coming:

  • ability to use variable assignment (this might be more complex than I want to support though, as we start building more closures and what not to make it work)

Usage

Install with npm

npm install --save-dev react-jade-transformer

In Code

Write standard Javascript, (even ES6 transpiled Javascript), and use a special tag (rj`) to denote Jade:

var SimpleComponent = React.createClass({
  exampleProps: {
    title: "The Title",
    subtitle: "The Subtitle"
  },
  render: function() {
    return rj`
      div.component.component--modifier
        h3.component__title= this.props.title
        p= this.props.children
    `
  }
});

The Transformer will process any code in between the rj`` backticks.

Browserify

var reactJade = require("react-jade-transformer");

var bundler = browserify({
  entries: ["./code.js"]
}).transform(reactJade.browserify());

If you wish to use the transformer in conjunction with another transpiler, for example ES6, Coffeescript or Typescript, you can break up the transform into two steps.

var reactJade = require("react-jade-transformer");
var babelify = require("babelify"); // es6 transformer

var bundler = browserify({
  entries: ["./code.js"]
}).transform(reactJade.browserify({prepare: true}))
  .transform(babelify)
  .transform(reactJade.browserify({transform: true}));

The prepare step converts any rj`` step into a function call:

// newlines broken for readability
___reactJadeTransform("\n
  div.component.component--modifier\n
    h3.component__title= this.props.title\n
    p= this.props.children\n
", this)

The transform step converts any call to "___reactJadeTransform" into a React Component. With this approach, the function call should survive through any intermediate steps by conventional transpilers, as it is just a standard Javascript function call with a string argument and this attached. The latter argument is used so the transformer can keep up with arrow function assignment of the this variable.

That said though, the transformation is now isolated to JUST the reactJadeTransforms themselves, so often you can do all React Jade Transformations before any other parsers, so long as the next step is the chain supports standard Javascript syntax.

Gulp

var reactJade = require("react-jade-transformer");

gulp.task("node", function() {
  return gulp.src("./test/*.js")
             .pipe(reactJade.gulp())
             .pipe(gulp.dest("./.tmp/testout"));
});

The Gulp Transformer supports the same options as Browserify:

var reactJade = require("react-jade-transformer");
var coffee = require("gulp-coffee");

gulp.task("node", function() {
  return gulp.src("./test/*.js")
             .pipe(reactJade.gulp({prepare: true}))
             .pipe(coffee())
             .pipe(reactJade.gulp({transform: true}))
             .pipe(gulp.dest("./.tmp/testout"));
});

Babel Plugin

Transformation must be done before the babel transform, so for example, using Babel directly you would do the following:

var babel = require("babel");
var reactJade = require("react-jade-transformer");

babel.transform("code", {
  plugins: [
    {transformer: reactJade.babel, position: "before"},
  ]
});

In this way, React Jade will play nice with any other transformations you have.

License

Copyright (c) 2015 Offsider, used under The MIT License (MIT)

License provided in LICENSE.md