Skip to content

Commit 768cce0

Browse files
authored
feature(format): support react fragment (#265)
* feature(format): support react fragment `react-element-to-jsx-string` could now render React fragments: ``` reactElementToJSXString( <> <h1>foo</h1> <p>bar</p> </> ) // Output: // <> // <h1>foo</h1> // <p>bar</p> // </> ``` See the React documentation for more informations on this feature: https://reactjs.org/docs/fragments.html * style(typo): Fix a typo in the `syntax` word
1 parent 1e50344 commit 768cce0

13 files changed

+350
-7
lines changed

Diff for: README.md

+12
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,18 @@ console.log(reactElementToJSXString(<div a="1" b="2">Hello, world!</div>));
116116

117117
Either to sort or not props. If you use this lib to make some isomorphic rendering you should set it to false, otherwise this would lead to react invalid checksums as the prop order is part of react isomorphic checksum algorithm.
118118

119+
**options.useFragmentShortSyntax: boolean, default true**
120+
121+
If true, fragment will be represented with the JSX short syntax `<>...</>` (when possible).
122+
123+
If false, fragment will always be represented with the JSX explicit syntax `<React.Fragment>...</React.Fragment>`.
124+
125+
According to [the specs](https://reactjs.org/docs/fragments.html):
126+
- A keyed fragment will always use the explicit syntax: `<React.Fragment key={...}>...</React.Fragment>`
127+
- An empty fragment will always use the explicit syntax: `<React.Fragment />`
128+
129+
Note: to use fragment you must use React >= 16.2
130+
119131
## Environment requirements
120132

121133
The environment you use to use `react-element-to-jsx-string` should have [ES2015](https://babeljs.io/learn-es2015/) support.

Diff for: src/formatter/formatProp.spec.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ describe('formatProp', () => {
115115
expect(formatPropValue).toHaveBeenCalledWith(false, true, 0, options);
116116
});
117117

118-
it('should format a truthy boolean prop (with explicit synthax)', () => {
118+
it('should format a truthy boolean prop (with explicit syntax)', () => {
119119
const options = {
120120
useBooleanShorthandSyntax: false,
121121
tabStop: 2,
@@ -135,7 +135,7 @@ describe('formatProp', () => {
135135
expect(formatPropValue).toHaveBeenCalledWith(true, true, 0, options);
136136
});
137137

138-
it('should format a falsy boolean prop (with explicit synthax)', () => {
138+
it('should format a falsy boolean prop (with explicit syntax)', () => {
139139
const options = {
140140
useBooleanShorthandSyntax: false,
141141
tabStop: 2,

Diff for: src/formatter/formatReactElementNode.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ export default (
196196
out += childrens
197197
.reduce(mergeSiblingPlainStringChildrenReducer, [])
198198
.map(formatOneChildren(inline, newLvl, options))
199-
.join(`\n${spacer(newLvl, tabStop)}`);
199+
.join(!inline ? `\n${spacer(newLvl, tabStop)}` : '');
200200

201201
if (!inline) {
202202
out += '\n';

Diff for: src/formatter/formatReactFragmentNode.js

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/* @flow */
2+
3+
import type { Key } from 'react';
4+
import formatReactElementNode from './formatReactElementNode';
5+
import type { Options } from './../options';
6+
import type {
7+
ReactElementTreeNode,
8+
ReactFragmentTreeNode,
9+
TreeNode,
10+
} from './../tree';
11+
12+
const REACT_FRAGMENT_TAG_NAME_SHORT_SYNTAX = '';
13+
const REACT_FRAGMENT_TAG_NAME_EXPLICIT_SYNTAX = 'React.Fragment';
14+
15+
const toReactElementTreeNode = (
16+
displayName: string,
17+
key: ?Key,
18+
childrens: TreeNode[]
19+
): ReactElementTreeNode => {
20+
let props = {};
21+
if (key) {
22+
props = { key };
23+
}
24+
25+
return {
26+
type: 'ReactElement',
27+
displayName,
28+
props,
29+
defaultProps: {},
30+
childrens,
31+
};
32+
};
33+
34+
const isKeyedFragment = ({ key }: ReactFragmentTreeNode) => Boolean(key);
35+
const hasNoChildren = ({ childrens }: ReactFragmentTreeNode) =>
36+
childrens.length === 0;
37+
38+
export default (
39+
node: ReactFragmentTreeNode,
40+
inline: boolean,
41+
lvl: number,
42+
options: Options
43+
): string => {
44+
const { type, key, childrens } = node;
45+
46+
if (type !== 'ReactFragment') {
47+
throw new Error(
48+
`The "formatReactFragmentNode" function could only format node of type "ReactFragment". Given: ${
49+
type
50+
}`
51+
);
52+
}
53+
54+
const { useFragmentShortSyntax } = options;
55+
56+
let displayName;
57+
if (useFragmentShortSyntax) {
58+
if (hasNoChildren(node) || isKeyedFragment(node)) {
59+
displayName = REACT_FRAGMENT_TAG_NAME_EXPLICIT_SYNTAX;
60+
} else {
61+
displayName = REACT_FRAGMENT_TAG_NAME_SHORT_SYNTAX;
62+
}
63+
} else {
64+
displayName = REACT_FRAGMENT_TAG_NAME_EXPLICIT_SYNTAX;
65+
}
66+
67+
return formatReactElementNode(
68+
toReactElementTreeNode(displayName, key, childrens),
69+
inline,
70+
lvl,
71+
options
72+
);
73+
};

Diff for: src/formatter/formatReactFragmentNode.spec.js

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/* @flow */
2+
3+
import formatReactFragmentNode from './formatReactFragmentNode';
4+
5+
const defaultOptions = {
6+
filterProps: [],
7+
showDefaultProps: true,
8+
showFunctions: false,
9+
tabStop: 2,
10+
useBooleanShorthandSyntax: true,
11+
useFragmentShortSyntax: true,
12+
sortProps: true,
13+
};
14+
15+
describe('formatReactFragmentNode', () => {
16+
it('should format a react fragment with a string as children', () => {
17+
const tree = {
18+
type: 'ReactFragment',
19+
childrens: [
20+
{
21+
value: 'Hello world',
22+
type: 'string',
23+
},
24+
],
25+
};
26+
27+
expect(formatReactFragmentNode(tree, false, 0, defaultOptions)).toEqual(
28+
`<>
29+
Hello world
30+
</>`
31+
);
32+
});
33+
34+
it('should format a react fragment with a key', () => {
35+
const tree = {
36+
type: 'ReactFragment',
37+
key: 'foo',
38+
childrens: [
39+
{
40+
value: 'Hello world',
41+
type: 'string',
42+
},
43+
],
44+
};
45+
46+
expect(formatReactFragmentNode(tree, false, 0, defaultOptions)).toEqual(
47+
`<React.Fragment key="foo">
48+
Hello world
49+
</React.Fragment>`
50+
);
51+
});
52+
53+
it('should format a react fragment with multiple childrens', () => {
54+
const tree = {
55+
type: 'ReactFragment',
56+
childrens: [
57+
{
58+
type: 'ReactElement',
59+
displayName: 'div',
60+
props: { a: 'foo' },
61+
childrens: [],
62+
},
63+
{
64+
type: 'ReactElement',
65+
displayName: 'div',
66+
props: { b: 'bar' },
67+
childrens: [],
68+
},
69+
],
70+
};
71+
72+
expect(formatReactFragmentNode(tree, false, 0, defaultOptions)).toEqual(
73+
`<>
74+
<div a="foo" />
75+
<div b="bar" />
76+
</>`
77+
);
78+
});
79+
80+
it('should format an empty react fragment', () => {
81+
const tree = {
82+
type: 'ReactFragment',
83+
childrens: [],
84+
};
85+
86+
expect(formatReactFragmentNode(tree, false, 0, defaultOptions)).toEqual(
87+
'<React.Fragment />'
88+
);
89+
});
90+
91+
it('should format an empty react fragment with key', () => {
92+
const tree = {
93+
type: 'ReactFragment',
94+
key: 'foo',
95+
childrens: [],
96+
};
97+
98+
expect(formatReactFragmentNode(tree, false, 0, defaultOptions)).toEqual(
99+
'<React.Fragment key="foo" />'
100+
);
101+
});
102+
103+
it('should format a react fragment using the explicit syntax', () => {
104+
const tree = {
105+
type: 'ReactFragment',
106+
childrens: [
107+
{
108+
value: 'Hello world',
109+
type: 'string',
110+
},
111+
],
112+
};
113+
114+
expect(
115+
formatReactFragmentNode(tree, false, 0, {
116+
...defaultOptions,
117+
...{ useFragmentShortSyntax: false },
118+
})
119+
).toEqual(
120+
`<React.Fragment>
121+
Hello world
122+
</React.Fragment>`
123+
);
124+
});
125+
});

Diff for: src/formatter/formatTreeNode.js

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* @flow */
22

33
import formatReactElementNode from './formatReactElementNode';
4+
import formatReactFragmentNode from './formatReactFragmentNode';
45
import type { Options } from './../options';
56
import type { TreeNode } from './../tree';
67

@@ -49,5 +50,9 @@ export default (
4950
return formatReactElementNode(node, inline, lvl, options);
5051
}
5152

53+
if (node.type === 'ReactFragment') {
54+
return formatReactFragmentNode(node, inline, lvl, options);
55+
}
56+
5257
throw new TypeError(`Unknow format type "${node.type}"`);
5358
};

Diff for: src/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const reactElementToJsxString = (
1414
functionValue,
1515
tabStop = 2,
1616
useBooleanShorthandSyntax = true,
17+
useFragmentShortSyntax = true,
1718
sortProps = true,
1819
maxInlineAttributesLineLength,
1920
displayName,
@@ -30,6 +31,7 @@ const reactElementToJsxString = (
3031
functionValue,
3132
tabStop,
3233
useBooleanShorthandSyntax,
34+
useFragmentShortSyntax,
3335
sortProps,
3436
maxInlineAttributesLineLength,
3537
displayName,

Diff for: src/index.spec.js

+56-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
/* eslint-disable react/no-string-refs */
44

5-
import React from 'react';
5+
import React, { Fragment } from 'react';
66
import { createRenderer } from 'react-test-renderer/shallow';
77
import reactElementToJSXString from './index';
88
import AnonymousStatelessComponent from './AnonymousStatelessComponent';
@@ -1032,4 +1032,59 @@ describe('reactElementToJSXString(ReactElement)', () => {
10321032
'<div fn={function noRefCheck() {}} />'
10331033
);
10341034
});
1035+
1036+
it('reactElementToJSXString(<Fragment><h1>foo</h1><p>bar</p></Fragment>)', () => {
1037+
expect(
1038+
reactElementToJSXString(
1039+
<Fragment>
1040+
<h1>foo</h1>
1041+
<p>bar</p>
1042+
</Fragment>
1043+
)
1044+
).toEqual(
1045+
`<>
1046+
<h1>
1047+
foo
1048+
</h1>
1049+
<p>
1050+
bar
1051+
</p>
1052+
</>`
1053+
);
1054+
});
1055+
1056+
it('reactElementToJSXString(<Fragment key="foo"><div /><div /></Fragment>)', () => {
1057+
expect(
1058+
reactElementToJSXString(
1059+
<Fragment key="foo">
1060+
<div />
1061+
<div />
1062+
</Fragment>
1063+
)
1064+
).toEqual(
1065+
`<React.Fragment key="foo">
1066+
<div />
1067+
<div />
1068+
</React.Fragment>`
1069+
);
1070+
});
1071+
1072+
it('reactElementToJSXString(<Fragment />)', () => {
1073+
expect(reactElementToJSXString(<Fragment />)).toEqual(`<React.Fragment />`);
1074+
});
1075+
1076+
it('reactElementToJSXString(<div render={<Fragment><div /><div /></Fragment>} />)', () => {
1077+
expect(
1078+
reactElementToJSXString(
1079+
<div
1080+
render={
1081+
<Fragment>
1082+
<div />
1083+
<div />
1084+
</Fragment>
1085+
}
1086+
/>
1087+
)
1088+
).toEqual(`<div render={<><div /><div /></>} />`);
1089+
});
10351090
});

Diff for: src/options.js

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type Options = {|
99
functionValue: Function,
1010
tabStop: number,
1111
useBooleanShorthandSyntax: boolean,
12+
useFragmentShortSyntax: boolean,
1213
sortProps: boolean,
1314

1415
maxInlineAttributesLineLength?: number,

Diff for: src/parser/parseReactElement.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
/* @flow */
22

3-
import React, { type Element as ReactElement } from 'react';
3+
import React, { type Element as ReactElement, Fragment } from 'react';
44
import type { Options } from './../options';
55
import {
66
createStringTreeNode,
77
createNumberTreeNode,
88
createReactElementTreeNode,
9+
createReactFragmentTreeNode,
910
} from './../tree';
1011
import type { TreeNode } from './../tree';
1112

13+
const supportFragment = Boolean(Fragment);
14+
1215
const getReactElementDisplayName = (element: ReactElement<*>): string =>
1316
element.type.displayName ||
1417
element.type.name || // function name
@@ -68,6 +71,10 @@ const parseReactElement = (
6871
.filter(onlyMeaningfulChildren)
6972
.map(child => parseReactElement(child, options));
7073

74+
if (supportFragment && element.type === Fragment) {
75+
return createReactFragmentTreeNode(key, childrens);
76+
}
77+
7178
return createReactElementTreeNode(
7279
displayName,
7380
props,

0 commit comments

Comments
 (0)