React JS Sortable Form Fields as Components - javascript

I'm trying to develop a fairly simplistic E-Mail template creator with React JS. I'm using the "react-sortable-hoc" library as a means to handle the ordering of elements on the page.
The goal is to allow users to create "Rows", rearrange rows, and within each row, have multiple "Columns" that can contain components like images, textboxes, etc...
But I keep running into the same issue with Sortable libraries. Form fields cannot maintain their own "state" when being dragged up or down. The State of a Component in React JS seems to be lost when it's in a draggable component. I've experienced similar issues with JQuery UI's Sortable but it required an equally ridiculous solution. Is it common to find that form fields are simply super difficult to rearrange?
As a "proof of concept", I am using a complex JSON object that stores all the information in the Letter.js component and passes it down as Props which are then passed down to each component. But as you can tell, this is becoming cumbersome.
Here is an example of my Letter component that handles the JSON object and sorting of Rows:
import React, {Component} from 'react';
import {render} from 'react-dom';
import {
SortableContainer,
SortableElement,
arrayMove
} from 'react-sortable-hoc';
import Row from './Row';
const SortableItem = SortableElement(({row, rowIndex, onChange, addPart}) => {
return (
<li>
<Row
row={row}
rowIndex={rowIndex}
onChange={onChange}
addPart={addPart} />
</li>
)
});
const SortableList = SortableContainer(({rows, onChange, addPart}) => {
return (
<ul id="sortableList">
{rows.map((row, index) => {
return (
<SortableItem
key={`row-${index}`}
index={index}
row={row}
rowIndex={index}
onChange={onChange}
addPart={addPart}
/> )
})}
</ul>
);
});
class Letter extends Component {
constructor(props) {
super(props);
this.state = {
rows: [],
}
this.onSortEnd = this.onSortEnd.bind(this);
this.onChange = this.onChange.bind(this);
this.addRow = this.addRow.bind(this);
this.addPart = this.addPart.bind(this);
}
addPart(event, index, value, rowIndex, columnIndex) {
console.log(value);
var part = {};
if(value === 'Text') {
part = {
type: 'Text',
value: ''
}
} else if(value === 'Image') {
part = {
type: 'Image',
value: ''
}
} else {
part = {
type: 'Empty',
}
}
const { rows } = this.state;
rows[rowIndex][columnIndex] = part;
this.setState({ rows: rows })
}
onChange(text, rowIndex, columnIndex) {
const { rows } = this.state;
const newRows = [...rows];
newRows[rowIndex][columnIndex].value = text;
this.setState({ rows: newRows });
}
addRow(columnCount) {
var rows = this.state.rows.slice();
var row = [];
for(var i = 0; i < columnCount; i++) {
var part = {
type: 'Empty',
}
row.push(part);
}
rows.push(row);
this.setState({ rows: rows })
}
onSortEnd = ({oldIndex, newIndex}) => {
this.setState({
rows: arrayMove(this.state.rows, oldIndex, newIndex),
});
};
render() {
console.log(JSON.stringify(this.state.rows));
const SideBar = (
<div className="sideBar">
<h3>Add a Row</h3>
<button className="uw-button" onClick={() => this.addRow(1)}>1 - Column</button><br/><br/>
<button className="uw-button" onClick={() => this.addRow(2)}>2 - Column</button><br/><br/>
<button className="uw-button" onClick={() => this.addRow(3)}>3 - Column</button>
<hr />
</div>
);
if(this.state.rows.length <= 0) {
return (
<div className="grid">
<p>This E-Mail is currently empty! Add some components to make a template.</p>
{SideBar}
</div>
)
}
return (
<div className="grid">
<SortableList
rows={this.state.rows}
onChange={this.onChange}
addPart={this.addPart}
lockAxis="y"
useDragHandle={true}
onSortStart={this.onSortStart}
onSortMove={this.onSortMove}
onSortEnd={this.onSortEnd}
shouldCancelStart={this.shouldCancelStart} />
{SideBar}
</div>
);
}
}
export default Letter;
And here is an example of Row:
import React, { Component } from 'react';
import { Text, Image } from './components/';
import { SortableHandle } from 'react-sortable-hoc';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import { Card, CardActions, CardHeader, CardMedia, CardTitle, CardText } from 'material-ui/Card';
import DropDownMenu from 'material-ui/DropDownMenu';
import MenuItem from 'material-ui/MenuItem';
const DragHandle = SortableHandle(() => <span className="dragHandle"></span>);
class Row extends Component {
constructor(props) {
super(props);
}
render() {
if(this.props.row !== undefined && this.props.row.length > 0) {
const row = this.props.row.map((column, columnIndex) => {
if(column.type === 'Empty') {
return (
<MuiThemeProvider key={columnIndex}>
<div className="emptyColumn">
<Card>
<DragHandle />
<CardTitle title="Empty Component"/>
<DropDownMenu value={"Empty"} onChange={(event, index, value) => this.props.addPart(event, index, value, this.props.rowIndex, columnIndex)}>
<MenuItem value={"Empty"} primaryText="Empty" />
<MenuItem value={"Text"} primaryText="Textbox" />
<MenuItem value={"Image"} primaryText="Image" />
</DropDownMenu>
</Card>
</div>
</MuiThemeProvider>
)
} else if(column.type === 'Text') {
return (
<MuiThemeProvider key={columnIndex}>
<div className="textColumn">
<Card>
<DragHandle />
<CardTitle title="Textbox"/>
<DropDownMenu value={"Text"} onChange={(event, index, value) => this.props.addPart(event, index, value, this.props.rowIndex, columnIndex)}>
<MenuItem value={"Empty"} primaryText="Empty" />
<MenuItem value={"Text"} primaryText="Textbox" />
<MenuItem value={"Image"} primaryText="Image" />
</DropDownMenu>
<Text
value={this.props.row[columnIndex].value}
onChange={this.props.onChange}
columnIndex={columnIndex}
rowIndex={this.props.rowIndex} />
</Card>
</div>
</MuiThemeProvider>
)
} else if(column.type === 'Image') {
return (
<MuiThemeProvider key={columnIndex}>
<div className="textColumn">
<Card>
<DragHandle />
<CardTitle title="Image"/>
<DropDownMenu value={"Image"} onChange={(event, index, value) => this.props.addPart(event, index, value, this.props.rowIndex, columnIndex)}>
<MenuItem value={"Empty"} primaryText="Empty" />
<MenuItem value={"Text"} primaryText="Textbox" />
<MenuItem value={"Image"} primaryText="Image" />
</DropDownMenu>
<Image
columnIndex={columnIndex}
rowIndex={this.props.rowIndex} />
</Card>
</div>
</MuiThemeProvider>
)
}
})
return (
<div className="row">
{row}
</div>
)
}
return <p>No components</p>;
}
}
export default Row;
Lastly, this is what Text.js looks like
import React, { Component } from 'react';
import ReactQuill from 'react-quill';
class Text extends Component {
constructor(props) {
super(props);
}
render() {
return (
<ReactQuill value={this.props.value}
onChange={(text) => this.props.onChange(text, this.props.rowIndex, this.props.columnIndex)} />
)
}
}
export default Text;
So, I keep having to pass ridiculous parameters to onChange functions and other functions in order to ensure that the state is maintained while sorting and editing. So, how should I be handling this? I don't want Letter.js (which is basically App.js) to handle all of my data handling. I want each component to handle it's own. I want Text.js to handle the onChange effects of its text. But I just can't see a way around passing everything down as props.

