stopPropagation does not work in a hierarchy of components - javascript

I am trying to avoid an onClick event that is on each particular row, whenever show/less is being clicked under one column's data. I have a grid that basically has select, and multi select features. Also there is show more/show less for one of the column. Whenever I click on Show More/Less, the row selection is triggered. I want to avoid this. Can someone tell what is wrong here. say If a have multiple instances of similar grids, I would want the row select functionality to that grid which has prop in it
Sandbox: https://codesandbox.io/s/react-table-row-table-alternate-single-row-working-5fr81
import * as React from "react";
import ReactTable from "react-table";
import "react-table/react-table.css";
export default class DataGrid extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedRows: []
};
}
rowClick = (state, rowInfo) => {
if (rowInfo) {
let selectedRows = new Set();
return {
onClick: e => {
e.stopPropagation();
if (e.ctrlKey) {
selectedRows = this.state.selectedRows;
rowInfo._index = rowInfo.index;
if (
selectedRows.filter(row => row._index === rowInfo._index)
.length === 0
)
selectedRows.push(rowInfo);
else
selectedRows = selectedRows.filter(
row => row._index !== rowInfo._index
);
this.setState({
selectedRows
});
} else {
selectedRows = [];
rowInfo._index = rowInfo.index;
selectedRows.push(rowInfo);
}
this.setState(
{
selectedRows
},
() => {
console.log("final values", this.state.selectedRows);
this.props.rowClicked(this.state.selectedRows);
}
);
},
style: {
background:
this.state.selectedRows.some(e => e._index === rowInfo.index) &&
"#9bdfff"
}
};
} else {
return "";
}
};
render() {
return (
<ReactTable
data={this.props.data}
columns={this.props.columns}
getTrProps={this.rowClick}
/>
);
}
}

On your ShowMore component, you can modify your onClick event handler to call event.stopPropagation(). This will stop further propagation of the current event in the capturing and bubbling phases, and allow you to click on Show More/Less without triggering row selection.
const toggleTruncate = (e) => {
e.stopPropagation();
setShowMore(!isShowMore);
}
React actually defines the synthetic event, which will be available on your click event handler, without the need to call document.addEventListener(). You may read more about event handling in React over here.
And answering your second part of the question, given that the rowClicked event is an optional props of the DataGrid component, you will need to manually check it on the DataGrid component to ensure that it will only be called if it is defined. This is how it can be done:
if (rowClicked) {
// do the rest
}
And this is how your rowClick method should look like. Do take note that I have destructured the props object on the second line. This will ensure that row selection logic will be carried out only if rowClicked is defined.
rowClick = (state, rowInfo) => {
const { rowClicked } = this.props;
if (rowInfo && rowClicked) {
let selectedRows = new Set();
return {
onClick: e => {
e.stopPropagation();
if (e.ctrlKey) {
selectedRows = this.state.selectedRows;
rowInfo._index = rowInfo.index;
if (
selectedRows.filter(row => row._index === rowInfo._index)
.length === 0
)
selectedRows.push(rowInfo);
else
selectedRows = selectedRows.filter(
row => row._index !== rowInfo._index
);
this.setState({
selectedRows
});
} else {
selectedRows = [];
rowInfo._index = rowInfo.index;
selectedRows.push(rowInfo);
}
this.setState(
{
selectedRows
},
() => {
rowClicked(this.state.selectedRows);
}
);
},
style: {
background:
this.state.selectedRows.some(e => e._index === rowInfo.index) &&
"#9bdfff"
}
};
} else {
return "";
}
};

Related

React suggestions Input setting state

