React: Stateless component triggers both OnClick - javascript

I have the following directory structure:
Todo -> TodoList -> TodoItem
I pass todos, onSelection, onDeletion function from todo towards the list and so on to the item.
Both the list and item components are stateless, the issue is that, when I click the item, it fires onClick() as well as onSelect() method too due to bubbling, I don't want to use event.stopProppogation in any case.
I've also tried the method in stateless component as following but it fires up when the component is loaded.
TodoItem:
const TodoItem = ({text, onSelection, onDeletion, id}) => {
const wrapDeletion = (id) => {
onDeletion(id);
}
return (
<li className="list-group-item" onClick={onSelection.bind(this, id)}>
{text}
<button className="btn btn-danger float-right" onClick={wrapDeletion(id)}>
{BTN_ACTIONS.DELETE}
</button>
</li>
);
}
Todo List:
<ul className="list-group">
{todos.map(todo =>
<TodoItem
key={todo.id}
id={todo.id}
onSelection={onSelection}
text={todo.text}
onDeletion={onDeletion}/>)
}
</ul>
Todo:
<TodoList
todos={this.state.todos}
onSelection={onSelection}
onDeletion={onDeletion}
/>

you should intercept the click event and stop it from propagation. To do so, wrap a div and apply it the stopPropagation like so
const TodoItem = ({text, onSelection, onDeletion, id}) => {
const wrapDeletion = (id) => {
onDeletion(id);
}
return (
<li className="list-group-item" onClick={onSelection.bind(this, id)}>
{text}
<div className="clickinterceptor" onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}>
<button className="btn btn-danger float-right" onClick={wrapDeletion(id)}>
{BTN_ACTIONS.DELETE}
</button>
</div>
</li>
);
}
The docs for this are here https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation

Make sure you aren’t calling the function when you pass it to the
component
You are giving a STRAIGHT CALL to a function, while passing it as a all back. Following is one of the solution, passing the callback as a "function returning the call".
const TodoItem = ({text, onSelection, onDeletion, id}) => {
const wrapDeletion = (id) => {
onDeletion(id);
}
return (
<li className="list-group-item" onClick={onSelection.bind(this, id)}>
{text}
<button className="btn btn-danger float-right" onClick={() => wrapDeletion(id)}>
{BTN_ACTIONS.DELETE}
</button>
</li>
);
}
You can further read it here!
https://reactjs.org/docs/faq-functions.html#how-do-i-pass-a-parameter-to-an-event-handler-or-callback

Related

How to update parent's component using state from child component in React?

