Adding a layout pass to SVG image components

text nodes are the only elements that need to be "measured". The
dimensions of all other image components can be determined based on the
dimensions of their children. This adds a pre-rendering pass to work out
dimensions so multiple renders don't need to happen
This commit is contained in:
Jeff Avallone 2019-01-26 17:25:38 -05:00
parent 21c392752e
commit b299d32fc3
11 changed files with 97 additions and 55 deletions

View File

@ -22,8 +22,8 @@ exports[`App removing rendered expression 1`] = `
<LoadNamespace(FormActions) /> <LoadNamespace(FormActions) />
</LoadNamespace(Form)> </LoadNamespace(Form)>
<Render <Render
data="LAYOUT(PARSED(test expression))"
onRender={[Function]} onRender={[Function]}
parsed="PARSED(test expression)"
/> />
</Fragment> </Fragment>
`; `;
@ -139,8 +139,8 @@ exports[`App rendering an expression 3`] = `
<LoadNamespace(FormActions) /> <LoadNamespace(FormActions) />
</LoadNamespace(Form)> </LoadNamespace(Form)>
<Render <Render
data="LAYOUT(PARSED(test expression))"
onRender={[Function]} onRender={[Function]}
parsed="PARSED(test expression)"
/> />
</Fragment> </Fragment>
`; `;
@ -167,8 +167,8 @@ exports[`App rendering image details 1`] = `
<LoadNamespace(FormActions) /> <LoadNamespace(FormActions) />
</LoadNamespace(Form)> </LoadNamespace(Form)>
<Render <Render
data="LAYOUT(PARSED(test expression))"
onRender={[Function]} onRender={[Function]}
parsed="PARSED(test expression)"
/> />
</Fragment> </Fragment>
`; `;
@ -201,8 +201,8 @@ exports[`App rendering image details 2`] = `
/> />
</LoadNamespace(Form)> </LoadNamespace(Form)>
<Render <Render
data="LAYOUT(PARSED(test expression))"
onRender={[Function]} onRender={[Function]}
parsed="PARSED(test expression)"
/> />
</Fragment> </Fragment>
`; `;

View File

@ -73,13 +73,13 @@ class App extends React.PureComponent {
`syntax/${ syntax }` `syntax/${ syntax }`
); );
const parsed = syntaxModule.parse(expr); const exprData = syntaxModule.layout(syntaxModule.parse(expr));
this.setState({ this.setState({
loading: false, loading: false,
render: { render: {
syntax, syntax,
parsed, exprData,
Component: syntaxModule.Render Component: syntaxModule.Render
} }
}); });
@ -119,7 +119,7 @@ class App extends React.PureComponent {
imageDetails, imageDetails,
render: { render: {
syntax: renderSyntax, syntax: renderSyntax,
parsed, exprData,
Component Component
} }
} = this.state; } = this.state;
@ -137,7 +137,7 @@ class App extends React.PureComponent {
}; };
const renderProps = { const renderProps = {
onRender: this.handleSvg, onRender: this.handleSvg,
parsed data: exprData
}; };
const doRender = renderSyntax === syntax; const doRender = renderSyntax === syntax;

View File

@ -6,6 +6,7 @@ import { App } from 'components/App';
jest.mock('syntax/js', () => ({ jest.mock('syntax/js', () => ({
parse: expr => `PARSED(${ expr })`, parse: expr => `PARSED(${ expr })`,
layout: parsed => `LAYOUT(${ parsed })`,
Render: () => '' Render: () => ''
})); }));

View File

