Skip to content

Commit

Permalink
feature(format): support react fragment (#265)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
armandabric authored Mar 14, 2018
1 parent 1e50344 commit 768cce0
Show file tree
Hide file tree
Showing 13 changed files with 350 additions and 7 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,18 @@ console.log(reactElementToJSXString(<div a="1" b="2">Hello, world!</div>));

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.

**options.useFragmentShortSyntax: boolean, default true**

If true, fragment will be represented with the JSX short syntax `<>...</>` (when possible).

If false, fragment will always be represented with the JSX explicit syntax `<React.Fragment>...</React.Fragment>`.

According to [the specs](https://reactjs.org/docs/fragments.html):
- A keyed fragment will always use the explicit syntax: `<React.Fragment key={...}>...</React.Fragment>`
- An empty fragment will always use the explicit syntax: `<React.Fragment />`

Note: to use fragment you must use React >= 16.2

## Environment requirements

The environment you use to use `react-element-to-jsx-string` should have [ES2015](https://babeljs.io/learn-es2015/) support.
Expand Down
4 changes: 2 additions & 2 deletions src/formatter/formatProp.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ describe('formatProp', () => {
expect(formatPropValue).toHaveBeenCalledWith(false, true, 0, options);
});

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

it('should format a falsy boolean prop (with explicit synthax)', () => {
it('should format a falsy boolean prop (with explicit syntax)', () => {
const options = {
useBooleanShorthandSyntax: false,
tabStop: 2,
Expand Down
2 changes: 1 addition & 1 deletion src/formatter/formatReactElementNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ export default (
out += childrens
.reduce(mergeSiblingPlainStringChildrenReducer, [])
.map(formatOneChildren(inline, newLvl, options))
.join(`\n${spacer(newLvl, tabStop)}`);
.join(!inline ? `\n${spacer(newLvl, tabStop)}` : '');

if (!inline) {
out += '\n';
Expand Down
73 changes: 73 additions & 0 deletions src/formatter/formatReactFragmentNode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/* @flow */

import type { Key } from 'react';
import formatReactElementNode from './formatReactElementNode';
import type { Options } from './../options';
import type {
ReactElementTreeNode,
ReactFragmentTreeNode,
TreeNode,
} from './../tree';

const REACT_FRAGMENT_TAG_NAME_SHORT_SYNTAX = '';
const REACT_FRAGMENT_TAG_NAME_EXPLICIT_SYNTAX = 'React.Fragment';

const toReactElementTreeNode = (
displayName: string,
key: ?Key,
childrens: TreeNode[]
): ReactElementTreeNode => {
let props = {};
if (key) {
props = { key };
}

return {
type: 'ReactElement',
displayName,
props,
defaultProps: {},
childrens,
};
};

const isKeyedFragment = ({ key }: ReactFragmentTreeNode) => Boolean(key);
const hasNoChildren = ({ childrens }: ReactFragmentTreeNode) =>
childrens.length === 0;

export default (
node: ReactFragmentTreeNode,
inline: boolean,
lvl: number,
options: Options
): string => {
const { type, key, childrens } = node;

if (type !== 'ReactFragment') {
throw new Error(
`The "formatReactFragmentNode" function could only format node of type "ReactFragment". Given: ${
type
}`
);
}

const { useFragmentShortSyntax } = options;

let displayName;
if (useFragmentShortSyntax) {
if (hasNoChildren(node) || isKeyedFragment(node)) {
displayName = REACT_FRAGMENT_TAG_NAME_EXPLICIT_SYNTAX;
} else {
displayName = REACT_FRAGMENT_TAG_NAME_SHORT_SYNTAX;
}
} else {
displayName = REACT_FRAGMENT_TAG_NAME_EXPLICIT_SYNTAX;
}

return formatReactElementNode(
toReactElementTreeNode(displayName, key, childrens),
inline,
lvl,
options
);
};
125 changes: 125 additions & 0 deletions src/formatter/formatReactFragmentNode.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/* @flow */

import formatReactFragmentNode from './formatReactFragmentNode';

const defaultOptions = {
filterProps: [],
showDefaultProps: true,
showFunctions: false,
tabStop: 2,
useBooleanShorthandSyntax: true,
useFragmentShortSyntax: true,
sortProps: true,
};

describe('formatReactFragmentNode', () => {
it('should format a react fragment with a string as children', () => {
const tree = {
type: 'ReactFragment',
childrens: [
{
value: 'Hello world',
type: 'string',
},
],
};

expect(formatReactFragmentNode(tree, false, 0, defaultOptions)).toEqual(
`<>
Hello world
</>`
);
});

it('should format a react fragment with a key', () => {
const tree = {
type: 'ReactFragment',
key: 'foo',
childrens: [
{
value: 'Hello world',
type: 'string',
},
],
};

expect(formatReactFragmentNode(tree, false, 0, defaultOptions)).toEqual(
`<React.Fragment key="foo">
Hello world
</React.Fragment>`
);
});

it('should format a react fragment with multiple childrens', () => {
const tree = {
type: 'ReactFragment',
childrens: [
{
type: 'ReactElement',
displayName: 'div',
props: { a: 'foo' },
childrens: [],
},
{
type: 'ReactElement',
displayName: 'div',
props: { b: 'bar' },
childrens: [],
},
],
};

expect(formatReactFragmentNode(tree, false, 0, defaultOptions)).toEqual(
`<>
<div a="foo" />
<div b="bar" />
</>`
);
});

it('should format an empty react fragment', () => {
const tree = {
type: 'ReactFragment',
childrens: [],
};

expect(formatReactFragmentNode(tree, false, 0, defaultOptions)).toEqual(
'<React.Fragment />'
);
});

it('should format an empty react fragment with key', () => {
const tree = {
type: 'ReactFragment',
key: 'foo',
childrens: [],
};

expect(formatReactFragmentNode(tree, false, 0, defaultOptions)).toEqual(
'<React.Fragment key="foo" />'
);
});

it('should format a react fragment using the explicit syntax', () => {
const tree = {
type: 'ReactFragment',
childrens: [
{
value: 'Hello world',
type: 'string',
},
],
};

expect(
formatReactFragmentNode(tree, false, 0, {
...defaultOptions,
...{ useFragmentShortSyntax: false },
})
).toEqual(
`<React.Fragment>
Hello world
</React.Fragment>`
);
});
});
5 changes: 5 additions & 0 deletions src/formatter/formatTreeNode.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* @flow */

import formatReactElementNode from './formatReactElementNode';
import formatReactFragmentNode from './formatReactFragmentNode';
import type { Options } from './../options';
import type { TreeNode } from './../tree';

Expand Down Expand Up @@ -49,5 +50,9 @@ export default (
return formatReactElementNode(node, inline, lvl, options);
}

if (node.type === 'ReactFragment') {
return formatReactFragmentNode(node, inline, lvl, options);
}

throw new TypeError(`Unknow format type "${node.type}"`);
};
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const reactElementToJsxString = (
functionValue,
tabStop = 2,
useBooleanShorthandSyntax = true,
useFragmentShortSyntax = true,
sortProps = true,
maxInlineAttributesLineLength,
displayName,
Expand All @@ -30,6 +31,7 @@ const reactElementToJsxString = (
functionValue,
tabStop,
useBooleanShorthandSyntax,
useFragmentShortSyntax,
sortProps,
maxInlineAttributesLineLength,
displayName,
Expand Down
57 changes: 56 additions & 1 deletion src/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

import React from 'react';
import React, { Fragment } from 'react';
import { createRenderer } from 'react-test-renderer/shallow';
import reactElementToJSXString from './index';
import AnonymousStatelessComponent from './AnonymousStatelessComponent';
Expand Down Expand Up @@ -1032,4 +1032,59 @@ describe('reactElementToJSXString(ReactElement)', () => {
'<div fn={function noRefCheck() {}} />'
);
});

it('reactElementToJSXString(<Fragment><h1>foo</h1><p>bar</p></Fragment>)', () => {
expect(
reactElementToJSXString(
<Fragment>
<h1>foo</h1>
<p>bar</p>
</Fragment>
)
).toEqual(
`<>
<h1>
foo
</h1>
<p>
bar
</p>
</>`
);
});

it('reactElementToJSXString(<Fragment key="foo"><div /><div /></Fragment>)', () => {
expect(
reactElementToJSXString(
<Fragment key="foo">
<div />
<div />
</Fragment>
)
).toEqual(
`<React.Fragment key="foo">
<div />
<div />
</React.Fragment>`
);
});

it('reactElementToJSXString(<Fragment />)', () => {
expect(reactElementToJSXString(<Fragment />)).toEqual(`<React.Fragment />`);
});

it('reactElementToJSXString(<div render={<Fragment><div /><div /></Fragment>} />)', () => {
expect(
reactElementToJSXString(
<div
render={
<Fragment>
<div />
<div />
</Fragment>
}
/>
)
).toEqual(`<div render={<><div /><div /></>} />`);
});
});
1 change: 1 addition & 0 deletions src/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type Options = {|
functionValue: Function,
tabStop: number,
useBooleanShorthandSyntax: boolean,
useFragmentShortSyntax: boolean,
sortProps: boolean,

maxInlineAttributesLineLength?: number,
Expand Down
9 changes: 8 additions & 1 deletion src/parser/parseReactElement.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
/* @flow */

import React, { type Element as ReactElement } from 'react';
import React, { type Element as ReactElement, Fragment } from 'react';
import type { Options } from './../options';
import {
createStringTreeNode,
createNumberTreeNode,
createReactElementTreeNode,
createReactFragmentTreeNode,
} from './../tree';
import type { TreeNode } from './../tree';

const supportFragment = Boolean(Fragment);

const getReactElementDisplayName = (element: ReactElement<*>): string =>
element.type.displayName ||
element.type.name || // function name
Expand Down Expand Up @@ -68,6 +71,10 @@ const parseReactElement = (
.filter(onlyMeaningfulChildren)
.map(child => parseReactElement(child, options));

if (supportFragment && element.type === Fragment) {
return createReactFragmentTreeNode(key, childrens);
}

return createReactElementTreeNode(
displayName,
props,
Expand Down
Loading

0 comments on commit 768cce0

Please sign in to comment.