im prety new to React and im trying to use an autocomplete input. Im having problems getting the value from it and clearing the input values after submitting. Any help would be greatly appretiated.
import React, { Component, Fragment } from "react";
import PropTypes from "prop-types";
import "../AutoComplete/styles.css"
class Autocomplete extends Component {
static propTypes = {
suggestions: PropTypes.instanceOf(Array)
};
static defaultProps = {
suggestions: [],
};
constructor(props) {
super(props);
this.state = {
// The active selection's index
activeSuggestion: 0,
// The suggestions that match the user's input
filteredSuggestions: [],
// Whether or not the suggestion list is shown
showSuggestions: false,
// What the user has entered
userInput: this.props.value ? this.props.value : "",
};
}
//Order by 'code'
generateSortFn(prop, reverse) {
return function (a, b) {
if (a[prop] < b[prop]) return reverse ? -1 : 1;
if (a[prop] > b[prop]) return reverse ? 1 : -1;
return 0;
};
}
onChange = e => {
const { suggestions } = this.props;
const userInput = e.currentTarget.value;
// Filter our suggestions that don't contain the user's input
const filteredSuggestions = suggestions.sort(this.generateSortFn('code', true)).filter(
(suggestion, i) => {
let aux = suggestion.descrp+"- "+suggestion.code
return aux.toLowerCase().indexOf(userInput.toLowerCase()) > -1
}
);
this.setState({
activeSuggestion: 0,
filteredSuggestions,
showSuggestions: true,
userInput: e.currentTarget.value
});
};
onClick = e => {
this.setState({
activeSuggestion: 0,
filteredSuggestions: [],
showSuggestions: false,
userInput: e.currentTarget.innerText
});
};
onKeyDown = e => {
const { activeSuggestion, filteredSuggestions } = this.state;
// User pressed the enter key
if (e.keyCode === 13) {
this.setState({
activeSuggestion: 0,
showSuggestions: false,
userInput: filteredSuggestions[activeSuggestion].code+" - "+filteredSuggestions[activeSuggestion].descrp
});
}
// User pressed the up arrow
else if (e.keyCode === 38) {
if (activeSuggestion === 0) {
return;
}
this.setState({ activeSuggestion: activeSuggestion - 1 });
}
// User pressed the down arrow
else if (e.keyCode === 40) {
if (activeSuggestion - 1 === filteredSuggestions.length) {
return;
}
this.setState({ activeSuggestion: activeSuggestion + 1 });
}
};
render() {
const {
onChange,
onClick,
onKeyDown,
state: {
activeSuggestion,
filteredSuggestions,
showSuggestions,
userInput
}
} = this;
let suggestionsListComponent;
if (showSuggestions && userInput) {
if (filteredSuggestions.length) {
suggestionsListComponent = (
<ul className="suggestions">
{filteredSuggestions.map((suggestion, index) => {
let className="";
// Flag the active suggestion with a class
if (index === activeSuggestion) {
className = "suggestion-active";
}
return (
<li className={className} key={suggestion.code} onClick={onClick}>
{suggestion.code+" - "+suggestion.descrp}
</li>
);
})}
</ul>
);
} else {
suggestionsListComponent = (
<div className="no-suggestions">
<p>Sin sugerencias</p>
</div>
);
}
}
and the return (this is where i think im wrong)
return (
<Fragment>
<label htmlFor="autocomplete-input" className="autocompleteLabel">{this.props.label}</label>
<div className="centerInput">
<input
className="autocomplete-input"
type="text"
onChange={onChange}
onKeyDown={onKeyDown}
defaultValue={this.props.initState}
value= {/* this.props.value ? this.props.value : */ userInput}
placeholder={this.props.placeholder}
selection={this.setState(this.props.selection)}
/>
{suggestionsListComponent}
</div>
</Fragment>
);
}
}
export default Autocomplete;
What I want is to use this component in different pages, so im passing the "selection" prop and setting the state there.
The input is working correctly (searches, gets the value and shows/hide the helper perfectly). The problem is i cant reset this inputs clearing them, and i suspect the error is in here.
I get the following warning (even with it somewhat functioning)
Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.
This is the Component usage with useState:
<Autocomplete label='Out cost Center:' placeholder='Set the out cost center' suggestions={dataCostCenterHelper} selection={(text) => setOutCostCenter(text.userInput)} value={outCostCenter} />
and last this is how im tryin to clear the state that is set in "selection":
const clearData = async () => {
setOutCostCenter('-');
// other inputs with the same component
setOutVendor('-');
setOutRefNumber('-');
}
This gets called inside the function that handles the button submitting the form.
Thanks in advance!
Looking at the code you posted this line might be the problem:
selection={this.setState(this.props.selection)}
You are updating state directly inside the render method, this is not recommended.
Try using a selection prop or state field and update the prop inside a componenteDidMount life cycle
selection={this.state.selection}

