Skip to content

Commit

Permalink
fix: TypeError when returning fragments or arrays from render prop (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
jasnross authored Nov 7, 2022
1 parent ce1c43c commit 2ebba93
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 42 deletions.
6 changes: 6 additions & 0 deletions .changeset/stale-needles-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@react-pdf/layout': patch
'@react-pdf/renderer': patch
---

fix: TypeError when returning fragments or arrays from render prop
39 changes: 0 additions & 39 deletions packages/layout/src/node/createInstance.js

This file was deleted.

59 changes: 59 additions & 0 deletions packages/layout/src/node/createInstances.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { castArray } from '@react-pdf/fns';
import { TextInstance } from '@react-pdf/primitives';

const isString = value => typeof value === 'string';

const isNumber = value => typeof value === 'number';

const isFragment = value =>
value && value.type === Symbol.for('react.fragment');

/**
* Transforms a react element instance to internal element format.
*
* Can return multiple instances in the case of arrays or fragments.
*
* @param {Object} React element
* @returns {Array} parsed react elements
*/
const createInstances = element => {
if (!element) return [];

if (isString(element) || isNumber(element)) {
return [{ type: TextInstance, value: `${element}` }];
}

if (isFragment(element)) {
return createInstances(element.props.children);
}

if (Array.isArray(element)) {
return element.reduce((acc, el) => acc.concat(createInstances(el)), []);
}

if (!isString(element.type)) {
return createInstances(element.type(element.props));
}

const {
type,
props: { style = {}, children = [], ...props },
} = element;

const nextChildren = castArray(children).reduce(
(acc, child) => acc.concat(createInstances(child)),
[],
);

return [
{
type,
style,
props,
box: {},
children: nextChildren,
},
];
};

export default createInstances;
6 changes: 4 additions & 2 deletions packages/layout/src/steps/resolvePagination.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import splitNode from '../node/splitNode';
import canNodeWrap from '../node/getWrap';
import getWrapArea from '../page/getWrapArea';
import getContentArea from '../page/getContentArea';
import createInstance from '../node/createInstance';
import createInstances from '../node/createInstances';
import shouldNodeBreak from '../node/shouldBreak';
import resolveTextLayout from './resolveTextLayout';
import resolveInheritance from './resolveInheritance';
Expand Down Expand Up @@ -142,7 +142,9 @@ const resolveDynamicNodes = (props, node) => {
const resolveChildren = (children = []) => {
if (isNodeDynamic) {
const res = node.props.render(props);
return [createInstance(res)].filter(Boolean);
return createInstances(res)
.filter(Boolean)
.map(n => resolveDynamicNodes(props, n));
}

return children.map(c => resolveDynamicNodes(props, c));
Expand Down
131 changes: 130 additions & 1 deletion packages/renderer/tests/node.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe('node', () => {

expect(fs.existsSync(path)).toBeTruthy();

fs.unlinkSync(path)
fs.unlinkSync(path);
});

test('should export font store', () => {
Expand Down Expand Up @@ -83,4 +83,133 @@ describe('node', () => {
test('should throw error when trying to use usePDF', () => {
expect(() => ReactPDF.usePDF()).toThrow();
});

test('should render a fragment', async () => {
const mock = jest.fn();

const doc = (
<Document onRender={mock}>
<Page>
<View>
<>
<View style={{ width: 20, height: 20, backgroundColor: 'red' }} />
<View style={{ width: 20, height: 20, backgroundColor: 'red' }} />
</>
</View>
</Page>
</Document>
);

await ReactPDF.renderToString(doc);

expect(mock.mock.calls).toHaveLength(1);
});

test('should render a fragment in render', async () => {
const renderMock = jest.fn().mockReturnValue(
<>
<View style={{ width: 20, height: 20, backgroundColor: 'red' }} />
<View style={{ width: 20, height: 20, backgroundColor: 'red' }} />
</>,
);

const doc = (
<Document>
<Page>
<View render={renderMock} />
</Page>
</Document>
);

await ReactPDF.renderToString(doc);

expect(renderMock.mock.calls).toHaveLength(2);
});

test('should render a child array', async () => {
const mock = jest.fn();

const children = [
<View
key="child1"
style={{ width: 20, height: 20, backgroundColor: 'red' }}
/>,
<View
key="child2"
style={{ width: 20, height: 20, backgroundColor: 'red' }}
/>,
];

const doc = (
<Document onRender={mock}>
<Page>
<View>{children}</View>
</Page>
</Document>
);

await ReactPDF.renderToString(doc);

expect(mock.mock.calls).toHaveLength(1);
});

test('should render a child array in render', async () => {
const children = [
<View style={{ width: 20, height: 20, backgroundColor: 'red' }} />,
<View style={{ width: 20, height: 20, backgroundColor: 'red' }} />,
];

const renderMock = jest.fn().mockReturnValue(children);

const doc = (
<Document>
<Page>
<View render={renderMock} />
</Page>
</Document>
);

await ReactPDF.renderToString(doc);

expect(renderMock.mock.calls).toHaveLength(2);
});

test('should render nested dynamic views', async () => {
const renderNode = (
<View
key="child1"
style={{ width: 20, height: 20, backgroundColor: 'red' }}
/>
);

const renderMock = jest.fn().mockReturnValue(renderNode);

const doc = (
<Document>
<Page>
<View render={renderMock} />
<View
render={() => {
return <View render={renderMock} />;
}}
/>
<View
render={() => {
return (
<View
render={() => {
return <View render={renderMock} />;
}}
/>
);
}}
/>
</Page>
</Document>
);

await ReactPDF.renderToString(doc);

expect(renderMock.mock.calls).toHaveLength(6);
});
});

0 comments on commit 2ebba93

Please sign in to comment.