I have 3 components.
In ListCard.js, I map cards array and based on the card the user click on, I call handleChangeCardData to update the modal's text.
My question is: How do I update/change the modal's text when my handleChangeCardData function is inside ListCard.js and my modal is on the same level. (Both are in Board.js)
Board.js
const [cardTitle, setCardTitle] = useState("");
return (
{columns.map((column, index) => (
<div className="column__container" key={index}>
<div className="column__header">
<div className="columnHeader__name">
<p>{column.name ? column.name : "..."}</p>
</div>
<div className="columnHeader__button">
<button
className="btn btn-sm --create-card-btn"
data-bs-toggle="modal"
data-bs-target="#modal-card"
onClick={() => setColumnId(column.id)}
>
New item
</button>
</div>
</div>
<Droppable droppableId={column.id}>
{(provided, snapshot) => (
<div
className="column"
ref={provided.innerRef}
{...provided.droppableProps}
>
<ListCard columnId={column.id} />
{provided.placeholder}
</div>
)}
</Droppable>
</div>
))}
<ViewCardModal cardTitle={cardTitle} />
)
LisCard.js
const handleChangeCardData = (cardTitle) => {
setCardTitle(cardTitle);
}
return (
{cards.map((card, index) => (
<>
<div key={index}>
<Draggable draggableId={card.id} index={index}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<div
className="card --listcard-card"
onClick={() => handleChangeCardData(card.title)}
data-bs-toggle="modal"
data-bs-target="#modal-card-details"
style={{ border: `2px solid ${card.color}` }}
>
<div className="card-body">
<p>{card.title}</p>
</div>
</div>
</div>
)}
</Draggable>
</div>
</>
))}
)
ViewCardModal.js
function ViewCardModal(props) {
return (
<div>{props.cardTitle}</div>
)
}
In general, lift state up. In this case, it sounds like that means moving the state into Board and then passing that state to whatever child components need it (as a prop), and the state setter to whatever (other) child components need it.
Here's a minimal example of lifting state up. I haven't tried to recreate the full complexity of your example, just to provide an example of Parent having state that ChildA uses and ChildB sets:
const {useState} = React;
const ChildA = React.memo(({counter}) => {
console.log("ChildA rendered");
return <div>Counter = {counter}</div>;
});
const ChildB = React.memo(({setCounter}) => {
console.log("ChildB rendered");
return <input
type="button"
value="Increment Counter"
onClick={() => setCounter(c => c + 1)}
/>;
});
const Parent = () => {
const [counter, setCounter] = useState(0);
return (
<div>
<ChildA counter={counter} />
<ChildB setCounter={setCounter} />
</div>
);
};
ReactDOM.render(<Parent />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.development.js"></script>
If there are several levels of hierarchy between where the state is being held and a descendant component that needs it, you might use context instead of props (although you might also look at component composition instead). See those links for details.
you cant do that directly, but must use props.
in list:
onClick={() => props.onClick(card.title)}
in board:
handleChangeCardData = (cardTitle) => {
setCardTitle(cardTitle);
}
<ListCard columnId={column.id} onClick={(e)=>handleChangeCardData(e)}/>
Inside ListCard:
const ListCard = ({setCardTitle}) => {...}
onClick={() => setCardTitle(card.title)}
In the parent:
<ListCard columnId={column.id} setCardTitle={setCardTitle} />

Using React Hooks How Can I Have two search inputs that work together to filter results

I apologize if this is unclear. Let me know if any of this need clarification.
I have two search input. One that sorts by name and another that sorts by tags. Filtering the names was easy enough since I was grabbing that data from an API. However, filtering the data by tags is proving difficult. What would be the best way to set this up?
I have three main components: Search.js Profile.js and Tags.js. Search just passes the user input down. Profile loops over the APi data and filters by name. The Tags component allows the user to add and remove tags. This is placed inside the .map in profile. Since the tags component is inserted into the profile it creates the desired effect of allowing each one to have it's own set of tags, but I can't figure out to transfer the tags info in to state and then filter the profiles that have the searched tag.
Profile Component
const createProfile = (profile) => {
const gradesToNum = profile.grades.map((num) => parseInt(num, 10));
const getAverage = gradesToNum.reduce((a, b) => a + b) / gradesToNum.length;
const getAllGrades = profile.grades.map(renderGrades);
return (
<div key={profile.id} className="profileWrapper">
<div className="profileCard">
<div className="studentImg">
<img src={profile.pic} alt={profile.firstName} />
</div>
<div className="studentBio">
<h3>
{profile.firstName} {profile.lastName}
</h3>
<ul className="studentInfo">
<li>Email: {profile.email}</li>
<li>Company: {profile.company}</li>
<li>Skill: {profile.skill}</li>
<li>Average: {getAverage}%</li>
</ul>
<div className={shownGrades[profile.id] ? 'show' : 'hide'}>
<ul>{getAllGrades}</ul>
<Tags />
</div>
</div>
</div>
<button className="expand-btn" onClick={() => toggleGrade(profile.id)}>
{shownGrades[profile.id] ? (
<i className="fas fa-minus"></i>
) : (
<i className="fas fa-plus"></i>
)}
</button>
</div>
);
};
const getProfile = () =>
props.students.filter(props.filterByName).map(createProfile);
return <section className="wrapper">{getProfile()}</section>;
**Tags Component**
```const Tags = (props) => {
const [tags, setTags] = useState([]);
const addTag = (e) => {
if (e.key === 'Enter' && e.target.value.length > 0) {
setTags([...tags, e.target.value]);
e.target.value = '';
}
};
const removeTags = (indexToRemove) => {
setTags(
tags.filter((x, index) => {
return index !== indexToRemove;
})
);
};
return (
<div className="tags-input">
<ul>
{tags.map((tag, index) => {
return (
<li key={index}>
<span>{tag}</span>
<i className="fas fa-times" onClick={() => removeTags(index)}></i>
</li>
);
})}
</ul>
<input
type="text"
placeholder="press enter to add tag"
onKeyUp={addTag}
id="tag-input"
/>
</div>
);
};```
If you have more complex state handling it's a good practice to use React's useReducer instead of useState for your TagsComponent. https://reactjs.org/docs/hooks-reference.html#usereducer
You can simplify your TagsComponent like:
const initialState = [];
function reducer(state, action) {
switch (action.type) {
case 'addTag':
return [...state, action.payload];
case 'removeTag':
return state.filter(tag => tag !== action.payload)
default:
throw new Error();
}
}
const Tags = (props) => {
const [state, dispatch] = useReducer(reducer, initialState);
const handleAddTag = event => {
if (e.key === 'Enter' && e.target.value.length > 0) {
dispatch({type: 'addTag', payload: e.target.value});
e.target.value = '';
}
}
};
return (
<div className="tags-input">
<ul>
{tags.map((tag, index) => {
return (
<li key={index}>
<span>{tag}</span>
<i className="fas fa-times" onClick={() => dispatch({type: 'removeTag', payload: index})}></i>
</li>
);
})}
</ul>
<input
type="text"
placeholder="press enter to add tag"
onKeyUp={handleAddTag}
id="tag-input"
/>
</div>
);
};

