I have the following structure in my Redux state:
"equipment" : {
"l656k75ny-r7w4mq" : {
"id" : "l656k75ny-r7w4mq",
"image" : "cam.png",
"isChecked" : false,
"list" : {
"2taa652p5-mongp0" : {
"id" : "2taa652p5-mongp0",
"image" : "dst.png",
"isChecked" : false,
"name" : "Dst"
},
"y3ds1rspk-ftdg2t" : {
"id" : "y3ds1rspk-ftdg2t",
"image" : "flm.png",
"isChecked" : false,
"name" : "Flm"
}
},
"name" : "EQ 1"
},
"lpbixy0f7-bmiwzl" : {
"image" : "prm.png",
"isChecked" : false,
"list" : {
"l40snp2y6-o7gudg" : {
"image" : "tlp.png",
"isChecked" : false,
"name" : "Tlp"
},
"qmxf9bn3v-9x1nky" : {
"image" : "prm.png",
"isChecked" : false,
"name" : "Prm"
}
},
"name" : "EQ 2"
}
}
I have an Equipment component (sort of higher order component), in which I subscribe to this part of the state and I pass this its data to my CustomExpansionPanel component:
class Equipment extends Component {
//...different methods
render() {
const { data } = this.props;
let content = error ? <p>Oops, couldn't load equipment list...</p> : <Spinner />;
if ( data.length > 0 ) {
content = (
<Fragment>
<CustomExpansionPanel data={data}/>
</Fragment>
);
}
return (
<SectionContainer>
{content}
</SectionContainer>
);
}
}
const mapStateToProps = state => {
return {
data: state.common.equipment,
};
};
connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(Equipment));
In my CustomExpansionPanel I map the received data and create an expansion panel for each section, while passing this section to CustomTable component, which is displayed as ExpansionPanelDetails content.
class CustomExpansionPanel extends Component {
//... different methods
render() {
const { data } = this.props;
return (
<Fragment>
{
data.map((section, index) => {
return (
<ExpansionPanel defaultExpanded={false} key={section.id} disabled={!section.hasOwnProperty('list')} >
<ExpansionPanelSummary expandIcon={<StyledIcon classes={{root: classes.expansionIcon}} />}>
<Avatar
alt={section.name}
src={images('./' + section.image)}
classes={{root: classes.avatar}}
/>
<Typography variant='body1' classes={{root: classes.summaryText}}>{section.name}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<CustomTable data={section} imageVariant={imageVariant} parentId={section.id} />
</ExpansionPanelDetails>
</ExpansionPanel>
);
})
}
</Fragment>
);
}
}
And in my CustomTable component I map the list property of the received section and display its data. When clicking on the row I build a Form component with the appropriate fields and display it for editing the list item object:
class CustomTable extends Component {
//...local state
//...some methods
cellClickHandler = (object, index) => {
this.setState({
editedObject: object,
editedObjectIndex: index
}, () => {
this.setFormData(object);
});
};
handleSaveClicked = data => {
data.id = this.state.editedObject.id;
if(this.props.parentId) {
data.parentId = this.props.parentId;
}
if ( this.state.editedObject && this.state.editedObject.list ) data.list = this.state.editedObject.list;
this.props.onSaveItemEditClicked(this.props.updatedSection, data, this.state.editItemLevel, this.state.editedObjectIndex, () => {
this.handleDialogState();
});
};
componentWillMount() {
if ( this.props.data.list ) {
if ( Array.isArray(this.props.data.list) ) {
this.setState({rows: [...this.props.data]});
} else {
let transformedData = [];
for (let [key, value] of Object.entries(this.props.data.list)) {
let obj = value;
obj['id'] = key;
transformedData.push(obj);
}
this.setState({
rows: [...transformedData],
sortedProperties: sortProperties(Object.keys(transformedData[0]))
});
}
} else if (Array.isArray(this.props.data) && this.props.data.length) {
this.setState({
rows: [...this.props.data],
sortedProperties: sortProperties(Object.keys(this.props.data[0]))
});
}
if (this.props.parentId) {
this.setState({editItemLevel: 'inner'})
}
}
componentWillReceiveProps(nextProps, nextContext) {
if ( nextProps.data.list ) {
if ( Array.isArray(nextProps.data.list) ) {
this.setState({rows: [...nextProps.data]});
} else {
let transformedData = [];
for (let [key, value] of Object.entries(nextProps.data.list)) {
let obj = value;
obj['id'] = key;
transformedData.push(obj);
}
this.setState({
rows: [...transformedData],
sortedProperties: sortProperties(Object.keys(transformedData[0]))
});
}
} else if (Array.isArray(nextProps.data) && nextProps.data.length) {
this.setState({
rows: [...nextProps.data],
sortedProperties: sortProperties(Object.keys(nextProps.data[0]))
});
}
}
render() {
// Table, TableHead, TableRow, TableCell, TableFooter definitions...
<FormDialog
open={this.state.dialogOpen}
title={this.state.dialogTitle}
content={this.state.dialogContentText}
handleClose={this.handleDialogState}
formElements={this.state.formElements}
action={this.handleSaveClicked}
onCancelClicked={this.handleDialogState} />
}
}
const mapStateToProps = state => {
return {
updatedSection: state.control.Update.updatedSection
};
};
const mapDispatchToProps = dispatch => {
return {
onSaveItemEditClicked: (updatedSection, object, level, index, callback) => dispatch(commonActions.onSaveEdit(updatedSection, object, level, index, callback))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(CustomTable));
The form opens, I can change, for example, the name of the object and hit the save button, that triggers the onSaveItemEditClicked method, which dispatches the onSaveEdit action.
onSaveEdit = (updatedSection, object, level='root', index, callback) => {
return dispatch => {
dispatch(controlActions.onSetSnackbarMessage(SnackbarMessages.PROCESS.UPDATING + ' \'' + capitalize(object.name) + '\''));
dispatch(controlActions.DBProcessStart());
let parentId = null;
let url = '/' + updatedSection + '/' + object.id;
if (level === 'inner') {
url = '/' + updatedSection + '/' + object.parentId + '/list/' + object.id;
parentId = object.parentId;
delete object.parentId;
}
updateData(url, object)
.then(response => {
dispatch(controlActions.onSetSnackbarMessage('\'' + capitalize(object.name) + '\' ' + SnackbarMessages.SUCCESS.UPDATED_SUCCESSFULLY));
dispatch(controlActions.DBProcessSuccess());
if (parentId) {
object.parentId = parentId;
}
dispatch(this[saveEditSuccess](updatedSection, object, level, index));
if (callback != null) {
callback();
}
})
.catch(error => {
console.error('onSaveEdit error', error);
});
}
};
[saveEditSuccess] = (updatedSection, object, level='root', index) => {
return {
type: CommonCommands.UPDATE.SUCCESS,
section: updatedSection,
object: object,
level: level,
index: index
};
};
And finally that's my reducer:
const onUpdateSuccess = (state, action) => {
switch (action.level) {
case 'root':
let section = [...state[action.section]];
section = [
...section.slice(0, action.index),
action.object,
...section.slice(action.index + 1),
];
return updateObject(state, {
[action.section]: section
});
case 'inner':
console.log('inner update success', action);
section = [...state[action.section]];
let parentId = action.object.parentId;
let subsectionIndex = state[action.section].findIndex(member => member.id === parentId);
delete action.object.parentId;
let updatedObject = {...section[subsectionIndex].list[action.object.id], ...action.object};
section[subsectionIndex].list[action.object.id] = updatedObject;
return updateObject(state, {
[action.section]: [...section]
});
default:
return state;
}
};
This is the updateObject method:
const updateObject = (oldObject, updatedProperties) => {
return {
...oldObject,
...updatedProperties
};
};
Now if I inspect the state with Debug Redux tool, I can see the new name on the object, I see that the state does change. But the CustomTable component doesn't rerender, I still see the old name.
The exact same methodology works for me in every other case. If I have an array or object in a state property and I display it in the same CustomTable - I can edit it and see the results immediately (hence I have these 'root' and 'inner' separations). But not in this case.
I tried to use Object.assign({}, state, {[action.section]:section}); instead of updateObject method, but that also didn't help.
Any ideas how to make that component rerender with the changed properties?
Related
I want to add new Objects when user click on checkbox. For example , When user click on group , it will store data {permission:{group:["1","2"]}}. If I click on topgroup , it will store new objects with previous one
{permission:{group:["1","2"]},{topGroup:["1","2"]}}.
1st : The problem is that I can not merge new object with previous one . I saw only one objects each time when I click on the group or topgroup.
onChange = value => checked => {
this.setState({ checked }, () => {
this.setState(prevState => {
Object.assign(prevState.permission, { [value]: this.state.checked });
});
});
};
<CheckboxGroup
options={options}
value={checked}
onChange={this.onChange(this.props.label)}
/>
Here is my codesanbox:https://codesandbox.io/s/stackoverflow-a-60764570-3982562-v1-0qh67
It is a lot of code because I've added set and get to set and get state. Now you can store the path to the state in permissionsKey and topGroupKey. You can put get and set in a separate lib.js.
In this example Row is pretty much stateless and App holds it's state, this way App can do something with the values once the user is finished checking/unchecking what it needs.
const Checkbox = antd.Checkbox;
const CheckboxGroup = Checkbox.Group;
class Row extends React.Component {
isAllChecked = () => {
const { options, checked } = this.props;
return checked.length === options.length;
};
isIndeterminate = () => {
const { options, checked } = this.props;
return (
checked.length > 0 && checked.length < options.length
);
};
render() {
const {
options,
checked,
onChange,
onToggleAll,
stateKey,
label,
} = this.props; //all data and behaviour is passed by App
return (
<div>
<div className="site-checkbox-all-wrapper">
<Checkbox
indeterminate={this.isIndeterminate()}
onChange={e =>
onToggleAll(e.target.checked, stateKey)
}
checked={this.isAllChecked()}
>
Check all {label}
</Checkbox>
<CheckboxGroup
options={options}
value={checked}
onChange={val => {
onChange(stateKey, val);
}}
/>
</div>
</div>
);
}
}
//helper from https://gist.github.com/amsterdamharu/659bb39912096e74ba1c8c676948d5d9
const REMOVE = () => REMOVE;
const get = (object, path, defaultValue) => {
const recur = (current, path) => {
if (current === undefined) {
return defaultValue;
}
if (path.length === 0) {
return current;
}
return recur(current[path[0]], path.slice(1));
};
return recur(object, path);
};
const set = (object, path, callback) => {
const setKey = (current, key, value) => {
if (Array.isArray(current)) {
return value === REMOVE
? current.filter((_, i) => key !== i)
: current.map((c, i) => (i === key ? value : c));
}
return value === REMOVE
? Object.entries(current).reduce((result, [k, v]) => {
if (k !== key) {
result[k] = v;
}
return result;
}, {})
: { ...current, [key]: value };
};
const recur = (current, path) => {
if (path.length === 1) {
return setKey(
current,
path[0],
callback(current[path[0]])
);
}
return setKey(
current,
path[0],
recur(current[path[0]], path.slice(1))
);
};
return recur(object, path, callback);
};
class App extends React.Component {
state = {
permission: { group: [] },
topGroup: [],
some: { other: [{ nested: { state: [] } }] },
};
permissionsKey = ['permission', 'group']; //where to find permissions in state
topGroupKey = ['topGroup']; //where to find top group in state
someKey = ['some', 'other', 0, 'nested', 'state']; //where other group is in state
onChange = (key, value) => {
//use set helper to set state
this.setState(set(this.state, key, arr => value));
};
isIndeterminate = () =>
!this.isEverythingChecked() &&
[
this.permissionsKey,
this.topGroupKey,
this.someKey,
].reduce(
(result, key) =>
result || get(this.state, key).length,
false
);
toggleEveryting = e => {
const checked = e.target.checked;
this.setState(
[
this.permissionsKey,
this.topGroupKey,
this.someKey,
].reduce(
(result, key) =>
set(result, key, () =>
checked
? this.plainOptions.map(({ value }) => value)
: []
),
this.state
)
);
};
onToggleAll = (checked, key) => {
this.setState(
//use set helper to set state
set(this.state, key, () =>
checked
? this.plainOptions.map(({ value }) => value)
: []
)
);
};
isEverythingChecked = () =>
[
this.permissionsKey,
this.topGroupKey,
this.someKey,
].reduce(
(result, key) =>
result &&
get(this.state, key).length ===
this.plainOptions.length,
true
);
plainOptions = [
{ value: 1, name: 'Apple' },
{ value: 2, name: 'Pear' },
{ value: 3, name: 'Orange' },
];
render() {
return (
<React.Fragment>
<h1>App state</h1>
{JSON.stringify(this.state)}
<div>
<Checkbox
indeterminate={this.isIndeterminate()}
onChange={this.toggleEveryting}
checked={this.isEverythingChecked()}
>
Toggle everything
</Checkbox>
</div>
{[
{ label: 'group', stateKey: this.permissionsKey },
{ label: 'top', stateKey: this.topGroupKey },
{ label: 'other', stateKey: this.someKey },
].map(({ label, stateKey }) => (
<Row
key={label}
options={this.plainOptions}
// use getter to get state selected value
// for this particular group
checked={get(this.state, stateKey)}
label={label}
onChange={this.onChange} //change behaviour from App
onToggleAll={this.onToggleAll} //toggle all from App
//state key to indicate what state needs to change
// used in setState in App and passed to set helper
stateKey={stateKey}
/>
))}
</React.Fragment>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<link href="https://cdnjs.cloudflare.com/ajax/libs/antd/4.0.3/antd.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/antd/4.0.3/antd.js"></script>
<div id="root"></div>
I rewrite all the handlers.
The bug in your code is located on the usage of antd Checkbox.Group component with map as a child component, perhaps we need some key to distinguish each of the Row. Simply put them in one component works without that strange state update.
As the demand during communication, the total button is also added.
And, we don't need many states, keep the single-source data is always the best practice.
import React from "react";
import ReactDOM from "react-dom";
import "antd/dist/antd.css";
import "./index.css";
import { Checkbox } from "antd";
const group = ["group", "top"];
const groupItems = ["Apple", "Pear", "Orange"];
const CheckboxGroup = Checkbox.Group;
class App extends React.Component {
constructor() {
super();
this.state = {
permission: {}
};
}
UNSAFE_componentWillMount() {
this.setDefault(false);
}
setDefault = fill => {
const temp = {};
group.forEach(x => (temp[x] = fill ? groupItems : []));
this.setState({ permission: temp });
};
checkLength = () => {
const { permission } = this.state;
let sum = 0;
Object.keys(permission).forEach(x => (sum += permission[x].length));
return sum;
};
/**
* For total
*/
isTotalIndeterminate = () => {
const len = this.checkLength();
return len > 0 && len < groupItems.length * group.length;
};
onCheckTotalChange = () => e => {
this.setDefault(e.target.checked);
};
isTotalChecked = () => {
return this.checkLength() === groupItems.length * group.length;
};
/**
* For each group
*/
isIndeterminate = label => {
const { permission } = this.state;
return (
permission[label].length > 0 &&
permission[label].length < groupItems.length
);
};
onCheckAllChange = label => e => {
const { permission } = this.state;
const list = e.target.checked ? groupItems : [];
this.setState({ permission: { ...permission, [label]: list } });
};
isAllChecked = label => {
const { permission } = this.state;
return !groupItems.some(x => !permission[label].includes(x));
};
/**
* For each item
*/
isChecked = label => {
const { permission } = this.state;
return permission[label];
};
onChange = label => e => {
const { permission } = this.state;
this.setState({ permission: { ...permission, [label]: e } });
};
render() {
const { permission } = this.state;
console.log(permission);
return (
<React.Fragment>
<Checkbox
indeterminate={this.isTotalIndeterminate()}
onChange={this.onCheckTotalChange()}
checked={this.isTotalChecked()}
>
Check all
</Checkbox>
{group.map(label => (
<div key={label}>
<div className="site-checkbox-all-wrapper">
<Checkbox
indeterminate={this.isIndeterminate(label)}
onChange={this.onCheckAllChange(label)}
checked={this.isAllChecked(label)}
>
Check all
</Checkbox>
<CheckboxGroup
options={groupItems}
value={this.isChecked(label)}
onChange={this.onChange(label)}
/>
</div>
</div>
))}
</React.Fragment>
);
}
}
ReactDOM.render(<App />, document.getElementById("container"));
Try it online:
Please try this,
onChange = value => checked => {
this.setState({ checked }, () => {
this.setState(prevState => {
permission : { ...prevSatate.permission , { [value]: this.state.checked }}
});
});
};
by using spread operator you can stop mutating the object. same way you can also use object.assign like this.
this.setState(prevState => {
permission : Object.assign({} , prevState.permission, { [value]: this.state.checked });
});
And also i would suggest not to call setState in a callback. If you want to access the current state you can simply use the current checked value which you are getting in the function itself.
so your function becomes ,
onChange = value => checked => {
this.setState({ checked });
this.setState(prevState => {return { permission : { ...prevSatate.permission, { [value]: checked }}
}});
};
Try the following
//Inside constructor do the following
this.state = {checkState:[]}
this.setChecked = this.setChecked.bind(this);
//this.setChecked2 = this.setChecked2.bind(this);
//Outside constructor but before render()
setChecked(e){
this.setState({
checkState : this.state.checkState.concat([{checked: e.target.id + '=>' + e.target.value}])
//Id is the id property for a specific(target) field
});
}
//Finally attack the method above.i.e. this.setChecked to a form input.
Hope it will address your issues
I created a suggestions search and its built to break up the fetch based on the current page. The state is console.loged correctly, but the render is one page click event behind. This is obviously not the behavior we want. It seems like the state is being updated fine. I have tried to refactor the code difference ways, and even tried this.forceUpdate()
Here is the code
SearchOrderBar.js
import React, { Component } from "react";
import {Input, Label, Table, Icon, Header, Menu} from 'semantic-ui-react';
import "./SearchOrderBar.css";
// import { resolve } from "dns";
// import PropTypes from 'prop-types';
import Pagination from '../Search/Pagination';
class SearchOrderBar extends Component {
constructor(props) {
super(props);
this.text = "";
this.state = {
suggestions: [],
addToQuery: false,
Query: [],
pagesNeeded: 0,
page: 1
};
let searchTerm = null;
const {pageLimit = null, keyTimer = null, } = props;
this.pageLimit = typeof pageLimit === 'number' ? pageLimit : 10;
this.handlePageClick = this.handlePageClick.bind(this);
this.fetchCallBack = this.fetchCallBack.bind(this);
// this.addToQuery = this.addToQuery.bind(this);
this.keyUpHandler = this.keyUpHandler.bind(this);
this.keyDownHandler = this.keyDownHandler.bind(this);
}
handlePageClick(page){
this.forceUpdate();
this.setState({
page: page
})
this.fetchCallBack();
}
//This fetch should be called in a dynamic switch case
fetchCallBack() {
let y = this.pageLimit;
let x = this.state.page > 1 ? (this.pageLimit*this.state.page) - this.pageLimit : 0;
// Return a promise
return new Promise((resolve, reject) => {
let searchTerm = this.searchTerm;
return fetch(`http://localhost:5000/api/searchorders/${searchTerm}/${x}/${y}`)
.then(res => {
if (!res.ok) {
throw res;
}
// Convert serialized response into json
return res.json()
}).then(data => {
//Use data
let searchTerm = data.map(data => {
let rData = {};
rData = data;
return rData;
})
this.item = searchTerm;
//console.log('here from callback')
this.setState({
suggestions: []
})
return searchTerm;
}).then( data => {
// console.log(this.totalRecords)sd
//console.log(data)
if (searchTerm.length === 0) {
this.setState({
suggestions: [],
rangeCount_URL: `http://localhost:5000/api/searchorderscount/${searchTerm}`
});
} else {
const suggestions = data.filter(function(v){
if(Object.values(v).includes(searchTerm.toLowerCase()) !== -1 || Object.values(v).includes(searchTerm.toUpperCase()) !== -1){
return v
}
})
console.log(suggestions)
this.text = searchTerm;
this.setState({ suggestions: suggestions.sort()});
}
})
})
}
pageCountCallBack(){
return new Promise((resolve, reject) => {
let searchTerm = this.searchTerm;
return fetch(`http://localhost:5000/api/searchorderscount/${searchTerm}/`)
.then(res => {
if (!res.ok) {
throw res;
}
// Convert serialized response into json
return res.json()
}).then(data => {
//Use data
let searchTerm = data.map(data => {
let rData = {};
rData = data;
return rData;
})
this.item = searchTerm;
// console.log('here from Page Count callback')
this.renderSuggestions();
resolve(searchTerm)
})
})
}
keyUpHandler = (e) => {
if(e.target.value.length >= 3){
this.keyTimer = setTimeout(this.countFetch(e), 1500);
} else {
this.setState(() => {
return {
suggestions : [],
pagesNeeded : 0
}
})
clearTimeout(this.keyTimer);
}
}
keyDownHandler = (e) => {
clearTimeout(this.keyTimer);
}
//Any time text is changed in the text field
countFetch = (e) => {
const value = e.target.value;
this.searchTerm = value;
this.pageCountCallBack().then(data => {
const totalRecords = data[0].rows;
this.setState(() => {
return {pagesNeeded : Math.ceil(totalRecords / this.pageLimit)}
})
//console.log("total" + totalRecords);
//console.log("page limit"+this.pageLimit);
//console.log("Needed" + this.state.pagesNeeded );
})
this.fetchCallBack();
}
renderSuggestions() {
//const { suggestions } = this.state;
const tableStyle = {
'tableLayout': 'fixed',
'overflowWrap': 'break-word'
}
return (
<Table style={tableStyle} celled>
{this.state.suggestions.length === 0 ?
(<Table.Body>
<Table.Cell colSpan="7">
<div className="ui fluid warning icon message">
<Icon name="exclamation triangle" size="huge" color="orange"/>
<div className="content">
<Header>No Records Found</Header>
<p>Try Seaching by one of the following:</p>
<ul>
<dt>Name</dt>
<dt>Order Number</dt>
<dt>Address (Shipping or Billing )</dt>
<dt>Phone Number</dt>
<dt>Email</dt>
</ul>
</div>
</div>
</Table.Cell>
</Table.Body>)
: (
<>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Order#</Table.HeaderCell>
<Table.HeaderCell>Billing Address</Table.HeaderCell>
<Table.HeaderCell>Shipping Address</Table.HeaderCell>
<Table.HeaderCell>Email</Table.HeaderCell>
<Table.HeaderCell>Phone Number</Table.HeaderCell>
<Table.HeaderCell>Sales Channel</Table.HeaderCell>
<Table.HeaderCell>Order Date</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{this.state.suggestions.map((item, index) => (
<Table.Row className="hoverRow">
<Table.Cell key={index} onClick={() => this.addToQuery(item)}>
{item.customerPO}
</Table.Cell>
<Table.Cell>
{item.billToAddress}
</Table.Cell>
<Table.Cell>{item.shipToAddress}</Table.Cell>
<Table.Cell>{item.email}</Table.Cell>
<Table.Cell>{item.phone}</Table.Cell>
<Table.Cell>{item.customerContact}</Table.Cell>
<Table.Cell>{item.dateCreated}</Table.Cell>
</Table.Row>
))}
</Table.Body>
</>
)
}
<Pagination key={this.state.pagesNeeded} tableCols="7" pagesNeeded={this.state.pagesNeeded} btnLimit={5} pageClick={this.handlePageClick} currPage={this.state.page} pageLimit={this.pageLimit}/>
</Table>
);
}
handleIconClick(){
console.log('icon clicked ' + this.state.Query )
}
render() {
const {text} = this.state
//console.log(this.state)
return (
<>
<div className="App-Component">
<div className="App-Search">
<Input icon={{ name: 'search', circular: true, link: true, onClick: () => this.handleIconClick() }} placeholder="Search" value={text} type="text" onKeyUp={this.keyUpHandler} onKeyDown={this.keyDownHandler} className="App-Search"/>
{this.renderSuggestions()}
</div>
</div>
</>
);
}
}
export default SearchOrderBar;
Here is the pagination but I don't think this matters as much for the solution. It is relevant for the page button click.
import React, {Component} from 'react';
import {Input, Label, Table, Icon, Header, Menu} from 'semantic-ui-react'
/**
* Helper Method for creating a range of Numbers
* Range )( )
*/
const range = (from, to, step = 1) => {
let i = from;
const range = [];
while (i<=to) {
range.push(i);
i+=step;
}
}
export default class Pagination extends Component {
constructor(props){
super(props)
const { totalRecords = null, pageNeighbours = 0, rangeCount_URL = this.props.rangeCount_URL, pageArray = [] } = props;
this.pageArray = typeof pageArray === 'array' ? pageArray : [];
}
renderPagination = () => {
//console.log("hello from pagination");
let n = this.props.pagesNeeded;
let pArray = [];
let page = this.props.currPage;
//console.log(n)
if (page > 1){
pArray.push(<Menu.Item as='a' icon onClick={() => this.props.pageClick(page-1)}>
<Icon name='chevron left' />
</Menu.Item>)
}
for(let i = (page >1 ? page-1: page); pArray.length < (page > this.props.btnLimit ? this.props.btnLimit+1 : this.props.btnLimit); i++){
//console.log(i);
pArray.push(<Menu.Item index={i} className={i == page ? 'active' : ''} onClick={() => this.props.pageClick(i)} as='a'>{i}</Menu.Item>)
}
if (page < n){
pArray.push(<Menu.Item as='a' icon onClick={() => this.props.pageClick(page+1)}>
<Icon name='chevron right' />
</Menu.Item>)
}
this.pageArray = pArray;
return pArray;
}
render(){
const pageCount = (() => {
const totalRecords = this.totalRecords;
if(totalRecords > 0){
return (this.totalPages = Math.ceil(this.totalRecords / this.props.pageLimit))
}
})();
//console.log(this.pageArray);
return(
<Table.Footer>
{ this.props.pagesNeeded > 1 &&
<Table.Row>
<Table.HeaderCell colSpan={this.props.tableCols}>
<Menu floated='right' pagination>
{this.renderPagination()}
</Menu>
</Table.HeaderCell>
</Table.Row>
}
</Table.Footer>
)
}
}
setState is batched and invoked asynchronously, meaning when you call to this.setState({page}) then read this.state.page in fetchCallBack you probably get the "old" page and not the new page.
Either pass the page directly to fetchCallBack
this.fetchCallBack(page)
And read the page from it and not directly from the state
Or call it as the second argument of setState which is a callback that react will invoke right after the state has been updated.
this.setState({ page }, this.fetchCallBack);
At the point fetchCallBack is called, this.state.page is not updated yet because setState is called asynchronously, that's why it's using the old value. Try this:
handlePageClick(page) {
this.setState({ page }, this.fetchCallBack);
}
The callback syntax allows you to run the function in the next iteration.
I am working with React and Vanilla JS. Demo
Main Code
import React from "react";
import { render } from "react-dom";
import { makeData } from "./Utils";
import Select from "react-select";
import "react-select/dist/react-select.css";
// Import React Table
import ReactTable from "react-table";
import "react-table/react-table.css";
import jsondata from "./sample";
class App extends React.Component {
constructor() {
super();
this.state = {
// data: makeData(),
data: jsondata,
filtered: [],
select2: null,
select3: null
};
this.uniqueOptions = this.uniqueOptions.bind(this);
}
onFilteredChangeCustom(value, accessor) {
console.log("The value is " + value);
let filtered = this.state.filtered;
console.log("the filtered items" + JSON.stringify(this.state.filtered));
let insertNewFilter = 1;
if (filtered.length) {
console.log("filtered.length " + filtered.length);
filtered.forEach((filter, i) => {
if (filter["id"] === accessor) {
if (value === "" || !value.length) filtered.splice(i, 1);
else filter["value"] = value;
insertNewFilter = 0;
}
});
}
if (insertNewFilter) {
filtered.push({ id: accessor, value: value });
}
this.setState({ filtered: filtered });
console.log("The filtered data is " + JSON.stringify(this.state.filtered));
}
uniqueOptions = (objectsArray, objectKey) => {
var a = objectsArray.map((o, i) => {
return o[objectKey];
});
return a.filter(function(i, index) {
return a.indexOf(i) >= index;
});
};
render() {
const { data } = this.state;
let first_names = null;
let last_names = null;
let first_names_label = null;
let last_names_label = null;
if (this.state.filtered.length == 1) {
if (this.state.filtered[0].id == "firstName") {
first_names_label = "First Name:";
} else {
first_names_label = "Last Name:";
}
first_names = this.state.filtered[0].value.map(name => <p>{name}</p>);
} else if (this.state.filtered.length == 2) {
first_names_label = "First Name:";
first_names = this.state.filtered[0].value.map(name => <p>{name}</p>);
last_names_label = "Last Name:";
last_names = this.state.filtered[1].value.map(name => <p>{name}</p>);
}
return (
<div>
<pre>
{first_names_label}
{first_names}
{last_names_label}
{last_names}
</pre>
<br />
<br />
Extern Select2 :{" "}
<Select
style={{ width: "50%", marginBottom: "20px" }}
onChange={entry => {
this.setState({ select2: entry });
this.onFilteredChangeCustom(
entry.map(o => {
return o.value;
}),
"firstName"
);
}}
value={this.state.select2}
multi={true}
options={this.uniqueOptions(this.state.data, "firstName").map(
(name, i) => {
return { id: i, value: name, label: name };
}
)}
/>
Extern Select3 :{" "}
<Select
style={{ width: "50%", marginBottom: "20px" }}
onChange={entry => {
this.setState({ select3: entry });
this.onFilteredChangeCustom(
entry.map(o => {
return o.value;
}),
"lastName"
);
}}
value={this.state.select3}
multi={true}
options={this.uniqueOptions(this.state.data, "lastName").map(
(name, i) => {
return { id: i, value: name, label: name };
}
)}
/>
<ReactTable
data={data}
filtered={this.state.filtered}
onFilteredChange={(filtered, column, value) => {
this.onFilteredChangeCustom(value, column.id || column.accessor);
}}
defaultFilterMethod={(filter, row, column) => {
const id = filter.pivotId || filter.id;
if (typeof filter.value === "object") {
return row[id] !== undefined
? filter.value.indexOf(row[id]) > -1
: true;
} else {
return row[id] !== undefined
? String(row[id]).indexOf(filter.value) > -1
: true;
}
}}
columns={[
{
Header: "Name",
columns: [
{
Header: "First Name",
accessor: "firstName"
},
{
Header: "Last Name",
id: "lastName",
accessor: d => d.lastName
}
]
},
{
Header: "Info",
columns: [
{
Header: "Age",
accessor: "age"
}
]
}
]}
defaultPageSize={10}
className="-striped -highlight"
/>
<br />
</div>
);
}
}
render(<App />, document.getElementById("root"));
I have a filtered state variable (this.state.filtered) which has all the filtered items from dropdown.
I want to show the filtered state variable like this
<div>
<div>id: val0,val1 (and a cross button to remove this filter)</div>
<div>id: val0,val1 (and a cross button to remove this filter)</div> .... so on
</div>
I have been able to do this for 2 variables. But in future I will have 10 to 15 dropdowns. That is why I want to display using a loop. How to do this?
The code I have in my demo do not have loop and I want to make it a generic code which can display all key value pairs of this.state.filtered. Without loop I will not be able to use this functionality when I have a huge number of dropdowns. Also the values should be comma separated. I also do not want to make the render function big and dirty. I want to call a function basically which will show the keys and values
This is my Demo Project - Demo. I am working with React Table. I am trying to create a custom box which can filter only the first column - firstName. Based on the first column search the corresponding rows of other columns will be displayed.
Filter work perfectly fine when we are adding characters, but once we start deleting characters filtering does not happen Demo
This is main Code (Parent Page)
import React from "react";
import { render } from "react-dom";
import TypeChecker from "typeco";
// import matchSorter from "match-sorter";
import SearchField from "./SearchField";
import "./App.css";
import Child from "./Child/Child";
import jsondata from "./sample";
// Import React Table
import ReactTable from "react-table";
import "react-table/react-table.css";
class App extends React.Component {
constructor() {
super();
this.state = {
// data: makeData(),
data: jsondata,
dataDefault: jsondata,
basicExampleList: jsondata,
filtered: [],
select2: null,
select3: null,
childOpen: false
};
this.openChild = this.openChild.bind(this);
this.applyFilter = this.applyFilter.bind(this);
this.showDefaultView = this.showDefaultView.bind(this);
this.onBasicExampleChange = this.onBasicExampleChange.bind(this);
this.onEnterExample = this.onEnterExample.bind(this);
this.onSearchClickExample = this.onSearchClickExample.bind(this);
this.getMatchedList = this.getMatchedList.bind(this);
}
getMatchedList(searchText) {
console.log("getMatchedList entered");
console.log("the search text is " + searchText);
const exampleList = this.state.basicExampleList;
if (TypeChecker.isEmpty(searchText)) {
console.log(
"if block of getMatchedList - > TypeChecker.isEmpty(searchText) " +
TypeChecker.isEmpty(searchText)
);
this.setState({ basicExampleList: exampleList });
return this.state.basicExampleList;
}
if (!TypeChecker.isEmpty(searchText)) {
console.log("if block of getMatchedList is not entered ");
return exampleList.filter(
item =>
item.firstName.includes(searchText) ||
item.firstName.includes(searchText)
);
}
}
onBasicExampleChange(value) {
console.log("onchange entered");
console.log(" the value is " + value);
this.setState({
basicExampleList: this.getMatchedList(value)
});
}
onEnterExample(value) {
this.setState({
basicExampleList: this.getMatchedList(value)
});
}
onSearchClickExample(value) {
this.setState({
basicExampleList: this.getMatchedList(value)
});
}
applyFilter(filtered) {
console.log("Entering Apply Filter Function of Parent");
console.log("The filtered data in parent " + JSON.stringify(filtered));
const currentStateChild = this.state.childOpen;
this.setState(
{
childOpen: !currentStateChild
},
() => {
console.log("Child is opened " + this.state.childOpen);
}
);
const filterArray = filtered;
const apidata = this.state.data;
let filteredData = apidata.filter((
item // filter jsondata
) =>
filterArray.every((
f // so every member of filter array
) => f.value.includes(item[f.id]))
); // has a corresponding item[id] in value
console.log("The filtered rows are " + JSON.stringify(filteredData));
this.setState(
{
data: filteredData
},
() => {
console.log("Manipulated rows " + this.state.data);
}
);
}
openChild() {
const currentStateChild = this.state.childOpen;
this.setState(
{
childOpen: !currentStateChild
},
() => {
console.log("Child is opened " + this.state.childOpen);
}
);
}
showDefaultView() {
const defaultDataParent = this.state.dataDefault;
this.setState(
{
data: defaultDataParent
},
() => {
console.log("Default rows " + this.state.data);
}
);
}
render() {
const { data } = this.state;
return (
<div>
<div className="clickMeToOpenChild" onClick={this.openChild}>
{" "}
Click Me to Open Child
</div>
<div className="clickMeToGetDefaultView" onClick={this.showDefaultView}>
{" "}
Click Me to show Default View
</div>
<br />
<SearchField
placeholder="Search item"
onChange={this.onBasicExampleChange}
onEnter={this.onEnterExample}
onSearchClick={this.onSearchClickExample}
/>
<br />
<br />
<ReactTable
data={this.state.basicExampleList}
filterable
filtered={this.state.filtered}
onFilteredChange={(filtered, column, value) => {
this.onFilteredChangeCustom(value, column.id || column.accessor);
}}
defaultFilterMethod={(filter, row, column) => {
const id = filter.pivotId || filter.id;
if (typeof filter.value === "object") {
return row[id] !== undefined
? filter.value.indexOf(row[id]) > -1
: true;
} else {
return row[id] !== undefined
? String(row[id]).indexOf(filter.value) > -1
: true;
}
}}
columns={[
{
Header: "Name",
columns: [
{
Header: "First Name",
accessor: "firstName"
},
{
Header: "Last Name",
id: "lastName",
accessor: d => d.lastName
}
]
},
{
Header: "Info",
columns: [
{
Header: "Age",
accessor: "age"
}
]
}
]}
defaultPageSize={10}
className="-striped -highlight"
/>
{this.state.childOpen && (
<Child data={data} applyFilter={this.applyFilter} />
)}
<br />
</div>
);
}
}
render(<App />, document.getElementById("root"));
Kindly help me to get rid of this issue.
The issue was that you were overwriting the real data, so it was ending up empty if you did a search with no results.
Fixed code here.
I am sorry If my solution does not work, for me it was duplicate data was filtering. So if you can make sure that the array for filter does not have duplicate data
I have implemented a MobX store in my React-Native app to keep track if a user is being followed or unfollowed. The follow/unfollow is registering, but the MobX store is not being updated. I am trying to update it directly with the this.follows.items[index] = { ...user, isFollowing: !user.isFollowing } but for some reason the store does not trigger an update.
Here is the View Component
#observer
class FollowsListView extends Component<Props> {
follows =
this.props.followType === 'followers'
? followsStore.getFollowersListLoader(this.props.userId)
: followsStore.getFollowingListLoader(this.props.userId);
componentDidMount = () => {
this.follows.lazyLoad();
};
render() {
return (
<>
<AppHeader
title={
this.props.followType === 'followers' ? 'FOLLOWERS' : 'FOLLOWING'
}
/>
<FlatList
contentContainerStyle={{ padding: 15 }}
data={this.follows.items}
keyExtractor={this.getId}
onEndReached={this.follows.loadMore}
onEndReachedThreshold={0.2}
onRefresh={this.follows.loadFromStart}
refreshing={this.follows.isLoading}
renderItem={this.renderFollows}
/>
</>
);
}
private getId = (user: { id: string }) => user.id;
renderUserActionButton(user: UserContainer) {
console.log(user);
return (
user.id !== _SessionManager.id && (
<TouchableOpacity
onPress={() => this.openActionMenu(user.following || user.owner)}
>
<Image source={Images.moreDots} />
</TouchableOpacity>
)
);
}
openActionMenu(user: User) {
const name = user.name || user.username;
const followOption = { name: 'follow', title: `Follow #${name}` };
const unfollowOption = { name: 'unfollow', title: `Unfollow #${name}` };
const options = {
customButtons: [user.isFollowing ? unfollowOption : followOption],
title: null,
takePhotoButtonTitle: null,
chooseFromLibraryButtonTitle: null,
};
ImagePicker.showImagePicker(options, ({ customButton }) => {
if (customButton === 'follow') {
this.props.changeIsFollowingUser(user.id, false);
}
if (customButton === 'unfollow') {
this.props.changeIsFollowingUser(user.id, true);
}
const index = this.follows.items.findIndex((user) => user.id);
this.follows.items[index] = { ...user, isFollowing: !user.isFollowing };
});
}
private renderFollows: ListRenderItem<UserContainer> = ({ item: user }) => {
const userId = user.following ? user.following.id : user.id;
return (
<UserRow
actionContent={this.renderUserActionButton(user)}
onPress={() => this.props.navigateTo('ProfilePublic', { userId })}
user={user.following || user.owner}
/>
);
};
}
const mapDispatchToProps = (dispatch: Function): MappedDispatch =>
bindActionCreators(
{
changeIsFollowingUser,
navigateTo,
},
dispatch
);
export default connect(
null,
mapDispatchToProps
)(FollowsListView);
Here is the Follows Store
import ListLoader from 'Network/ListLoader';
import { Follows } from 'Follows/Types';
import _API from 'Network/API';
class FollowsStore {
followers = new Map<string, Follows>();
followersList = new Map<string, ListLoader<Follows>>();
following = new Map<string, Follows>();
followingList = new Map<string, ListLoader<Follows>>();
getFollowersListLoader(userId: string) {
const list = this.followersList.get(userId);
if (list) return list;
const newList = new ListLoader<Follows>({
fetchData: async (params) => {
const url = `users/${userId}/followers`;
const response = await _API.get(url, { params });
return response.data;
},
onLoad: (data) => {
for (const user of data.items) {
this.followers.set(user.id, user);
}
},
});
this.followersList.set(userId, newList);
return newList;
}
getFollowingListLoader(userId: string) {
const list = this.followingList.get(userId);
if (list) return list;
const newList = new ListLoader<Follows>({
fetchData: async (params) => {
const url = `users/${userId}/following`;
const response = await _API.get(url, { params });
return response.data;
},
onLoad: (data) => {
for (const user of data.items) {
this.following.set(user.id, user);
}
},
});
this.followingList.set(userId, newList);
console.log(newList);
return newList;
}
}
const followsStore = new FollowsStore();
export default followsStore;
In MobX in order to change the state you would need to use an action. Set/decorate your openActionMenu as an action or extract the state changing code to another function which you decorate as action to be cleaner.