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
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
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;
I'm trying to get height of the collapsible but it always same value which is uncollapsed div's height.
Is there a way to get actual content height of div inside the accordion ?
When I print current ref's height it always returns a fixed value. But if I click the same collapsible it returns correct value so I guess it takes uncollapsed div height of collapsible.
Accordion.js
const Accordion = ({ children }) => {
const [ selected, select ] = useState(null);
const [ currentState, changeCurrentState ] = useState(true);
const onCollapsibleSelected = (selectedItem, selectedState, ref) => {
select(selectedItem);
changeCurrentState(!selectedState);
// Always same - because accordion is not expanded yet
console.log(ref.current.offsetHeight)
};
const collapsibleChildren = children.map((item, index) => {
let collapsible;
if (item && item.props) {
const hidden = selected && selected === index ? currentState : true;
collapsible = (
<Collapsible { ...item.props }
onSelect={ onCollapsibleSelected }
key={ index }
index={ index }
collapsed={ hidden }
/>
);
}
return collapsible;
});
return (
<Fragment>
{ collapsibleChildren }
</Fragment>
);
};
Accordion.displayName = 'Accordion';
Accordion.propTypes = {
children: PropTypes.any
};
export default Accordion;
.. index.js
const Collapsible = ({ content, title, onSelect, collapsed, index }) => {
const collapsibleRef = useRef();
const trackCollapsibleBlockClick = () => {
const event = collapsed ? OPEN_COLLAPSIBLE_BLOCK: CLOSE_COLLAPSIBLE_BLOCK;
trackEvent({ ... event, name: title });
};
const onTitleClicked = () => {
trackCollapsibleBlockClick();
onSelect(index, collapsed, collapsibleRef);
};
return (
<CollapsibleStyle ref={ collapsibleRef } onClick={ onTitleClicked }>
<Title collapsed={ collapsed }>{ title }</Title>
<Content hidden={ collapsed }>{ content }</Content>
</CollapsibleStyle>
);
};
Collapsible.displayName = 'Collapsible';
Collapsible.propTypes = {
content: PropTypes.array,
title: PropTypes.string,
onSelect: PropTypes.func,
collapsed: PropTypes.bool,
index: PropTypes.number
};
export default Collapsible;
CollapsibleStyle.js
import styled from 'styled-components';
export default styled.div`
&& {
background-color: ${ ({ theme: { collapsible } }) => collapsible.backgroundColor };
border-radius: ${ ({ theme: { collapsible } }) => collapsible.borderRadius };
margin-bottom: ${ ({ theme: { collapsible } }) => collapsible.marginBottom };
padding: ${ ({ theme: { collapsible } }) => collapsible.paddingVertical }
${ ({ theme: { experience: { wrapper } } }) => wrapper.padding.md };
position: relative;
cursor: pointer;
}
`;
I can actually see the correct height of the child when I print ref.current.children. But when I try to access like ref.current.children[1].clientHeight it returns wrong value.
Solved as below. Using useEffect as it produces lifecycle events. I check if the collapsible item is changed by collapsed check.
useEffect(() => {
handleCollapsibleViewPort();
}, [collapsed]);
const handleCollapsibleViewPort = () => {
if(uncollapsedHeight !== 0 && collapsibleRef.current.clientHeight > uncollapsedHeight ) {
scrollElementToTop(collapsibleRef.current.getBoundingClientRect().top);
}
}
I am trying to make each individual list item clickable.
Here I have a color change state so that the color changes to red. But everytime I click one list item, it makes all of the boxes red. Again I want to select which ones to turn red, not turn all of them red. a hint in the right direction or a link would do fine.
import React, { Component } from 'react';
import './App.css';
import MainTool from './components/Layout/MainTool/MainTool.js';
import Aux from './hoc/Aux.js';
class App extends Component {
state = {
MainList: [
"GamePlay",
"Visuals",
"Audio",
"Story"
],
color: "white"
}
changeEle = () =>{
this.setState({color : "red"});
}
render() {
return (
<Aux>
<MainTool MainList = {this.state.MainList}
change = {this.changeEle}
color = {this.state.color}/>
</Aux>
);
}
}
export default App;
This MainTool just shoots my state arguments to Checkblock. Just for reference. Its unlikely the error is here although I have been wrong plenty times before.
import React from 'react';
import './MainTool.css';
import CheckBlock from '../../CheckBlock/CheckBlock';
const MainTool = props => {
return (
<div className = "mtborder">
<CheckBlock MainList = {props.MainList}
change = {props.change}
color = {props.color}/>
</div>
);
};
export default MainTool;
And here is my best guess at where the problem is. I used a loop to iterate through my state object array and print out the list and divs next to each list item. the divs being the elements I want to make clickable individually.
import React from 'react';
import './CheckBlock.css';
import Aux from '../../hoc/Aux';
const CheckBlock = props => {
console.log(props.change);
console.log(props.color);
let mainList = [];
for(let i = 0; i <= 3; i++)
{
mainList[i] = <li key = {i}
className = "nameBox">{props.MainList[i]}
<div onClick = {props.change}
style = {{backgroundColor: props.color}}
className = "clickBox"></div></li>
}
//console.log(dubi());
return (
<Aux>
<ul className = "mainList">{mainList}</ul>
<button>Enter</button>
</Aux>
);
};
export default CheckBlock;
You need a state based ListItem component with internal color state. No need to pass function as prop to change color. Use internal method
class ListItem extends Component {
state = { color : 'white' };
onClick = () => {
this.setState({ color: 'red' });
}
render () {
return (
<li key={i} className="nameBox">
{this.props.value}
<div onClick={this.onClick} style={{backgroundColor: props.color}}
className="clickBox">
</div>
</li>
);
}
}
const CheckBlock = props => {
console.log(props.change);
console.log(props.color);
let mainList = [];
for(let i = 0; i <= 3; i++)
{
mainList[i] = <ListItem key={i} value={props.MainList[i]} />
}
return (
<Aux>
<ul className = "mainList">{mainList}</ul>
<button>Enter</button>
</Aux>
);
};
I put jsfiddle together:
Let me know if there is a better practice to toggle element in React;
Cheers!
List item toggle jsfiddle
// ----------------APP-------------------
class App extends React.Component {
state = {
mainList: [
{
label: 'GamePlay',
id: 1,
},
{
label: 'Visuals',
id: 2,
},
{
label: 'Audio',
id: 3,
},
{
label: 'Story',
id: 4,
},
],
}
render() {
const { mainList } = this.state;
return (
<div>
<List mainList={mainList} />
</div>
);
}
}
// ----------------LIST-------------------
const List = ({ mainList }) => (
<div>
{mainList.map((listItem) => {
const { label, id } = listItem;
return (
<ListItem key={id} id={id} label={label} />
);
})}
</div>
);
// ----------------LIST-ITEM-------------------
class ListItem extends React.Component{
state = {
selected: false,
}
changeColor = () => {
const { selected } = this.state;
this.setState({selected: !selected})
}
render(){
const { label, id } = this.props;
const { selected } = this.state;
return console.log(selected ,' id - ', id ) || (
<button
className={selected ? 'green' : 'red'}
onClick= {() => this.changeColor()}
>
{label}
</button>
)
}
}
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