Antd Virtual table fixed header - javascript

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

Related

Adding sortInfo and/or filterValue to DataGrid breaks sorting/filtering functionality

I am trying to add some 'save preferences' functionality to a DataGrid using https://reactdatagrid.io/docs/miscellaneous but when I add sortInfo makes columns unsortable, the same happens for filterValue (trying to save filtering strings/data). Code here:
import React, { useCallback, useState } from 'react';
import DataGrid from '#inovua/reactdatagrid-enterprise';
import { columnFilters, eeOverviewColumns, filterTypes } from "./overview-columns";
import '#inovua/reactdatagrid-enterprise/index.css';
import { TypeRowProps, TypeRowSelection } from '#inovua/reactdatagrid-community/types';
import { TypeOnSelectionChangeArg } from "#inovua/reactdatagrid-community/types/TypeDataGridProps"
import { Button, FormControl, MenuItem, Select, SelectChangeEvent, TextField } from '#mui/material';
import { TypeColumn, TypeFilterValue, TypeSortInfo } from '#inovua/reactdatagrid-enterprise/types';
interface StoreLayout {
columns: TypeColumn[];
sortInfo: TypeSortInfo;
columnOrder : string[];
filterValue: TypeFilterValue;
}
let STORE: StoreLayout = {
columns: eeOverviewColumns,
sortInfo: [],
columnOrder: eeOverviewColumns.map(ee => ee.name) as string[],
filterValue: columnFilters,
}
let VIEWS= [
{
id: 0,
name: "Default view",
state: STORE
}
]
const EEOverview = (props: any) => {
const dataSource = props.eeData
// Checkbox selection
const [selected, setSelected] = useState<TypeRowSelection>();
const onSelectionChange = useCallback(
(config: TypeOnSelectionChangeArg) => {
setSelected(config.selected)
},
[],
);
const goToEe = useCallback((rowProps: TypeRowProps) => {
window.location.href = `${window.location.href}/${rowProps.data.key}`;
}, [])
const initialState = Object.assign({}, STORE, {});
const [state, setState] = useState(initialState);
const [viewName, setViewName] = useState('')
const sendViewName = (viewName: string) => { setViewName(viewName) }
const saveState = () => {
if (!viewName || viewName.length === 0 ) {
alert("View name not provided")
return
}
STORE = {
columnOrder: state.columnOrder,
columns: state.columns,
sortInfo: state.sortInfo,
filterValue: state.filterValue
}
setState(Object.assign({}, state, {}))
if(VIEWS.map(view => view.name).some(name => name === viewName)) {
const view = VIEWS.find(view => view.name === viewName)!
view.state = state
} else {
VIEWS.push({
id: VIEWS.length,
name: viewName,
state: state
})
}
}
const onSortInfoChange = (sortInfo: TypeSortInfo) => {
setState(Object.assign({}, state, { sortInfo }));
}
const onColumnOrderChange = (columnOrder: string[]) => {
setState(Object.assign({}, state, { columnOrder }));
}
const onFilterValueChange = (filterValue: TypeFilterValue) => {
setState(Object.assign({}, state, { filterValue }));
}
const onBatchColumnResize = (batchColumnInfo: any, { reservedViewportWidth }: any) => {
const colsMap = batchColumnInfo.reduce((acc: any, colInfo: any) => {
const { column, width, flex } = colInfo
acc[column.name] = { width, flex }
return acc
}, {})
const columns = state.columns.map((c: any) => {
return Object.assign({}, c, colsMap[c.name])
})
setState(Object.assign({}, state, {
columns,
reservedViewportWidth
}))
}
return (
<div>
<ViewSelector state = {state} onChange = {setState} ></ViewSelector>
<ViewText onChange = {sendViewName} ></ViewText>
<Button sx={{ mx: 2, my: 2, minWidth: 80 }} variant="contained"
onClick = {saveState}
>
Save view
</Button>
<DataGrid
idProperty="key"
theme="default-light"
className="data-grid"
defaultFilterValue={columnFilters}
filterTypes={filterTypes}
filterValue={state.filterValue} //<- here
onRowClick={goToEe}
columns={state.columns}
sortInfo={state.sortInfo} //<- and here
columnOrder={state.columnOrder}
pagination="local"
dataSource={dataSource}
onSelectionChange={onSelectionChange}
sortable={true}
checkboxColumn
selected={selected}
enableSelection={true}
onSortInfoChange={onSortInfoChange}
onBatchColumnResize={onBatchColumnResize}
onColumnOrderChange={onColumnOrderChange}
onFilterValueChange={onFilterValueChange}
/>
</div>
);
}
export default EEOverview;
const ViewText = ({onChange}: {onChange: any}) => {
const onViewNameChange = (viewName: string) => {
onChange(viewName)
}
return (
<TextField sx={{ mx: 2, my: 2 }}
id="search"
variant="outlined"
size="small"
label="Name current view"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { onViewNameChange(e.target.value) }}
/>
)
}
const ViewSelector = ({state, onChange}: {state:any, onChange: any}) => {
const [selectedView, setSelectedView] = useState(0)
const handleViewChange = (event: SelectChangeEvent<number>) => {
const selectedView = VIEWS.find(view => view.id===event.target.value)!
setSelectedView(selectedView.id)
const selectedState = selectedView.state
onChange(Object.assign({},selectedState, {}))
}
return (
<FormControl sx={{ m: 2, minWidth: 140 }} >
<Select labelId="view" variant="standard" size="medium" value={selectedView}
renderValue={(selected: any) => { return <div>{VIEWS[selected].name}</div>; }}
onChange={handleViewChange}>
{VIEWS.map((val, key) => (
<MenuItem value={key}>{val.name}</MenuItem>
))}
</Select>
</FormControl>
)
}
If I remove sortInfo/filterValue from passing to DataGrid, it behaves correctly, but it won't be saved to preferences.
Tried to move dataSource from props to state but it has the same behaviour