Related

The initial render of the useEffect is disturbing the values of the cryptos. What is the solution?

I want that the Input field acts as a search bar and shows me the typed cryptocurrencies but because of the initial render of the useEffect, the value of the cryptos is being set to undefined and because of this no crypto is being shown on the page. Please suggest any alternate way to implement this functionality and also How can I stop the useEffect to get render at the starting .
import { Card, Row, Col, Input, Typography } from 'antd'
import React from 'react'
import { Link } from 'react-router-dom'
import { useGetCryptosQuery } from '../services/CryptoApi'
import { useState, useEffect } from 'react'
import millify from 'millify'
import { filter } from 'htmlparser2/node_modules/domutils'
const Cryptocurrencies = ( props ) => {
const count = props.simplified ? 10 : 100;
const { data: cryptoList, isFetching } = useGetCryptosQuery( count )
const [cryptos, setCryptos] = useState( cryptoList?.data?.coins )
const [searchTerm, setSearchTerm] = useState( '' )
useEffect( () => {
const filteredData = cryptoList?.data?.coins.filter( ( coin ) => { coin.name.toLowerCase().includes( searchTerm.toLowerCase() ) } )
setCryptos( filteredData )
}, [searchTerm, cryptoList] )
console.log( cryptos )
if ( isFetching ) {
return "loading...";
}
return (
<div>
{
<>
<div>
<Input placeholder="Search Cryptocurrency" onChange={( e ) => setSearchTerm( e.target.value )} className="" />
</div>
<Row gutter={[32, 32]} className="crypto-card-container">
{cryptos?.map( ( currency ) => {
return (
<Col xs={24} sm={12} lg={6} className="crypto-card" key={currency.id}>
<Link to={`/ crypto / ${currency.id} `}>
<Card
title={`${currency.rank}.${currency.name} `}
extra={<img className="crypto-image" src={currency.iconUrl} />}
hoverable
>
<p>Price : ${millify( currency.price )}</p>
<p>Market Cap : {millify( currency.marketCap )}</p>
<p>Daily Change : {millify( currency.change )}%</p>
</Card>
</Link>
</Col>
)
} )}
</Row>
</>
}
</div>
)
}
export default Cryptocurrencies
Simply can add a conditional if statement to solve the issue.
Let's take a look at the useEffect, As you describe at the component did mount, the searchTerm is still an empty string. By adding a simple if statement to check the seatchTerm value, filteredData and setCryptos methods only works when searchTerm has a string with a minimum of one letter.
useEffect( () => {
if(searchTerm.length > 0) {
const filteredData = cryptoList?.data?.coins.filter((coin) => { coin.name.toLowerCase().includes(searchTerm.toLowerCase())})
setCryptos(filteredData)
}
}, [searchTerm, cryptoList] )

