Add JSX Fragment syntax support (#6552)

* Add JSX Fragments to babel-types

* Support JSX fragments in the transform-react-jsx plugin

* Add tests JSX fragments

* Update helper-builder and transform plugin documentations for jsx fragment

* Add generator for jsx fragments

* Add test for jsx fragment generator

* Split jsx transform example into normal and fragment examples

* Remove unnecessary fields from ElementState in babel-helper-builder-react-jsx

* inline [skip ci]
This commit is contained in:
Clement Hoang
2017-11-03 07:43:48 -07:00
committed by Henry Zhu
parent 9e2828322e
commit 1a7194a22f
20 changed files with 322 additions and 29 deletions

View File

@@ -46,12 +46,76 @@ var profile = <div>
var dom = require("deku").dom;
var profile = dom( "div", null,
var profile = dom("div", null,
dom("img", { src: "avatar.png", className: "profile" }),
dom("h3", null, [user.firstName, user.lastName].join(" "))
);
```
### Fragments
Fragments are a feature available in React 16.2.0+.
#### React
**In**
```javascript
var descriptions = items.map(item => (
<>
<dt>{item.name}</dt>
<dd>{item.value}</dd>
</>
));
```
**Out**
```javascript
var descriptions = items.map(item => React.createElement(
React.Fragment,
null,
React.createElement("dt", null, item.name),
React.createElement("dd", null, item.value)
));
```
#### Custom
**In**
```javascript
/** @jsx dom */
/** @jsxFrag DomFrag */
var { dom, DomFrag } = require("deku"); // DomFrag is fictional!
var descriptions = items.map(item => (
<>
<dt>{item.name}</dt>
<dd>{item.value}</dd>
</>
));
```
**Out**
```javascript
/** @jsx dom */
/** @jsxFrag DomFrag */
var { dom, DomFrag } = require("deku"); // DomFrag is fictional!
var descriptions = items.map(item => dom(
DomFrag,
null,
dom("dt", null, item.name),
dom("dd", null, item.value)
));
```
Note that if a custom pragma is specified, then a custom fragment pragma must also be specified if the `<></>` is used. Otherwise, an error will be thrown.
## Installation
```sh
@@ -79,6 +143,7 @@ With options:
"plugins": [
["@babel/transform-react-jsx", {
"pragma": "dom", // default pragma is React.createElement
"pragmaFrag": "DomFrag", // default is React.Fragment
"throwIfNamespace": false // defaults to true
}]
]
@@ -109,6 +174,12 @@ Replace the function used when compiling JSX expressions.
Note that the `@jsx React.DOM` pragma has been deprecated as of React v0.12
### `pragmaFrag`
`string`, defaults to `React.Fragment`.
Replace the component used when compiling JSX fragments.
### `useBuiltIns`
`boolean`, defaults to `false`.

View File

@@ -2,11 +2,23 @@ import jsx from "@babel/plugin-syntax-jsx";
import helper from "@babel/helper-builder-react-jsx";
export default function({ types: t }, options) {
const pragma = options.pragma || "React.createElement";
const throwIfNamespace =
const THROW_IF_NAMESPACE =
options.throwIfNamespace === undefined ? true : !!options.throwIfNamespace;
const PRAGMA_DEFAULT = options.pragma || "React.createElement";
const PRAGMA_FRAG_DEFAULT = options.pragmaFrag || "React.Fragment";
const JSX_ANNOTATION_REGEX = /\*?\s*@jsx\s+([^\s]+)/;
const JSX_FRAG_ANNOTATION_REGEX = /\*?\s*@jsxFrag\s+([^\s]+)/;
// returns a closure that returns an identifier or memberExpression node
// based on the given id
const createIdentifierParser = (id: string) => () => {
return id
.split(".")
.map(name => t.identifier(name))
.reduce((object, property) => t.memberExpression(object, property));
};
const visitor = helper({
pre(state) {
@@ -23,27 +35,49 @@ export default function({ types: t }, options) {
state.callee = pass.get("jsxIdentifier")();
},
throwIfNamespace,
throwIfNamespace: THROW_IF_NAMESPACE,
});
visitor.Program = function(path, state) {
const { file } = state;
visitor.Program = {
enter(path, state) {
const { file } = state;
let id = pragma;
for (const comment of (file.ast.comments: Array<Object>)) {
const matches = JSX_ANNOTATION_REGEX.exec(comment.value);
if (matches) {
id = matches[1];
break;
let pragma = PRAGMA_DEFAULT;
let pragmaFrag = PRAGMA_FRAG_DEFAULT;
let pragmaSet = !!options.pragma;
let pragmaFragSet = !!options.pragmaFrag;
for (const comment of (file.ast.comments: Array<Object>)) {
const jsxMatches = JSX_ANNOTATION_REGEX.exec(comment.value);
if (jsxMatches) {
pragma = jsxMatches[1];
pragmaSet = true;
}
const jsxFragMatches = JSX_FRAG_ANNOTATION_REGEX.exec(comment.value);
if (jsxFragMatches) {
pragmaFrag = jsxFragMatches[1];
pragmaFragSet = true;
}
}
}
state.set("jsxIdentifier", () =>
id
.split(".")
.map(name => t.identifier(name))
.reduce((object, property) => t.memberExpression(object, property)),
);
state.set("jsxIdentifier", createIdentifierParser(pragma));
state.set("jsxFragIdentifier", createIdentifierParser(pragmaFrag));
state.set("usedFragment", false);
state.set("pragmaSet", pragmaSet);
state.set("pragmaFragSet", pragmaFragSet);
},
exit(path, state) {
if (
state.get("pragmaSet") &&
state.get("usedFragment") &&
!state.get("pragmaFragSet")
) {
throw new Error(
"transform-react-jsx: pragma has been set but " +
"pragmafrag has not been set",
);
}
},
};
visitor.JSXAttribute = function(path) {

View File

@@ -0,0 +1,12 @@
<div>
< >
<>
<span>Hello</span>
<span>world</span>
</>
<>
<span>Goodbye</span>
<span>world</span>
</>
</>
</div>

View File

@@ -0,0 +1 @@
React.createElement("div", null, React.createElement(React.Fragment, null, React.createElement(React.Fragment, null, React.createElement("span", null, "Hello"), React.createElement("span", null, "world")), React.createElement(React.Fragment, null, React.createElement("span", null, "Goodbye"), React.createElement("span", null, "world"))));

View File

@@ -0,0 +1,3 @@
/** @jsx dom */
<div>no fragment is used</div>

View File

@@ -0,0 +1,2 @@
/** @jsx dom */
dom("div", null, "no fragment is used");

View File

@@ -0,0 +1,4 @@
/** @jsx dom */
/** @jsxFrag DomFrag */
<></>

View File

@@ -0,0 +1,4 @@
/** @jsx dom */
/** @jsxFrag DomFrag */
dom(DomFrag, null);

View File

@@ -0,0 +1,2 @@
/** @jsx dom */
dom(React.Fragment, null);

View File

@@ -0,0 +1,3 @@
{
"throws": "transform-react-jsx: pragma has been set but pragmafrag has not been set"
}