Adjust user to point in slider

I try to build sliders with different categories that each user has his point.
The informant comes from the json server
What I need I do not succeed in having the customer choose a user that is numbered and the dot will be colored in the slider How do I do that?
In addition he has the option to delete and return the point.
I was able to delete the points by deleting them in the object. But I could not return, is there a possibility to return?
Broker.jsx
import React, { useEffect, useState } from 'react';
import './style.css';
import Combo from '../components/Combo/Combo';
import Sliders from '../components/Sliders/Sliders';
const GetUsersDataFromManipulation = (users, field) => {
const state = users.reduce((store, user) => {
const userId = user.user
const currentManipulationUserData = user.profileManipulation[field]
if (currentManipulationUserData.length === 0) {
return store
}
store[userId] = currentManipulationUserData[0].bid
return store;
}, {})
return state;
};
function Broker({ manipulations }) {
const users = manipulations[2].users
const [hiddenUser, setHiddenUser] = useState(() => {
const visible = {};
for (let user of users) {
visible[user.user] = true;
}
return visible;
})
const GetUsersBid = (profileManipulation) => {
const data = GetUsersDataFromManipulation(users, `${profileManipulation}`); if (!Object.keys(data).length) {
return null
}
return data;
};
const gender = GetUsersBid('gender');
const age = GetUsersBid('age');
const marital = GetUsersBid('marital');
const children = GetUsersBid('children');
const education = GetUsersBid('education');
const interests = GetUsersBid('interests');
const dynamicInterests = GetUsersBid('dynamicInterests');
const showUser = (user_id) => {
const new_hidden = { ...hiddenUser }
new_hidden[user_id] = true;
setHiddenUser(new_hidden);
}
const hideUser = (user_id) => {
const new_hidden = { ...hiddenUser }
console.log(user_id)
new_hidden[user_id] = false;
setHiddenUser(new_hidden);
}
const [userInformation, setUserInformation] = useState([
{ name: 'gender', bids: gender },
{ name: 'age', bids: age },
{ name: 'marital', bids: marital },
{ name: 'children', bids: children },
{ name: 'education', bids: education },
{ name: 'interests', bids: interests },
{ name: 'dynamicInterests ', bids: dynamicInterests },
]);
useEffect(() => {
const curret_User_Info = [...userInformation]
for (let user of Object.keys(hiddenUser)) {
for (let i = 0; i < curret_User_Info.length; i++) {
if (curret_User_Info[i].bids !== null) {
if (hiddenUser[user] === false) {
delete curret_User_Info[i].bids[user]
}
else {
//What am I returning here? So that the bids will return?
}
}
}
}
setUserInformation(curret_User_Info)
}, [hiddenUser])
return (
<div>
<div className="button" >
{userInformation && <Combo users={users} showUser={showUser} hideUser={hideUser} userInformation={userInformation} />}
</div>
<div className='slid'>
{userInformation.map(sliderDetails => {
return (
<div className={sliderDetails.name} key={sliderDetails.name} >
{sliderDetails.bids && (<Sliders className="sliders" hiddenUserChange={hiddenUser} name={sliderDetails.name} userBids={sliderDetails.bids} setUserInformation={setUserInformation} userInformation={userInformation} />)}
</div>
)
})}
</div>
</div>
);
}
export default Broker;
ComboBox.jsx
import React, { useEffect, useRef, useState } from 'react';
import ComboBox from 'react-responsive-combo-box';
import { Button } from '#mui/material';
import 'react-responsive-combo-box/dist/index.css';
import "./style.css"
function Combo({ users, showUser, hideUser, userInformation }) {
const [selectedOption, setSelectedOption] = useState();
const [choosing, setChoosing] = useState();
useEffect(() => {
}, [users])
const onShow = () => {
showUser(users[selectedOption - 1].user)
}
const onHide = () => {
hideUser(users[selectedOption - 1].user)
}
const colorChange = (numOption) => {
const id = users[numOption - 1].user
}
return (
<div className="combo_box">
<ComboBox
onSelect={(option) => { setSelectedOption(option); colorChange(option) }}
options={[...Array.from({ length: users.length }, (_, i) => i + 1)]}
/>
<div className='button' >
<Button style={{ "marginRight": 20 }} variant="contained" onClick={onShow}>Show</Button>
<Button variant="contained" onClick={onHide}>Hide</Button>
</div>
</div>
);
}
export default Combo;
Sliders.jsx
import React, { useEffect, useState } from 'react'
import "./style.css"
import 'rc-slider/assets/index.css';
import Slider from 'rc-slider';
const Sliders = ({ hiddenUserChange, name, userBids, setUserInformation, userInformation }) => {
const [bids, setBids] = useState()
useEffect(() => {
setBids(Object.values(userBids))
}, [hiddenUserChange, userBids])
const updateFieldChanged = (newValue, e) => {//OnChanged Slider
setUserInformation(state => {
return state.map(manipulation => {
if (manipulation.name === name) {
Object.entries(manipulation.bids).forEach(([userId, bidValue], index) => {
manipulation.bids[userId] = newValue[index]
console.log(manipulation.bids[userId])
})
}
return manipulation
})
});
}
const handleChange = (event, newValue) => {
setBids(event)
};
return (
<>
<h1 className='headers'>{name}</h1>
{
<Slider
style={{ "marginRight": "20rem", "width": "30rem", "left": "20%" }}
range={true}
trackStyle={[{ backgroundColor: '#3f51b5' }]}
max={100}
RcSlider={true}
railStyle={{ backgroundColor: '#3f51b5' }}
activeDotStyle={{ left: 'unset' }}
ariaLabelForHandle={Object.keys(hiddenUserChange)}
tabIndex={(Object.keys(userBids))}
ariaLabelledByForHandle={bids}
value={(bids)}
onChange={handleChange}
onAfterChange={updateFieldChanged}
tipProps
tipFormatter
/>
}
</>
)
}
export default Sliders
enter image description here
Thank you all!

