To render certain SVG elements in my app, I need to first measure some other SVG elements.
For example, imagine a <text>
element that is randomly positioned in a square (0,0) - (100,100), and can have various font sizes, font families, etc.
If the text is positioned at (10,20), and have a width of 30 and a height of 40, I'd like to set the containing <svg>
width to 40 (= 10 + 30) and height to 60 (= 20 + 40).
The main point is: <text>
needs to be measured before rendering the <svg>
.
To help with <text>
measurement, I created the following component:
class MeasureSvgElements extends React.Component {
storeSvgReference = svg => {
if (svg !== null) {
this.svg = svg;
}
};
measure() {
const childElements = Array.from(this.svg.children);
const dimensions = childElements
.map(element => element.getBoundingClientRect())
.map(({ width, height }) => ({ width, height }));
this.props.onChange(dimensions);
}
componentDidMount() {
this.measure();
}
componentDidUpdate() {
this.measure();
}
render() {
return (
<svg width="0" height="0" style={{ display: 'block' }} ref={this.storeSvgReference}>
{this.props.children}
</svg>
);
}
}
which can be used to measure multiple elements at once:
<MeasureSvgElements onChange={onChange}>
{['Hello', 'Stack', 'Overflow'].map(str => <text>{str}</text>)}
</MeasureSvgElements>
onChange
will be called once the dimensions are ready.
Now, I'm not sure what's the best way to use <MeasureSvgElements>
to render the containing <svg>
using the dimensions provided by onChange
.
Or, is there a better approach?
To get the width of an Element in React: Set the ref prop on the element. In the useLayoutEffect hook, update the state variable for the width. Use the offsetWidth property to get the width of the element.
You can change the size using CSS transform: scale(2) in <ComponentName /> , which in React can be achieved using className or a global CSS file. Note: To change the color you can use . componentClass path { fill: "color" } , but if you change the scale on .
To do this, open up the SVG file in a text editor, and copy-paste the code into a new component: export const ArrowUndo = () => { return ( <svg xmlns="http://www.w3.org/2000/svg" className="ionicon" viewBox="0 0 512 512" > <path d="M245.
One solution might be to mock-up each text
element prior to render, gather the heights and widths (in addition to positions), and then iterate through the dimensions to determine what sizes should be applied to the parent.
Working CodeSandbox: https://codesandbox.io/s/stack-43880276-dynamic-svg-textbox-id3rt
import React, { useState, useEffect } from "react";
import styled from "styled-components";
const yourTextData = [
{
id: 0,
posX: 0,
posY: 0,
str: "~~Hello~~",
fontFamily: "URW Chancery L, cursive",
fontSize: "30"
},
{
id: 1,
posX: 20,
posY: 20,
str: "~~Stack~~",
fontFamily: "URW Chancery L, cursive",
fontSize: "30"
},
{
id: 2,
posX: 40,
posY: 40,
str: "~~Overflow~~",
fontFamily: "URW Chancery L, cursive",
fontSize: "30"
},
{
id: 3,
posX: 0,
posY: 80,
str: "This SVG with text sets its own",
fontFamily: "Arial, sans-serif",
fontSize: "30"
},
{
id: 4,
posX: 40,
posY: 120,
str: "d i m e n s i o n s",
fontFamily: "FreeMono, monospace",
fontSize: "30"
}
];
const DynamicSVGText = (props) => {
const [svgWidth, setSvgWidth] = useState(0);
const [svgHeight, setSvgHeight] = useState(0);
const [textDims, setTextDims] = useState([]); // array of objects, defining dims/pos for each texteach text
This will be called when creating each text
element and saved to the textDims
hook.
const measure = (data) => {
// create a mock element
let newText = document.createElement("text");
newText.setAttribute("id", data.id);
document.body.appendChild(newText);
// append text data
let theTextEle = document.getElementById(`${data.id}`);
theTextEle.innerHTML += data.str;
// append text font / size, bc might as well be fancy
theTextEle.style.fontFamily = data.fontFamily;
theTextEle.style.fontSize = `${data.fontSize}px`;
// measure element
let width = theTextEle.getBoundingClientRect().width;
let height = theTextEle.getBoundingClientRect().height;
// delete element
theTextEle.parentNode.removeChild(theTextEle);
// set data
let dimData = [width, height];
//console.log(dimData);
// return dimension data
return dimData;
};
text
elements: const SvgText = ({ text }) =>
text.map((data, i) => {
let dimensions = measure(data);
let updatedTextDims = textDims;
updatedTextDims[data.id] = {
x: data.posX,
y: data.posY,
w: dimensions[0],
h: dimensions[1]
};
return (
<StyledText
fontFamily={data.fontFamily}
fontSize={data.fontSize}
key={data.id}
x={data.posX.toString()}
y={(data.posY + dimensions[1]).toString()}
>
{data.str}
</StyledText>
);
});
textDims
hook changes useEffect(() => {
let tw = 0; // longest width
let th = 0; // tallest height
// loop through text elements and their dimensions,
// set total width/height to the greatest dimensions across objects
for (let d = 0; d < textDims.length; d++) {
let thisWidth = textDims[d].x + textDims[d].w;
let thisHeight = textDims[d].y + textDims[d].h;
if (thisWidth > tw) {
tw = thisWidth;
}
if (thisHeight > th) {
th = thisHeight;
}
}
setSvgWidth(tw);
setSvgHeight(th);
}, [textDims]);
return (
<svg
width={svgWidth.toString()}
height={svgHeight.toString()}
style={{ display: "block" }}
>
<SvgText text={props.text} />
</svg>
);
};
export default function App() {
return (
<Container>
<DynamicSVGText text={yourTextData} />
</Container>
);
}
const Container = styled.div`
display: flex;
flex-direction: row;
justify-content: center;
`;
const StyledText = styled.text``;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With