I feel like I've done this a million times, but possibly not with a mapping function. I have a collection of activities that I'm creating checkboxes with the code is as follows (__map is coming from Lodash by the way):
<div className="activity-blocks">
{
__map(this.state.activities, (activity, i) => {
return (
<div key={ i } className="single-activity">
<input id={ `register-activity-${ i }` }
type="checkbox" className="register-multiselect"
name="main_activity" checked={ !!activity.checked }
onChange={ (e) => this.toggleActivity(e, activity.id) }
/>
<label htmlFor={ `register-activity-${ i }` }>{ activity.name }</label>
</div>
)
})
}
My onChange handler looks like this:
toggleActivity(e, activityId) {
let activities = { ...this.state.activities };
__forEach(this.state.activities, (activity) => {
if (activityId === activity.id) {
activity = __assign(activity, { checked: e.target.checked });
return false;
}
});
this.setState({ activities: activities });
}
The handler works just fine, it's for reference mostly. My issue is that the label is not firing the handler. I have tested only the checkbox input with no label and it fires off the handler. Does anyone know why the label is not doing so?
More info: I've had this problem before but it usually turns out to be an id mismatch or something simple like that. I don't believe so this time.
Edit
I have deleted pretty much all the code from my file and incorporated Slawa's answer below and it still doesn't behave as expected. Here is the code including all imports, the class structure and all unnecessary code stripped out, including styles:
import React from 'react';
import __forEach from 'lodash/forEach';
import __assign from 'lodash/assign';
import __map from 'lodash/map';
export default class RegisterActivities extends React.Component {
constructor(props) {
super(props);
this.state = {
activities: [
{
id: 1,
name: 'label-1'
},
{
id: 2,
name: 'label-2'
},
{
id: 3,
name: 'label-3'
}
],
}
}
toggleActivity(e, activityId) {
const { activities } = this.state;
const updated = activities.map((activity) => {
if (activity.id === activityId){
return {
...activity,
checked: e.target.checked
}
}
return activity;
});
}
render() {
return (
<div>
<p>what do you do?</p>
<div>
{
__map(this.state.activities, (activity, i) => {
return (
<div key={ i }>
<input id={ `register-activity-${ i }` }
type="checkbox"
name="main_activity" checked={ !!activity.checked }
onChange={ (e) => this.toggleActivity(e, activity.id) }
/>
<label htmlFor={ `register-activity-${ i }` }>{ activity.name }</label>
</div>
)
})
}
</div>
</div>
);
}
}
Edit2
I'm playing around and decided to replce onChange with onClick and now I'm able to click the actual checkbox to hit toggleActivity. Does anyone have an idea on why onChange isn't firing?
The answer to my question was found on this SO post. It turns out that my label's onClick event (behind the scenes of html/React, not coded myself) was bubbling up to the parent instead of handling the input. I needed to tell the label to stop propagating by doing onClick={ e => e.stopPropagation() }.
I think, you have an error in your toggleActivity function, activities is array and try to convert it to object, you can find my full solution of this problem here: https://codesandbox.io/s/wqyolw50vl
toggleActivity(e, activityId) {
const { activities } = this.state;
const updated = activities.map( activity => {
if (activity.id === activityId){
return {
...activity,
checked: !activity.checked
}
}
return activity;
})
this.setState({ activities: updated });
}
I was having this same issue. It was driving me mad, but what my team and I discovered is there must be some bug with the htmlFor they tell you to use in the docs. We removed it from our checkbox prop and now it functions as expected with no weird side effects. I'm opening up a GH issue on it. I'll edit later with the link/update
Code that didn't work:
<label htmlFor={`${label}-checkbox`} className={checked ? "checked data-filter-checkbox" : "data-filter-checkbox"} >
<input id={`${label}-checkbox`} type="checkbox" onChange={(event) => handleCheck(event)} value={label} name={label} checked={checked} />
{label}
<span>{count}</span>
</label>
Code I'm using now:
<label className={checked ? "checked data-filter-checkbox" : "data-filter-checkbox"} >
<input id={`${label}-checkbox`} type="checkbox" onChange={(event) => handleCheck(event)} value={label} name={label} checked={checked} />
{label}
<span>{count}</span>
</label>
Related
I have a simple To do app that takes user input through a prompt and adds it to a list. I want to change the prompt and replace it with a text input bar. I've been having trouble implenting onChange and I'm hitting a wall trying to figure this out. I really appreciate the help as a beginner trying to learn.
import React, { useState } from 'react';
let id = 0
const Todo = props => (
<li>
<input type="checkbox" checked={props.todo.checked} onChange={props.onToggle} />
<button onClick = {props.onDelete}> delete</button>
<span>{props.todo.text}</span>
</li>
)
class App extends React.Component {
constructor() {
super()
this.state = {
todos: [],
}
}
addTodo() {
const text = prompt("TODO TEXT PLEASE!")
this.setState({
todos: [...this.state.todos, {id: id++, text: text, checked: false},
]
})
}
removeTodo(id) {
this.setState({
todos: this.state.todos.filter(todo => todo.id !== id )
})
}
toggleTodo(id) {
this.setState({
todos: this.state.todos.map(todo => {
if (todo.id !== id) return todo
return {
id: todo.id,
text: todo.text,
checked: !todo.checked,
}
})
})
}
render() {
return (
<div>
<div> Todo Count: {this.state.todos.length}</div>
<div> Unchecked Count: {this.state.todos.filter(todo => !todo.checked).length} </div>
<div className="App">
<label> Task Name:</label>
<input type="text" id="task"
/* onChange={(e)=> {
setTaskName(e.target.value);*/
/>
<button onClick={() => this.addTodo()}> Add ToDo </button>
</div>
<ul>
{this.state.todos.map(todo => (
<Todo
onToggle={() => this.toggleTodo(todo.id)}
onDelete={() => this.removeTodo(todo.id)}
todo={todo}
/>
))}
</ul>
</div>
)
}
}
To resolve this issue you are having, you will need to also add a local state to store your input field data (you could get this with a Controlled or Uncontrolled approaches):
this.state = {
currentTodo: "",
todos: [],
}
And then in your input onChange event as you already did (you were using a hook based approach though), you could add a state update for your currentTodo value as you write. And also the current state value in the input tag (as Cesare observed):
<input type="text" id="task"
value={this.state.currentTodo}
onChange={(e)=> {
this.setState({ ...this.state, currentTodo: e.target.value});
}
/>
Finally, to obtain the wroten text you can get it in your addTodo method.
const text = this.state.currentTodo;
My onchange function on my checkbox is always undefined. I made a sandbox to simplify my issue. I've tried changing it back and forth between an arrow and a regular function, and leaving the binding as its default state vs tying it to its binding in the constructor.
import "./styles.css";
import React, { Component } from "react";
class App extends Component {
constructor(props) {
super(props);
this.renderCheckboxes = this.renderCheckboxes.bind(this);
this.checkboxChange = this.checkboxChange.bind(this);
}
checkboxChange = (event) => {
console.log("CHANGED");
}
renderCheckboxes(checkboxes) {
return checkboxes.map(function (obj, idx) {
return (
<div>
<label>{obj.name}</label>
<input type="checkbox" onChange={this.checkboxChange} />
</div>
);
});
}
render() {
const cbList = [
{ name: "A", mood: "Happy" },
{ name: "B", mood: "Sad" },
{ name: "C", mood: "Mad" }
];
return <>{this.renderCheckboxes(cbList)}</>;
}
}
export default App;
My Sandbox
Your function is undefined because you are losing your context when your checkboxes.map happens.
renderCheckboxes(checkboxes) {
return checkboxes.map(function (obj, idx) { // here
return (
<div>
<label>{obj.name}</label>
<input type="checkbox" onChange={this.checkboxChange} />
</div>
);
});
}
You can change the function for an arrow function, so you won't lose context:
renderCheckboxes(checkboxes) {
return checkboxes.map((obj, idx) => { // here
return (
<div>
<label>{obj.name}</label>
<input type="checkbox" onChange={this.checkboxChange} />
</div>
);
});
}
It might not be the case, but if you need to send some parameter on your checkboxChange call, you could use it like this:
checkboxChange = (value) => { // need to set the variable here
console.log(value);
}
renderCheckboxes(checkboxes) {
return checkboxes.map((obj, idx) => {
return (
<div>
<label>{obj.name}</label>
<input type="checkbox" onChange={() => this.checkboxChange('test')} /> <!-- so you can pass it from here -->
</div>
);
});
}
Easy fix, it's because the binding doesn't work within the map function like you have now. To see this in action, try just rendering one checkbox, without map and you'll see your checkbox change method work.
Two changes I suggest:
Change map method to an arrow function:
return checkboxes.map((obj, idx) => {
...
}
Change onChange to an arrow function:
onChange={() => this.checkboxChange()}
import React, { Component, createElement } from "react";
export default class TodoList extends Component {
state = {
todo: [],
inputValue: "",
};
addTodo = () => {
this.setState({ todo: [...this.state.todo, this.state.inputValue] });
// I want to insert separate paragraph tags (todos from this.state.todo) into the list element here
};
handleKeyDown = (e) => {
if (e.keyCode === 13) {
if (this.state.inputValue === "") return;
this.addTodo();
}
};
handleChange = (e) => {
this.setState({ inputValue: e.target.value });
};
render() {
return (
<div className="list"></div>
<input
type="text"
className="insertTodo"
placeholder="Add a new todo!"
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
value={this.state.inputValue}
/>
);
}
}
I am creating a Todo List where the user types into an input, and the todo is then inserted into the div with class list element. I'm new to React so I don't know the best way I should go about doing this.
Any help is appreciated, thanks!
You can map the array, inside the .list div, and render each todo item, by wrapping it in p tag. I have added a button element, to handle the addTodo() function.
Also, you may want to move the .list div, below the input.
import React, { Component, createElement } from "react";
export default class TodoList extends Component {
state = {
todo: [],
inputValue: ""
};
addTodo = () => {
this.setState({ todo: [...this.state.todo, this.state.inputValue] });
// I want to insert separate paragraph tags (todos from this.state.todo) into the list element here
};
handleKeyDown = (e) => {
if (e.keyCode === 13) {
if (this.state.inputValue === "") return;
this.addTodo();
}
};
handleChange = (e) => {
this.setState({ inputValue: e.target.value });
};
render() {
return (
<div>
<div className="list">
{this.state.todo.map((todo) => {
return <p>{todo}</p>;
})}
</div>
<input
type="text"
className="insertTodo"
placeholder="Add a new todo!"
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
value={this.state.inputValue}
/>
<button onClick={this.addTodo}>Add Todo</button>
</div>
);
}
}
Codesandbox link - https://codesandbox.io/s/busy-pascal-txh55?file=/src/App.js
class TodoList extends Component {
state = {
todo: [],
inputValue: "",
};
addTodo = () => {
// I want to insert separate paragraph tags (todos from this.state.todo) into the list element here
// Hint: when you want to add a todo, you simply set input value to empty here.
this.setState({
todo: [...this.state.todo, this.state.inputValue],
inputValue: "",
});
};
handleKeyDown = (e) => {
if (e.keyCode === 13) {
if (this.state.inputValue === "") return;
this.addTodo();
}
};
handleChange = (e) => {
// Hint: I prefer adding "...this.state" every time before updating.
this.setState({ ...this.state, inputValue: e.target.value });
};
render() {
return (
<>
{
// Hint: use React fragment ("<> ... </>") when there's
more than one element in the first level.
}
<div className="list">
{
// Hint: Adding the current list with map in here
}
<ul>
{this.state.todo.map((t, i) => (
<li key={i}>{t}</li>
))}
</ul>
</div>
{
// I would prefer adding it inside a form element and instead of onKeyDown, use onSubmit on the form
// (on enter it will submit automatically, but you will have to do an e.preventDefault() to not refresh the page).
}
<input
type="text"
className="insertTodo"
placeholder="Add a new todo!"
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
value={this.state.inputValue}
/>
</>
);
}
}
This is a working example with a few comments. Also setState runs asyncrounously so it's not a good idea to run multiple one at the same time. Hope this helps.
Using map like TechySharnav mentioned is a quick way of doing it. But if you need to do some more complex operations/layout stuff, then writing a custom function and calling it in the render jsx might be cleaner. So, you could have a function like:
renderItems() {
var rows = []
this.state.todo.forEach((elem, idx) => {
// for example
rows.push(
<p>{elem}</p>
)
});
return rows;
}
Then call it inside render:
//...
<div className="list">
{this.renderItems()}
</div>
//...
js map will certainly solve the problem.
this small snippet for printing the list,
render() {
return (
<div className="list">
{ this.state.todo.map((item) => {
return <p>{item}</p>
})}
</div>
<input
type="text"
className="insertTodo"
placeholder="Add a new todo!"
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
value={this.state.inputValue}
/>
);
}
This is my simple functional component in Vuejs
import { Label, Input, Checkmark } from "./styles";
export default {
functional: true,
model: {
prop: "checked",
event: "change"
},
props: {
checked: Boolean
},
// eslint-disable-next-line
render(h, { props, listeners}) {
console.log(listeners);
const changeHandler = listeners.change ? listeners.change : () => {};
return (
<Label>
<Input
type="checkbox"
checked={props.checked}
onChange={e => {
console.log("checked", e.target.checked);
changeHandler(e);
}}
/>
<Checkmark class="checkmark" />
</Label>
);
}
};
The component simply won't console.log when used like
<VCheckbox
checked={this.checkSelections[idx]}
onChange={e => {
this.$set(this.checkSelections, idx, e.target.checked);
}}
/>
I'm using Vuejs - 2.5.x
If I console.log my listeners they definitely log change.
Second, if I use nativeOnChange instead, it does fire.
Last and importantly, In the very same fashion, I also have a button component (functional ) and it works fine with onClick ( no use of native there ). Can anyone please let me know what's going on ?
Update -
Similar is the case with nativeOnMouseover.
So I've set up a file to setState() for onBlur and onFocus in the SocialPost.js file. But when I onClick a <div> in the SocialPostList.js (the parent) where it activates the parameterClicked() function in the SocialPost.js file, the <input> in SocialPost.js becomes blurred.
How do I make it so that the <button> onClick in SocialPostList.js does not take the focus() from the <input> in SocialPost.js?
I've tried e.preventDefault() and e.stopPropagation() without success. The files are below, any help would be appreciated!!!
SocialPostList.js
import React, { Component } from 'react'
import { graphql, gql } from 'react-apollo'
import SocialPost from './SocialPost'
class SocialPostList extends Component {
render() {
const PostListArray = () => {
return(
<div onClick={(e) => {e.preventDefault(); e.stopPropagation()}}>
{this.props.allParametersQuery.allParameters.map((parameter, index) => (
<div
key={index}
onClick={(e) => {e.preventDefault();e.stopPropagation();this.child.parameterClicked(parameter.param, parameter.id)}}
>{'{{' + parameter.param + '}}'}</div>
))}
</div>)
}
return (
<div>
...
<PostListArray />
{this.props.allSocialPostsQuery.allSocialPosts.map((socialPost, index) => (
<SocialPost
ref={instance => {this.child = instance}}
key={socialPost.id}
socialPost={socialPost}
index={index}
deleteSocialPost={this._handleDeleteSocialPost}
updateSocialPost={this._handleUpdateSocialPost}
allParametersQuery={this.props.allParametersQuery}/>
))}
...
</div>
)
}
}
const ALL_SOCIAL_POSTS_QUERY = gql`
query AllSocialPostsQuery {
allSocialPosts {
id
default
message
}}`
export default graphql(ALL_SOCIAL_POSTS_QUERY, {name: 'allSocialPostsQuery'})(SocialPostList)
SocialPost.js
import React, { Component } from 'react'
class SocialPost extends Component {
constructor(props) {
super(props)
this.state = {
message: this.props.socialPost.message,
focus: false
}
this._onBlur = this._onBlur.bind(this)
this._onFocus = this._onFocus.bind(this)
}
_onBlur() {
setTimeout(() => {
if (this.state.focus) {
this.setState({ focus: false });
}}, 0);
}
_onFocus() {
if (!this.state.focus) {
this.setState({ focus: true });
}
}
render() {
return (
<div className='socialpostbox mb1'>
<div className='flex'>
<input
onFocus={this._onFocus}
onBlur={this._onBlur}
type='text'
value={this.state.message}
onChange={(e) => { this.setState({ message: e.target.value})}}/>
</div>
</div>
)
}
parameterClicked = (parameterParam) =>{
if (!this.state.focus) return
let message = this.state.message
let newMessage = message.concat(' ' + parameterParam)
this.setState({ message: newMessage })
}
export default SocialPost
Well, I don't think that's a React thing. It appears the blur event fires before the onClick, so the latter cannot prevent the former, and I'd expect event.stopPropagation to stop events bubbling from child to parent, not the other way around. In other words, I don't know how to stop it.
In all fairness this behaviour is expected - clicking somewhere else makes you lose focus. That said, here and elsewhere a solution is presented where you set up a flag on mouse down. Then, when blur fires, if it encounters the 'click flag' it may abstain from producing effects and may even refocus back.
If you choose to refocus, it is trivial to save a reference to the button or input, or querySelecting it (it's not too late or anything like that). Just be cautious that it is all too easy to set focus traps or mess up navigation for screen readers when you mix focus and javascript.