I am trying to find the best table to use with my react apps, and for now, the react-table offers everything I need (pagination, server-side control, filtering, sorting, footer row).
This being said, I can't seem to be able to select a row. There are no examples that show this.
Some things, that I have tried include trying to set a className on click of the row. But I can't seem to find the calling element in e nor t. Also, I don't like this approach, because it is not how a react app should do things.
<ReactTable
...
getTrProps={(state, rowInfo, column, instance) => {
return {
onClick: (e, t) => {
t.srcElement.classList.add('active')
},
style: {
}
}
}}
/>
Some possible workaround would be to render checkboxes as a first column, but this is not optimal as it limits the area to click to 'activate' the row. Also, the visual feedback will be less expressive.
Am I missing the elephant in the room? And if not, do you know another library that supports the things that I've described earlier?
Thank you!
EDIT:
Another option, this being open source, is to suggest an edit. And maybe this is the proper thing to do.
EDIT 2
Another thing, suggested by Davorin Ruševljan in the comments, but I couldn't make it work was:
onRowClick(e, t, rowInfo) {
this.setState((oldState) => {
let data = oldState.data.slice();
let copy = Object.assign({}, data[rowInfo.index]);
copy.selected = true;
copy.FirstName = "selected";
data[rowInfo.index] = copy;
return {
data: data,
}
})
}
....
getTrProps={(state, rowInfo, column) => {
return {
onClick: (e, t) => { this.onRowClick(e, t, rowInfo) },
style: {
background: rowInfo && rowInfo.row.selected ? 'green' : 'red'
}
}
}}
This sets the 'FirstName' column to 'selected', but does not set the class to 'green'
I found the solution after a few tries, I hope this can help you. Add the following to your <ReactTable> component:
getTrProps={(state, rowInfo) => {
if (rowInfo && rowInfo.row) {
return {
onClick: (e) => {
this.setState({
selected: rowInfo.index
})
},
style: {
background: rowInfo.index === this.state.selected ? '#00afec' : 'white',
color: rowInfo.index === this.state.selected ? 'white' : 'black'
}
}
}else{
return {}
}
}
In your state don't forget to add a null selected value, like:
state = { selected: null }
There is a HOC included for React-Table that allows for selection, even when filtering and paginating the table, the setup is slightly more advanced than the basic table so read through the info in the link below first.
After importing the HOC you can then use it like this with the necessary methods:
/**
* Toggle a single checkbox for select table
*/
toggleSelection(key: number, shift: string, row: string) {
// start off with the existing state
let selection = [...this.state.selection];
const keyIndex = selection.indexOf(key);
// check to see if the key exists
if (keyIndex >= 0) {
// it does exist so we will remove it using destructing
selection = [
...selection.slice(0, keyIndex),
...selection.slice(keyIndex + 1)
];
} else {
// it does not exist so add it
selection.push(key);
}
// update the state
this.setState({ selection });
}
/**
* Toggle all checkboxes for select table
*/
toggleAll() {
const selectAll = !this.state.selectAll;
const selection = [];
if (selectAll) {
// we need to get at the internals of ReactTable
const wrappedInstance = this.checkboxTable.getWrappedInstance();
// the 'sortedData' property contains the currently accessible records based on the filter and sort
const currentRecords = wrappedInstance.getResolvedState().sortedData;
// we just push all the IDs onto the selection array
currentRecords.forEach(item => {
selection.push(item._original._id);
});
}
this.setState({ selectAll, selection });
}
/**
* Whether or not a row is selected for select table
*/
isSelected(key: number) {
return this.state.selection.includes(key);
}
<CheckboxTable
ref={r => (this.checkboxTable = r)}
toggleSelection={this.toggleSelection}
selectAll={this.state.selectAll}
toggleAll={this.toggleAll}
selectType="checkbox"
isSelected={this.isSelected}
data={data}
columns={columns}
/>
See here for more information:
https://github.com/tannerlinsley/react-table/tree/v6#selecttable
Here is a working example:
https://codesandbox.io/s/react-table-select-j9jvw
I am not familiar with, react-table, so I do not know it has direct support for selecting and deselecting (it would be nice if it had).
If it does not, with the piece of code you already have you can install the onCLick handler. Now instead of trying to attach style directly to row, you can modify state, by for instance adding selected: true to row data. That would trigger rerender. Now you only have to override how are rows with selected === true rendered. Something along lines of:
// Any Tr element will be green if its (row.age > 20)
<ReactTable
getTrProps={(state, rowInfo, column) => {
return {
style: {
background: rowInfo.row.selected ? 'green' : 'red'
}
}
}}
/>
if u want to have multiple selection on select row..
import React from 'react';
import ReactTable from 'react-table';
import 'react-table/react-table.css';
import { ReactTableDefaults } from 'react-table';
import matchSorter from 'match-sorter';
class ThreatReportTable extends React.Component{
constructor(props){
super(props);
this.state = {
selected: [],
row: []
}
}
render(){
const columns = this.props.label;
const data = this.props.data;
Object.assign(ReactTableDefaults, {
defaultPageSize: 10,
pageText: false,
previousText: '<',
nextText: '>',
showPageJump: false,
showPagination: true,
defaultSortMethod: (a, b, desc) => {
return b - a;
},
})
return(
<ReactTable className='threatReportTable'
data= {data}
columns={columns}
getTrProps={(state, rowInfo, column) => {
return {
onClick: (e) => {
var a = this.state.selected.indexOf(rowInfo.index);
if (a == -1) {
// this.setState({selected: array.concat(this.state.selected, [rowInfo.index])});
this.setState({selected: [...this.state.selected, rowInfo.index]});
// Pass props to the React component
}
var array = this.state.selected;
if(a != -1){
array.splice(a, 1);
this.setState({selected: array});
}
},
// #393740 - Lighter, selected row
// #302f36 - Darker, not selected row
style: {background: this.state.selected.indexOf(rowInfo.index) != -1 ? '#393740': '#302f36'},
}
}}
noDataText = "No available threats"
/>
)
}
}
export default ThreatReportTable;
The answer you selected is correct, however if you are using a sorting table it will crash since rowInfo will became undefined as you search, would recommend using this function instead
getTrGroupProps={(state, rowInfo, column, instance) => {
if (rowInfo !== undefined) {
return {
onClick: (e, handleOriginal) => {
console.log('It was in this row:', rowInfo)
this.setState({
firstNameState: rowInfo.row.firstName,
lastNameState: rowInfo.row.lastName,
selectedIndex: rowInfo.original.id
})
},
style: {
cursor: 'pointer',
background: rowInfo.original.id === this.state.selectedIndex ? '#00afec' : 'white',
color: rowInfo.original.id === this.state.selectedIndex ? 'white' : 'black'
}
}
}}
}
If you are using the latest version (7.7 at the time) it is possible to select rows using toggleRoWSelected() see full example;
<tr
{...row.getRowProps()}
className="odd:bg-white even:bg-gray-100"
onClick={() => row.toggleRowSelected()}
>
{row.cells.map((cell) => {
return (
<td {...cell.getCellProps()} className="p-2">
{cell.render("Cell")}
</td>
);
})}
</tr>;
Another mechanism for dynamic styling is to define it in the JSX for your component. For example, the following could be used to selectively style the current step in the React tic-tac-toe tutorial (one of the suggested extra credit enhancements:
return (
<li key={move}>
<button style={{fontWeight:(move === this.state.stepNumber ? 'bold' : '')}} onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
Granted, a cleaner approach would be to add/remove a 'selected' CSS class but this direct approach might be helpful in some cases.
Multiple rows with checkboxes and select all using useState() hooks. Requires minor implementation to adjust to own project.
const data;
const [ allToggled, setAllToggled ] = useState(false);
const [ toggled, setToggled ] = useState(Array.from(new Array(data.length), () => false));
const [ selected, setSelected ] = useState([]);
const handleToggleAll = allToggled => {
let selectAll = !allToggled;
setAllToggled(selectAll);
let toggledCopy = [];
let selectedCopy = [];
data.forEach(function (e, index) {
toggledCopy.push(selectAll);
if(selectAll) {
selectedCopy.push(index);
}
});
setToggled(toggledCopy);
setSelected(selectedCopy);
};
const handleToggle = index => {
let toggledCopy = [...toggled];
toggledCopy[index] = !toggledCopy[index];
setToggled(toggledCopy);
if( toggledCopy[index] === false ){
setAllToggled(false);
}
else if (allToggled) {
setAllToggled(false);
}
};
....
Header: state => (
<input
type="checkbox"
checked={allToggled}
onChange={() => handleToggleAll(allToggled)}
/>
),
Cell: row => (
<input
type="checkbox"
checked={toggled[row.index]}
onChange={() => handleToggle(row.index)}
/>
),
....
<ReactTable
...
getTrProps={(state, rowInfo, column, instance) => {
if (rowInfo && rowInfo.row) {
return {
onClick: (e, handleOriginal) => {
let present = selected.indexOf(rowInfo.index);
let selectedCopy = selected;
if (present === -1){
selected.push(rowInfo.index);
setSelected(selected);
}
if (present > -1){
selectedCopy.splice(present, 1);
setSelected(selectedCopy);
}
handleToggle(rowInfo.index);
},
style: {
background: selected.indexOf(rowInfo.index) > -1 ? '#00afec' : 'white',
color: selected.indexOf(rowInfo.index) > -1 ? 'white' : 'black'
},
}
}
else {
return {}
}
}}
/>
# react-table with edit button #
const [rowIndexState, setRowIndexState] = useState(null);
const [rowBackGroundColor, setRowBackGroundColor] = useState('')
{...row.getRowProps({
onClick: (e) => {
if (!e.target.cellIndex) {
setRowIndexState(row.index);
setRowBackGroundColor('#f4f4f4')
}
},
style: {
background: row.index === rowIndexState ? rowBackGroundColor : '',
},
})}
Related
I have a component in my application that handles two somewhat expensive functions, but not always at the same time. In one application I have a globe that uses window.requestAnimationFrame() to update the rotation on every request and spin. This runs smoothly when I'm interacting with the other parts of component.
The problem arises when the user interacts with a search, that filters an array of objects.
When I'm typing the globe spin animation runs smoothly, once the getItems() function is called it stutters/stops until the getItems() function is done.
The Map component and the Search component are seperate but are both rendered by a higher component. When the user is typing in the search, there is no reason for the higher component to rerender, and thus the map component does not rerender. This suggests that the problem is the expensive function that hinders the application as a whole.
The item list is defined when the component mounts in an UseEffect hook. The json file that holds the objects lives in the public folder, and is about 30MB. When the users searches, it runs the getItems() functions. I already have a setTimeout on the inputValueChange so it only runs once the user is presumable done typing. Here is the code in question.
import { useEffect, useState, useRef, useCallback, CSSProperties } from 'react'
import { useVirtual } from 'react-virtual'
import { useCombobox, UseComboboxStateChange } from 'downshift'
import { v4 as uuid } from 'uuid';
import { Hint } from 'react-autocomplete-hint';
import MarkerObject from '../../../interfaces/MarkerObjectInterface';
interface Props {
items: Array<MarkerObject>;
onSelect(item: MarkerObject): void;
onHoverEnter: (item: MarkerObject) => void;
onHoverLeave: () => void;
}
const Search = (props: Props) => {
//Initialize the state and refs
const [inputValue, setInputValue] = useState('')
const [items, setItems] = useState<MarkerObject[]>([])
const listRef = useRef<HTMLElement>(null)
//Waits for the user to stop typing for half a second before updating the list of items
//This helps a lot with an unnecescary rerender of the somewhat expensive getItems function
useEffect(() => {
const delayInput = setTimeout(() => {
setItems(getItems(inputValue))
}, 500);
return () => clearTimeout(delayInput);
}, [inputValue])
//Listens for a selected item, then passes that item back to TestMaped Item: ", selectedItem)
const handleSelectedItemChange = (changes: UseComboboxStateChange<any>) => {
props.onSelect(changes.selectedItem)
// props.onHoverLeave(changes.selectItem)
}
//Filters the items based on seach query
function getItems(search: string) {
if (search.length === 0)
return []
return props.items.filter((n) => {
if (n.type === "city") {
const citystatecountry = (n.name + ", " + n.state_name + ", " + n.country_name).toLowerCase()
const citystatecodecountry = (n.name + ", " + n.state_code + ", " + n.country_name).toLowerCase()
const citystatecodecountrycode = (n.name + ", " + n.state_code + ", " + n.country_code).toLowerCase()
const citystate = (n.name + ", " + n.state_name).toLowerCase()
const citystatecode = (n.name + ", " + n.state_code).toLowerCase()
const citycountry = (n.name + ", " + n.country_name).toLowerCase()
const citycountrycode = (n.name + ", " + n.country_code).toLowerCase()
const city = n.name.toLowerCase()
if (citystatecountry.startsWith(search.toLowerCase()))
return true
else if (citystate.startsWith(search.toLowerCase()))
return true
else if (citycountry.startsWith(search.toLowerCase()))
return true
else if (city.startsWith(search.toLowerCase()))
return true
else if (citystatecodecountry.startsWith(search.toLowerCase()))
return true
else if (citystatecodecountrycode.startsWith(search.toLowerCase()))
return true
else if (citystatecode.startsWith(search.toLowerCase()))
return true
else if (citycountrycode.startsWith(search.toLowerCase()))
return true
}
else if (n.type === "state") {
const state = (n.name).toLowerCase()
const statecountry = (n.name + ", " + n.country_name).toLowerCase()
if (state.startsWith(search.toLowerCase()))
return true
else if (statecountry.startsWith(search.toLowerCase()))
return true
}
else {
const country = (n.name).toLowerCase()
if (country.startsWith(search.toLowerCase()))
return true
}
return false
})
}
//Initialies the itemToString funciton to be passed to useCombobox
const itemToString = (item: MarkerObject | null) => (item ? item.display_text : '')
//Initializes the rowVirtualizer using a fixed size function
const rowVirtualizer = useVirtual({
size: items.length,
parentRef: listRef,
estimateSize: useCallback(() => 30, []),
overscan: 1,
})
const {
getInputProps,
getItemProps,
getLabelProps,
getMenuProps,
highlightedIndex,
selectedItem,
getComboboxProps,
isOpen,
selectItem
} = useCombobox({
items,
itemToString,
inputValue,
onSelectedItemChange: (changes) => handleSelectedItemChange(changes),
onInputValueChange: (d) => {
console.log(d.inputValue)
if (d.inputValue || d.inputValue === "")
setInputValue(d.inputValue)
},
scrollIntoView: () => { },
onHighlightedIndexChange: ({ highlightedIndex }) => {
if (highlightedIndex)
rowVirtualizer.scrollToIndex(highlightedIndex)
},
})
return (
<div className="" onMouseLeave={() => props.onHoverLeave()}>
<div className="">
<label {...getLabelProps()}>Choose an element:</label>
<div {...getComboboxProps()}>
<input
className={inputCss}
{...getInputProps(
{
type: 'text',
placeholder: 'Enter Place',
})}
/>
</div>
</div>
<ul className="-webkit-scrollbar-track:purple-500"
{...getMenuProps({
ref: listRef,
style: menuStyles,
})}
>
{isOpen && (
<>
<li key="total-size" style={{ height: rowVirtualizer.totalSize }} />
{rowVirtualizer.virtualItems.map((virtualRow) => (
<li
key={uuid()}
{...getItemProps({
index: virtualRow.index,
item: items[virtualRow.index],
style: {
backgroundColor:
highlightedIndex === virtualRow.index
? 'lightgray'
: 'inherit',
fontWeight:
selectedItem &&
selectedItem.id === items[virtualRow.index].id
? 'bold'
: 'normal',
position: 'absolute',
fontSize: '1.2em',
top: 0,
left: 0,
width: '100%',
height: virtualRow.size,
transform: `translateY(${virtualRow.start}px)`,
},
})}
onMouseEnter={() => props.onHoverEnter(items[virtualRow.index])}
onMouseLeave={() => props.onHoverLeave()}
onMouseOut={() => props.onHoverLeave()}
>
{items[virtualRow.index].display_text}
</li>
))}
</>
)}
</ul>
</div>
)
}
export default Search
I'm also using react-virtual with downshift (example https://www.downshift-js.com/use-combobox#virtualizing-items-with-react-virtual). This definitely improves performance when interacting with the dropdown box as there is virtually no stutter on the animation.
I'm curious about what my options are here. I'm also using firestore with my application. Is there way to allocate memory or limit the memory used by this function or component? Is there a better algorithm or function I should be using in place of .filter()? Should I even have that large of a file in the public folder?
Or should I put the whole json object in a firestore collection? Because its 30MBs and I would want to query across the document, I would have to separate the data into documents. This seems like it would add a huge amount of document reads. Any help or suggestions would be greatly appreciated!
I'm having trouble with creating multiple tabs form in React. Example image:
Every new tab mounts a new component. Example code:
const handleAddTab = tabIndex => {
const exampleTab = {
name: `${Date.now()} / 16.02.2022 г.`,
jsx: <Document />,
deletable: true
}
const updatedTabs = state.tabs.map((t, i) => {
if (tabIndex === i) t.subtabs.push(exampleTab)
return t
})
setState(prev => ({
...prev,
tabs: updatedTabs
}))
}
And I'm rendering those components. Example code:
{state.activeSubtabIndex === 0 ?
<Documents />
:
getScreen().subtabs.map((s, i) =>
i === 0 ?
<>
</>
:
<div
style={state.activeSubtabIndex != i ? { display: 'none' } : {}}
>
{s.jsx}
</div>
)
}
I use getScreen() to fetch the current tab and get the subtabs. Example code:
const getScreen = () => {
const typeId = query.get('type_id')
const screen = state.tabs.find(t => t.typeId == typeId)
return screen
}
The way I remove a tab is like so:
const handleCloseTab = (tabIndex, subtabIndex) => {
const updatedTabs = state.tabs.filter((t, i) => {
if (tabIndex === i) {
t.subtabs = t.subtabs.filter((ts, i) => {
return subtabIndex != i
})
}
return t
})
setState(prev => ({
...prev,
tabs: updatedTabs
}))
}
The problem is that every time I delete (for example) the first tab, the second one gets the state from the first one (based on the index it was mapped).
I solved that problem by adding an extra key-value pair (deleted: false) to the exampleTab object
const handleAddTab = tabIndex => {
const exampleTab = {
name: `${Date.now()} / 16.02.2022 г.`,
jsx: <Document />,
deletable: true,
deleted: false <====
}
const updatedTabs = state.tabs.map((t, i) => {
if (tabIndex === i) t.subtabs.push(exampleTab)
return t
})
setState(prev => ({
...prev,
tabs: updatedTabs
}))
}
Whenever I close a tab I'm not unmounting It and removing its data from the array. I'm simply checking if deleted === true and applying style={{ display: 'none' }} to both the tab and component. Example code:
{state.activeSubtabIndex === 0 ?
<Documents />
:
getScreen().subtabs.map((s, i) =>
i === 0 ?
<>
</>
:
<div
style={state.activeSubtabIndex != i || s.deleted ? { display: 'none' } : {}}
key={s.typeId}
>
{s.jsx}
</div>
)}
I have a list that I can sort with drag and drop using react, and it works fine. The way it works is onDragEnter, the items get replaced. What I want to do though, is show a placeholder element once the dragging item is hovering over available space. So the final placement would happen in onDragEnd. I have two functions that handle dragging:
const handleDragStart = (index) => {
draggingItem.current = index;
};
const handleDragEnter = (index) => {
if (draggingWidget.current !== null) return;
dragOverItem.current = index;
const listCopy = [...rows];
const draggingItemContent = listCopy[draggingItem.current];
listCopy.splice(draggingItem.current, 1);
listCopy.splice(dragOverItem.current, 0, draggingItemContent);
if (draggingItem.current === currentRowIndex) {
setCurrentRowIndex(dragOverItem.current);
}
draggingItem.current = dragOverItem.current;
dragOverItem.current = null;
setRows(listCopy);
};
and in react jsx template, I have this:
{rows.map((row, index) => (
<div
key={index}
draggable
onDragStart={() => handleDragStart(index)}
onDragEnter={() => handleDragEnter(index)}
onDragOver={(e) => e.preventDefault()}
onDragEnd={handleDragEndRow}
>
...
</div>
Can anyone come with any tips as to how I might solve this?
To display a placeholder indicating where you are about to drop the dragged item, you need to compute the insertion point according to the current drag position.
So dragEnter won't do, dargOver is best suited to do that.
When dragging over the first half of the dragged overItem, the placeholder insertion point will be before the dragged over item, when dragging over the second half, it will be after. (see getBouldingClientRect, height/2 usages, of course if dragging horizontally width will need to be accounted for).
The actual insertion point (in the data, not the UI), if drag succeeds, will depend on if we're dropping before or after the initial position.
The following snippet demonstrate a way of doing that with the following changes in your initial code:
Avoided numerous refs vars by putting everything in state, especially because changing these will have an effect on the UI (will need rerender)
Avoided separate useState calls by putting all vars in a common state variable and a common setState modifier
Avoided unnecessary modifications of the rows state var, rows should change only when drag ends as it's easier to reason about it => the placeholder is not actually part of the data, it serves purpose only in the ui
Avoided defining handler in the render code onEvent={() => handler(someVar)} by using dataset key data-drag-index, the index can retrieved after using this key: const index = element.dataset.dragIndex. The handler can live with the event only which is automatically passed.
Avoided recreating (from the children props point of view) these handlers at each render by using React.useCallback.
The various css class added show the current state of each item but serves no functionnal purpose.
StateDisplay component also serves no purpose besides showing what happens to understand this answer.
Edit: Reworked and fixed fully working solution handling all tested edge cases
const App = () => {
const [state,setState] = React.useState({
rows: [
{name: 'foo'},
{name: 'bar'},
{name: 'baz'},
{name: 'kazoo'}
],
draggedIndex: -1,
overIndex: -1,
overZone: null,
placeholderIndex: -1
});
const { rows, draggedIndex, overIndex, overZone, placeholderIndex } = state;
const handleDragStart = React.useCallback((evt) => {
const index = indexFromEvent(evt);
setState(s => ({ ...s, draggedIndex: index }));
});
const handleDragOver = React.useCallback((evt) => {
var rect = evt.target.getBoundingClientRect();
var x = evt.clientX - rect.left; // x position within the element.
var y = evt.clientY - rect.top; // y position within the element.
// dataset variables are strings
const newOverIndex = indexFromEvent(evt);
const newOverZone = y <= rect.height / 2 ? 'top' : 'bottom';
const newState = { ...state, overIndex: newOverIndex, overZone: newOverZone }
let newPlaceholderIndex = placeholderIndexFromState(newOverIndex, newOverZone);
// if placeholder is just before (==draggedIndex) or just after (===draggedindex + 1) there is not need to show it because we're not moving anything
if (newPlaceholderIndex === draggedIndex || newPlaceholderIndex === draggedIndex + 1) {
newPlaceholderIndex = -1;
}
const nonFonctionalConditionOnlyForDisplay = overIndex !== newOverIndex || overZone !== newOverZone;
// only update if placeholderIndex hasChanged
if (placeholderIndex !== newPlaceholderIndex || nonFonctionalConditionOnlyForDisplay) {
newState.placeholderIndex = newPlaceholderIndex;
setState(s => ({ ...s, ...newState }));
}
});
const handleDragEnd = React.useCallback((evt) => {
const index = indexFromEvent(evt);
// we know that much: no more dragged item, no more placeholder
const updater = { draggedIndex: -1, placeholderIndex: -1,overIndex: -1, overZone: null };
if (placeholderIndex !== -1) {
// from here rows need to be updated
// copy rows
updater.rows = [...rows];
// mutate updater.rows, move item at dragged index to placeholderIndex
if (placeholderIndex > index) {
// inserting after so removing the elem first and shift insertion index by -1
updater.rows.splice(index, 1);
updater.rows.splice(placeholderIndex - 1, 0, rows[index]);
} else {
// inserting before, so do not shift
updater.rows.splice(index, 1);
updater.rows.splice(placeholderIndex, 0, rows[index]);
}
}
setState(s => ({
...s,
...updater
}));
});
const renderedRows = rows.map((row, index) => (
<div
key={row.name}
data-drag-index={index}
className={
`row ${
index === draggedIndex
? 'dragged-row'
: 'normal-row'}`
}
draggable
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
>
{row.name}
</div>
));
// there is a placeholder to show, add it to the rendered rows
if (placeholderIndex !== -1) {
renderedRows.splice(
placeholderIndex,
0,
<Placeholder />
);
}
return (
<div>
{renderedRows}
<StateDisplay state={state} />
</div>
);
};
const Placeholder = ({ index }) => (
<div
key="placeholder"
className="row placeholder-row"
></div>
);
function indexFromEvent(evt) {
try {
return parseInt(evt.target.dataset.dragIndex, 10);
} catch (err) {
return -1;
}
}
function placeholderIndexFromState(overIndex, overZone) {
if (overZone === null) {
return;
}
if (overZone === 'top') {
return overIndex;
} else {
return overIndex + 1;
}
}
const StateDisplay = ({ state }) => {
return (
<div className="state-display">
{state.rows.map(r => r.name).join()}<br />
draggedIndex: {state.draggedIndex}<br />
overIndex: {state.overIndex}<br />
overZone: {state.overZone}<br />
placeholderIndex: {state.placeholderIndex}<br />
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
.row { width: 100px; height: 30px; display: flex; align-items: center; justify-content: center; }
.row:nth-child(n+1) { margin-top: 5px; }
.row.normal-row { background: #BEBEBE; }
.row.placeholder-row { background: #BEBEFE; }
.row.normal-row:hover { background: #B0B0B0; }
.row.placeholder-row:hover { background: #B0B0F0; }
.row.dragged-row { opacity: 0.3; background: #B0B0B0; }
.row.dragged-row:hover { background: #B0B0B0; }
.state-display { position: absolute; right: 0px; top: 0px; width: 200px; }
<html><body><div id="root"></div><script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.7.0-alpha.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.7.0-alpha.0/umd/react-dom.production.min.js"></script></body></html>
I am looking for a way to only add groups(hide the add rule button) on base level with only OR operator selected(AND would be disabled here). Whenever the said group is added the operator allowed will only be OR (hiding AND operator). Now, nested groups are allowed but only to one level i.e. hide the Add Groups button whenever a group is added on base level.
I am using React 16.8.0 with react-querybuilder: "^3.1.2"
For ex. as shown below i want to hide the marked buttons. Would it be possible?
import QueryBuilder, { formatQuery } from "react-querybuilder";
....
<QueryBuilder
fields={
data
}
controlClassnames={{
combinators: "form-control-sm",
addRule: "btn btn-primary btn-sm ml-2 mb-1",
addGroup: "btn btn-secondary btn-sm ml-2 mb-1",
ruleGroup: "ruleGroup",
removeGroup: "ml-2",
rule: "rule",
fields: "mr-2 form-control-sm",
operators: "mr-2 form-control-sm",
value: "mr-2 form-control-sm"
}}
onQueryChange={query => setQueryOutput(formatQuery(query, "sql"))}
/>
<div className="mt-5">
<h5>Query Output:</h5>
{queryOutput}
</div>
Thanks for the support
===UPDATE===
To ensure that the base group's operator is always OR combinator and all the subsequent sub-groups will be AND operator. I have tried(as below) using control elements but I am not able to set the props.
addGroupAction: props => {
let customRule = props.rules.map(x => {
x.combinator = "or";
return x;
});
let customProps = { ...props, rules: customRule };
return (
<button
className={props.className}
title={props.title}
onClick={e => {
setCombinatorOptions(["AND"]);
return customProps.handleOnClick(e);
}}
>
{props.label}
</button>
);
}
Replicated the same at https://stackblitz.com/edit/react-6g5ygt
I think you can do it with css easily:
.queryBuilder .ruleGroup .ruleGroup .ruleGroup-addGroup {
display : none
}
I have applied the same css on below link and it's working as you expected :
https://sapientglobalmarkets.github.io/react-querybuilder/
I see this has already been marked as answered using CSS, but I wanted to show a way to do it with custom components that may be a little more flexible. This version also meets the requirement of limiting the outermost group's combinator to "or", which I don't think the CSS version does.
See the code below, or this codesandbox for a working example.
import { useEffect, useState } from "react";
import QueryBuilder, {
ActionElement,
ActionWithRulesProps,
CombinatorSelectorProps,
formatQuery,
RuleGroupType,
ValueSelector
} from "react-querybuilder";
import "react-querybuilder/dist/query-builder.css";
const fields = [
{ name: "first_name", label: "First Name" },
{ name: "email", label: "Email" },
{ name: "last_name", label: "Last Name" }
];
const defaultQuery: RuleGroupType = {
id: "root",
combinator: "and",
rules: [
{
id: "ruleGroup",
combinator: "and",
rules: []
}
]
};
const addRuleAction = (props: ActionWithRulesProps) => {
if (props.level === 0) {
return null;
}
return <ActionElement {...props} />;
};
const addGroupAction = (props: ActionWithRulesProps) => {
if (props.level > 0) {
return null;
}
return <ActionElement {...props} />;
};
const CombinatorSelector = (props: CombinatorSelectorProps) => {
const { level, value, handleOnChange, options } = props;
useEffect(() => {
if (level === 0 && value !== "or") {
handleOnChange("or");
}
}, [level, value, handleOnChange]);
return (
<ValueSelector
{...{
...props,
options: level === 0 ? options.filter((c) => c.name === "or") : options,
value: level === 0 ? "or" : value
}}
/>
);
};
export default function App() {
const [query, setQuery] = useState<RuleGroupType>(defaultQuery);
return (
<>
<QueryBuilder
fields={fields}
onQueryChange={setQuery}
query={query}
controlElements={{
addRuleAction,
addGroupAction,
combinatorSelector: CombinatorSelector
}}
/>
<h3>Output:</h3>
<code>{formatQuery(query, "sql")}</code>
</>
);
}
And the result:
Here is the original example of group checkbox of antd that I need and its fine:
const plainOptions = ['Apple', 'Pear', 'Orange'];
const defaultCheckedList = ['Apple', 'Orange'];
class App extends React.Component {
state = {
checkedList: defaultCheckedList,
indeterminate: true,
checkAll: false,
};
onChange = checkedList => {
this.setState({
checkedList,
indeterminate: !!checkedList.length && checkedList.length < plainOptions.length,
checkAll: checkedList.length === plainOptions.length,
});
};
onCheckAllChange = e => {
this.setState({
checkedList: e.target.checked ? plainOptions : [],
indeterminate: false,
checkAll: e.target.checked,
});
};
render() {
return (
<div>
<div style={{ borderBottom: '1px solid #E9E9E9' }}>
<Checkbox
indeterminate={this.state.indeterminate}
onChange={this.onCheckAllChange}
checked={this.state.checkAll}
>
Check all
</Checkbox>
</div>
<br />
<CheckboxGroup
options={plainOptions}
value={this.state.checkedList}
onChange={this.onChange}
/>
</div>
);
}
}
My question is how can I replace the plainOptions and defaultCheckedList by object array instead of simple array and using attribute name for this check boxes?
For example this object:
const plainOptions = [
{name:'alex', id:1},
{name:'milo', id:2},
{name:'saimon', id:3}
];
const defaultCheckedList = [
{name:'alex', id:1},
{name:'milo', id:2}
];
I want to use attribute name as the key in this example.
Problem solved. I should use "Use with grid" type of group checkbox. It accepts object array. The only think I could do was creating a function that inject "label" and "value" to my object. It makes some duplicates but no problem.
function groupeCheckboxify(obj, labelFrom) {
for (var i = 0; i < obj.length; i++) {
if (obj[i][labelFrom]) {
obj[i]['label'] = obj[i][labelFrom];
obj[i]['value'] = obj[i][labelFrom];
}
if (i == obj.length - 1) {
return obj;
}
}
}
// for calling it:
groupeCheckboxify( myObject , 'name');
I'd this same problem and couldn't find any answer on the entire web. But I tried to find a good way to handle it manually.
You can use this code:
import { Checkbox, Dropdown } from 'antd';
const CheckboxGroup = Checkbox.Group;
function CheckboxSelect({
title,
items,
initSelectedItems,
hasCheckAllAction,
}) {
const [checkedList, setCheckedList] = useState(initSelectedItems || []);
const [indeterminate, setIndeterminate] = useState(true);
const [checkAll, setCheckAll] = useState(false);
const onCheckAllChange = (e) => {
setCheckedList(e.target.checked ? items : []);
setIndeterminate(false);
setCheckAll(e.target.checked);
};
const onChangeGroup = (list) => {
if (hasCheckAllAction) {
setIndeterminate(!!list.length && list.length < items.length);
setCheckAll(list.length === items.length);
}
};
const updateItems = (el) => {
let newList = [];
if (el.target.checked) {
newList = [...checkedList, el.target.value];
} else {
newList = checkedList.filter(
(listItem) => listItem.id !== el.target.value.id,
);
}
setCheckedList(newList);
};
useEffect(() => {
setCheckedList(initSelectedItems);
}, []);
const renderItems = () => {
return (
<div classname="items-wrapper">
{hasCheckAllAction ? (
<Checkbox
indeterminate={indeterminate}
onChange={onCheckAllChange}
checked={checkAll}
>
All
</Checkbox>
) : null}
<CheckboxGroup onChange={onChangeGroup} value={checkedList}>
<>
{items.map((item) => (
<Checkbox
key={item.id}
value={item}
onChange={($event) => updateItems($event)}
>
{item.name}
</Checkbox>
))}
</>
</CheckboxGroup>
</div>
);
};
return (
<Dropdown overlay={renderItems()} trigger={['click']}>
<div>
<span className="icon icon-arrow-down" />
<span className="title">{title}</span>
</div>
</Dropdown>
);
}
It looks like the only difference you are talking about making is using an array of objects instead of strings? If that's the case, when looping through the array to create the checkboxes, you access the object attributes using dot notation. It should look something like this if I understand the problem correctly.
From CheckboxGroup component:
this.props.options.forEach(el => {
let name = el.name;
let id = el.id;
//rest of code to create checkboxes
or to show an example in creating components
let checkboxMarkup = [];
checkboxMarkup.push(
<input type="checkbox" id={el.id} name={el.name} key={`${el.id} - ${el.name}`}/>
);
}
'el' in this case refers to each individual object when looping through the array. It's not necessary to assign it to a variable, I just used that to show an example of how to access the properties.