Trying to delete a key and values from an object, but when I try to delete it breaks some functionality

I am trying to create an add div button and a delete div button. When you select a certain div and click delete, I want to delete only that key from the object. The issue is when I delete and then try to create a new div, it doesn't create the new divs anymore...Not sure what i'm doing wrong or why it only kind of works.
import "./styles.css";
import {
useEffect,
useState
} from "react";
// The parent component
const App = () => {
const [textBoxDivs, setTextBoxDivs] = useState({});
const addNewTextBox = () => {
const numOfTextBoxDivs = Object.keys(textBoxDivs).length;
console.log(numOfTextBoxDivs, "num");
setTextBoxDivs({
...textBoxDivs,
[`div${numOfTextBoxDivs + 1}`]: {
isSelected: false,
innerText: "text"
}
});
};
const selectItem = (e) => {
const nextState = { ...textBoxDivs
};
Object.keys(nextState).forEach((k) => {
nextState[k].isSelected = false;
});
nextState[e.target.id].isSelected = true;
setTextBoxDivs(nextState);
};
const removeSelectedItem = () => {
const nextState = { ...textBoxDivs
};
if (Object.keys(textBoxDivs).length > 0) {
Object.keys(textBoxDivs).map((key) => {
if (textBoxDivs[key].isSelected) {
delete nextState[key];
return setTextBoxDivs(nextState);
}
return null;
});
}
};
return ( <
div >
<
button onClick = {
() => addNewTextBox()
} >
Click me to create a selectable div <
/button> <
button onClick = {
(e) => removeSelectedItem(e)
} >
Click me to delete a selectable div <
/button> {
Object.keys(textBoxDivs).length > 0 &&
Object.keys(textBoxDivs).map((key, index) => {
return ( <
div style = {
{
border: textBoxDivs[key].isSelected ?
"2px solid green" :
"unset"
}
}
onClick = {
(e) => selectItem(e)
}
key = {
index
}
id = {
key
} >
{
textBoxDivs[key].innerText
} <
/div>
);
})
} <
/div>
);
};
export default App;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
The problem in your code in the function addNewTextBox, specifically in the line
[`div${numOfTextBoxDivs + 1}`]: {
Because it does not necessarily mean that your are adding a new line. In this case, you are assigning a value to (div + number), but sometimes that already exists. For example, of you change that line for a truly unique number, such as date, it works:
const addNewTextBox = () => {
const numOfTextBoxDivs = Object.keys(textBoxDivs).length;
console.log(numOfTextBoxDivs, "num");
setTextBoxDivs({
...textBoxDivs,
[`div${new Date().getTime()}`]: {
isSelected: false,
innerText: "text"
}
});
};
Update selectItem() then it works:
const selectItem = (e) => {
const nextState = { ...textBoxDivs, setTextBoxDivs }; // setTextBoxDivs was missing
Object.keys(nextState).forEach((k) => {
nextState[k].isSelected = false;
});
nextState[e.target.id].isSelected = true;
setTextBoxDivs(nextState);
};

In React how do I handle 100 checkboxes in React-Table and get their value

I have a react-table where I'm generating about 100 checkboxes dynamically. I have tried everything I can think of, but checking/unchecking the checkboxes takes a very long time before I can see any changes. It seems to be re-rendering every single checkbox before I can see a tick on the checkbox I just clicked on (the page freezes for about 5-10 secs). I'm using a change handler defined in a wrapper component passing as a prop.
// Generate columns for table
// this is added to the column props of React-Table
function setActionsColumns() {
return rolesActionsList.map((action, index) => {
const actionName = get(action, 'name');
return {
Header: actionName,
accessor: actionName,
Cell: row => {
const objectName = get(row, 'original.name');
let data = formData.permissions.filter(
perm => row.tdProps.rest.action === perm.action
);
let roleData = data[0];
let checked;
if (get(roleData, 'object') === row.tdProps.rest.object) {
checked = get(roleData, 'checked');
}
return (
<Checkbox
key={`${actionName}_${index}`}
name={actionName}
onChange={onCheckboxChange}
data-object={objectName}
checked={checked}
/>
);
},
};
});
}
// Checkbox handler defined in a class component
handleCheckboxChange = (event: SyntheticEvent<HTMLInputElement>) => {
const { roleData } = this.state;
const target = event.currentTarget;
const actionName = target.name;
const checked = target.checked;
const objectName = target.dataset.object.toUpperCase();
const checkedItem = {
action: actionName,
object: objectName,
checked,
};
let checkedItemsList = [];
let isInArray = roleData.permissions.some(perm => {
return perm.action && perm.object === checkedItem.object;
});
if (!isInArray) {
checkedItemsList = [...roleData.permissions, checkedItem];
} else {
checkedItemsList = roleData.permissions.filter(
perm => perm.action !== checkedItem.action
);
}
this.setState({
roleData: {
...this.state.roleData,
permissions: checkedItemsList,
},
});
};

Deleting elements onClick in ReactJS?

I have a Row Component that i am importing into my App.js component and apparently i but the delete function doesn't seem to be deleting the correct row. Even if i delete the second one, it deletes the last one first. So basically if i click delete on second row, it should delete the second one not the third etc.
My App.js or my root component looks like this: -
import React, {Component} from 'react';
import './App.css';
import Row from './Row.js'
class App extends Component {
state = {
rows: [
{value: 'row1', options:[1,2,3,4,5] },
{value: 'row2', options:[1,2,3,4,5] },
{value: 'row3', options:[ 1,2,3,4,5 ]}
]
}
updateValue = (e, idx) => {
const rows = [...this.state.rows];
rows[idx].value = e.target.value;
this.setState({
rows,
});
}
addRow = () => {
const rows = [...this.state.rows, {value:'', options: [1,2,3,4,5}];
this.setState({
rows,
});
}
deleteRow = (idx) => {
const copy = [...this.state.rows]
copy.splice(idx,1)
this.setState ({
rows:copy
})
}
render() {
return (
<div>
{
this.state.rows.map ( (row,idx) => {
return (
<Row
key = {idx}
value = { row.value }
onChange = { (e) => this.updateValue(e,idx) }
delete = { this.deleteRow.bind(this,idx) } />
)
})
}
<button onClick = {this.addRow}> Add </button>
</div>
)
}
}
export default App;
And my row component looks like this:-
const Row = ( props) => {
let options = props.options.map(opt => <option key={opt}>{opt}</option>);
return (
<div>
<input value= { props.value } onChange={ props.onChange }></input>
<select>
{options}
</select>
<button onClick = { props.delete} > Delete </button>
</div>
)
}
export default Row
I asked a similar question yesterday link but didn't realize at the time that the items weren't being deleted from the right position?
Try this
deleteRow = (item) => {
let filtered = this.state.rows.filter(row=>row.value!==item.value);
this.setState ({
rows: filtered
})
}
pass row to your delete function
<Row
key = {idx}
value = { row.value }
onChange = { (e) => this.updateValue(e,idx) }
delete = {()=>this.deleteRow(row) } />
)
if every row(object) is having unique id then use that id instead of value
like-
let filtered = this.state.rows.filter(row=>row.id!==item.id);

How to Drag & Drop Multiple Elements In React?

This is my first question on StackOverflow. I want to build a little game with React, where users can drag and drop tetrominos onto a grid and also reposition or rotating them to their liking. The tetrominos are represented by a matrix and then every block gets rendered in a li element.
Example for the z-tetromino:
[0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0]
Unfortunately I cannot post images yet, that would make things easier.
The grid is too respresented by a matrix.
Now what I want to do is basically drag and drop these block matrices onto the grid, so that the values in the grid change accordingly (0 → 1 etc.).
The problem is, I have no clue how to drag multiple li elements at once with the standard HTML5 DnD API or with React DnD. When the user clicks on one li element of a certain tetromino, the whole piece should move. Maybe I could solve this using jQuery UI, but since in React no direct DOM manipulation is allowed, I'm left wondering how to do it.
I tried to drag one block onto the grid which worked semi optimally, because one block took the place of an entire row of grid blocks, even with display: inline-block set in CSS.
Here is some simple code from the first experiment.
onDragStart = e => {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text', e.target.id);
// e.dataTransfer.setDragImage(e.target.parentNode, 20, 20);
};
handleDrop = e => {
const pieceOrder = e.dataTransfer.getData('text');
// console.log(document.getElementById(pieceOrder));
// e.target.appendChild(document.getElementById(pieceOrder));
// console.log(pieceOrder);
e.target.replaceWith(document.getElementById(pieceOrder));
e.target.remove();
};
renderEmptyBoardCell(i) {
return (
<li key={i} className="emptyBoardCell" onDragOver={(e) => e.preventDefault()} onDrop={(e) => this.handleDrop(e)}>></li>
);
}
renderTemplateBoardCell(i) {
return (
<li key={i} className="templateBoardCell" onDragOver={(e) => e.preventDefault()} onDrop={(e) => this.handleDrop(e)}>></li>
);
}
renderEmptyCell(i) {
return (
<li key={i} className="emptyCell"></li>
);
}
renderFilledCell(piece_config, i) {
return (
<li key={i} id={i} className={`filledCell ${piece_config}`} draggable onDragStart={this.onDragStart}></li>
);
}
So the question is, would that be theoretically possible with React DnD or any other library? If yes, what would be the approximate solution to DnD multiple elements at once.
Thanks for your time!
In case anyone is looking for a solution in 2020. Here is my current solution with react-dnd and react hooks. You can try the live demo here.
Here is another simpler example, you can check out the codesandbox here.
You can only drag one item at a time using react-dnd. Either use a different library, or somehow group together the different pieces into one item first, and then drag and drop that one item.
I know its a bit late but have you looked into: panResponder. I am looking into multiple d'n'd elements and panResponder is the most likely fit
A nice choice for your need could be react-beautiful-dnd.
Try this out It will surely work in your case!
react-beautiful-dnd multi drag pattern
https://github.com/atlassian/react-beautiful-dnd/tree/master/stories/src/multi-drag
demo: https://react-beautiful-dnd.netlify.app/?path=/story/multi-drag--pattern
import React, { Component } from 'react';
import styled from '#emotion/styled';
import { DragDropContext } from 'react-beautiful-dnd';
import initial from './data';
import Column from './column';
import type { Result as ReorderResult } from './utils';
import { mutliDragAwareReorder, multiSelectTo as multiSelect } from './utils';
import type { DragStart, DropResult, DraggableLocation } from 'react-beautiful-dnd';
import type { Task, Id } from '../types';
import type { Entities } from './types';
const Container = styled.div`
display: flex;
user-select: none;
justify-content: center;
`;
type State = {
entities: Entities,
selectedTaskIds: Id[],
columnFlag: false,
// sad times
draggingTaskId: Id,
};
const getTasks = (entities: Entities, columnId: Id): Task[] =>
entities.columns[columnId].taskIds.map(
(taskId: Id): Task => entities.tasks[taskId],
);
export default class TaskApp extends Component<any, State> {
state: State = {
entities: initial,
selectedTaskIds: [],
draggingTaskId: '',
};
componentDidMount() {
window.addEventListener('click', this.onWindowClick);
window.addEventListener('keydown', this.onWindowKeyDown);
window.addEventListener('touchend', this.onWindowTouchEnd);
}
componentWillUnmount() {
window.removeEventListener('click', this.onWindowClick);
window.removeEventListener('keydown', this.onWindowKeyDown);
window.removeEventListener('touchend', this.onWindowTouchEnd);
}
onDragStart = (start: DragStart) => {
const id: string = start.draggableId;
const selected: Id = this.state.selectedTaskIds.find(
(taskId: Id): boolean => taskId === id,
);
// if dragging an item that is not selected - unselect all items
if (!selected) {
this.unselectAll();
}
this.setState({
draggingTaskId: start.draggableId,
});
};
onDragEnd = (result: DropResult) => {
const destination = result.destination;
const source = result.source;
const draggableId = result.draggableId;
const combine = result.combine;
console.log('combine',combine);
console.log('destination',destination);
console.log('source',source);
console.log('draggableId',draggableId);
// nothing to do
if (!destination || result.reason === 'CANCEL') {
this.setState({
draggingTaskId: '',
});
return;
}
const processed: ReorderResult = mutliDragAwareReorder({
entities: this.state.entities,
selectedTaskIds: this.state.selectedTaskIds,
source,
destination,
});
this.setState({
...processed,
draggingTaskId: null,
});
};
onWindowKeyDown = (event: KeyboardEvent) => {
if (event.defaultPrevented) {
return;
}
if (event.key === 'Escape') {
this.unselectAll();
}
};
onWindowClick = (event: KeyboardEvent) => {
if (event.defaultPrevented) {
return;
}
this.unselectAll();
};
onWindowTouchEnd = (event: TouchEvent) => {
if (event.defaultPrevented) {
return;
}
this.unselectAll();
};
toggleSelection = (taskId: Id) => {
const selectedTaskIds: Id[] = this.state.selectedTaskIds;
const wasSelected: boolean = selectedTaskIds.includes(taskId);
console.log('hwwo',this.state.entities.columns);
console.log('hwwo',this.state.entities.columns.done.taskIds);
// if there is change in entities - update the state
const newTaskIds: Id[] = (() => {
// Task was not previously selected
// now will be the only selected item
if (!wasSelected) {
return [taskId];
}
// Task was part of a selected group
// will now become the only selected item
if (selectedTaskIds.length > 1) {
return [taskId];
}
// task was previously selected but not in a group
// we will now clear the selection
return [];
})();
this.setState({
selectedTaskIds: newTaskIds,
});
};
toggleSelectionInGroup = (taskId: Id) => {
const selectedTaskIds: Id[] = this.state.selectedTaskIds;
const index: number = selectedTaskIds.indexOf(taskId);
// if not selected - add it to the selected items
if (index === -1) {
this.setState({
selectedTaskIds: [...selectedTaskIds, taskId],
});
return;
}
// it was previously selected and now needs to be removed from the group
const shallow: Id[] = [...selectedTaskIds];
shallow.splice(index, 1);
this.setState({
selectedTaskIds: shallow,
});
};
// This behaviour matches the MacOSX finder selection
multiSelectTo = (newTaskId: Id) => {
const updated: string[] | null | undefined = multiSelect(
this.state.entities,
this.state.selectedTaskIds,
newTaskId,
);
if (updated == null) {
return;
}
this.setState({
selectedTaskIds: updated,
});
};
unselect = () => {
this.unselectAll();
};
unselectAll = () => {
this.setState({
selectedTaskIds: [],
});
};
render() {
const entities = this.state.entities;
const selected = this.state.selectedTaskIds;
console.log('entities', entities);
console.log('selected', selected);
return (
<DragDropContext
onDragStart={this.onDragStart}
onDragEnd={this.onDragEnd}
>
<Container>
{entities.columnOrder.map((columnId: Id) => (
<Column
column={entities.columns[columnId]}
tasks={getTasks(entities, columnId)}
selectedTaskIds={selected}
key={columnId}
draggingTaskId={this.state.draggingTaskId}
toggleSelection={this.toggleSelection}
toggleSelectionInGroup={this.toggleSelectionInGroup}
multiSelectTo={this.multiSelectTo}
entities={entities}
/>
))}
</Container>
</DragDropContext>
);
}
}

Categories

Resources