Create an interactive SVG component using React - svg

Let's say I have an SVG element with paths for all US states.
<svg>
<g id="nh">
<title>New Hampshire</title>
<path d="m 880.79902,142.42476 0.869,-1.0765 1.09022,..." id="NH" class="state nh" />
</g>
...
</svg>
The SVG data is saved in a separate file with a .svg extension. Say I want to create a React component of that map, with complete control over it so that I can modify the styling of individual states based on some external input.
Using Webpack, as far as I can tell, I have two options for loading the SVG markup: Insert it as raw markup using the raw-loader and create a component using dangerouslySetInnerHTML:
var InlineSvg = React.createClass({
render() {
var svg = require('./' + this.props.name + '.svg');
return <div dangerouslySetInnerHTML={{__html: svg}}></div>;
}
});
or manually convert the markup to valid JSX:
var NewComponent = React.createClass({
render: function() {
return (
<svg>
<g id="nh">
<title>New Hampshire</title>
<path d="m 880.79902,142.42476 0.869,-1.0765 1.09022,..." id="NH" className="state nh" />
</g>
...
</svg>
);
});
Finally, let's say that in addition to the SVG map, there's a simple HTML list of all the states. Whenever a user hovers over a list item, the corresponding SVG path should shift fill color.
Now, what I can't seem to figure out is how to update the React SVG component to reflect the hovered state. Sure, I can reach out into the DOM, select the SVG state by classname and change its color, but that doesn't seem to be the "react" way to do it. A pointing finger would be much appreciated.
PS. I'm using Redux to handle all communication between components.

You need to do two things:
1) Set an event listener on each list item to inform your app of the highlighted item.
<li
onMouseOver={() => this.handleHover('NH')}
onMouseOut={() => this.handleUnhover()}
>
New Hampshire
</li>
2) Capture the data, and propagate it your SVG component.
This is the more complicated part, and it comes down to how you've structured your app.
If your entire app is a single React component, then handleHover would simply update the component state
If your app is divided into multiple components, then handleHover would trigger a callback passed in as a prop
Let's assume the latter. The component methods might look like this:
handleHover(territory) {
this.props.onHighlight(territory);
}
handleUnhover() {
this.props.onHighlight(null);
}
Assuming you have a parent component, which contains both the SVG map and the list, it might look something like this:
class MapWrapper extends React.Component {
constructor(props) {
super(props);
this.state = {
highlighted: null;
};
}
setHighlight(territory) {
this.setState({
highlighted: territory
});
}
render() {
const highlighted = { this.state };
return (
<div>
<MapDiagram highlighted={highlighted} />
<TerritoryList onHighlight={(terr) => this.setHighlight(terr)} />
</div>
);
}
}
The key here is the highlighted state variable. Every time a new hover event occurs, highlighted changes in value. This change triggers a re-render, and the new value is passed onto MapDiagram which can then determine which part of the SVG to highlight.

Related

Why does svg not get requested with app load

I've got a live site https://courageous-kulfi-58c13b.netlify.app/. Whenever a checkbox is clicked, it should turn green with a tick svg on it. However, on the first app load, clicking a checkbox will show a noticeable delay between the background turning green and the tick svg actually showing. NextJS only requests for the svg once I click on the checkbox. The svg is used in my css as a background image in a ::before pseudo element. How can I make next request for the svg in initial render so there isn't this delay?
const Checkbox = (props: Props) => {
const [checked, setChecked] = useState(false);
const handleClick = () => {
setChecked((prevChecked) => !prevChecked);
};
return (
<>
<input
type="checkbox"
onClick={handleClick}
className={cn({
"before:bg-tick": checked,
})}
></input>
</>
);
};
tailwind.config.js
module.exports = {
extend: {
backgroundImage: {
tick: "url(/Tick.svg)",
},
}
}
You can inline the svg with something like the following. Then there will be no delay in downloading the svg.
.tick:before {
content: url('data:image/svg+xml; utf8, <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 342.357 342.357" style="enable-background:new 0 0 342.357 342.357" xml:space="preserve"><path d="M290.04 33.286 118.861 204.427l-66.541-66.52L0 190.226l118.862 118.845L342.357 85.606z"/></svg>');
}
Note I have run the svg through svgo to optimize it.
Example here: https://play.tailwindcss.com/TlRvKKs53I?file=css
Preload image
The other option would be to preload the image in the html head.
<link rel="preload" as="image" href="/Tick.svg">
The old school hacky way was to include an image tag on the page somewhere, but use css to make it not visible, but the browser would still load it.

Leaflet markers not always applying

