I want to change the parent of a Node in a way that
<div>
<children/>
</div>
becomes
<div>
<NewParent>
<children/>
</NewParent>
</div>
I need this to put a mui Tooltip above a component that overflows with ellipsis.
I implemented a small algorithm to find the needed element but when I try to use portals for this case this happens. enter image description here
My NewParent becomes the sibbling of the old parent.
Later I learned that usePortal brings the children to the parent and doesn't wrap the parent to the children so my question is what can I do to wrap a new parent to the node and make the old parent be the grandfather as per my first example
Current component
import React, { useRef, useEffect, useState } from 'react';
import { Tooltip } from '#mui/material';
import { Theme } from '#mui/material';
import { makeStyles } from '#mui/styles'
import { GridCellProps, GridCell } from '#mui/x-data-grid';
import { createPortal } from 'react-dom';
const useStyles = makeStyles<Theme>(() => ({
overflowEllipsis: {
width: '100%',
},
}))
const OverflowTip = React.forwardRef(({ children, ...props }: GridCellProps) => {
const [ portaled, setPortaled ] = useState(false)
const textElementRef = useRef<HTMLDivElement>();
const TooltipRef = useRef<HTMLDivElement>();
const classes = useStyles()
const compareSize = () => {
if (!textElementRef.current) {
return
}
const compare =
textElementRef.current.scrollWidth > textElementRef.current.clientWidth;
setHover(compare);
};
const findLowestChildren = (currentElement) => {
if (!currentElement) {
return
}
if (currentElement.children.length === 0) {
console.log(currentElement);
console.log(TooltipRef);
setPortaled(true)
createPortal(currentElement, TooltipRef.current)
currentElement.className += ` ${classes.overflowEllipsis}`
}
const arr = [].slice.call(currentElement.children);
return arr.forEach((ch) => {
if (ch.tagName === 'DIV' || ch.tagName === 'P' || ch.tagName === 'SPAN') {
return findLowestChildren(ch)
}
});
}
// compare once and add resize listener on "componentDidMount"
useEffect(() => {
compareSize();
window.addEventListener('resize', compareSize);
if (!portaled) {
findLowestChildren(textElementRef.current)
}
}, []);
// remove resize listener again on "componentWillUnmount"
useEffect(() => () => {
window.removeEventListener('resize', compareSize);
}, []);
// Define state and function to update the value
const [ hoverStatus, setHover ] = useState(false);
// console.log(props);
return (
<div ref={textElementRef}>
<GridCell
{...props}>
{children}
</GridCell>
<div ref={TooltipRef} className="wwwwwwww"><Tooltip title="QWEW"><></></Tooltip></div>
</div>
);
// return (
// <Tooltip
// title={children}
// disableHoverListener={!hoverStatus}
// >
// <BoxContainer
// ref={textElementRef}
// style={{
// whiteSpace: 'nowrap',
// overflow: 'hidden',
// textOverflow: 'ellipsis',
// }}>
// <GridCell
// {...props}
// >
// {children}
// </GridCell>
// </BoxContainer>
// </Tooltip>
// );
});
export default OverflowTip;
Related
Based on this documentation https://4x.ant.design/components/table/#components-table-demo-virtual-list I've created VirtualTable component.
import React, { useState, useRef, useEffect } from 'react'
import { Table } from 'antd'
import classNames from 'classnames'
import ResizeObserver from 'rc-resize-observer'
import { VariableSizeGrid as Grid } from 'react-window'
const VirtualTable = (props) => {
const { columns, scroll } = props
const [tableWidth, setTableWidth] = useState(0)
const widthColumnCount = columns.filter(({ width }) => !width).length
const mergedColumns = columns.map((column) => {
if (column.width) {
return column
}
return { ...column, width: Math.floor(tableWidth / widthColumnCount) }
})
const gridRef = useRef()
const [connectObject] = useState(() => {
const obj = {}
Object.defineProperty(obj, 'scrollLeft', {
get: () => {
if (gridRef.current) {
return gridRef.current?.state?.scrollLeft
}
return null
},
set: (scrollLeft) => {
if (gridRef.current) {
gridRef.current.scrollTo({
scrollLeft,
})
}
},
})
return obj
})
const resetVirtualGrid = () => {
gridRef?.current?.resetAfterIndices({
columnIndex: 0,
shouldForceUpdate: true,
})
}
useEffect(() => resetVirtualGrid, [tableWidth])
const renderVirtualList = (rawData, { scrollbarSize, ref, onScroll }) => {
ref.current = connectObject
const totalHeight = rawData.length * 54
return (
<Grid
ref={gridRef}
className="virtual-grid"
columnCount={mergedColumns.length}
columnWidth={(index) => {
const { width } = mergedColumns[index]
return totalHeight > scroll.y && index === mergedColumns.length - 1 ? width - scrollbarSize - 1 : width
}}
height={scroll.y}
rowCount={rawData.length}
rowHeight={() => 54}
width={tableWidth}
onScroll={({ scrollLeft }) => {
onScroll({
scrollLeft,
})
}}
>
{({ columnIndex, rowIndex, style }) => (
<div
className={classNames('virtual-table-cell', {
'virtual-table-cell-last': columnIndex === mergedColumns.length - 1,
})}
style={style}
>
{mergedColumns[columnIndex].dataIndex !== '' && !mergedColumns[columnIndex].render ? (
<div> {rawData[rowIndex][mergedColumns[columnIndex].dataIndex]} </div>
) : (
<div>
{mergedColumns[columnIndex].render(
rawData[rowIndex][mergedColumns[columnIndex].dataIndex],
rawData[rowIndex]
)}
</div>
)}
</div>
)}
</Grid>
)
}
return (
<ResizeObserver
onResize={({ width }) => {
setTableWidth(width)
}}
>
<Table
{...props}
className="virtual-table"
columns={mergedColumns}
pagination={false}
components={{
body: renderVirtualList,
}}
/>
</ResizeObserver>
)
}
export default VirtualTable
However, when I tried to create a fixed header like this https://4x.ant.design/components/table/#components-table-demo-fixed-header I broke this table.
Any thoughts on how to implement a fixed header for Antd table based on the virtual table?
UPDATE:
I found that ant table based on https://table-react-component.vercel.app/demo/virtual-list but in my case renderVirtualList is not called
SOLUTION
Remove the style={{ fill: 'green' }} from the component and add this to the css file:
/*index.css*/
/* INFO: the sidebarleft-button contains a child that is an <svg> */
.sidebarleft-button-active > * {
fill: rgb(192, 192, 192);
}
.sidebarleft-button:hover > * {
fill: rgb(192, 192, 192);
}
PROBLEM(That is now solved)
I am working on a SideBar for my Application. The Sidebar contains Buttons. Each Button contains an Icon. I want to change the color of the icon if I hover with my mouse over the button. I tried many things and I wonder why no re-rendering is triggered because the icon changed and a change means re-rendering. I can see that the style.fill gets changed to blue in the inspector window of my browser.
I show you my code. The comments inside the code may help you.
//sideBarLeft.jsx
import React, { useState } from 'react';
import SideBarButton from './sideBarButton';
import { DIAGRAMTYPE } from '../diagramComponent/diagramUtils';
import { ReactComponent as BarChartIcon } from '../../icon/barChart.svg';
const SideBarLeft = (props) => {
const [btn0Active, setBtn0Active] = useState(true);
const [btn1Active, setBtn1Active] = useState(false);
const deactivateOtherButtonsAndActiveThisButton = (
idOfButtonThatWasClicked
) => {
switch (idOfButtonThatWasClicked) {
case 0:
setBtn1Active(false);
setBtn0Active(true);
break;
case 1:
setBtn0Active(false);
setBtn1Active(true);
break;
default:
setBtn0Active(false);
setBtn1Active(false);
}
};
return (
<div className={'sidebarleft'}>
<SideBarButton
active={btn0Active}
id={0}
deactivateOtherButtonsAndActiveThisButton={
deactivateOtherButtonsAndActiveThisButton
}
icon={<BarChartIcon style={{ fill: 'green' }} />}
diagramType={DIAGRAMTYPE.barChartPos}
/>
<SideBarButton
active={btn1Active}
id={1}
deactivateOtherButtonsAndActiveThisButton={
deactivateOtherButtonsAndActiveThisButton
}
icon={<BarChartIcon style={{ fill: 'green' }} />}
diagramType={DIAGRAMTYPE.barChartWordOccurence}
/>
</div>
);
};
export default SideBarLeft;
import React, { useState, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../state/index';
const SideBarButton = (props) => {
const dispatch = useDispatch();
const { switchToDiagramType } = bindActionCreators(actionCreators, dispatch);
const [cssClass, setCssClass] = useState('sidebarleft-button');
const onClickFn = () => {
props.deactivateOtherButtonsAndActiveThisButton(props.id);
switchToDiagramType(props.diagramType);
};
const [icon, setIcon] = useState(props.icon);
const buttonHandler = () => {};
const onMouseEnter = (component) => {
console.log('in onMouseEnter()');
const classOfComponent = component.target.classList.value;
if (classOfComponent === 'sidebarleft-button-active') {
console.log('component is active');
} else if (classOfComponent === 'sidebarleft-button') {
console.log('before');
console.log(props.icon); //even without mousenter, the component is already blue...
let changedIcon = props.icon;
props.icon.props.style.fill = 'blue';
setIcon(changedIcon); //in the inspector
//there is props->style->fill == 'blue'
//why does it not get rendered then in blue???
console.log('after');
console.log(changedIcon);
console.log('component is not active');
// console.log(component);
}
};
console.log('beforebefore');
console.log(icon);
return (
<button
onClick={onClickFn}
className={cssClass}
onMouseEnter={(component) => onMouseEnter(component)}
>
{icon}
</button>
);
};
export default SideBarButton;
Why not a css or sass approach?
.sidebarleft-button-active {
&:hover {
fill: blue;
}
}
I am trying to implement TABS in my demo application.
condition : I want to render those TABS or menu item which have length less than 100px
I created tab with following steps
export const TabsContext = React.createContext({
selectedTabId: "",
setSelectedTabId: () => {},
tabsVisible: [],
tabsHidden: [],
setTabsVisible: () => {},
setTabsHidden: () => {}
});
Created a context which have selectedIndex, visibleArray, hiddenArray is present
In TABLIST component I checked all element width .if it is greater than 100 I pushed to visible else hidden.
but how to render visible array/ ITEM
const TabList = ({
isAlign,
isSize,
isBoxed,
isToggle,
isToggleRounded,
isFullwidth,
children,
...rest
}) => {
const tabsRef = useRef(null);
const { tabsVisible, tabsHidden, setTabsVisible, setTabsHidden } = useContext(
TabsContext
);
const getWidth = () => {
let hidden = [],
visible = [];
[...tabsRef.current.children].forEach((element) => {
console.log(element.offsetWidth);
if (element.offsetWidth > 100) {
hidden.push(element);
} else {
visible.push(element);
}
});
};
useEffect(() => {
getWidth();
}, []);
return (
<div>
<ul className="tabs" ref={tabsRef}>
{children}
</ul>
</div>
);
};
TabList.displayName = "Tabs.TabList";
export { TabList };
here is my whole code
https://codesandbox.io/s/intelligent-wescoff-dd3xd?file=/src/tablist.js:141-924
Getting Error while rendering
Objects are not valid as a React child (found: [object HTMLLIElement]).
I set the default value of visible array
<Tabs
defaultTabId="1"
defaultTabsVisible={data.map((i) => (
<Tabs.Tab tabId={i} key={i}>
{i}
</Tabs.Tab>
))}
>
<Tabs.TabList isSize="medium"></Tabs.TabList>
</Tabs>
and try to updated array
const getWidth = () => {
let hidden = [],
visible = [];
[...tabsRef.current.children].forEach((element) => {
console.log(element.offsetWidth);
if (element.offsetWidth > 100) {
hidden.push(element);
} else {
visible.push(element);
}
});
setTabsVisible(visible);
setTabsHidden(hidden);
};
while update I am getting this error
Objects are not valid as a React child (found: [object HTMLLIElement]). I
Ok so this is what I came up with
import React, { useRef, useContext, useEffect, useState } from "react";
import classNames from "classnames";
import { TabsContext } from "./context";
const TabList = ({
isAlign,
isSize,
isBoxed,
isToggle,
isToggleRounded,
isFullwidth,
children,
...rest
}) => {
const tabsRef = useRef(null);
const { tabsVisible, tabsHidden, setTabsVisible, setTabsHidden } = useContext(
TabsContext
);
const [firstRender, setFirstRender] = useState(true);
const getWidth = () => {
let hidden = [],
visible = [];
[...tabsRef.current.children].forEach((element, i) => {
console.log(element.offsetWidth);
if (element.offsetWidth > 100) {
hidden.push(children[i]);
} else {
visible.push(children[i]);
}
});
setTabsVisible(visible);
setTabsHidden(hidden);
if(tabsRef.current)
setFirstRender(false)
};
useEffect(() => {
getWidth();
}, []);
return (
<div>
<ul className="tabs" ref={tabsRef}>
{firstRender ? children : tabsVisible }
</ul>
</div>
);
};
TabList.displayName = "Tabs.TabList";
export { TabList };
This will check all children size on initial render and update the visible array. Then after the first render it will only display the visible children.
Update - aria-hidden
{firstRender ?
(<ul className="tabs" ref={tabsRef} aria-hidden={false}>
{children}
</ul>)
:
(<>
<ul className="tabs" ref={tabsRef} aria-hidden={false}>
{tabsVisible}
</ul>
<ul className="tabs-hidden" ref={tabsRef} aria-hidden={true}>
{tabsHidden}
</ul>
</>)
}
If you want aria-hidden property to be individual for each tab, you would have to check for element width inside the tab component also. Then add the aria-hidden hidden property in there.
You have to use the property opacity of CSS to hide your tabs, after this step you have to create a state tabsToDisplay to store the tabs that verify your condition.
styles.css
.tabs-hidden {
opacity: 0; // <=== hide your tabs
display: flex;
list-style: none;
}
tabList.js
import React, { useRef, useContext, useEffect, useState } from "react";
import classNames from "classnames";
import { TabsContext } from "./context";
const TabList = ({
isAlign,
isSize,
isBoxed,
isToggle,
isToggleRounded,
isFullwidth,
children,
...rest
}) => {
const tabsRef = useRef(null);
const { tabsVisible, tabsHidden, setTabsVisible, setTabsHidden } = useContext(
TabsContext
);
const [tabsToDisplay, setTabsToDisplay] = useState([]); // <=== create this state to store the tabs that match your condition
const getWidth = () => {
let hidden = [],
visible = [],
tabsToDisplay = [];
[...tabsRef.current.children].forEach((element, i) => {
if (element.offsetWidth > 100) {
hidden.push(element);
} else {
visible.push(element);
tabsToDisplay.push(children[i]);
}
});
console.log("test", tabsToDisplay);
setTabsToDisplay(tabsToDisplay);
};
useEffect(() => {
getWidth();
}, []);
return (
<div>
<ul className="tabs-hidden" ref={tabsRef}>
{children}
</ul>
<ul className="tabs">{tabsToDisplay}</ul>
</div>
);
};
TabList.displayName = "Tabs.TabList";
export { TabList };
You can check the demo:
https://codesandbox.io/s/hungry-galileo-qk09i?file=/src/tablist.js:0-1156
As you're using ref, you can do it using them.
This is an alternative to current answers, using useLayoutEffect
const TabList = ({
isAlign,
isSize,
isBoxed,
isToggle,
isToggleRounded,
isFullwidth,
children,
...rest
}) => {
const tabsRef = useRef(null);
const { tabsVisible, tabsHidden, setTabsVisible, setTabsHidden } = useContext(
TabsContext
);
React.useLayoutEffect(() => {
let hidden = [],
visible = [];
[...tabsRef.current.children].forEach((element) => {
console.log(element.offsetWidth);
if (element.offsetWidth > 100) {
element.style.display = "none";
element.setAttribute("aria-hidden", "true");
hidden.push(element);
} else if (element.style.display !== 'none') {
element.style.display = "block";
element.setAttribute("aria-hidden", "false");
visible.push(element);
}
});
setTabsVisible(visible)
setTabsHidden(hidden)
}, [setTabsVisible, setTabsHidden, tabsRef])
return (
<div>
<ul className="tabs" ref={tabsRef}>
{children}
</ul>
</div>
);
};
Changed tabs to prevent infinite rerendering
return React.createElement(
TabsContext.Provider,
{
value: {
selectedTabId,
setSelectedTabId,
tabsVisible,
tabsHidden,
setTabsVisible,
setTabsHidden
}
},
children
);
Also note the
} else if (element.style.display !== "none") {
This prevents the element that is displayed with none, to be displayed as having width < 100 on a further rerender
https://codesandbox.io/s/pedantic-kirch-ykdc3?file=/src/tablist.js
I am near the end of creating my application.
So it is for banks accounts where they ask you to give the first letter of your password, then for example fourth, etc.
I'm tired of counting on my own so I created this app.
But there is the last bug that I don't know how to fix.
So when I press "1" I get "1 - H", and then when I press "4" I want to get:
"1 - H" (clicked before)
"4 - X" (clicked just now)
but instead, I get:
"4 - X" (clicked just now)
"4 - X" (clicked just now)
So it is caused by the way handleResults() function works inside my Input component, but for now it is my only concept how to approach this...
import React, { Component } from 'react';
import TextField from 'material-ui/TextField';
import './style.css';
import Buttons from '../Buttons';
import Results from '../Results';
class Input extends Component {
constructor(props) {
super(props);
this.state = {
password: 'Hh9Xzke2ayzcEUPHuIfS',
selectedButtons: [],
};
this.handleButtonSelectTwo = this.handleButtonSelectTwo.bind(this);
}
handleInputChange(pass) {
this.setState({ password: pass });
}
handleButtonSelectTwo(selected) {
this.setState({
selectedButtons: [...this.state.selectedButtons, selected],
});
}
handleResults() {
return this.state.selectedButtons.map(el => (
<Results key={el} appState={this.state} />
));
}
render() {
return (
<div>
<div className="Input-textfield">
<TextField
hintText="Paste your password here to begin"
value={this.state.password}
onChange={event => this.handleInputChange(event.target.value)}
/>
</div>
<div>
<Buttons
handleButtonSelectOne={this.handleButtonSelectTwo}
array={this.state.password.length}
/>
{this.handleResults()}
</div>
</div>
);
}
}
export default Input;
and here is Results component code:
import React, { Component } from 'react';
import _ from 'lodash';
import Avatar from 'material-ui/Avatar';
import List from 'material-ui/List/List';
import ListItem from 'material-ui/List/ListItem';
import './style.css';
const style = {
avatarList: {
position: 'relative',
left: -40,
},
avatarSecond: {
position: 'relative',
top: -40,
left: 40,
},
};
class Results extends Component {
resultsEngine(arg) {
const { selectedButtons, password } = this.props.appState;
const passwordArray = password.split('').map(el => el);
const lastSelectedButton = _.last(selectedButtons);
const passwordString = passwordArray[_.last(selectedButtons) - 1];
if (arg === 0) {
return lastSelectedButton;
}
if (arg === 1) {
return passwordString;
}
return null;
}
render() {
if (this.props.appState.selectedButtons.length > 0) {
return (
<div className="test">
<List style={style.avatarList}>
<ListItem
disabled
leftAvatar={<Avatar>{this.resultsEngine(0)}</Avatar>}
/>
<ListItem
style={style.avatarSecond}
disabled
leftAvatar={<Avatar>{this.resultsEngine(1)}</Avatar>}
/>
</List>
</div>
);
}
return <div />;
}
}
export default Results;
Anyone has an idea how should I change my code inside handleResults() function to achieve my goal? Any help with solving that problem will be much appreciated.
Buttons component code:
import React from 'react';
import OneButton from '../OneButton';
const Buttons = props => {
const arrayFromInput = props.array;
const buttonsArray = [];
for (let i = 1; i <= arrayFromInput; i++) {
buttonsArray.push(i);
}
const handleButtonSelectZero = props.handleButtonSelectOne;
const allButtons = buttonsArray.map(el => (
<OneButton key={el} el={el} onClick={handleButtonSelectZero} />
));
if (arrayFromInput > 0) {
return <div>{allButtons}</div>;
}
return <div />;
};
export default Buttons;
And OneButton code:
import React, { Component } from 'react';
import RaisedButton from 'material-ui/RaisedButton';
const style = {
button: {
margin: 2,
padding: 0,
minWidth: 1,
},
};
class OneButton extends Component {
constructor() {
super();
this.state = { disabled: false };
}
handleClick() {
this.setState({ disabled: !this.state.disabled });
this.props.onClick(this.props.el);
}
render() {
return (
<RaisedButton
disabled={this.state.disabled}
key={this.props.el}
label={this.props.el}
style={style.button}
onClick={() => this.handleClick()}
/>
);
}
}
export default OneButton;
In your resultsEngine function in the Results component you are specifying that you always want the _.last(selectedButtons) to be used. This is what it is doing, hence you always see the last button clicked. What you actually want is the index of that iteration to show.
const lastSelectedButton = selectedButtons[this.props.index];
const passwordString = passwordArray[selectedButtons[this.props.index]];
To get an index you have to create and pass one in, so create it when you map over the selected Buttons in the handleResults function in your Input component.
handleResults() {
return this.state.selectedButtons.map((el, index) => (
<Results key={el} appState={this.state} index={index} />
));
}
I am working on a simple version of ReactDND before I implement this code into my image uploader.
Each time an image is added, it is added to state and passed through to ReactDND so that it is draggable and also droppable (so users can rearrange their images).
Everything works great, except for one thing. The problem I am having is after adding multiple images, is that once I drag and drop and image (works), the State no longer updates for ReactDND and I cannot add new images.
Here is my code below (note I am just using a button to add extra items to state):
Main Component:
import React from 'react';
// Drag and drop stuff
import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import Container from './Container';
class ImageUploader extends React.Component {
constructor(props) {
super(props);
this.state = {
list: [],
listCount: 1
};
this.onAddItem = this.onAddItem.bind(this);
}
onAddItem(e) {
e.preventDefault();
var listArray = this.state.list;
var buildObject = {
text: 'Jeremy' + this.state.listCount.toString(),
age: '25',
id: this.state.listCount
};
listArray.push(buildObject);
let newListCount = this.state.listCount + 1;
this.setState({
list: listArray,
listCount: newListCount
});
console.log(this.state.list);
}
render() {
return (
<div>
<h1>Add to List</h1>
<button onClick={this.onAddItem}>Add Item</button>
<h1>The List</h1>
<Container id={1} list={this.state.list} />
</div>
)
}
}
export default DragDropContext(HTML5Backend)(ImageUploader);
Container:
import React, { Component } from 'react';
import update from 'react/lib/update';
import Card from './Card';
import { DropTarget } from 'react-dnd';
class Container extends Component {
constructor(props) {
super(props);
this.state = { cards: props.list };
}
pushCard(card) {
this.setState(update(this.state, {
cards: {
$push: [ card ]
}
}));
}
removeCard(index) {
this.setState(update(this.state, {
cards: {
$splice: [
[index, 1]
]
}
}));
}
moveCard(dragIndex, hoverIndex) {
const { cards } = this.state;
const dragCard = cards[dragIndex];
this.setState(update(this.state, {
cards: {
$splice: [
[dragIndex, 1],
[hoverIndex, 0, dragCard]
]
}
}));
}
render() {
const { cards } = this.state;
const { canDrop, isOver, connectDropTarget } = this.props;
const isActive = canDrop && isOver;
const style = {
width: "200px",
height: "404px",
border: '1px dashed gray'
};
const backgroundColor = isActive ? 'lightgreen' : '#FFF';
return connectDropTarget(
<div className="houzes-dropbox">
{cards.map((card, i) => {
return (
<Card
key={card.id}
index={i}
listId={this.props.id}
card={card}
removeCard={this.removeCard.bind(this)}
moveCard={this.moveCard.bind(this)} />
);
})}
</div>
);
}
}
const cardTarget = {
drop(props, monitor, component ) {
const { id } = props;
const sourceObj = monitor.getItem();
if ( id !== sourceObj.listId ) component.pushCard(sourceObj.card);
return {
listId: id
};
}
}
export default DropTarget("CARD", cardTarget, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
canDrop: monitor.canDrop()
}))(Container);
Card:
import React, { Component } from 'react';
import { findDOMNode } from 'react-dom';
import { DragSource, DropTarget } from 'react-dnd';
import flow from 'lodash/flow';
const style = {
border: '1px dashed grey',
padding: '0.5rem 1rem',
margin: '.5rem',
backgroundColor: 'white',
cursor: 'move'
};
class Card extends Component {
render() {
const { card, isDragging, connectDragSource, connectDropTarget } = this.props;
const opacity = isDragging ? 0 : 1;
// Background URL
let backgroundUrl = {
backgroundImage: "url(" + "http://localhost:4000/uploads/2017/8/a3ff91dc-2f80-42f7-951a-e9a74bf954d7-1200x800.jpeg" + ")"
};
console.log(card);
return connectDragSource(connectDropTarget(
<div className={`uploadedImageWrapper col-md-6 col-sm-12`}>
<div className="uploadedImage">
<span style={backgroundUrl} />
{card.text}
{card.age}
</div>
</div>
));
}
}
const cardSource = {
beginDrag(props) {
return {
index: props.index,
listId: props.listId,
card: props.card
};
},
endDrag(props, monitor) {
const item = monitor.getItem();
const dropResult = monitor.getDropResult();
if ( dropResult && dropResult.listId !== item.listId ) {
props.removeCard(item.index);
}
}
};
const cardTarget = {
hover(props, monitor, component) {
const dragIndex = monitor.getItem().index;
const hoverIndex = props.index;
const sourceListId = monitor.getItem().listId;
// Don't replace items with themselves
if (dragIndex === hoverIndex) {
return;
}
// Determine rectangle on screen
const hoverBoundingRect = findDOMNode(component).getBoundingClientRect();
// Get vertical middle
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
// Determine mouse position
const clientOffset = monitor.getClientOffset();
// Get pixels to the top
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
// Only perform the move when the mouse has crossed half of the items height
// When dragging downwards, only move when the cursor is below 50%
// When dragging upwards, only move when the cursor is above 50%
// Dragging downwards
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return;
}
// Dragging upwards
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return;
}
// Time to actually perform the action
if ( props.listId === sourceListId ) {
props.moveCard(dragIndex, hoverIndex);
// Note: we're mutating the monitor item here!
// Generally it's better to avoid mutations,
// but it's good here for the sake of performance
// to avoid expensive index searches.
monitor.getItem().index = hoverIndex;
}
}
};
export default flow(
DropTarget("CARD", cardTarget, connect => ({
connectDropTarget: connect.dropTarget()
})),
DragSource("CARD", cardSource, (connect, monitor) => ({
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging()
}))
)(Card);
So just to recap, I can add items to state, and they become draggable and droppable. But after having dragged and dropped an element, I can no longer add anymore items to state.
Any ideas as to what the solution would be? What am I doing wrong?
Thank-you for looking through this, and any answers. Cheers.
#Notorious.
I have checked your code in my side and solved the issue.
When you drag and drop an element that changes the state of Container but not the state of ImageUploader.
So I made a function to inform the state of Container has changed.
Also I inserted componentWillReceiveProps() function to Container and updated the state of Container in that function.
Finally the problem solved.
Here's the changed code.
Main Component:
import React from 'react';
// Drag and drop stuff
import {DragDropContext} from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import Container from './Container';
class ImageUploader extends React.Component {
constructor(props) {
super(props);
this.state = {
list: [],
listCount: 1
};
this.onAddItem = this
.onAddItem
.bind(this);
this.listChanged = this.listChanged.bind(this);
}
onAddItem(e) {
e.preventDefault();
var listArray = this.state.list;
var buildObject = {
text: 'Jeremy' + this
.state
.listCount
.toString(),
age: '25',
id: this.state.listCount
};
listArray.push(buildObject);
let newListCount = this.state.listCount + 1;
this.setState({list: listArray, listCount: newListCount});
}
listChanged(newList) {
this.setState({
list: newList
})
}
render() {
return (
<div>
<h1>Add to List</h1>
<button onClick={this.onAddItem}>Add Item</button>
<h1>The List</h1>
<Container id={1} list={this.state.list} listChanged={this.listChanged}/>
</div>
)
}
}
export default DragDropContext(HTML5Backend)(ImageUploader);
Container:
import React, { Component } from 'react';
import update from 'react/lib/update';
import Card from './Card';
import { DropTarget } from 'react-dnd';
class Container extends Component {
constructor(props) {
super(props);
this.state = { cards: this.props.list };
}
pushCard(card) {
this.setState(update(this.state, {
cards: {
$push: [ card ]
}
}));
}
removeCard(index) {
this.setState(update(this.state, {
cards: {
$splice: [
[index, 1]
]
}
}));
}
moveCard(dragIndex, hoverIndex) {
const { cards } = this.state;
const dragCard = cards[dragIndex];
this.setState(update(this.state, {
cards: {
$splice: [
[dragIndex, 1],
[hoverIndex, 0, dragCard]
]
}
}));
}
componentWillReceiveProps(nextProps) {
// You don't have to do this check first, but it can help prevent an unneeded render
if (nextProps.list !== this.state.cards) {
this.props.listChanged(this.state.cards);
}
}
render() {
const { cards } = this.state;
const { canDrop, isOver, connectDropTarget } = this.props;
const isActive = canDrop && isOver;
const style = {
width: "200px",
height: "404px",
border: '1px dashed gray'
};
const backgroundColor = isActive ? 'lightgreen' : '#FFF';
return connectDropTarget(
<div className="houzes-dropbox">
{cards.map((card, i) => {
return (
<Card
key={card.id}
index={i}
listId={this.props.id}
card={card}
removeCard={this.removeCard.bind(this)}
moveCard={this.moveCard.bind(this)} />
);
})}
</div>
);
}
}
const cardTarget = {
drop(props, monitor, component ) {
const { id } = props;
const sourceObj = monitor.getItem();
if ( id !== sourceObj.listId ) component.pushCard(sourceObj.card);
return {
listId: id
};
}
}
export default DropTarget("CARD", cardTarget, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
canDrop: monitor.canDrop()
}))(Container);
I am really happy if this helped you.
Thanks for reading my post.
Vladimir