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

View File

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

View File

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

View File

@ -28,13 +28,21 @@ const render = (data, extraProps) => {
class Render extends React.PureComponent {
static propTypes = {
parsed: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
onRender: PropTypes.func.isRequired
}
svgContainer = React.createRef()
provideSVGData = () => {
componentDidMount() {
this.provideSVGData();
}
componentDidUpdate() {
this.provideSVGData();
}
provideSVGData() {
if (!this.svgContainer.current) {
return;
}
@ -48,10 +56,10 @@ class Render extends React.PureComponent {
}
render() {
const { parsed } = this.props;
const { data } = this.props;
return <div className={ style.render } ref={ this.svgContainer }>
{ render(parsed, { onReflow: this.provideSVGData }) }
{ render(data, { onReflow: this.provideSVGData }) }
</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 = {
onReflow: PropTypes.func,
children: PropTypes.node,
padding: PropTypes.number
padding: PropTypes.number,
innerWidth: PropTypes.number,
innerHeight: PropTypes.number
}
static defaultProps = {
@ -36,18 +38,11 @@ class SVG extends React.PureComponent {
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() {
const { width, height } = this.state;
const { padding, children } = this.props;
const { padding, innerWidth, innerHeight, children } = this.props;
const width = Math.round(innerWidth + 2 * padding);
const height = Math.round(innerHeight + 2 * padding);
const svgProps = {
width,
@ -60,9 +55,7 @@ class SVG extends React.PureComponent {
return <svg { ...svgProps }>
<metadata dangerouslySetInnerHTML={{ __html: metadata }}></metadata>
<g transform={ `translate(${ padding } ${ padding })` }>
{ React.cloneElement(React.Children.only(children), {
onReflow: this.handleReflow
}) }
{ children }
</g>
</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 {
static propTypes = {
quoted: PropTypes.bool,
transform: PropTypes.string,
onReflow: PropTypes.func,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
@ -13,31 +14,6 @@ class Text extends React.PureComponent {
]).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() {
const { children, quoted } = this.props;
@ -53,12 +29,11 @@ class Text extends React.PureComponent {
}
render() {
const { transform } = this.state;
const { transform } = this.props;
const textProps = {
style: style.text,
transform,
ref: this.textRef
transform
};
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 layout from 'layout';
const parse = expr => {
return {
@ -19,5 +20,6 @@ const parse = expr => {
export {
parse,
layout,
Render
};

View File

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