I have a Legend, which contains multiple Legend.Items children. I'm having a problem where currently onClick it is possible to deselect all of the Legend Items which has consequences that I'd like to avoid. Is it possible to set some sort of onClick handler in the Legend component that can have some state clicked and check whether there are n - 1 legend items "selected/faded", n being the total number of legend items? I looked at the JSX Spread Attributes, but because I'm using {this.props.children}, I'm not sure how to use them or if they would work in this context.
I also took a look at this blogpost (http://jaketrent.com/post/send-props-to-children-react/), but it looked a bit hacky to me and I thought there might be a more conventional way. I'm new to ReactJS so if I need to provide more context, let me know!
MY CODE:
LEGEND.JSX
var React = require('react');
var cn = require('classnames');
// Have legend hold state about how many clicked
var Legend = React.createClass({
getInitialState: function () {
return { clicked: 0 }
},
render: function () {
console.log(this.props.children);
return (
<ul className="legend inline-list">
{this.props.children}
</ul>
);
},
});
Legend.Item = React.createClass({
getInitialState: function () {
return { hover: false, clicked: false };
},
handleMouseOver: function () {
this.setState({ hover: true });
this.props.mouseOver(this.props.name);
},
handleMouseOut: function () {
this.setState({ hover: false });
this.props.mouseOut(this.props.name);
},
handleClick: function() {
if (this.state.clicked) {
this.setState({ clicked: false });
this.props.click(this.props.name);
} else {
this.setState({ clicked: true });
this.props.click(this.props.name);
};
},
render: function () {
var swatchClasses = cn({ 'swatch': true, 'legend-item-fade': this.state.hover, 'c3-legend-item-hidden': this.state.clicked })
var spanClasses = cn({ 'legend-item-fade': this.state.hover, 'c3-legend-item-hidden': this.state.clicked })
return (
<li className="legend-item">
<i className={swatchClasses}
onClick={this.handleClick}
onMouseEnter={this.handleMouseOver}
onMouseLeave={this.handleMouseOut}
style={{ "backgroundColor": this.props.color }}></i>
<span className={spanClasses}
onClick={this.handleClick}
onMouseEnter={this.handleMouseOver}
onMouseLeave={this.handleMouseOut}>
{this.props.name}
</span>
</li>
);
},
});
module.exports = {
Legend: Legend,
};
RESPONSE.JSX RENDER FUNCTION
<Legend>
{newColumns.map(function (column) {
return (
<Legend.Item name={column.name}
color={column.color}
click={this.onLegendClick}
mouseOut={this.onLegendMouseOut}
mouseOver={this.onLegendMouseOver}/>
);
}.bind(this))}
</Legend>
I think the best and simplest way is to use callbacks.
In Legend recreate the components from the children, augmenting their props with a callback to Legend:
let legendItems = React.Children.map(this.props.children, child =>
React.cloneElement(child, { updateLegendCounter: this.updateLegend})
);
The callback in Legend is something like this:
updateLegend() {
this.setState({clicked: clicked + 1})
}
And finally, in your render method, you discriminate when
if (this.state.clicked === children.length-1)
Also, I would pass the initial state of clicked as a prop to the Item element. In this way it becomes really easy to select/deselect all.
Related
I have a header component where I need to render three buttons, so every three buttons have three props. One is the class name, click handler and text.
So out of three buttons, two buttons act as a toggle button, so based on the click the text should change.
See the below code:
class App extends Component(){
state = {
navigationList: [{
text: 'Signout',
onClickHandler: this.signoutHandler,
customClassName: 'buttonStyle'
}, {
text: this.state.isStudents ? 'Students' : 'Teachers',
onClickHandler: this.viewMode,
customClassName: 'buttonStyle'
}, {
text: this.state.activeWay ? 'Active On' : 'Active Hidden',
onClickHandler: this.activeWay,
customClassName: 'buttonStyle'
}]
}
signoutHandler = () => {
// some functionality
}
viewMode = () => {
this.setState({
isStudents: !this.state.isStudents
})
}
activeWay = () => {
this.setState({
activeWay: !this.state.activeWay
})
}
render(){
return (
<Header navigationList={this.state.navigationList}/>
)
}
}
const Header = ({navigationList}) => {
return (
<>
{navigationList && navigationList.map(({text, onClickHandler, customClassName}) => {
return(
<button
onClick={onClickHandler}
className={customClassName}
>
{text}
</button>
)
})}
</>
)
}
The other way is I can pass all the props one by one and instead of an array I can write three button elements render it, but I am thinking to have an array and render using a map.
So which method is better, the problem that I am facing is if use the array. map render
the approach I need to set the initial value as a variable outside and how can I set the state.
And I am getting the onClick method is undefined, is it because the function is not attached to the state navigation list array.
Update
I declared the functions above the state so it was able to call the function.
So in JS, before the state is declared in the memory the functions should be hoisted isn't.
class App extends React.Component {
constructor(props){
super();
this.state = {
isStudents:false,
activeWay:false,
}
}
createList(){
return [{
text: 'Signout',
onClickHandler: this.signoutHandler.bind(this),
customClassName: 'buttonStyle'
}, {
text: this.state.isStudents ? 'Students' : 'Teachers',
onClickHandler: this.viewMode.bind(this),
customClassName: 'buttonStyle'
}, {
text: this.state.activeWay ? 'Active On' : 'Active Hidden',
onClickHandler: this.activeWay.bind(this),
customClassName: 'buttonStyle'
}];
}
signoutHandler(){
}
viewMode(){
this.setState({
isStudents: !this.state.isStudents
})
}
activeWay(){
this.setState({
activeWay: !this.state.activeWay
})
}
render(){
return (
<div>
<div>ddd</div>
<Header navigationList={this.createList()} />
</div>
)
}
}
const Header = ({navigationList}) => {
console.log(navigationList);
return (
<div>
{navigationList && navigationList.map(({text, onClickHandler, customClassName}) => {
return(
<button
onClick={onClickHandler}
className={customClassName}
>
{text}
</button>
)
})}
</div>
)
}
ReactDOM.render(<App />, document.querySelector("#app"))
https://jsfiddle.net/luk17/en9h1bpr/
Ok I will try to explain, If you see you are using function expressions in your class and as far as hoisting is concerned in JavaScript, functions expressions are not hoisted in JS only function declarations are hoisted, function expressions are treated as variables in JS.
Now for your case you don't have to shift your functions above the state, you can simply use constructor for initializing state as
constructor(props) {
super(props);
this.state = {
isStudents: false,
activeWay: false,
navigationList: [
{
text: "Signout",
onClickHandler: this.signoutHandler,
customClassName: "buttonStyle"
},
{
text: "Teachers",
onClickHandler: this.viewMode,
customClassName: "buttonStyle"
},
{
text: "Active Hidden",
onClickHandler: this.activeWay,
customClassName: "buttonStyle"
}
]
};
}
Now you will have your handlers available as it is
Sandbox with some modification just to show
EDIT:
You can have default text for buttons and change it when clicking,
Sandbox updated
Hope it helps
CODE:
var React = require('react');
var Recipe = require('./Recipe.jsx');
var AddRecipe = require('./AddRecipe.jsx');
var EditRecipe = require('./EditRecipe.jsx');
var RecipeBox = React.createClass({
getInitialState: function () {
return {
recipesArray: [],
adding: false,
editing: false,
currentIndex: 0
};
},
handleClick: function () {
this.setState({
adding: true
});
},
handleEditClick: function(index) {
this.setState({
editing: true,
currentIndex: index
});
},
handleDeleteClick: function(index) {
var newRecipesArray = this.state.recipesArray;
newRecipesArray.splice(index-1,1);
this.setState({
recipesArray: newRecipesArray
});
},
handleClose: function() {
this.setState({
adding: false,
editing: false
});
},
handleAdd: function(newRecipe) {
this.setState({
recipesArray: this.state.recipesArray.concat(newRecipe)
});
},
handleEdit: function(newRecipe, index) {
var newRecipesArray = this.state.recipesArray;
newRecipesArray[index-1] = newRecipe;
this.setState({
recipesArray: newRecipesArray
});
},
render: function() {
var i = 0;
var that = this;
var recipes = this.state.recipesArray.map(function(item) {
i++
return (
<div key={"div"+i} className="table">
<Recipe key={i} name={item.name} ingredients={item.ingredients} />
<button key ={"edit"+i} onClick={() => { that.handleEditClick(i)}} className="btn edit btn-primary">Edit</button>
<button key ={"delete"+i} onClick={() => { that.handleDeleteClick(i)}} className="btn delete btn-danger">Delete</button>
</div>
);
});
return (
<div>
<h1>React.js Recipe Box</h1>
<button className="btn btn-primary" onClick={this.handleClick}>Add Recipe</button>
<table>
<tbody>
<tr>
<th>RECIPES</th>
</tr>
</tbody>
</table>
{recipes}
{ this.state.adding ? <AddRecipe handleClose={this.handleClose} handleAdd={this.handleAdd} /> : null }
{ this.state.editing ? <EditRecipe currentIndex = {this.state.currentIndex} handleClose={this.handleClose} handleEdit={this.handleEdit}/> : null }
</div>
);
},
});
module.exports = RecipeBox;
QUESTION:
How should I implement saving state to localStorage ?
What would be the most elegant implementation ?
Currently learning React and looking to write clean and elegant code.
Whenever an update to state is fired, it will trigger the lifecycle method of componentDidUpdate. You can hook into that method in order to save the state of the component.
componentDidUpdate() {
window.localStorage.setItem('state', JSON.stringify(this.state));
}
Depending on your use case, you should be able to load it back up on componentDidMount.
componentDidMount() {
// there is a chance the item does not exist
// or the json fails to parse
try {
const state = window.localStorage.getItem('state');
this.setState({ ...JSON.parse(state) });
} catch (e) {}
}
I would warn you, you probably want a solution more like redux with a localStorage adapter for a "full-fledged" solution. This one is pretty frail in a few different ways.
I would take a look at plugins that make localstorage easier (not browser specific). An example would be this:
https://github.com/artberri/jquery-html5storage
The page above has all the information you need to get started. If that one doesn't work then I would continue to search. There are plenty out there. There may be newer ones that use React as well. The jQuery plugins have worked for me when I was learning/doing Angular.
http://jsfiddle.net/adamchenwei/3rt0930z/20/
I just trying to create an example to learn how state works in a list.
What I intent to do is to allow a particular value that got repeated in a list, to change, in ALL items in the list, by using state. For example, in this case, I want to change all the list item's name to 'lalala' when I run changeName of onClick.
However I have this warning (issue at fiddle version 11, resolved at version 15)
Any help on resolving it to achieve purpose above?
Actual Code
var items = [
{ name: 'Believe In Allah', link: 'https://www.quran.com' },
{ name: 'Prayer', link: 'https://www.quran.com' },
{ name: 'Zakat', link: 'https://www.quran.com' },
{ name: 'Fasting', link: 'https://www.quran.com' },
{ name: 'Hajj', link: 'https://www.quran.com' },
];
var ItemModule = React.createClass({
getInitialState: function() {
return { newName: this.props.name }
},
changeName() {
console.log('changed name');
this.setState({ newName: 'lalala' });
},
render() {
//<!-- <a className='button' href={this.props.link}>{this.props.name}</a> -->
return (
<li onClick={this.changeName}>
{this.state.newName}
</li>
);
}
});
var RepeatModule = React.createClass({
getInitialState: function() {
return { items: [] }
},
render: function() {
var listItems = this.props.items.map(function(item) {
return (
<div>
<ItemModule
key={item.name}
name={item.name} />
</div>
);
});
return (
<div className='pure-menu'>
<h3>Islam Pillars</h3>
<ul>
{listItems}
</ul>
</div>
);
}
});
ReactDOM.render(<RepeatModule items={items} />,
document.getElementById('react-content'));
-UPDATE-
fiddle version 16
updated fidle, now there is issue with key, also, the onClick did not update the value for all the list item. Is there something wrong I did?
-UPDATE-
fiddle version 20
Now the only issue is change all the list item's name to 'lalala' when I run changeName of onClick.
remove the parenthesis from
onClick={this.changeName()},
so
onClick={this.changeName}
you want to call the function onClick, but you are calling it on render that way
I think you meant to do onClick={this.changeName}
In the way you have it you are calling the changeName function on render instead of on click.
I have a component which is updated by a parent component by passing a prop. Within the componentWillReceiveProps i would like to change a state (availableData) which contains the newly added data from the prop (newData).
The prop is named newData, and the state which is updated is named availableData.
When i attempt to access the availableData where i concatenate new (unique) data i get following error:
Uncaught TypeError: Cannot read property 'availableData' of undefinedInline JSX script:79
And the code snippet:
var DataList = React.createClass({
getInitialState: function() {
return {availableData: []};
},
componentWillReceiveProps: function(nextProps) {
var availableData = this.state.availableData;
var newData = nextProps.newData;
if (_.isEmpty(availableData)) {
this.setState({availableData: nextProps.newData});
} else {
_.each(newData, function(_newData) {
var isDuplicate = false;
_.each(availableData, function(_availableData) {
if(isSameData(_availableData, _newData)) {
isDuplicate = true;
}
});
if (!isDuplicate) {
console.log(_newData);
this.setState({ availableData: this.state.availableData.concat([_newData]) });
}
});
}
},
handleClick: function (_data) {
},
render: function() {
var dataItems = this.state.availableData.map(function (_data, index) {
return <DataItem data={_data} key={index} onClick={this.handleClick.bind(this, _data)} />;
}, this);
return (
<div className="col-lg-3">
<ul className="list-group">
<li className="list-group-item active">Data</li>
{dataItems}
</ul>
</div>
);
}
});
Failing on:
this.setState({ availableData: this.state.availableData.concat([_newData]) });
UPDATE:
Solved by setting var _this = this; outside the loop and referring to _this, unfortunately all setStates are not being initialized.
this isn't the component in the context of your duplicate. You need to pass the outer most this to your _.each.
_.each(list, iteratee, [context])
I have a dynamic list of children, that are form inputs.
ex:
var FormRows = React.createClass({
getInitialState: function() {
return {
rows: []
}
},
createRows: function() {
this.props.values.maps(value){
rows.push(<FormRow ...handlers... ...props... value={value} />
}
},
addNewRow{
// add a new row
},
render: function() {
return (
<div>
{this.state.rows}
</div>
);
});
var FormRow = React.createClass({
getInitialState: function() {
return {
value: this.props.value || null
}
},
render: function() {
<input type='text' defaultValue={this.state.value} ...changeHandler ... }
}
});
This is a dumbed down version , but the idea, is a its a dynamic form, where the user can click a plus button to add a row, and a minus button, which will set the row to visibility to hidden.
This state is nested n levels deep. What is the best way to actually get the state out of the children, and submit the form? I can use 'ref' add a function to getFormValue(): { return this.state.value } to the FormRow button, but i'm not sure if thats the best practice way.
I find myself using this pattern quite often, an array of undetermined size of children, that need to pass the state up.
Thanks
It’s not a dumb question at all, and a good example of using flux principals in React. Consider something like this:
var App
// The "model"
var Model = {
values: ['foo', 'bar'],
trigger: function() {
App.forceUpdate()
console.log(this.values)
},
update: function(value, index) {
this.values[index] = value
this.trigger()
},
add: function() {
this.values.push('New Row')
this.trigger()
}
}
var FormRows = React.createClass({
addRow: function() {
Model.add()
},
submit: function() {
alert(Model.values);
},
render: function() {
var rows = Model.values.map(function(value, index) {
return <FormRow key={index} onChange={this.onChange} index={index} value={value} />
}, this)
return (
<div>{rows}<button onClick={this.addRow}>Add row</button><button onClick={this.submit}>Submit form</button></div>
)
}
})
var FormRow = React.createClass({
onChange: function(e) {
Model.update(e.target.value, this.props.index)
},
render: function() {
return <input type='text' defaultValue={this.props.value} onChange={this.onChange} />
}
});
App = React.render(<FormRows />, document.body)
I used a simplified model/event example using Array and forceUpdate but the point here is to let the model "own" the form data. The child components can then make API calls on that model and trigger a re-render of the entire App with the new data (Flux).
Then just use the model data on submit.
Demo: http://jsfiddle.net/ekr41bzr/
Bind values of inputs to some model (for example build in Backbone or Flux) and on submit retrieve values from there, without touching inputs.