@ -28,13 +28,21 @@ const render = (data, extraProps) => {
class Render extends React.PureComponent { class Render extends React.PureComponent {
static propTypes = { static propTypes = {
parsed: PropTypes.object.isRequired, data: PropTypes.object.isRequired,
onRender: PropTypes.func.isRequired onRender: PropTypes.func.isRequired
} }
svgContainer = React.createRef() svgContainer = React.createRef()
provideSVGData = () => { componentDidMount() {
this.provideSVGData();
}
componentDidUpdate() {
this.provideSVGData();
}
provideSVGData() {
if (!this.svgContainer.current) { if (!this.svgContainer.current) {
return; return;
} }
@ -48,10 +56,10 @@ class Render extends React.PureComponent {
} }
render() { render() {
const { parsed } = this.props; const { data } = this.props;
return <div className={ style.render } ref={ this.svgContainer }> return <div className={ style.render } ref={ this.svgContainer }>
{ render(parsed, { onReflow: this.provideSVGData }) } { render(data, { onReflow: this.provideSVGData }) }
</div>; </div>;
} }
} }

22
src/layout.js Normal file
View File

@ -0,0 +1,22 @@
import SVG from 'rendering/SVG/layout';
import Text from 'rendering/Text/layout';
const nodeTypes = {
SVG,
Text
};
const layout = data => {
if (typeof data == 'string') {
return data;
}
const { type } = data;
return nodeTypes[type]({
props: {},
...data
});
};
export default layout;

View File

@ -24,7 +24,9 @@ class SVG extends React.PureComponent {
static propTypes = { static propTypes = {
onReflow: PropTypes.func, onReflow: PropTypes.func,
children: PropTypes.node, children: PropTypes.node,
padding: PropTypes.number padding: PropTypes.number,
innerWidth: PropTypes.number,
innerHeight: PropTypes.number
} }
static defaultProps = { static defaultProps = {
@ -36,18 +38,11 @@ class SVG extends React.PureComponent {
height: 0 height: 0
} }
handleReflow = box => {
const { padding } = this.props;
this.setState({
width: Math.round(box.width + 2 * padding),
height: Math.round(box.height + 2 * padding)
}, () => this.props.onReflow(this));
}
render() { render() {
const { width, height } = this.state; const { padding, innerWidth, innerHeight, children } = this.props;
const { padding, children } = this.props;
const width = Math.round(innerWidth + 2 * padding);
const height = Math.round(innerHeight + 2 * padding);
const svgProps = { const svgProps = {
width, width,
@ -60,9 +55,7 @@ class SVG extends React.PureComponent {
return <svg { ...svgProps }> return <svg { ...svgProps }>
<metadata dangerouslySetInnerHTML={{ __html: metadata }}></metadata> <metadata dangerouslySetInnerHTML={{ __html: metadata }}></metadata>
<g transform={ `translate(${ padding } ${ padding })` }> <g transform={ `translate(${ padding } ${ padding })` }>
{ React.cloneElement(React.Children.only(children), { { children }
onReflow: this.handleReflow
}) }
</g> </g>
</svg>; </svg>;
} }

View File

@ -0,0 +1,12 @@
import layout from 'layout';
const layoutSVG = data => {
const child = layout(data.children[0]);
data.props.innerWidth = child.box.width;
data.props.innerHeight = child.box.height;
return data;
};
export default layoutSVG;

View File

@ -6,6 +6,7 @@ import * as style from './style';
class Text extends React.PureComponent { class Text extends React.PureComponent {
static propTypes = { static propTypes = {
quoted: PropTypes.bool, quoted: PropTypes.bool,
transform: PropTypes.string,
onReflow: PropTypes.func, onReflow: PropTypes.func,
children: PropTypes.oneOfType([ children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node), PropTypes.arrayOf(PropTypes.node),
@ -13,31 +14,6 @@ class Text extends React.PureComponent {
]).isRequired ]).isRequired
} }
state = {
transform: ''
}
textRef = React.createRef()
componentDidMount() {
this.reflow();
}
componentDidUpdate() {
this.reflow();
}
reflow() {
const box = this.textRef.current.getBBox();
const transform = `translate(${ -box.x } ${ -box.y })`;
if (transform === this.state.transform) {
return; // No update required
}
this.setState({ transform }, () => this.props.onReflow(box));
}
renderContent() { renderContent() {
const { children, quoted } = this.props; const { children, quoted } = this.props;
@ -53,12 +29,11 @@ class Text extends React.PureComponent {
} }
render() { render() {
const { transform } = this.state; const { transform } = this.props;
const textProps = { const textProps = {
style: style.text, style: style.text,
transform, transform
ref: this.textRef
}; };
return <text { ...textProps }> return <text { ...textProps }>

View File

@ -0,0 +1,27 @@
import React from 'react';
import ReactDOM from 'react-dom';
import Text from 'rendering/Text';
const layoutText = data => {
const container = document.createElement('div');
document.body.appendChild(container);
ReactDOM.render(
<svg width="0" height="0" viewBox="0 0 0 0">
<Text { ...data.props }>{ data.children }</Text>
</svg>,
container);
const box = container.querySelector('svg > text').getBBox();
document.body.removeChild(container);
data.box = {
width: box.width,
height: box.height
};
data.props.transform = `translate(${ -box.x } ${ -box.y })`;
return data;
};
export default layoutText;

View File

@ -1,4 +1,5 @@
import Render from 'components/Render'; import Render from 'components/Render';
import layout from 'layout';
const parse = expr => { const parse = expr => {
return { return {
@ -19,5 +20,6 @@ const parse = expr => {
export { export {
parse, parse,
layout,
Render Render
}; };

View File

@ -1,4 +1,5 @@
import Render from 'components/Render'; import Render from 'components/Render';
import layout from 'layout';
const parse = expr => { const parse = expr => {
return { return {
@ -19,5 +20,6 @@ const parse = expr => {
export { export {
parse, parse,
layout,
Render Render
}; };