How to reparent Node in React

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;

React converting class into function component issues

I am trying to use React Scheduler with my shifts database. The current state after trying to use hooks instead of class is that I cannot edit any field in the form. I have deleted some of the code to make it cleaner, for now I am trying only to add a shift.
React Scheduler original code:
import * as React from 'react';
import Paper from '#material-ui/core/Paper';
import { ViewState, EditingState } from '#devexpress/dx-react-scheduler';
import {
Scheduler,
Appointments,
AppointmentForm,
AppointmentTooltip,
WeekView,
} from '#devexpress/dx-react-scheduler-material-ui';
import { appointments } from '../../../demo-data/appointments';
export default class Demo extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
data: appointments,
currentDate: '2018-06-27',
addedAppointment: {},
appointmentChanges: {},
editingAppointment: undefined,
};
this.commitChanges = this.commitChanges.bind(this);
this.changeAddedAppointment = this.changeAddedAppointment.bind(this);
this.changeAppointmentChanges = this.changeAppointmentChanges.bind(this);
this.changeEditingAppointment = this.changeEditingAppointment.bind(this);
}
changeAddedAppointment(addedAppointment) {
this.setState({ addedAppointment });
}
changeAppointmentChanges(appointmentChanges) {
this.setState({ appointmentChanges });
}
changeEditingAppointment(editingAppointment) {
this.setState({ editingAppointment });
}
commitChanges({ added, changed, deleted }) {
this.setState((state) => {
let { data } = state;
if (added) {
const startingAddedId = data.length > 0 ? data[data.length - 1].id + 1 : 0;
data = [...data, { id: startingAddedId, ...added }];
}
return { data };
});
}
render() {
const {
currentDate, data, addedAppointment, appointmentChanges, editingAppointment,
} = this.state;
return (
<Paper>
<Scheduler
data={data}
height={660}
>
<ViewState
currentDate={currentDate}
/>
<EditingState
onCommitChanges={this.commitChanges}
addedAppointment={addedAppointment}
onAddedAppointmentChange={this.changeAddedAppointment}
appointmentChanges={appointmentChanges}
onAppointmentChangesChange={this.changeAppointmentChanges}
editingAppointment={editingAppointment}
onEditingAppointmentChange={this.changeEditingAppointment}
/>
<WeekView
startDayHour={9}
endDayHour={17}
/>
<Appointments />
<AppointmentTooltip
showOpenButton
showDeleteButton
/>
<AppointmentForm />
</Scheduler>
</Paper>
);
}
}
My function component code:
import React, { useState } from 'react';
import Paper from '#material-ui/core/Paper';
import { ViewState, EditingState } from '#devexpress/dx-react-scheduler';
import {
Scheduler,
Appointments,
AppointmentForm,
AppointmentTooltip,
WeekView,
ConfirmationDialog,
} from '#devexpress/dx-react-scheduler-material-ui';
const DataSheet = ( { addShift, shifts, deleteShift } ) => {
const [data, setData] = useState(shifts)
const [currentDate, setCurrentDate] = useState('2018-06-27')
const [addedAppointment, setAddedAppointment] = useState({})
const [appointmentChanges, setAppointmentChanges] = useState({})
const [editingAppointment, setEditingAppointment] = useState(undefined)
const changeAddedAppointment = (addedAppointment) => {
setAddedAppointment({ addedAppointment });
}
const changeAppointmentChanges = (appointmentChanges) => {
setAppointmentChanges({ appointmentChanges });
}
const changeEditingAppointment = (editingAppointment) => {
setEditingAppointment({ editingAppointment });
}
const commitChanges = ({ added, changed, deleted }) => {
setData ((????) => {
let { data } = data;
console.log(data); //returns undefined
if (added) {
const startingAddedId = data > 0 ? data[data.length - 1].id + 1 : 0;
data = [...data, { id: startingAddedId, ...added }];
addShift(added);
}
return { data };
});
}
return (
<Paper>
<Scheduler
data={data}
height={660}
>
<ViewState
currentDate={currentDate}
/>
<EditingState
onCommitChanges={commitChanges}
addedAppointment={addedAppointment}
onAddedAppointmentChange={changeAddedAppointment}
appointmentChanges={appointmentChanges}
onAppointmentChangesChange={changeAppointmentChanges}
editingAppointment={editingAppointment}
onEditingAppointmentChange={changeEditingAppointment}
/>
<WeekView
startDayHour={9}
endDayHour={17}
/>
<Appointments />
<AppointmentTooltip
showOpenButton
showDeleteButton
/>
<AppointmentForm />
</Scheduler>
</Paper>
);
}
export default DataSheet
App.js:
import React from 'react';
import backgroundImage from './Resources/BennyBackground.jpeg'
import Header from "./components/Header";
import { useState, useEffect } from "react"
import DataSheet from './components/DataSheet';
const containerStyle= {
width: '100vw',
height: '100vh',
backgroundImage: `url(${backgroundImage})`,
backgroundPosition: 'center',
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
}
const App = () => {
const [shifts, setShifts] = useState([])
useEffect(() => {
const getShifts = async () => {
const shiftsFromServer = await fetchShifts()
setShifts(shiftsFromServer)
}
getShifts()
}, [])
const fetchShifts = async () => {
const res = await fetch(`http://localhost:5000/shifts/`)
const data = await res.json()
return data
}
const addShift = async (shift) => {
const startingAddedId = shifts.length > 0 ? shifts[shifts.length - 1].id + 1 : 0;
shift.id = startingAddedId;
const res = await fetch(`http://localhost:5000/shifts/`,{
method: 'POST',
headers: {
'Content-type': 'application/json'
},
body: JSON.stringify(shift)
})
const data = await res.json()
setShifts([...shifts, data])
}
return (
<div className="container"
style={containerStyle} >
<div className='secondary_container'>
<Header />
<DataSheet shifts={shifts} addShift={addShift}/>
</div>
</div>
);
}
export default App;
I know it is a lot of code and a lot to ask and I would highly appreciate help with this.
I believe the issue is that you are using setXxx as you would use this.setState. In class components, you have one function that modifies all the state (this.setState), while in function components you have a setter function for each field.
So change this:
const changeAddedAppointment = (addedAppointment) => {
setAddedAppointment({ addedAppointment });
}
to this:
const changeAddedAppointment = (addedAppointment) => {
setAddedAppointment(addedAppointment);
}
As far as the commitChanges function goes, you can do the data manipulation before using setData. Also I'm not sure that this let { data } = data would work since there is already a data field. You can try this:
const commitChanges = ({ added, changed, deleted }) => {
let newData = [...data.data];
if (added) {
const startingAddedId = newData > 0 ? newData [data.length - 1].id + 1 : 0;
newData = [...newData , { id: startingAddedId, ...added }];
addShift(added);
}
setData(newData);
};

