How to increase the edge drop zone using React Flow - reactjs

I am using react-flow to create a node graph. There are little dots that appear above and below each node to create new edges. The selection and drop zones for these edges are so pixel accurate that it makes it hard for users to link items. Is there any way to increase the connection zone? I want users to be able to drag an edge to anywhere on the node and it will link the two together.
import ReactFlow, { removeElements, addEdge, isNode, Background, Elements, BackgroundVariant, FlowElement, Node, Edge, Connection, OnLoadParams } from 'react-flow-renderer';
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element);
const initialElements: Elements = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' },
{ id: 'e1-2', source: '1', target: '2', animated: true },
];
const BasicFlow = () =>
{
const [rfInstance, setRfInstance] = useState<OnLoadParams | null>(null);
const [elements, setElements] = useState<Elements>(initialElements);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
const onConnect = (params: Edge | Connection) => setElements((els) => addEdge(params, els));
const onLoad = (reactFlowInstance: OnLoadParams) => setRfInstance(reactFlowInstance);
return (
<ReactFlow
elements={elements}
onLoad={onLoad}
onElementClick={onElementClick}
onElementsRemove={onElementsRemove}
onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
>
<Background variant={BackgroundVariant.Lines} />
</ReactFlow>
);
};
export default BasicFlow;```

I did that passing a custom node with its own handles:
const NODE_TYPES = {
yourType: CustomNode,
};
...
<ReactFlow
nodeTypes={NODE_TYPES}
...
/>
Then, at the CustomNode, I used the Handle component with custom height and width:
import { Handle, Position } from 'react-flow-renderer';
const CustomNode = (...) => {
...
return <Box>
...
<Handle
type="target"
position={Position.Left}
style={{ // Make the handle invisible and increase the touch area
background: 'transparent',
zIndex: 999,
border: 'none',
width: '20px',
height: '20px',
}}
/>
<CircleIcon
style={{}} // Fix the position of the icon over here
/>
</Box>;
}
I think its a bit hacky, but that's the way I found to accomplish it.

Based on adding a width and height suggested by Leonardo, I added a class and this worked for me too. Background, border etc are optional possibilities as per your need.
I had to add !important as it wasn't overriding the default settings otherwise.
.node-custom-handle {
width: 15px!important;
height: 15px!important;
background: whitesmoke!important;
border: 1px solid black!important;
border-radius: 4px!important;
}
<Handle
type="target"
position="top"
id="t"
className="node-custom-handle"
onConnect={(params) => console.log("handle onConnect", params)}
/>

Related

Struggling to correctly update and re-render chart on react-chartjs-2

I am building an app for gym-goers to record and track their progress. For each exercise they input, it should render a chart showing their previous track record. This is working fine.
Users can then add another entry to this track record, but it does not update the chart unless you refresh the page. I can't work out why or how to fix it.
There are a number of different components involved - a parent Exercise.js one, then an ExerciseFooter.js one, which contains the buttons to adjust the target or add a new entry to the exercise, and then AddHistory.js and SetTarget.js components which contain modals and the logic to update the exercise via Redux and MongoDB.
A minimal version of the Exercise.js page is here (I've collapsed the stuff that's mainly styling into single lines as much as possible):
import React, { useState, useEffect } from "react";
import { ExerciseFooter } from "./ExerciseFooter";
import { Line } from "react-chartjs-2";
import { useLocation } from "react-router-dom";
import { useSelector } from "react-redux";
export const Exercise = (props) => {
const location = useLocation();
const users = useSelector((state) => state.auth);
const localUser = JSON.parse(localStorage.getItem("profile"));
const [user, setUser] = useState("");
const [exerciseProp, setExerciseProp] = useState({
history: [""],
target: 0,
});
useEffect(() => {
localUser &&
localUser?.result &&
users.length > 0 &&
setUser(
users.filter(
(filteredUser) => filteredUser._id == props.match.params.userId
)[0]
);
if (!localUser) setUser("");
setExerciseProp(
user?.exercises?.filter(
(exercise) => exercise._id == props.match.params.exerciseId
)[0]
);
}, [users, location]);
//styling for chart
const [barData, setBarData] = useState({
labels: [""],
datasets: [
{ label: "Target", fill: false, radius: 0, data: [""], borderColor: ["rgba(35, 53, 89)"], borderWidth: [3], },
{ label: "You did", data: [""], tension: 0.3, borderColor: ["white"], backgroundColor: ["white"], borderWidth: 3, },
],
});
//updating chart data
var weightArr = [];
var dateArr = [];
var targetArr = [];
if (exerciseProp) {
exerciseProp.history.map((hist) =>
weightArr.push(parseInt(hist.weight) || 0)
);
exerciseProp.history.map((hist) => dateArr.push(hist.date));
for (let i = 0; i < exerciseProp.history.length; i++) {
targetArr.push(exerciseProp.target);
}
}
useEffect(() => {
if (exerciseProp) {
setBarData({
labels: dateArr,
datasets: [
{
label: "Target",
fill: false,
radius: 0,
data: targetArr,
borderColor: ["rgba(35, 53, 89)"], borderWidth: [3],
},
{
label: "Weight",
data: weightArr,
tension: 0.3, borderColor: ["white"], backgroundColor: ["white"], borderWidth: 3,
},
],
});
}
}, [users]);
//render chart ones exerciseProp is populated
if (exerciseProp) {
return (
<div style={{ marginTop: "200px" }}>
<Line
data={barData}
options={{ plugins: { title: { display: false, }, legend: { display: false, }, },
scales: { x: { grid: { color: "white", font: { family: "Dongle", size: 20, }, }, ticks: { color: "white", font: { family: "Dongle", size: 20, }, }, }, y: { grid: { color: "white", }, ticks: { color: "white", font: { family: "Dongle", size: 20, }, }, }, }, }}
/>
{exerciseProp && <ExerciseFooter user={user} exercise={exerciseProp} />}
</div>
);
} else {
return <>Loading...</>;
}
};
I've tried doing a few different things but nothing has worked. I tried adding an 'update' state variable which was updated by a function passed down to the the various dispatches, and then added it to the dependencies of the useEffects, but that didn't seem to make any difference.
Any help much appreciated! As I say, if I just force a refresh then it works fine but know that's bad practice so trying to work out why it isn't re-rendering correctly.
Thanks!
You just have to enable redraw prop
like this
<Line
redraw={true}
data={barData}
options={{ plugins: { title: { display: false, }, legend: { display: false, }, },
scales: { x: { grid: { color: "white", font: { family: "Dongle", size: 20, }, }, ticks: { color: "white", font: { family: "Dongle", size: 20, }, }, }, y: { grid: { color: "white", }, ticks: { color: "white", font: { family: "Dongle", size: 20, }, }, }, }, }}/>
this all you have to do
redraw={true}

react-redux prevent rerendering of list items when the state changes

I have a list item callled Row. The Row has three sections, I have stored the width of each sections in a state
rowWidth: [
{
id: 'name',
width: 200,
},
{
id: 'size',
width: 70,
},
{
id: 'status',
width: 150,
},
{
id: 'progress',
width: 200,
},
{
id: 'lasttry',
width: 200,
},
{
id: 'eta',
width: 50,
},
]
I change width of a single column by an external handle which updates the store.
switch (actions.type) {
case consts.CHANGE_HEADER_WIDTH : return {
...state,
headerWidth: state.headerWidth.map((item) =>
item.id === actions.payload.id ? { ...item, width: actions.payload.neWidth } : item
),
}
break;
default : return state;
}
This is the row
const headerWidth = useSelector((state) => state.ui.headerWidth)
const getWidth = useCallback((id) => {
const l = _.find(headerWidth, function (item) {
return item.id === id
})
return l.width
})
return(
<Row>
<Section1 style={{ width: getRow('name') }}/>
<Section2 style={{ width: getRow('size') }}/>
<Section3 style={{ width: getRow('status') }}/>
</Row>
)
My question is - how do I change the width of these divs without rerendering row
Thank you all for the help. I understand that there has to be a rerender in order to view the state change.

React testing library fireEvent not firing / found answer

I have an issue with using react testing library and its fireEvent function.
I want to test my component and its style after hovering.
Here it is my component written in tsx:
import React from 'react'
import { makeStyles } from '#material-ui/core'
import Paper, { PaperProps } from '#material-ui/core/Paper'
import Box from '#material-ui/core/Box'
import posed from 'react-pose'
import grey from '#material-ui/core/colors/grey'
import green from '#material-ui/core/colors/green'
import lightBlue from '#material-ui/core/colors/lightBlue'
import teal from '#material-ui/core/colors/teal'
function styleProducer(variant: BigTileProps['variant'] = 'default', color: BigTileProps['color'] = 'default'): Function {
type colrMapType = {
[k: string]: { tile: { [pr: string]: string } }
}
const colorMap: colrMapType = {
default: { tile: {} },
grey: {
tile: { background: grey[700], color: 'white' }
},
green: {
tile: { background: green[300] }
},
teal: {
tile: { background: teal[400], color: 'white' }
},
blue: {
tile: { background: lightBlue[200] }
},
}
interface variantsKeys {
small: (theme: any) => object
default: (theme: any) => object
big: (theme: any) => object
large: (theme: any) => object
}
type variantsType = {
[k in keyof variantsKeys]: variantsKeys[k]
}
const variants: variantsType = {
small: (theme: any) => ({
tile: Object.assign({
minWidth: theme.spacing(10),
height: theme.spacing(10),
background: grey[500],
position: 'relative',
'&:hover': {
cursor: 'pointer'
}
}, colorMap[color].tile),
content: {
fontSize: '2rem',
fontWeight: 'bold'
},
title: {
textTransform: 'uppercase'
},
icon: {
position: 'absolute',
top: 0,
left: 0
}
}),
default: (theme: any) => ({
tile: Object.assign({
minWidth: theme.spacing(15),
height: theme.spacing(15),
position: 'relative',
'&:hover': {
cursor: 'pointer'
}
}, colorMap[color].tile),
content: {
fontSize: '2rem',
fontWeight: 'bold'
},
title: {
textTransform: 'uppercase'
},
icon: {
position: 'absolute',
top: 0,
left: 0
}
}),
big: (theme: any) => ({
tile: Object.assign({
minWidth: theme.spacing(20),
height: theme.spacing(20),
position: 'relative',
'&:hover': {
cursor: 'pointer'
}
}, colorMap[color].tile),
content: {
fontSize: '2rem',
fontWeight: 'bold'
},
title: {
textTransform: 'uppercase'
},
icon: {
position: 'absolute',
top: 0,
left: 0
}
}),
large: (theme: any) => ({
tile: Object.assign({
minWidth: theme.spacing(25),
height: theme.spacing(25),
position: 'relative',
'&:hover': {
cursor: 'pointer'
}
}, colorMap[color].tile),
content: {
fontSize: '2rem',
fontWeight: 'bold'
},
title: {
textTransform: 'uppercase'
},
icon: {
position: 'absolute',
top: 0,
left: 0
}
})
}
return makeStyles(variants[variant])
}
type BigTileProps = {
color?: 'default' | 'grey' | 'green' | 'blue' | 'teal',
variant?: 'small' | 'default' | 'big' | 'large',
width?: string, // 15px , 10rem etc
height?: string, // 15px , 10rem etc
title?: string,
content?: string
icon?: React.FC | React.ReactElement
}
const PosedPaper = posed(Paper)({
onHover: {
scale: 1.05
},
none: {
scale: 1
}
})
const BigTile: React.FC<BigTileProps> = ({
variant = 'default',
color = 'default',
width,
height,
children,
title,
content,
icon,
...props
}) => {
const [hover, setHover] = React.useState(false)
const useStyles = styleProducer(variant, color)
const classes = useStyles()
const onHoverHandle = (bool: boolean) => () => {
setHover(bool)
}
const style = {height, width}
if (!height) delete style['height']
if (!width) delete style['width']
return (
<PosedPaper className={classes.tile} style={{ height, width }} pose={hover ? 'onHover' : 'none'} onMouseEnter={onHoverHandle(true)} onMouseLeave={onHoverHandle(false)}>
<Box className={classes.icon} p={1}>
{icon}
</Box>
<Box height="100%" width="100%" display="flex" justifyContent="center" alignItems="center" flexDirection="column">
<Box mb={1} className={classes.content}>{children ? children : content}</Box>
<div className={classes.title}>{title}</div>
</Box>
</PosedPaper>
)
}
export default BigTile
And here is my test :
it('BigTile hover check', ()=>{
const { container } = render(<BigTile />)
const elem = container.querySelector<HTMLElement>('.MuiPaper-root')
if (!elem) fail("Element is null.")
fireEvent.mouseOver(elem);
const elemAfterHover = container.querySelector<HTMLElement>('.MuiPaper-root')
if (!elemAfterHover) fail("Element after hover is null.")
console.log(elemAfterHover.style)
})
Here i cant see changed style after mouseover event. Console log shows me only transform:none
when it should be something like transform: scale(1.05).
Please help me with firing this event properly and if you have some advise to the code itself it would be great to take some advices.
//Answer
The event was firing every time i started the test.
To check it i just put some console.log(..) inside onHoverHandle which is firing after moseenter/leave.
Of course i didn't need to find element once again after fireEvent, becouse i had this element before. Main idea is that i needed to wait for event to end before checking for style change. So waitFor is good for that.
I read somewhere than using userEvent from '#testing-library/user-event' is better than fireEvent , so i did.
Final solution :
it('BigTile hover check.', async ()=>{
const { container } = render(<BigTile />)
const elem = container.firstChild as HTMLElement
if (!elem) fail("Element is null.")
userEvent.hover(elem);
await waitFor(() => {
expect(elem.style.transform).toEqual("scale(1.05) translateZ(0)")
})
})

Looping and rendering in reactjs

const user1 = [
{
id: 1,
orderDetails: [
{
id: 1,
custFN: 'Jon',
custLN: 'Smith',
serverDetails:
[{
id: 123,
sFN: 'Barack',
sLN: 'Obama',
}],
orderFinish: false,
}
],
shopF: [
{
maxPX: 10,
maxPY: 10,
}
],
furniture: [
{
id: 1,
type: 'table',
pX: 2,
pY: 0,
label: 'VIP Table',
color: 'green',
},
{
id: 2,
type: 'table',
pX: 2,
pY: 1,
label: 'VIP Table',
color: 'green',
},
{
id: 3,
type: 'chair',
pX: 1,
pY: 0,
label: 'VIP Chair',
color: 'brown',
},
{
id: 4,
type: 'chair',
pX: 1,
pY: 1,
label: 'VIP Chair',
color: 'pink',
},
],
}
];
My app is like a dining area where there are tables, chairs, plants etc. Think of it as tiles. So anyway,
Above is my sample data. shopF determines the maximum tiles that will be made(more like x and y coordinates). furniture consists of its position, type of furniture etc.
My problem is that I want to render an element that will serve as tiles and while looping it, determine if a furniture exists in x and y coordinates. I'm thinking of nested for loop.
return (
<React.Fragment>
{
// For loop y
// If furniture is equal to y then render also furniture else just render empty tiles
// For loop x
// If furniture is equal to x then render also furniture else just render empty tiles
}
</React.Fragment>
);
I've tried using map functionality but I don't know how to exactly do it.
Below image is the layout for the 'tiles' and its x and y coordinates.
Here's how I would do it, first create a Tilemap using this method:
const BuildTilesMap = (data, sizeX, sizeY) => {
let arr = new Array(sizeY).fill(null).map(() =>
new Array(sizeX).fill(null).map(() => ({
type: "empty" // chnage this to fit your need
}))
);
for (const item of data) {
console.log(item);
arr[item.posY][item.posX] = item;
}
return arr;
};
Then render a Row which would have a simple div which style set display: flex inside with an map of items like so:
const RenderRow = ({ data }) => {
return (
<div style={{ display: "flex" }}>
{data.map(e => (
<Tiles data={e} />
))}
</div>
);
};
const Tiles = ({ data }) => {
return (
<div style={{ height: 80, width: 80, border: "1px solid #555" }}>
{data.type}
</div>
);
};
Here's the codesandbox

breaking big component into smaller one

I'm working on separating code from index.tsx into two different files viz: firstTab.tsx and secondTab.tsx. I haven't started working on secondTab.tsx yet.
I separated first tab related code into firstTab.tsx as shown in the following code editor: The full functional code with both tabs working are in index.tsx is pasted below:
import React, { Component } from "react";
import { render } from "react-dom";
import "jqwidgets-scripts/jqwidgets/styles/jqx.base.css";
import JqxButton from "jqwidgets-scripts/jqwidgets-react-tsx/jqxbuttons";
import * as ReactDOM from "react-dom";
import JqxWindow from "jqwidgets-scripts/jqwidgets-react-tsx/jqxwindow";
import JqxInput from "jqwidgets-scripts/jqwidgets-react-tsx/jqxinput";
import JqxChart, {
IChartProps
} from "jqwidgets-scripts/jqwidgets-react-tsx/jqxchart";
import JqxGrid, {
IGridProps,
jqx
} from "jqwidgets-scripts/jqwidgets-react-tsx/jqxgrid";
import JqxTabs from "jqwidgets-scripts/jqwidgets-react-tsx/jqxtabs";
import JqxDropDownList, {
IDropDownListProps
} from "jqwidgets-scripts/jqwidgets-react-tsx/jqxdropdownlist";
import firstTab from './firstTab';
interface AppProps {}
interface AppState {
name: string;
}
interface IProps extends IGridProps {
dropdownlistSource: IDropDownListProps["source"];
}
class App extends Component<{}, IProps> {
private myTabs = React.createRef<JqxTabs>();
private gridElement = React.createRef<HTMLDivElement>();
private myGrid = React.createRef<JqxGrid>();
private gridElementTwo = React.createRef<HTMLDivElement>();
private myGrid2 = React.createRef<JqxGrid>();
constructor(props: {}) {
super(props);
this.state = {
dropdownlistSource: [
{ value: 0, label: "Affogato" },
{ value: 1, label: "Americano" },
{ value: 2, label: "Bicerin" },
{ value: 3, label: "Breve" }
]
};
}
public render() {
return (
<JqxTabs
ref={this.myTabs}
// #ts-ignore
width={400}
height={560}
initTabContent={this.initWidgets}
>
<ul>
<li style={{ marginLeft: 30 }}>
<div style={{ height: 20, marginTop: 5 }}>
<div
style={{
marginLeft: 4,
verticalAlign: "middle",
textAlign: "center",
float: "left"
}}
>
US Indexes
</div>
</div>
</li>
<li>
<div style={{ height: 20, marginTop: 5 }}>
<div
style={{
marginLeft: 4,
verticalAlign: "middle",
textAlign: "center",
float: "left"
}}
>
NASDAQ compared to S&P 500
</div>
</div>
</li>
</ul>
<div style={{ overflow: "hidden" }}>
<div id="jqxGrid" ref={this.gridElement} />
<div style={{ marginTop: 10, height: "15%" }} />
</div>
<div style={{ overflow: "hidden" }}>
<div id="jqxGrid2" ref={this.gridElementTwo} />
<div style={{ marginTop: 10, height: "15%" }} />
</div>
</JqxTabs>
);
}
private initGrid = () => {
const source = {
datafields: [{ name: "Date" }, { name: "S&P 500" }, { name: "NASDAQ" }],
datatype: "csv",
localdata: `1/2/2014,1831.98,4143.07
1/3/2014,1831.37,4131.91
1/6/2014,1826.77,4113.68
1/7/2014,1837.88,4153.18
1/8/2014,1837.49,4165.61
1/9/2014,1838.13,4156.19
2/6/2014,1773.43,4057.12
2/7/2014,1797.02,4125.86`
};
const dataAdapter = new jqx.dataAdapter(source, {
async: false,
loadError: (xhr: any, status: any, error: any) => {
console.log(xhr, status, error);
}
});
const columns: IGridProps["columns"] = [
{ cellsformat: "d", datafield: "Date", text: "Date", width: 250 },
{ datafield: "S&P 500", text: "S&P 500", width: 150 },
{ datafield: "NASDAQ", text: "NASDAQ" }
];
const grid = (
<JqxGrid
ref={this.myGrid}
width={"100%"}
height={400}
source={dataAdapter}
columns={columns}
/>
);
render(grid, this.gridElement.current!);
};
private initGrid2 = () => {
const source = {
datafields: [{ name: "Date" }, { name: "S&P 500" }, { name: "NASDAQ" }],
datatype: "csv",
localdata: `1/2/2014,1831.98,4143.07
1/3/2014,1831.37,4131.91
1/6/2014,1826.77,4113.68
1/7/2014,1837.88,4153.18
1/8/2014,1837.49,4165.61
1/9/2014,1838.13,4156.19
1/10/2014,1842.37,4174.67
2/7/2014,1797.02,4125.86`
};
const dataAdapter = new jqx.dataAdapter(source, {
async: false,
loadError: (xhr: any, status: any, error: any) => {
console.log(xhr, status, error);
}
});
const columns: IGridProps["columns"] = [
{ cellsformat: "d", datafield: "Date", text: "Date", width: 250 },
{ datafield: "S&P 500", text: "S&P 500", width: 150 },
{ datafield: "NASDAQ", text: "NASDAQ" }
];
const grid = (
<JqxGrid
ref={this.myGrid2}
width={"100%"}
height={400}
source={dataAdapter}
columns={columns}
/>
);
render(grid, this.gridElementTwo.current!);
};
private initWidgets = (tab: any) => {
switch (tab) {
case 0:
this.initGrid();
break;
case 1:
this.initGrid2();
break;
}
};
}
render(<App />, document.getElementById("root"));
Question:
Since I've already moved private initGrid = () => { inside a separate file firstTab.tsx, in index.tsx where should I put {firstTab.tsx} to make sure both tabs in index.tsx works fine? I mean, even if I comment out private initGrid = () => { function from index.tsx both tabs should work fine.
Thanks
If I would refactor this I would consider the next approach:
Create Parent component Table (probably some more appropriate name)
Create a component for US Indexes
Create a component for NASDAQ compared to S&P 500
Based on the active tab render the proper component.
You could also create a separate file that contains only exports with your data.
If you then import that into your files with the functions you can use that there, keeps it cleaner.
And if you pass that data as a prop / param to your initGrid() functions, you don't have to repeat that code, can reuse it.

Resources