Test onClick function using Jest within internal component

I have a parent component for a navigation bar and would like to test an imported Toggle component that acts as a button with an onClick prop.
const Header = ({ brandLogo, links }: HeaderProps) => {
const [navOpen, setNav] = useState(false);
return (
<Container>
<div className='toggle-container'>
<Toggle active={navOpen} onClick={() => setNav(!navOpen)} />
</div>
<div className={navOpen ? 'menu open' : 'menu closed'}>
{links ? (
<ul>
{links.map(link => (
<li key={uuid.v4()} className='page-links'>
<a href={link.href}>{link.textKey}</a>
</li>
))}
</ul>
) : null}
<div className='menu-buttons'>
<Button text='Log In' />
<Button text='Register' />
</div>
</div>
{brandLogo ? (
<a className='brand-logo' href={brandLogo.href}>
{brandLogo.image}
</a>
) : null}
{links ? (
<ul className='links-container'>
{links.map(link => (
<li key={uuid.v4()} className='page-links'>
<a href={link.href}>{link.textKey}</a>
</li>
))}
</ul>
) : null}
<div className='button-container'>
<Button text='Log In' />
<Button text='Register' />
</div>
</Container>
);
};
export default Header;
Segment I would like to test
<Toggle active={navOpen} onClick={() => setNav(!navOpen)} />
I have seen much documentation on testing the onClick functionality on the parent component but not on a child component.
current tests im running are -
describe('Footer Component', () => {
it('Component should render with all config set', () => {
const { asFragment } = render(<Header {...config} />);
expect(asFragment()).toMatchSnapshot();
});
it('Component should only render links if they are present', () => {
const { asFragment } = render(<Header {...onlyLinks} />);
expect(asFragment()).toMatchSnapshot();
});
it('Component should only render links if they are present', () => {
const { asFragment } = render(<Header {...onlyBrandLogo} />);
expect(asFragment()).toMatchSnapshot();
});
});
Any help would be appreciate thanks :)

I want Popup to close, how do I do it?