Drag n drop with virtual lists and different size items

I'm trying to implement a Trello with virtual lists and different sized items, using react-beautiful-dnd and react-virtuoso (virtual lists with automated calculation of item sizes).
react-virtuoso is not part of the examples of beautiful-react-dnd, I'm facing 2 issues:
I can't scroll while dragging an item
I get this error often: Invariant failed: Can only recollect Droppable client for Droppables that have a scroll container.
Here's a codesandbox
DroppableList.tsx
import * as React from "react";
import { useState } from "react";
import "./styles.css";
import { Virtuoso } from "react-virtuoso";
import {
Draggable,
DragDropContext,
Droppable,
DropResult,
ResponderProvided
} from "react-beautiful-dnd";
import { Item } from "./Item";
import { reorder } from "./App";
import { createItemList } from "./data";
import { ItemList, ItemType } from "./dtos";
const itemCount = 30;
export const VirtualDragDropList = () => {
const [itemList, setItemList] = useState<ItemList>(createItemList(itemCount));
const onDragEnd = (result: DropResult, provided: ResponderProvided) => {
// dropped outside the list
if (!result.destination) {
return;
}
const items = reorder(
itemList,
result.source.index,
result.destination.index
);
setItemList(items);
};
return (
<DragDropContext onDragEnd={onDragEnd}>
<Droppable
droppableId="droppable"
mode="virtual"
renderClone={(provided, snapshot, rubric) => {
// console.log("provided", provided);
// console.log("snapshot", snapshot);
// console.log("rubric", rubric);
return (
<Item
itemData={itemList[(rubric as any).source.index]}
provided={provided}
index={(rubric as any).source.index} // typing seems wrong, hence the any.
/>
);
}}
>
{droppableProvided => (
<div ref={droppableProvided.innerRef}>
<Virtuoso
style={{ width: "300px", height: "400px" }}
totalCount={itemCount}
// item={index => <Item itemData={itemList[index]} />}
item={index => <Row itemData={itemList[index]} index={index} />}
/>
</div>
)}
</Droppable>
</DragDropContext>
);
};
const Row = React.memo((args: { itemData: ItemType; index: number }) => {
const { itemData, index } = args;
return (
<Draggable draggableId={itemData.id} index={index} key={itemData.id}>
{(provided, snapshot) => (
<Item itemData={itemData} index={index} provided={provided} />
)}
</Draggable>
);
});
Item.tsx
import * as React from "react";
import { ItemType } from "./dtos";
export const Item: React.FC<{
index?: number;
itemData: ItemType | undefined;
provided?: any;
}> = props => {
const height = (props.itemData ? props.itemData.height : 10) * 3;
const style = {
margin: ".3rem",
padding: ".3rem",
display: "flex",
border: "1px solid lightgrey",
height: `${height}px`
};
return (
<div
ref={props.provided && props.provided.innerRef}
{...props.provided && props.provided.draggableProps}
{...props.provided && props.provided.dragHandleProps}
style={{ ...props.provided.draggableProps.style, ...style }}
>
{props.itemData && props.itemData.text}
</div>
);
};
data.ts
import { ItemList } from "./dtos";
export const createItemList = (itemCount: number): ItemList => {
const itemList: ItemList = [];
for (let i = 0; i < itemCount; i++) {
itemList.push({
id: i.toString(),
text: `Item ${i}`,
height: Math.random() * 20
});
}
return itemList;
}
remove ref={droppableProvided.innerRef} from div and add
scrollerRef={droppableProvided.innerRef} to virtuoso

Categories