Skip to content

Instantly share code, notes, and snippets.

@lygaret
Last active January 18, 2023 17:26
Show Gist options
  • Save lygaret/a68220defa69174bdec5 to your computer and use it in GitHub Desktop.
Save lygaret/a68220defa69174bdec5 to your computer and use it in GitHub Desktop.
ES6 Quasi-Literal for JSX

JSX Quasi-Literal

I've been struggling to get the JSX transpiler playing nicely with the traceur compiler, specifically the flags hidden behind --experimental.

The problem is that the since both the JSX transpiler and the traceur compiler are actually parsing the full javascript AST, they would have to mutually agree on the syntax extensions you use: traceur can't parse the faux-xml syntax JSX adds, and JSX can't parse the async or await keywords, for example, or generator functions.

This proof-of-concept is a potential solution: instead of using an external JSX transpiler, we'll parse the faux-xml ourselves, using an ES6 feature called quasi-literals.

Example

define(function(require) {

    var React   = require('react');
    var jsx     = require('lib/jsxquasi');

    var EchoComponent = React.createClass({
        getInitialState: function() {
            return { value: '' };
        },

        handleChange: function() {
            this.setState({ value: this.refs.input.getDOMNode().value });
        },

        render: function() {
            return jsx`
                <div>
                    <input 
                        ref='input' 
                        onChange='${this.handleChange}' 
                        defaultValue='${this.state.value}' />
                    ${this.state.value}
                </div>
            `;
        }
    })

    return function() {
        var comp = jsx`<${EchoComponent} />`;
        React.renderComponent(comp, document.body);
    };
});

A couple of things to notice:

  1. This is valid javascript! Or harmony or es6 or whatever, but importantly, it's not happening outside the js environment. This also allows us to use our standard tooling: the traceur compiler knows how to turn jsx`<div>Hello</div>`; into the equivalent browser compatible es3, and hence we can use anything the traceur compile accepts!

  2. This is not exactly the same as JSX according to the spec: it includes quotes around the attributes, etc. This is because this parser is based on DOMParser, and hence needs to be valid XML. It would be straighforward though to change it so it matched exactly, or to remove the browser dependency (so it could run on the server, eg.)

define(function(require) {
var React = require('react');
var paramRegex = /__(\d)+/;
var parser = new DOMParser();
var errorDoc = parser.parseFromString('INVALID', 'text/xml');
var errorNs = errorDoc.getElementsByTagName("parsererror")[0].namespaceURI;
// turns the array of string parts into a DOM
// throws if the result is an invalid XML document.
function quasiToDom(parts) {
// turn ["<div class='", "'>Hi</div>"]
// into "<div class='__0'>Hi</div>"
var xmlstr = parts.reduce((xmlstr, part, i) => {
xmlstr += part;
if (i != parts.length - 1) { // the last part has no ending param
xmlstr += `__${i}`;
}
return xmlstr;
}, "");
// parse into DOM, check for a parse error
// browser's DOMParser is neat, but error handling is awful
var doc = parser.parseFromString(xmlstr, 'text/xml');
var errors = doc.getElementsByTagNameNS(errorNs, 'parsererror');
var error = '';
if (errors.length > 0) {
error = errors[0].textContent.split('\n')[0];
throw `invalid jsx: ${error}\n${xmlstr}`;
}
return doc;
}
// turn a document into a tree of react components
// replaces tags, attribute values and text nodes that look like the param
// placeholder we add above, with the values from the parameters array.
function domToReact(node, params) {
var match;
// text node, comment, etc
if (node.nodeValue) {
var value = node.nodeValue.trim();
if (value.length === 0) {
return undefined;
}
match = value.match(paramRegex);
return match ? params[parseInt(match[1])] : value;
}
// node to get react for
// if the node name is a placeholder, assume the param is a component class
var reactNode;
match = node.localName.match(paramRegex)
reactNode = match ? params[parseInt(match[1])] : React.DOM[node.localName];
// if we don't have a component, give a better error message
if (reactNode === undefined) {
throw `Unknown React component: ${node.localName}, bailing.`;
}
// attributes of the node
var reactAttrs = {};
for (var i = node.attributes.length - 1; i >= 0; i--) {
var attr = node.attributes[i];
reactAttrs[attr.name] = attr.value;
match = attr.value.match(paramRegex);
if (match) {
reactAttrs[attr.name] = params[parseInt(match[1])];
}
}
// recursively turn children into react components
var reactChildren = [];
for (var i = 0; i < node.childNodes.length; i++) {
var child = node.childNodes[i];
var reactChild = domToReact(child, params);
if (reactChild) {
reactChildren.push(reactChild);
}
}
return reactNode(reactAttrs, reactChildren);
}
return function jsx(parts, ...params) {
var doc = quasiToDom(parts);
var react = domToReact(doc.firstChild, params);
return react;
}
});
@af
Copy link

af commented Jul 14, 2014

Very cool! BTW DOMParser apparently does support parsing html (in most browsers anyways), so you could go that way if you wanted to drop the quote requirement. The html parser wraps things in <html><body>, so you'd want to return the contents of the body.

@dagingaa
Copy link

Do you have any plans for this going forward? Publish as a proper repo maybe? I'm using it for testing right now together with SystemJS and React for a nice workflow, and it seems to work quite nicely.

@trueadm
Copy link

trueadm commented Jun 11, 2015

I didn't even realise this project existed till a college today pointed out this gist to me. I wrote a library that did very much the same thing - https://github.com/trueadm/t7. It also works with other Virtual DOM frameworks/libraries.

@lygaret
Copy link
Author

lygaret commented Jul 30, 2015

@trueadm nice! I did this, used something similar for a while at work, but haven't been doing React for a while (pulled into an angular project) and forgot I had this out there! Glad we ended up with the same thought!

@dagingaa sorry I didn't see your comment; I didn't then nor now have the motivation to take this past proof of concept, but if you ended up doing something more serious with this, I'd love to see it 😸

@af (a year later 😉) Sorry I didn't see your comment, but great that DOMParser handles html; I recall trying and having it not work that well, but it was a year ago now and I don't remember exactly.

I think I'm gonna try and dust this off a bit, I'm inspired that people found it useful!

@Cekun
Copy link

Cekun commented Apr 12, 2017

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