How to share state between React function and re-render on changes

I have a react component that renders a forms with checkboxes and a chart (Victory). I want the component to show the active checkboxes in the graph (each checkboxes has its api url, for simplicity, the code below shows useless data and no fetching). There are multiple problems in this code:
1) The activestock state always seem to be one step behind in the point of view of the console.log, in the console it show the previous state instead of the actual content of the array. But in the React Dev Tools it shows the activestock correctly.
2) The VictoryChart doesnt get re-rendered even though its props (which is the activestock) changes, even though I can see in the React Dev Tools that multiple VictoryLines components exist. It's like the parents doesn't rerender on state changes, but I thought that by passing a state in props, you shared state between components?
3) Because the hooks rerenders the components, the checkboxes doesn't show a check when clicked.
Here is a sandbox link: https://codesandbox.io/s/crazy-murdock-3re1x?fontsize=14&hidenavigation=1&theme=dark
import React, {useState, useEffect}from 'react'
import Card from 'react-bootstrap/Card';
import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import { VictoryLine, VictoryChart } from 'victory';
export default function HomeCards(props){
const [activestock, setActiveStock] = useState([])
function handleActiveStocks(props){
if (activestock.includes(props) === false){
setActiveStock([...activestock, props])
console.log(activestock)
}
else {
setActiveStock(activestock.filter(activestock => activestock !== props))
console.log(activestock)
}
}
function Lines(props){
const charts = []
if (props.chart !== undefined){
props.chart.map(function(val, index){
charts.push(
<VictoryLine data={[
{x:index+1, y:index+2},
{x:index+2, y:index+4}
]}/>
)
})
return charts
}
return null
}
function RealCharts(props){
return(
<VictoryChart>
<Lines chart={props.stocks}></Lines>
</VictoryChart>
)
}
function Forms(props){
const hg=[]
props.text.map(function(val, index){
hg.push(
<form>
{val} <input type='checkbox' onClick={()=> handleActiveStocks(val)}/> {index}
</form>
)
})
return(hg)
}
return(
<Container fluid>
<Row style={{position:'relative', top:'2vh'}}>
<Card style={{width:props.width}}>
<Card.Header>U.S Equity</Card.Header>
<Forms
text={['S&P 500', 'NASDAQ',' DOW', 'Russel 1000', 'Large Cap', 'Small Cap', 'MSFT', 'FB']}
/>
</Card>
<Card>
{/* <VictoryChart >
<Lines chart={activestock}/>
</VictoryChart> */}
<RealCharts stocks={activestock}/>
</Card>
</Row>
</Container>
)
}
You need a checked={some_value_here} for checkbox to be checked.
import React, { useState, useEffect } from "react";
import Card from "react-bootstrap/Card";
import Container from "react-bootstrap/Container";
import Row from "react-bootstrap/Row";
import { VictoryLine, VictoryChart } from "victory";
export default function HomeCards(props) {
const [activestock, setActiveStock] = useState([]);
function handleActiveStocks(props) {
if (activestock.includes(props) === false) {
setActiveStock([...activestock, props]);
console.log(activestock);
} else {
setActiveStock(activestock.filter(activestock => activestock !== props));
console.log(activestock);
}
}
function Lines(props) {
const charts = [];
if (props.chart !== undefined) {
props.chart.map(function(val, index) {
charts.push(
<VictoryLine
data={[
{ x: index + 1, y: index + 2 },
{ x: index + 2, y: index + 4 }
]}
/>
);
});
return charts;
}
return null;
}
function RealCharts(props) {
return (
<VictoryChart>
<Lines chart={props.stocks} />
</VictoryChart>
);
}
// useEffect()
function Forms(props) {
const hg = [];
props.text.map(function(val, index) {
console.log(activestock)
// check the value exists in the activeStock
const checked=activestock.includes(val);
hg.push(
<form>
{val}{" "}
<input type="checkbox" checked={checked} onClick={() => handleActiveStocks(val)} />{" "}
{index}
</form>
);
});
return hg;
}
return (
<Container fluid>
<Row style={{ position: "relative", top: "2vh" }}>
<Card style={{ width: props.width }}>
<Card.Header>U.S Equity</Card.Header>
<Forms
text={[
"S&P 500",
"NASDAQ",
" DOW",
"Russel 1000",
"Large Cap",
"Small Cap",
"MSFT",
"FB"
]}
/>
</Card>
<Card>
{/* <VictoryChart >
<Lines chart={activestock}/>
</VictoryChart> */}
<RealCharts stocks={activestock} />
</Card>
</Row>
</Container>
);
}
The shared state is not wrong, you just need to add necessary check before return it.

