Apologies, I probably have all of my terminology wrong here, but I am chunking up a React app into modules and am trying to combine two modules which do the same thing and come into an issue.
Specifically, I am trying to sum up totals of both income and expenditure, and have been calling two different react classes via:
<ExpenditureTotal type={this.state.cashbook.expenditure} addTotal={this.addTotal} /> and another <IncomeTotal... which does the same).
The Class is as follows (expenditure shown, but income is the same:
/* Expenditure Running Total */
var ExpenditureTotal = React.createClass({
render: function() {
var expIds = Object.keys(this.props.expenditure);
var total = expIds.reduce((prevTotal, key) => {
var expenditure = this.props.expenditure[key];
return prevTotal + (parseFloat(expenditure.amount));
}, 0);
this.props.addTotal('expenditure', total);
return(
<h2 className="total">Total: {h.formatPrice(total)}</h2>
);
}
});
I wanted to combine the two by making it more generic, so I made the following:
/* Cashflow Running Total */
var TotalCashflow = React.createClass({
render: function() {
var expIds = Object.keys(this.props.type);
var total = expIds.reduce((prevTotal, key) => {
var type = this.props.type[key];
return prevTotal + (parseFloat(type.amount));
}, 0);
this.props.addTotal('type', total);
return(
<h2 className="total">Total: {h.formatPrice(total)}</h2>
);
}
});
The only reason it doesn't work properly is when I add the summed total to the relevant state object which for this is income or expenditure in totals: (totals: { income: val, expenditure: val}).
Where I was previously explicitly specifying 'expenditure' or 'income' in the module, (seen above in ExpenditureTotal as this.props.addTotal('expenditure', total);, I am not sure in React how to pass the total value to the relevant place - it needs to read either 'expenditure' or 'income' to populate the correct state totals key.
Apologies if this is a bit garbled, struggling to explain it clearly.
Thank you :)
Update: App component in full:
import React from 'react';
// Firebase
import Rebase from 're-base';
var base = Rebase.createClass("FBURL")
import h from '../helpers';
import Expenditure from './Expenditure';
import Income from './Income';
import TotalCashflow from './TotalCashflow';
import AddForm from './AddForm';
import Available from './Available';
var App = React.createClass({
// Part of React lifecycle
getInitialState: function() {
return {
cashbook: {
expenditure: {},
income: {}
},
totals: {},
available: {}
}
},
componentDidMount: function() {
// Two way data binding
base.syncState('cashbook', {
context: this,
state: 'cashbook'
});
},
addExpenditure: function(expenditure) {
var timestamp = (new Date()).getTime();
// update state object
this.state.cashbook.expenditure['expenditure-' + timestamp] = expenditure;
// set state
this.setState({
cashbook: { expenditure: this.state.cashbook.expenditure }
});
},
addIncome: function(income) {
var timestamp = (new Date()).getTime();
// update state object
this.state.cashbook.income['income-' + timestamp] = income;
// set state
this.setState({
cashbook: { income: this.state.cashbook.income }
});
},
removeExpenditure: function(key) {
this.state.cashbook.expenditure[key] = null;
this.setState({
cashbook: { expenditure: this.state.cashbook.expenditure }
});
},
renderExpenditure: function(key) {
var details = this.state.cashbook.expenditure[key];
return(
<tr className="item" key={key}>
<td><strong>{details.name}</strong></td>
<td><strong>{h.formatPrice(details.amount)}</strong></td>
<td>{details.category}</td>
<td>{details.type}</td>
<td>{details.date}</td>
<td><button className="remove-item" onClick={this.removeExpenditure.bind(null, key)}>Remove</button></td>
</tr>
);
},
removeIncome: function(key) {
this.state.cashbook.income[key] = null;
this.setState({
cashbook: { income: this.state.cashbook.income }
});
},
renderIncome: function(key) {
var details = this.state.cashbook.income[key];
return(
<tr className="item" key={key}>
<td><strong>{details.name}</strong></td>
<td><strong>{h.formatPrice(details.amount)}</strong></td>
<td>{details.category}</td>
<td>{details.type}</td>
<td>{details.date}</td>
<td><button className="remove-item" onClick={this.removeIncome.bind(null, key)}>Remove</button></td>
</tr>
);
},
listInventory: function() {
return
},
addTotal: function(type, total) {
this.state.totals[type] = total;
},
render: function() {
return(
<div className="cashbook">
<Expenditure
cashbook={this.state.cashbook.expenditure}
renderExpenditure={this.renderExpenditure} />
<TotalCashflow
type={this.state.cashbook.expenditure}
addTotal={this.addTotal}
identifier='expenditure'
/>
<AddForm addCashflow={this.addExpenditure} />
<Income
cashbook={this.state.cashbook.income}
renderIncome={this.renderIncome} />
<TotalCashflow
type={this.state.cashbook.income}
addTotal={this.addTotal}
identifier='income'
/>
<AddForm addCashflow={this.addIncome} />
<Available totals={this.state.totals} />
</div>
);
}
});
export default App;
If I got you correctly, you want to have a component, that renders the totals of 'expenditure' and/or 'income'. Notice, you should not modify the state within the render function, as these will trigger over and over again, with each state change it is invoking itself.
Instead, try to compute the necessary data from within the parent components componentDidMount/componentDidUpdate, so that the TotalCashflow can be served the data, without it needing to do any more logic on it.
This might be the parent component:
/* Cashflow-Wrapper */
var TotalCashflowWrapper = React.createClass({
componentDidMount: function(){
this.setState({
income: this.addTotal(/*x1*/)
expenditure: this.addTotal(/*x1*/)
});
/*x1: hand over the relevant 'type' data that you use in the child components in your example*/
}
addTotal: function(type) {
var expIds = Object.keys(type);
return expIds.reduce((prevTotal, key) => {
var x = type[key];
return prevTotal + (parseFloat(x.amount));
}, 0);
}
render: function() {
return(
<div>
<TotalCashflow total={this.state.expenditure}/>
<TotalCashflow total={this.state.income}/>
</div>
);
}
});
This would be your total component:
/* Cashflow Running Total */
var TotalCashflow = React.createClass({
render: function() {
return(
<h2 className="total">Total: {h.formatPrice(this.props.total)}</h2>
);
}
});
Edit
There are a few issues with your App component, but first of all, here is how you could redesign/simplify it. You should also move the markup from renderExpenditure and renderIncome to your Income/Expenditure components. I've been using ES6, as I've noticed you're already using arrow functions.
var App = React.createClass({
getInitialState: function () {
return {
cashbook: {
expenditure: {},
income: {}
},
totals: {},
available: {}
}
},
componentDidMount: function () {
// Two way data binding
base.syncState('cashbook', {
context: this,
state: 'cashbook'
}).then(()=> {
// after syncing cashbook, calculate totals
this.setState({
totals: {
income: this.getTotal(this.state.cashbook.income),
expenditure: this.getTotal(this.state.cashbook.expenditure),
}
});
});
},
// Get total of obj income/expenditure
getTotal: function (obj) {
var expIds = Object.keys(obj);
return expIds.reduce((prevTotal, key) => {
var type = obj[key];
return prevTotal + (parseFloat(type.amount));
}, 0);
},
addCashflow: function (identifier, amount) {
var timestamp = (new Date()).getTime();
// clone cashbook and clone cashbook[identifier] and set cashbook[identifier][identifier + '-' + timestamp] to amount
var cashbook = {
...this.state.cashbook,
[identifier]: {
...this.state.cashbook[identifier],
[identifier + '-' + timestamp]: amount
}
};
// set state
this.setState({
cashbook: cashbook,
// Update totals
totals: {
...this.state.totals,
[identifier]: this.getTotal(cashbook[identifier])
}
});
},
removeCashflow: function (identifier, key) {
// clone cashbook and clone cashbook[identifier]
var cashbook = {...this.state.cashbook, [identifier]: {...this.state.cashbook[identifier]}};
delete cashbook[identifier][key];
this.setState({
cashbook: cashbook,
totals: {
...this.state.totals,
// Update totals
[identifier]: this.getTotal(cashbook[identifier])
}
});
},
render: function () {
return (
<div className="cashbook">
<Expenditure cashbook={this.state.cashbook.expenditure} removeCashflow={(key)=>this.removeCashflow('expenditure', key)}/>
<TotalCashflow total={this.state.cashbook.expenditure} identifier='expenditure'/>
{/*or drop TotalCashflow and do <h2 className="total">Total: {h.formatPrice(this.state.totals.expandature)}</h2>*/}
<AddForm addCashflow={(amount)=>this.addCashflow('expenditure', amount)}/>
<Income cashbook={this.state.cashbook.income} removeCashflow={(key)=>this.removeCashflow('income', key)}/>
<TotalCashflow type={this.state.cashbook.income} identifier='income' />
<AddForm addCashflow={(amount)=>this.addCashflow('income', amount)}/>
<Available totals={this.state.totals}/>
</div>
);
}
});
export default App;
Issues with your current App component
Issue: You are not removing the key, you're just setting it to null; this could lead to exceptions when iterating Object.keys() and you're expecting the values to be numbers, like when calculating the totals
removeIncome: function(key) {
// this.state.cashbook.income[key] = null;
delete this.state.cashbook.income[key]
this.setState({
cashbook: { income: this.state.cashbook.income }
});
},
Bad design: You're defining the markup of a child component within your parent component, although it seems unnecessary
renderExpenditure: function(key) {
var details = this.state.cashbook.expenditure[key];
return(
<tr className="item" key={key}>
<td><strong>{details.name}</strong></td>
<td><strong>{h.formatPrice(details.amount)}</strong></td>
<td>{details.category}</td>
<td>{details.type}</td>
<td>{details.date}</td>
<td><button className="remove-item" onClick={this.removeExpenditure.bind(null, key)}>Remove</button></td>
</tr>
);
},
This could easily be moved to your Expenditure/Income component.
Issues: Mutating state, Overwriting cashbook in state and loosing expenditure/income
addExpenditure: function(expenditure) {
var timestamp = (new Date()).getTime();
// You are mutating the state here, better clone the object and change the clone, then assign the cloned
this.state.cashbook.expenditure['expenditure-' + timestamp] = expenditure;
// You are overwriting cashbook with an object, that only contains expenditure, thus loosing other properties like income
this.setState({
cashbook: { expenditure: this.state.cashbook.expenditure }
});
}
// Fixed
addExpenditure: function(expenditure) {
var timestamp = (new Date()).getTime();
// clone object
var cashbook = Object.assign({}, this.state.cashbook);
cashbook.expenditure = Object.assign({}, cashbook.expenditure);
// define latest expenditure
cashbook.expenditure['expenditure-' + timestamp] = expenditure;
// set state
this.setState({
cashbook: cashbook
});
}
// ES6
addExpenditure: function(expenditure) {
var timestamp = (new Date()).getTime();
this.setState({
cashbook: {
...this.state.cashbook,
expenditure: {
...this.state.cashbook.expenditure,
['expenditure-' + timestamp]: expenditure
}
}
});
}
You would be better off, if you'd flatten your state and cashbook object
so instead of
this.state = {
cashbook: {
expenditure: {},
income: {}
},
totals: {},
available: {}
}
having
this.state = {
expenditure: {},
income: {}
totals: {},
available: {}
}
so you could just
addExpenditure: function(expenditure) {
var timestamp = (new Date()).getTime();
var exp = Object.assign({}, this.state.cashbook.expenditure);
exp['expenditure-' + timestamp] = expenditure;
this.setState({
expenditure: exp
});
}
or es6
addExpenditure: function(expenditure) {
var timestamp = (new Date()).getTime();
this.setState({
expenditure: {
...this.state.cashbook.expenditure,
['expenditure-' + timestamp]: expenditure
}
});
}
of course you'd need to update the rebase binding and your models, dunno if this is something you want
componentDidMount: function() {
base.syncState('expenditure', {
context: this,
state: 'expenditure'
});
base.syncState('income', {
context: this,
state: 'income'
});
}
Related
I have attributes in the state, I would like to ensure that by specifying the function the attribute name changes the value contained in the state.
It seems to work, the problem that if I have an object of this type in the state:
companyInfo: {
name: "",
vatNumber: "",
legalRepresentative: ""
}
It does not work properly, as the code is now set in the state in this case a new attribute is created.
So I'd like to do something like this:
handleChangeField("companyInfo.name")
It is changed to the state atrribute name of the obj companyInfo that is in the state.
Can you give me some advice?
Link: codesandbox
Code:
import ReactDOM from "react-dom";
import React, { Component } from "react";
import ReactJson from "react-json-view";
class Todo extends Component {
constructor(props) {
super(props);
this.state = {
email: "email0",
role: "role0",
companyInfo: {
name: "",
vatNumber: "",
legalRepresentative: ""
}
};
}
returnStateElement = (...elements) => {
const copy = Object.assign({}, this.state);
return elements.reduce((obj, key) => ({ ...obj, [key]: copy[key] }), {});
};
handleChangeField = field => evt => {
let state = {};
state[field] = evt.target.value;
this.setState(state);
};
handleSubmit = () => {
let el = this.returnStateElement(
"name",
"email",
"vatNumber",
"legalRepresentative",
"role"
);
let { name, email, legalRepresentative, vatNumber, role } = el;
let dataSender = {};
dataSender.email = email;
dataSender.role = role;
dataSender.companyInfo = {};
dataSender.companyInfo.name = name;
dataSender.companyInfo.legalRepresentative = legalRepresentative;
dataSender.companyInfo.vatNumber = vatNumber;
console.log(this.state);
//console.log(dataSender)
};
render() {
return (
<div>
<input onChange={this.handleChangeField("email")} />
<br />
<br />
<input onChange={this.handleChangeField("companyInfo.name")} />
<br />
<br />
<button onClick={() => this.handleSubmit()}>send</button>
<br />
<br />
<ReactJson src={this.state} theme="solarized" />
</div>
);
}
}
ReactDOM.render(<Todo />, document.getElementById("root"));
Edit: I came up with a much better answer where one mutates the specific key of the oldState using a reduce. Less code, much more elegant and should work at any object depth.
Working example here
setNestedField(object, fields, newValue) {
fields.reduce((acc, field, index) => {
if (index === fields.length - 1) {
acc[field] = newValue;
}
return acc[field];
}, object);
return object;
}
handleChangeField = field => evt => {
const fields = field.split(".");
let oldState = this.state;
const newState = this.setNestedField(
{ ...oldState },
fields,
evt.target.value
);
this.setState(newState);
};
OLD ANSWER
handleChangeFields looks like this:
handleChangeField = field => evt => {
//first you split by '.' to get all the keys
const fields = field.split(".").reverse();
// you'll need the previous state
let oldState = this.state;
let newState = fields.reduce((acc, value, index) => {
if (index === 0) {
// you add the event value to the first key
acc[value] = evt.target.value;
return acc;
}
//copy acc to use it later
const tmp = { ...acc };
//delete previous key added to acc
delete acc[fields[index - 1]];
acc[value] = { ...oldState[value], ...tmp };
return acc;
}, {});
this.setState(newState);
};
What's going on step by step in the reduce function, if you do handleChangeField('company.name') with evt.target.value = "Big Corp":
1) you get the array ['name','company']
2) you go in the reduce function
when index = 0, acc = {}, key='name' => {name: 'Big Corp'}
when index=1, acc= {name: 'Big Corp'},key='company' => acc = { company: {name: 'Big Corp'}, name: 'BigCorp} so before returning we delete the previous key (name here) to return => { company: {name: 'Big Corp'}
I'm trying to activate shuffle.js component functionality (search, filter and sort) with react.js. However, the documentation on the website is very limited. I know that I need to add a search input and some buttons to do what I want, yet I'm not sure how to connect my search box input and other button events to manipulate the photogrid (or other elements within a container) that is being rendered by react.
I have imported shuffle.js as node module and initialised it on the react page. The basic code that they provide seems to be working and displays the photo grid, however, that's pretty much it. I also want to implement the search, filtering and sorting functionality but there isn't documentation on how to do that in react.js. The code below shows the photogrid implementation but nothing else.
import React, {Component} from "react";
import Shuffle from 'shufflejs';
class PhotoGrid extends React.Component {
constructor(props) {
super(props);
const grayPixel = 'data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==';
const blackPixel = 'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=';
const greenPixel = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mO02Vz4HwAE9AJhcLBN6AAAAABJRU5ErkJggg==';
this.state = {
photos: [{
id: 4,
src: grayPixel
},
{
id: 5,
src: blackPixel
},
{
id: 6,
src: greenPixel
},
],
searchTerm: '',
sortByTitle: '',
sortByDate: '',
sortByPopularity: '',
filterCategory: ''
};
this.filters = {
cat1: [],
cat2: [],
};
this.wb = this.props.dataWB;
this.element = React.createRef();
this.sizer = React.createRef();
this._handleSearchKeyup = this._handleSearchKeyup.bind(this);
this._handleSortChange = this._handleSortChange.bind(this);
this._handleCategory1Change = this._handleCategory1Change.bind(this);
this._handleCategory2Change = this._handleCategory2Change.bind(this);
this._getCurrentCat1Filters = this._getCurrentCat1Filters.bind(this);
this._getCurrentCat2Filters = this._getCurrentCat2Filters.bind(this);
}
/**
* Fake and API request for a set of images.
* #return {Promise<Object[]>} A promise which resolves with an array of objects.
*/
_fetchPhotos() {
return new Promise((resolve) => {
setTimeout(() => {
resolve([{
id: 4,
username: '#stickermule',
title:'puss',
date_created: '2003-09-01',
popularity: '233',
category1:'animal',
category2:'mammals',
name: 'Sticker Mule',
src: 'https://images.unsplash.com/photo-1484244233201-29892afe6a2c?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=800&h=600&fit=crop&s=14d236624576109b51e85bd5d7ebfbfc'
},
{
id: 5,
username: '#prostoroman',
date_created: '2003-09-02',
popularity: '232',
category1:'industry',
category2:'mammals',
title:'city',
name: 'Roman Logov',
src: 'https://images.unsplash.com/photo-1465414829459-d228b58caf6e?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=800&h=600&fit=crop&s=7a7080fc0699869b1921cb1e7047c5b3'
},
{
id: 6,
username: '#richienolan',
date_created: '2003-09-03',
popularity: '231',
title:'nature',
category1:'art',
category2:'insect',
name: 'Richard Nolan',
src: 'https://images.unsplash.com/photo-1478033394151-c931d5a4bdd6?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=800&h=600&fit=crop&s=3c74d594a86e26c5a319f4e17b36146e'
}
]);
}, 300);
});
}
_whenPhotosLoaded(photos) {
return Promise.all(photos.map(photo => new Promise((resolve) => {
const image = document.createElement('img');
image.src = photo.src;
if (image.naturalWidth > 0 || image.complete) {
resolve(photo);
} else {
image.onload = () => {
resolve(photo);
};
}
})));
}
_handleSortChange(evt) {
var value = evt.target.value.toLowerCase();
function sortByDate(element) {
return element.getAttribute('data-created');
}
function sortByPopularity(element) {
return element.getAttribute('data-popularity');
}
function sortByTitle(element) {
return element.getAttribute('data-title').toLowerCase();
}
let options;
if (value == 'date-created') {
options = {
reverse: true,
by: sortByDate,
};
} else if (value == 'title') {
options = {
by: sortByTitle,
};
} else if (value == 'popularity') {
options = {
reverse: true,
by: sortByPopularity,
};
} else if (value == 'default') {
this.shuffle.filter('all');
} else {
options = {};
}
this.shuffle.sort(options);
};
_getCurrentCat1Filters = function () {
return this.filters.cat1.filter(function (button) {
return button.classList.contains('active');
}).map(function (button) {
console.log('button value: '+button.getAttribute('data-value'))
return button.getAttribute('data-value');
});
};
_getCurrentCat2Filters = function () {
return this.filters.cat2.filter(function (button) {
return button.classList.contains('active');
}).map(function (button) {
console.log('button value: '+button.getAttribute('data-value'))
// console.log('button value: '+button.getAttribute('data-value'))
return button.getAttribute('data-value');
});
};
_handleCategory1Change = function (evt) {
var button = evt.currentTarget;
console.log(button)
// Treat these buttons like radio buttons where only 1 can be selected.
if (button.classList.contains('active')) {
button.classList.remove('active');
} else {
this.filters.cat1.forEach(function (btn) {
btn.classList.remove('active');
});
button.classList.add('active');
}
this.filters.cat1 = this._getCurrentCat1Filters();
console.log('current cat contains : '+this.filters.cat1);
this.filter();
};
/**
* A color button was clicked. Update filters and display.
* #param {Event} evt Click event object.
*/
_handleCategory2Change = function (evt) {
var button = evt.currentTarget;
// Treat these buttons like radio buttons where only 1 can be selected.
if (button.classList.contains('active')) {
button.classList.remove('active');
} else {
this.filters.cat2.forEach(function (btn) {
btn.classList.remove('active');
});
button.classList.add('active');
}
this.filters.cat2 = this._getCurrentCat2Filters();
console.log('current cat contains : '+this.filters.cat2);
this.filter();
};
filter = function () {
if (this.hasActiveFilters()) {
this.shuffle.filter(this.itemPassesFilters.bind(this));
} else {
this.shuffle.filter(Shuffle.ALL_ITEMS);
}
};
itemPassesFilters = function (element) {
var cat1 = this.filters.cat1;
var cat2 = this.filters.cat2;
var cat1 = element.getAttribute('data-category1');
var cat2 = element.getAttribute('data-category2');
// If there are active shape filters and this shape is not in that array.
if (cat1.length > 0 && !cat1.includes(cat1)) {
return false;
}
// If there are active color filters and this color is not in that array.
if (cat2.length > 0 && !cat2.includes(cat2)) {
return false;
}
return true;
};
/**
* If any of the arrays in the `filters` property have a length of more than zero,
* that means there is an active filter.
* #return {boolean}
*/
hasActiveFilters = function () {
return Object.keys(this.filters).some(function (key) {
return this.filters[key].length > 0;
}, this);
};
_handleSearchKeyup(event) {
this.setState({
searchTerm: event.target.value.toLowerCase()
}, () => {
this.shuffle.filter((element) => {
return element.dataset.name.toLowerCase().includes(this.state.searchTerm) || element.dataset.username.toLowerCase().includes(this.state.searchTerm);
})
})
}
componentDidMount() {
// The elements are in the DOM, initialize a shuffle instance.
this.shuffle = new Shuffle(this.element.current, {
itemSelector: '.js-item',
sizer: this.sizer.current,
});
// Kick off the network request and update the state once it returns.
this._fetchPhotos()
.then(this._whenPhotosLoaded.bind(this))
.then((photos) => {
this.setState({
photos
});
});
}
componentDidUpdate() {
// Notify shuffle to dump the elements it's currently holding and consider
// all elements matching the `itemSelector` as new.
this.shuffle.resetItems();
}
componentWillUnmount() {
// Dispose of shuffle when it will be removed from the DOM.
this.shuffle.destroy();
this.shuffle = null;
}
render() {
return (
<div>
<div id='searchBar'>
<input type="text" className='js-shuffle-search' onChange={ this._handleSearchKeyup } value={ this.state.searchTerm } />
</div>
<div id='gridActions'>
<h2>Filter By cat 1</h2>
<button onClick={ this._handleCategory1Change } value='all'>All</button>
<button onClick={ this._handleCategory1Change } value='art'>Art</button>
<button onClick={ this._handleCategory1Change } value='industry'>Industry</button>
<button onClick={ this._handleCategory1Change } value='animal'>Animal</button>
<h2>Filter By cat 2</h2>
<button onClick={ this._handleCategory2Change } value='all'>All</button>
<button onClick={ this._getCurrentCat1Filters } value='mammals'>Mammals</button>
<button onClick={ this._getCurrentCat2Filters } value='insects'>Insects</button>
<h2>Sort By</h2>
<button onClick={ this._handleSortChange } value='default'>Default</button>
<button onClick={ this._handleSortChange } value='date-created'>By Date</button>
<button onClick={ this._handleSortChange } value='title'>By Title</button>
<button onClick={ this._handleSortChange } value='popularity'>By Popularity</button>
</div>
<div ref={ this.element } id='grid' className="row my-shuffle-container shuffle"> {
this.state.photos.map(image =>
<PhotoItem { ...image } />)}
<div ref={ this.sizer } className="col-1#xs col-1#sm photo-grid__sizer"></div>
</div>
</div>
);
}
}
function PhotoItem({id, src, category1, category2, date_created, popularity, title, name, username }) {
return (
<div key={id}
className="col-lg-3 js-item"
data-name={name}
data-title={title}
data-date-created={date_created}
data-popularity={popularity}
data-category1={category1}
data-cetagory2={category2}
data-username={username}>
<img src={src} style={{width : "100%",height :"100%"}}/>
</div>
)
}
export default PhotoGrid;
The photogrid right now does nothing, just displays photos which I'm unable to search, filter and sort.
Only judging by the documentation, I haven't tried it yet, but should work.
The instance of Shuffle has a filter method, which takes a string, or an array of strings, to filter the elements by "groups", or a callback function to perform more complicated search. You should call this.shuffle.filter after updating the state of your component, i.e.:
_handleSearchKeyup(event){
this.setState({searchTerm : event.target.value}, () => {
this.shuffle.filter((element) => { /* use this.state.searchTerm to return matching elements */ } );
})
}
Edited after building a fiddle.
The callback function looks at data-name and data-username attributes to check if they contain the search string
_handleSearchKeyup(event){
this.setState({searchTerm : event.target.value.toLowerCase()}, () => {
this.shuffle.filter((element) => {
return (
element.dataset.name.toLowerCase().includes(this.state.searchTerm) ||
element.dataset.username.toLowerCase().includes(this.state.searchTerm)
);
})
})
}
For the above to work you also need to add these attributes to the DOM nodes, so update the PhotoItem component:
function PhotoItem({ id, src, name, username }) {
return (
<div key={id}
className="col-md-3 photo-item"
data-name={name}
data-username={username}>
<img src={src} style={{width : "100%",height :"100%"}}/>
</div>
)
}
In opposition to pawel's answer I think that this library operates on DOM. It makes this not react friendly.
Classic input handlers saves values within state using setState method. As an effect to state change react refreshes/updates the view (using render() method) in virtual DOM. After that react updates real DOM to be in sync with virtual one.
In this case lib manipulates on real DOM elements - calling render() (forced by setState()) will overwritte earlier changes made by Shuffle. To avoid that we should avoid using setState.
Simply save filter and sorting parameters directly within component instance (using this):
_handleSearchKeyup(event){
this.searchTerm = event.target.value;
this.shuffle.filter((element) => { /* use this.searchTerm to return matching elements */ } );
}
Initialize all the params (f.e. filterCategories, searchTerm, sortBy and sortOrder) in constructor and use them in one this.shuffle.filter() call (second parameter for sort object) on every parameter change. Prepare some helper to create combined filtering function (mix of filtering and searching), sorting is far easier.
setState can be used for clear all filters button - forced rerendering - remember to clear all parameters within handler.
UPDATE
For sorting order declare
this.reverse = true; // in constructor
this.orderBy = null;
handlers
_handleSortOrderChange = () => {
this.reverse = !this.reverse
// call common sorting function
// extracted from _handleSortChange
// this._commonSortingFunction()
}
_handleSortByChange = (evt) => {
this.orderBy = evt.target.value.toLowerCase();
// call common sorting function
// extracted from _handleSortChange
// this._commonSortingFunction()
}
_commonSortingFunction = () => {
// you can declare sorting functions in main/component scope
let options = { reverse: this.reverse }
const value = this.orderBy;
if (value == 'date-created') {
options.by = sortByDate // or this.sortByDate
} else if (value == 'title') {
options.by = sortByTitle
//...
//this.shuffle.sort(options);
You can also store ready options sorting object in component instance (this.options) updated by handlers. This value can be used by _commonSortingFunction() to call this.shuffle.sort but also by filtering functions (second parameter).
reversing button (no need to bind)
<button onClick={this._handleSortOrder}>Reverse order</button>
UPDATE 2
If you want to work with 'normal' react, setState you can move (encapsulate) all the filtering (searchBar, gridActions) into separate component.
State update will force rerendering only for 'tools', not affecting elements managed in real DOM by shuffle (parent not rerendered). This way you can avoid manual css manipulations ('active') by using conditional rendering (plus many more possibilities - list active filters separately, show order asc/desc, show reset only when sth changed etc.).
By passing this.shuffle as prop you can simply invoke search/filter/sort in parent.
I am using fullcalendar-scheduler plugin for following calendar. Currently I have integrated it with react and rails. In order to change the positions of the element I have called the select function from inside viewRender function of fullCalendar instead of render on react. On this case how do we change state when select option is changed and fetch the data again from api?
import React from "react";
import PropTypes from "prop-types";
import axios from "axios";
class TestCalendar extends React.Component {
constructor(props) {
super(props);
this.state = {
cars: [],
events: [],
price: [],
selectDates: [],
startDate: moment(),
endDate: moment().add(3, 'years')
}
}
componentDidMount() {
const headers = {
'Content-Type': 'application/json'
}
axios.get('/api/v1/test_calendars?date_from=' + this.state.startDate.format(), { headers: headers })
.then(res => {
const cars = res.data;
this.setState({ cars });
});
axios.get('/api/v1/test_calendars/events?date_from=' + this.state.startDate.format(), { headers: headers })
.then(res => {
const events = res.data;
this.setState({ events });
});
axios.get('/api/v1/test_calendars/prices?date_from=' + this.state.startDate.format(), { headers: headers })
.then(res => {
const price = res.data;
this.setState({ price });
});
this.updateEvents(this.props.hidePrice);
}
componentDidUpdate() {
console.log('componentDidUpdate');
this.updateEvents(this.props.hidePrice);
console.log(this.state.cars);
}
componentWillUnmount() {
$('#test_calendar').fullCalendar('destroy');
};
handleChange(e) {
debugger;
}
updateEvents(hidePrice) {
function monthSelectList() {
let select = '<div class="Select select-me"><select id="months-tab" class="Select-input">' +
'</select></div>'
return select
}
function getDates(startDate, stopDate) {
var dateArray = [];
while(startDate.format('YYYY-MM-DD') <= stopDate.format('YYYY-MM-DD')) {
dateArray.push(startDate.format('YYYY-MM'));
startDate = startDate.add(1, 'days');
};
return dateArray;
}
$('#test_calendar').fullCalendar('destroy');
$('#test_calendar').fullCalendar({
selectable: false,
defaultView: 'timelineEightDays',
defaultDate: this.props.defaultDate,
views: {
timelineEightDays: {
type: 'timeline',
duration: { days: 8 },
slotDuration: '24:00'
}
},
header: {
left: 'prev',
right: 'next'
},
viewRender: function(view, element) {
let uniqueDates;
$("span:contains('Cars')").empty().append(
monthSelectList()
);
$("#months-tab").on("change", function() {
let index, optionElement, month, year, goToDate;
index = this.selectedIndex;
optionElement = this.childNodes[index];
month = optionElement.getAttribute("data-month");
year = optionElement.getAttribute("data-year");
goToDate = moment([year, (month - 1), 1]).format("YYYY-MM-DD");
$("#test_calendar").fullCalendar('gotoDate', moment(goToDate));
$("#months-tab").find("option[data-month=" + month + "][data-year=" + year + "]").prop("selected", true);
this.handleChange.bind(this)
});
let dates = getDates(moment(), moment().add(3, "years"));
uniqueDates = [...new Set(dates)];
$('#months-tab option').remove();
$.each(uniqueDates, function(i, date) {
$('#months-tab').append($('<option>', {
value: i,
text: moment(date).format('MMMM') + " " + moment(date).format('YYYY'),
'data-month': moment(date).format('MM'),
'data-year': moment(date).format('YYYY'),
}));
});
},
resources: this.state.cars,
resourceRender: function(resourceObj, labelTds, bodyTds) {
labelTds.css('background-image', "url(" + resourceObj.header_image + ")");
labelTds.css('background-size', "160px 88px");
labelTds.css('background-repeat', "no-repeat");
labelTds.css("border-bottom", "1px solid");
labelTds.addClass('resource-render');
labelTds.children().children().addClass("car-name");
},
resourceLabelText: 'Cars',
dayClick: function(date, jsEvent, view, resource) {
},
dayRender: function(date, cell){
cell.addClass('dayrender');
},
select: function(startDate, endDate, jsEvent, view, resource) {
},
events: this.state.events.concat(this.state.price),
eventRender: function(event, element, view){
},
schedulerLicenseKey: 'CC-Attribution-NonCommercial-NoDerivatives'
});
// Should stay after full component is initialized to avoid fc-unselectable class on select tag for months
$("#months-tab").on("mousedown click", function(event){event.stopPropagation()});
$(".prev-link").on("click", function(event){event.stopPropagation()});
$(".next-link").on("click", function(event){event.stopPropagation()});
}
render () {
return (
<div id='test_calendar'>
</div>
);
}
}
export default TestCalendar;
Here your onchange callback doesn't have the react component context so you cannot change the state without giving access to the proper context. One solution I may quickly suggest is to change your updateEvents function like bellow. I have only kept the changed code.
updateEvents(hidePrice) {
let context = this;
... // your code
$('#test_calendar').fullCalendar({
... // your code
viewRender: function(view, element) {
... // your code
$("#months-tab").on("change", function() {
... // your code
// Call the handleChange with the context.
context.handleChange.bind(context)(this); // edited here
});
... // your code
});
... // your code
}
Then you will be able to call the setState method from the handleChange function.
You must be facing an issue with the this reference, as you are trying to access the method handleChange which is associated with the component this but you are using the normal function for viewRender instead you should use arrow function
see the updated code below, it will solve the issue,
updateEvents(hidePrice) {
$('#test_calendar').fullCalendar({
...
viewRender: (view, element) => { // convert to arrow function so, this (component instance) will be accessible inside.
// store the reference of this (component instance).
const $this = this;
$("#months-tab").on("change", function (e) {
...
// always bind methods in constructor.
$this.handleChange(e);
...
});
},
...
});
}
thanks.
Here I create TodoItems component, and I try to store array as a state, but console says that my entries is not defined:
var TodoItems = React.createClass({
getInitialState: function() {
return {
entries: this.props.entries
};
},
removeItem: function(key){
var itemArray = this.state.entries;
for (var i = 0; i < itemArray.length; i++)
if (itemArray[i.key] === key) {
itemArray.splice(i, 1);
break;
}
this.setState({
entries: entries
})
},
This is the prop I'm trying to store:
</div>
<TodoItems entries={this.state.items}/>
</div>
You are doing a mistake, setting state as entries in removeItem function whereby you should be setting it to itemArray, also , assign itemArray as var itemArray = [...this.state.entries]; because in your style of declaration it will refer to the state entries itself and will mutate state directly, which is not advisable, and getInitialState is called only on the first render of TodoItems so if you are assigning a state with props make sure to do that in the componentWillReceiveProps function as well, since it will be called everytime the parent renders and will have the updated props
var TodoItems = React.createClass({
getInitialState: function() {
return {
entries: this.props.entries
};
},
componentWillReceiveProps(nextProps) {
this.setState({entries: nextProps.entries})
}
removeItem: function(key){
var itemArray = [...this.state.entries];
for (var i = 0; i < itemArray.length; i++)
if (itemArray[i.key] === key) {
itemArray.splice(i, 1);
break;
}
this.setState({
entries: itemArray
})
},
I have a component that I am writing automated tests for. I want to test a method of this component. THe purpose if this test if if the method will call an imported function when passed the wrong parameters.
This is my component (I removed the non-relevant code):
//Util
import { getMenu } from '../utils/request';
const Dashboard = React.createClass({
getInitialState() {
let today = new Date();
if (today.getDay() == 6) {
today.setDate(today.getDate() + 2);
} else if (today.getDay() == 0) {
today.setDate(today.getDate() + 1);
}
return {
selectedDate: today.toISOString().substring(0, 10)
}
},
componentDidMount() {
getMenu(this.state.selectedDate, (data) => {
if (data.error) {
this.setState({
error: data.error
})
} else {
this.setState({
dailyMenu: data.dailyMenu,
loading: false
})
}
})
},
handleDateChange(date) {
var newDate = date.toISOString().substring(0, 10);
if(newDate !== this.state.selectedDate) {
getMenu(newDate, (data) => {
if (data.error) {
this.setState({
error: data.error
})
} else {
this.setState({
dailyMenu: data.dailyMenu,
loading: false,
selectedDate: newDate
})
}
})
}
},
render() {
return (
<MuiThemeProvider>
<div className="Dashboard">
<div className="Dashboard-body">
<Sidebar dateClick={this.handleDateChange}/>
</div>
</div>
</MuiThemeProvider>
)
}
});
module.exports = Dashboard;
I am mocking the getMenu function using jest.mock:
// Components
import Dashboard from '../components/Dashboard';
// Utils
import { getMenu } from '../utils/request';
jest.mock('../utils/request', () => ({getMenu: jest.fn()}))
Then I have written two tests. The first one passes different dates, the second one passes the same date. So in the second case the getMenu mock should not get called. But for both tests I get the result that the function was called one time:
it('handleDateChange should call getMenu when NOT passed state date', ()=> {
const dashboard = shallow(<Dashboard/>);
const today = new Date();
const longAgo = new Date(1).toISOString().substring(0, 10);
dashboard.setState({ selectedDate: longAgo });
dashboard.instance().handleDateChange(today);
expect(getMenu).toHaveBeenCalledTimes(1);
});
it('handleDateChange should NOT call getMenu when passed state date', ()=> {
const dashboard = shallow(<Dashboard/>);
const today = new Date();
const selectedDate = today.toISOString().substring(0, 10);
dashboard.setState({ selectedDate: selectedDate });
dashboard.instance().handleDateChange(today);
expect(getMenu).toHaveBeenCalledTimes(0);
});
So I did some console logs and so far everything seems as expected:
console.log(today.toISOString().substring(0, 10));
console.log(dashboard.state('selectedDate'));
console.log(today.toISOString().substring(0, 10) !== selectedDate);
This outputs:
2017-01-12
2017-01-12
false
What am I doing wrong?