When I press the AddAction button from the Addaction component, I want the popup to close. ?
in fact, if I reach the onCloseAddActionModal method in my component which is popup from AddAction component, my problem will be solved.
AddAction Component:
class AddAction extends React.Component {
constructor() {
super();
this.state = {
items: [{id:null, actiontype: null}],
error: null,
isLoaded: false,
selectId: null,
}
this.handleCheckChieldElement =
this.handleCheckChieldElement.bind(this); // set this, because you need get methods from CheckBox
}
componentDidMount = () => {
....
}
fetchAdd = (carid, offboardingactiontypeid) => {
...
}
handleCheckChieldElement = (id, e) => {
this.setState({selectId: id})
}
render() {
const items = this.state.items;
return (
<div>
<ul className="popupAddAction">
{
items.map((item) => {
return (
<li className="list" key={item.id}>
<input key={item.id} onClick=
{(e)
=>
this.handleCheckChieldElement(item.id,
e)} type="checkbox" />
{item.actiontype}
</li>
)
})
}
</ul>
<div className="popupAddAction--btn">
<button
onClick=
{ () =>
this.fetchAdd(this.props.id, this.state.selectId)}
className="btn btn-primary
popupAddAction--btn"
>
Add Action
</button>
</div>
</div>
);
}
}
export default AddAction;
Popup Component:
class OffBoarding extends Component {
this.state = {
openAddAction: false
};
onOpenAddActionModal = () => {
this.setState({ openAddAction: true });
};
onCloseAddActionModal = () => {
this.setState({ openAddAction: false });
};
render(){
return{
<div>
<Button className="btn btn-danger commentPlus" onClick=
{this.onOpenAddActionModal}> <FontAwesomeIcon icon=
{faPlus}/></Button>
</div>
{this.state.openAddAction ?
<div style={styles}>
<Modal open=
{this.state.openAddAction} onClose=
{this.onCloseAddActionModal} center>
<AddAction id=
{this.state.carid}
close=
{this.state.openAddAction}/>
</Modal>
</div> : null
}}
}}
You can simply pass the onCloseAddActionModal method as prop while rendering AddAction component from OffBoarding component. And then, you can call that passed function as prop on "Add Action" button click i.e.
So in you popup component, change this:
<AddAction id=
{this.state.carid}
close={this.state.openAddAction}/>
to this (passing function as prop):
<AddAction id=
{this.state.carid}
close={this.state.openAddAction}
closeDialog={this.onCloseAddActionModal}/>
And then in your AddAction component, change this:
<button
onClick={() =>
this.fetchAdd(this.props.id, this.state.selectId)}
className="btn btn-primary popupAddAction--btn">
Add Action
</button>
to this (calling function passed as prop in previous step):
<button
onClick=
{() =>{
this.fetchAdd(this.props.id, this.state.selectId);
this.props.closeDialog();
}}
className="btn btn-primary popupAddAction--btn">
Add Action
</button>
If openAddAction flag is true then only addaction component will display right. Instead of open and close modal add below code to modal and in add action modal in fetch method set openAddAction to false. in your code you have communication from child to parent but you are trying to close modal based on child but modal exist in parent so make a communication to parent to child for that pass function through component
<Modal isOpen = {this.state.openAddAction} center>
<AddAction id= {this.state.carid}
closeModa={this.onCloseAddActionModal} />
</Modal>
In addAction modal you have to add like this
fetchAdd = (carid, offboardingactiontypeid) => {
this.setState({openAddAction:false});
this.props.onCloseAddActionModal();
}
Call this closeModal method in fetchAdd method

<UL> list item not being added in React

So this is my props if I console.log(this.props)
list:Array(1):
{user: "Jack Nicholson", userid: "5b684ed8d3eb1972b6e04d32", socket: "1c0-Jb-kxe6kzPbPAAAD"}
However when I map through my list and use component <UserItem user={user.user} />; My UserItem keeps returning undefined.
render() {
const UserItem = user => (
<li>
<div className="btn-group dropup">
<button
type="button"
className="btn btn-secondary dropdown-toggle"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
{user}
{console.log(user)}
</button>
<div className="dropdown-menu" />
</div>
</li>
);
return (
<ul>
{this.props.list.map((user, i) => {
console.log(this.props);
<UserItem user={user.user} />;
})}
</ul>
);
}
Arrow function in JS come in two forms: "concise body" and "block body". In concise form, as in your const UserItem = user => ( function, the provided expression is implicitly returned.
However, in block form, as in your this.props.list.map((user, i) => { function, you must use an explicit return statement.
Try adding one to your code:
{this.props.list.map((user, i) => {
console.log(this.props);
return <UserItem user={user.user} />;
})}
You could simply use round brackets instead of curly braces;
return (
<ul>
{
this.props.list.map((user, i) => (
<UserItem user={user.user} />
))
}
</ul>
);
If you are getting the list as API response then handle the initial rendering also.However,return your UserItem component in the map function.try this code
return (
<ul>
{this.props.list && this.props.list.map((user, i) => {
console.log(this.props);
return <UserItem user={user.user} />;
})}
</ul>
);
You need to add a return statement before <UserItem user={user.user} /> ,also make sure to check if this.props.list exists before rendering it.
like so:
{this.props.list? this.props.list.map((user, i) => {
console.log(this.props);
return <UserItem user={user.user} />;
}):<span/>}

Categories