OnClick, apply style on one array element

When I type something in my textarea, and then click on the button, this new element is stocked inside an array and displayed in a list in my react app. I want the array's elements to be crossed when I click on them.
I've written a function to change the state of 'crossed' to its opposite when i click on the element, and then the style of the elements would change depending on whether it's true or false.
app.js:
import React from 'react';
import Tasks from './tasks.js';
import Item from './component.js';
import './App.css';
class App extends React.Component {
state = {
todolist: [],
crossed: false
}
addData(val) {
this.setState({ todolist: this.state.todolist.concat(val) },
() => console.log(this.state.todolist))
}
cross() {
this.setState({ crossed: !this.state.crossed },
() => console.log(this.state.crossed))
}
render() {
return (
<div className="App">
<Tasks onClick={value => this.addData(value)} />
{
(this.state.crossed) ? (<ul>
{this.state.todolist.map((e) => {
return <Item
item={e}
onClick={(e) => this.cross(e)}
style={{ textDecoration : 'line-through' }} />}
)
}
</ul>) : (
<ul>
{this.state.todolist.map((e) => {
return <Item
item={e}
onClick={(e) => this.cross(e)}
/>}
)
}
</ul>
)
}
</div>
);
}
}
export default App;
component.js:
import React from 'react'
class Item extends React.Component{
render(){ return(
<li onClick={this.props.onClick} style={this.props.style}>{this.props.item}
</li>
);
}}
export default Item
tasks.js :
import React from 'react'
class Tasks extends React.Component {
constructor(props) {
super(props)
this.state = {
value: '',
}
}
handleChange = e => {
this.setState({ value: e.target.value })
}
render() {
return (<div>
<textarea value={this.state.value} onChange={this.handleChange} ></textarea>
<button onClick={() => this.props.onClick(this.state.value)}>Add task</button>
</div>)
}
}
export default Tasks
I want each element to be crossed on its own when I click on it, but all the elements get crossed when I click on any one of them.
You should have some key for each object to differentiate,
addData(val) {
const tempObj = {
val: val,
crossed: false
}
this.setState({ todolist: this.state.todolist.concat(tempObj) },
() => console.log(this.state.todolist))
}
Now you will have crossed key for each object. I have not run the code, but this should work.
cross = e => {
e.crossed = !e.crossed;
}
(
<ul>
{this.state.todolist.map(e => {
return <Item
item={e}
onClick={(e) => this.cross(e)}
style={e.crossed && { textDecoration : 'line-through' }} />} // use ternary operator or this kind of && condition here
)
}
</ul>
)