So I'm using leaflet-react and I need to add some circle markers.
Now this code works...sometimes. On map click a circle marker should be added but sometimes it is not. Seemingly randomly it will just not add a visible marker. Sometimes the marker will become visible if I change the zoom level but not always. All the code runs each time so it's not that addMarker() isn't been called, also the removal of the last marker(by clearing the mark layer) always runs.
Thanks, Ed.
It appears that you aren't using the react-leaflet package. I'd recommend trying that out. Here is some example code for how you'd add markers to the map on click events:
const React = window.React;
const { Map, TileLayer, Marker, Popup } = window.ReactLeaflet;
class SimpleExample extends React.Component {
constructor() {
super();
this.state = {
markers: [[51.505, -0.09]]
};
}
addMarker = (e) => {
const {markers} = this.state
markers.push(e.latlng)
this.setState({markers})
}
render() {
return (
<Map
center={[51.505, -0.09]}
onClick={this.addMarker}
zoom={13}
>
<TileLayer
attribution='© OpenStreetMap contributors'
url='http://{s}.tile.osm.org/{z}/{x}/{y}.png'
/>
{this.state.markers.map((position, idx) =>
<Marker key={`marker-${idx}`} position={position}>
<Popup>
<span>A pretty CSS3 popup. <br/> Easily customizable.</span>
</Popup>
</Marker>
)}
</Map>
);
}
}
window.ReactDOM.render(<SimpleExample />,
document.getElementById('container'));
And here's a jsfiddle showing the implementation: https://jsfiddle.net/q2v7t59h/413/

How to change the values in MultiSlider in react-native?

I am working on react-native <MultiSlider> component, but one thing i just want to know, how do i change the value when i am sliding.
Default Value:
Sliding Values:
Code:
constructor() {
super();
this.state = {
priceRange : [0,10],
};
}
sliderOnChangeValue(values){
return(
<Text style={Styles.filter_label_label}>0 - 35,000</Text>
);
}
<View>
<View>
<Text>PRICING</Text>
</View>
<View>
{this.sliderOnChangeValue()}
</View>
</View>
<MultiSlider
values={this.state.priceRange}
sliderLength={300}
onValuesChange={this.sliderOnChangeValue} />
So on the above code i am calling sliderOnChangeValue() function onValuesChange i want to change the <Text> component values on range change.
Please kindly go through my above post and let me know if you find any solution.
Thanks
I recommend you refamiliarize yourself with the basics of React. Your approach is rather fundamentally flawed.
Callback functions cannot render things, they must update your component's state and your render function should output the UI based on state and props.
Follow the React Native Slider example on the docs website. It's nearly exactly what you want.
Try below code:
values={[parseInt(this.state.multiSliderValue[0]), parseInt(this.state.multiSliderValue[1])]}
and notice above code
<MultiSlider
values={[parseInt(this.state.multiSliderValue[0]), parseInt(this.state.multiSliderValue[1])]}
sliderLength={290}
onValuesChangeFinish={this.multiSliderValuesChange}
selectedStyle={{backgroundColor: '#f5a540'}}
min={16}
markerStyle={{backgroundColor: '#f5a540'}}
max={99}
step={1}
snapped
/>
Its working with me.

How to create an SVG component dynamically in Angular2?