React Bottleneck TextInput Inputfield

I've got a big react app (with Redux) here that has a huge bottleneck.
We have implemented a product search by using product number or product name and this search is extremely laggy.
Problem: If a user types in some characters, those characters are shown in the InputField really retarded. The UI is frozen for a couple of seconds.
In Internet Explorer 11, the search is almost unusable.
It's a Material UI TextField that filters products.
What I already did for optimization:
Replaced things like style={{
maxHeight: 230,
overflowY: 'scroll',
}} with const cssStyle={..}
Changed some critical components from React.Component to React.PureComponent
Added shouldComponentUpdate for our SearchComponent
Removed some unnecessary closure bindings
Removed some unnecessary objects
Removed all console.log()
Added debouncing for the input field (that makes it even worse)
That's how our SearchComponent looks like at the moment:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Downshift from 'downshift';
import TextField from '#material-ui/core/TextField';
import MenuItem from '#material-ui/core/MenuItem';
import Paper from '#material-ui/core/Paper';
import IconTooltip from '../helper/icon-tooltip';
import { translate } from '../../utils/translations';
const propTypes = {
values: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
legend: PropTypes.string,
helpText: PropTypes.string,
onFilter: PropTypes.func.isRequired,
selected: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
isItemAvailable: PropTypes.func,
};
const defaultProps = {
legend: '',
helpText: '',
selected: '',
isItemAvailable: () => true,
};
const mapNullToDefault = selected =>
(selected === null || selected === undefined ? '' : selected);
const mapDefaultToNull = selected => (!selected.length ? null : selected);
class AutoSuggestField extends Component {
shouldComponentUpdate(nextProps) {
return this.props.selected !== nextProps.selected;
}
getLegendNode() {
const { legend, helpText } = this.props;
return (
<legend>
{legend}{' '}
{helpText && helpText.length > 0 ? (
<IconTooltip helpText={helpText} />
) : (
''
)}
</legend>
);
}
handleEvent(event) {
const { onFilter } = this.props;
const value = mapDefaultToNull(event.target.value);
onFilter(value);
}
handleOnSelect(itemId, item) {
const { onFilter } = this.props;
if (item) {
onFilter(item.label);
}
}
render() {
const { values, selected, isItemAvailable } = this.props;
const inputValue = mapNullToDefault(selected);
const paperCSSStyle = {
maxHeight: 230,
overflowY: 'scroll',
};
return (
<div>
<div>{this.getLegendNode()}</div>
<Downshift
inputValue={inputValue}
onSelect={(itemId) => {
const item = values.find(i => i.id === itemId);
this.handleOnSelect(itemId, item);
}}
>
{/* See children-function on https://github.com/downshift-js/downshift#children-function */}
{({
isOpen,
openMenu,
highlightedIndex,
getInputProps,
getMenuProps,
getItemProps,
ref,
}) => (
<div>
<TextField
className="searchFormInputField"
InputProps={{
inputRef: ref,
...getInputProps({
onFocus: () => openMenu(),
onChange: (event) => {
this.handleEvent(event);
},
}),
}}
fullWidth
value={inputValue}
placeholder={translate('filter.autosuggest.default')}
/>
<div {...getMenuProps()}>
{isOpen && values && values.length ? (
<React.Fragment>
<Paper style={paperCSSStyle}>
{values.map((suggestion, index) => {
const isHighlighted = highlightedIndex === index;
const isSelected = false;
return (
<MenuItem
{...getItemProps({ item: suggestion.id })}
key={suggestion.id}
selected={isSelected}
title={suggestion.label}
component="div"
disabled={!isItemAvailable(suggestion)}
style={{
fontWeight: isHighlighted ? 800 : 400,
}}
>
{suggestion.label}
</MenuItem>
);
})}
</Paper>
</React.Fragment>
) : (
''
)}
</div>
</div>
)}
</Downshift>
</div>
);
}
}
AutoSuggestField.propTypes = propTypes;
AutoSuggestField.defaultProps = defaultProps;
export default AutoSuggestField;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.5.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.5.0/umd/react-dom.production.min.js"></script>
It seems, that I did not find the performance problem as it still exists. Can someone help here?

React DnD drags whole list of cards instead of single card

I am trying to use react DnD in my react Project. In my render method I define a variable named Populate like show below, which returns a list of cards like this
render() {
var isDragging = this.props.isDragging;
var connectDragSource = this.props.connectDragSource;
var Populate = this.props.mediaFiles.map((value) => {
return(
<div>
<MuiThemeProvider>
<Card style= {{marginBottom: 2, opacity: isDragging ? 0 : 1}} id={value.id} key={value.id}
onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}
//onTouchTap={() => {this.handleClick(value.id)}}
zDepth={this.state.shadow}>
<CardHeader
title={value.Episode_Name}
//subtitle={value.description}
actAsExpander={false}
showExpandableButton={false}
/>
</Card>
</MuiThemeProvider>
</div>
)
});
And my return of render method looks like this
return connectDragSource (
<div>
<MuiThemeProvider>
<div className="mediaFilesComponent2">
{Populate}
</div>
</MuiThemeProvider>
</div>
)
Problem is when I try using drag, then the whole list of cards gets selected for drag. I want all the cards having individual drag functionality.
If you want each card to have drag functionality than you'll have to wrap each card in a DragSource, and not the entire list. I would split out the Card into it's own component, wrapped in a DragSource, like this:
import React, { Component, PropTypes } from 'react';
import { ItemTypes } from './Constants';
import { DragSource } from 'react-dnd';
const CardSource = {
beginDrag: function (props) {
return {};
}
};
function collect(connect, monitor) {
return {
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging()
}
}
class CardDragContainer extends React.Component {
render() {
return this.props.connectDragSource(
<div>
<Card style= {{marginBottom: 2, opacity: this.props.isDragging ? 0 : 1}} id={value.id} key={value.id}
onMouseOver={this.props.onMouseOver}
onMouseOut={this.props.onMouseOut}
zDepth={this.props.shadow}>
<CardHeader
title={props.title}
actAsExpander={false}
showExpandableButton={false}
/>
</Card>
</div>
)
}
}
export default DragSource(ItemTypes.<Your Item Type>, CardSource, collect)(CardDragContainer);
Then you would use this DragContainer in render of the higher level component like this:
render() {
var Populate = this.props.mediaFiles.map((value) => {
return(
<div>
<MuiThemeProvider>
<CardDragContainer
value={value}
onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}
shadow={this.state.shadow}
/>
</MuiThemeProvider>
</div>
)
});
return (
<div>
<MuiThemeProvider>
<div className="mediaFilesComponent2">
{Populate}
</div>
</MuiThemeProvider>
</div>
);
}
That should give you a list of Cards, each of which will be individually draggable.

Categories

Resources