I am creating a web application which uses SVG.
I have created components consist of SVG element, and they are put into a root svg element.
They have attribute selector, because SVG/XML document tree is strict so I cannot use element selector.
And they have a template starts with svg:g tag:
#Component({
selector:'[foo]',
template: '<svg:g>...</svg:g>',
})
In the application, I want to create a component when a user press a button,
and simultaneously start dragging it.
I thought it can be achieved by creating a component dynamically using ComponentResolver:
#ViewChild('dynamicContentPlaceHolder', {read: ViewContainerRef})
protected dynamicComponentTarget: ViewContainerRef
private componentResolver: ComponentResolver
onMouseDown() {
this.componentResolver
.resolveComponent(FooComponent)
.then((factory) => {
const dynamicComponent = this.dynamicComponentTarget.createComponent(factory, 0)
const component: FooComponent = dynamicComponent.instance
const element = dynamicComponent.location.nativeElement
// add event listener to start dragging `element`.
})
}
Component is created when onMouseDown() called, but its DOM element is div, so it is illegal element in svg document and cannot be displayed.
I have tried with selector='svg:g[foo]', then g element is created, but its namespace is not for SVG (http://www.w3.org/2000/svg), but normal HTML namespace (http://www.w3.org/1999/xhtml) and its class is HTMLUnknownElement > g.
I also tried with selector='svg:svg[foo]', then svg:svg element is created and it is displayed. But svg:svg cannot move with transform attribute so this doesn't work well for my application.
How can I dynamically create svg:g element for attribute selector component?
I am using Angular2: 2.0.0-rc4.
You're right about the namespacing issues keeping the g element from rendering as svg. Unfortunately, attaching the node as an svg element is the only way to feasibly get the component to namespace properly.
However, this doesn't mean this won't work. If you add the drag functionality as a directive on the g element in the template, it will be compiled with your component, and you can offset your logic into that directive. The top level svg will be namespaced correctly, and the template will inherit this accordingly.
import {Component, Input} from '#angular/core';
#Component({
selector: 'svg:svg[customName]', // prevent this from hijacking other svg
template: '<svg:g dragDirective>...</svg:g>', // note the directive added here
style: []
})
export class GComponent {
constructor() { }
}
This may not be ideal, but until https://github.com/angular/angular/issues/10404 is resolved, there's not much of an alternative.
I am not sure that my solution is right but it works for me (even with ivy):
#Component({
...
})
class ParentComponent {
constructor(
private injector: Injector,
private appRef: ApplicationRef,
private componentFactoryResolver: ComponentFactoryResolver,
) {}
createDynamicComponent() {
let node = document.createElementNS("http://www.w3.org/2000/svg", "svg");
let factory = this.componentFactoryResolver.resolveComponentFactory(DynamicComponent);
let componentRef = factory.create(this.injector, [], node);
this.appRef.attachView(componentRef.hostView);
}
}
After that you must manually append node into DOM.
Instead of trying to create your component to the view with the Component Resolver I will do this instead.
Create a object with properties which match the attributes you want to pass to your SVG Component.
Append this object to an array (ex.svgItems).
Add *ngFor="svgItem in svgItems" to the SVG component you want to create dynamically.
Hope it's clear and solve your problem.

SVG + Angular2 code sample does not work with interpolation

My goal is to convert code from Angular 1.3 to Angular 2 (with SVG in both cases).
I tried the following simple test code, which works in case #1 that does not involve interpolation, but does not work in case #2 (which uses interpolation), and AFAICS the only difference in the generated SVG code is the inclusion of an extra attribute in the element: class="ng-binding"
Is there a way to suppress the preceding class attribute, or is there another solution?
Btw I wasn't able to get the formatting quite right (my apologies).
Contents of HTML Web page:
<html>
<head>
<title>SVG and Angular2</title>
<script src="quickstart/dist/es6-shim.js"></script>
</head>
<body>
<!-- The app component created in svg1.es6 -->
<my-svg></my-svg>
<script>
// Rewrite the paths to load the files
System.paths = {
'angular2/*':'/quickstart/angular2/*.js', // Angular
'rtts_assert/*': '/quickstart/rtts_assert/*.js', // Runtime assertions
'svg': 'svg1.es6' // The my-svg component
};
System.import('svg');
</script>
</body>
</html>
Contents of the JS file:
import {Component, Template, bootstrap} from 'angular2/angular2';
#Component({
selector: 'my-svg'
})
#Template({
//case 1 works:
inline: '<svg><ellipse cx="100" cy="100" rx="80" ry="50" fill="red"></ellipse></svg>'
//case 2 does not work:
//inline: "<svg>{{graphics}}</svg>"
})
class MyAppComponent {
constructor() {
this.graphics = this.getGraphics();
}
getGraphics() {
// return an array of SVG elements (eventually...)
var ell1 =
'<ellipse cx="100" cy="100" rx="80" ry="50" fill="red"></ellipse>';
return ell1;
}
}
bootstrap(MyAppComponent);
SVG elements do not use the same namespace as HTML elements. When you insert SVG elements into the DOM, they need to be inserted with the correct SVG namespace.
Case 1 works because you are inserting the whole SVG, including the <svg> tags, into the HTML. The browser will automatically use the right namespace because it sees the <svg> tag and knows what to do.
Case 2 doesn't work because you are just inserting an <ellipse> tag and the browser doesn't realise it is supposed be created with the svg namespace.
If you inspect both SVGs with the browser's DOM inspector, and look at the <ellipse> tag's namespace property, you should see the difference.
You can use outerHtml of an HTML element like:
#Component({
selector: 'my-app',
template: `
<!--
<svg xmlns='http://www.w3.org/2000/svg'><ellipse cx="100" cy="100" rx="80" ry="50" fill="red"></ellipse></svg>
-->
<span [outerHTML]="graphics"></span>`
})
export class App {
constructor() {
this.graphics = this.getGraphics();
}
getGraphics() {
// return an array of SVG elements (eventually...)
var ell1 =
'<svg><ellipse cx="100" cy="100" rx="80" ry="50" fill="red"></ellipse></svg>';
return ell1;
}
}
note that the added string has to contain the <svg>...</svg>
See also How can I add a SVG graphic dynamically using javascript or jquery?

Resources