Have a CRA set up and all is working fine, there is one component in particular though that onClick events on an element are not firing. Based on Tim Smith's method here.
Code:
import React, { Component } from 'react'
import './SearchForm.css'
class SearchForm extends Component {
constructor(props) {
super(props)
this.state = {
filtered: []
}
this.handleChange = this.handleChange.bind(this)
this.handleSelect = this.handleSelect.bind(this)
}
componentDidMount() {
this.setState({
filtered: this.props.items
})
}
componentWillReceiveProps(nextProps) {
this.setState({
filtered: nextProps.items
})
}
handleChange(event) {
// Variable to hold the original version of the list
let currentList = []
// Variable to hold the filtered list before putting into state
let newList = []
// If the search bar isn't empty
if (event.target.value !== "") {
// Assign the original list to currentList
currentList = this.props.items
// Use .filter() to determine which items should be displayed
// based on the search terms
newList = currentList.filter(item => {
// change current item to lowercase
const lc = item.name.toLowerCase()
// change search term to lowercase
const filter = event.target.value.toLowerCase()
// check to see if the current list item includes the search term
// If it does, it will be added to newList. Using lowercase eliminates
// issues with capitalization in search terms and search content
return lc.includes(filter)
})
} else {
// If the search bar is empty, set newList to original task list
newList = this.props.items
}
// Set the filtered state based on what our rules added to newList
this.setState({
filtered: newList
})
}
handleSelect(string) {
console.log(string)
}
render() {
var _this = this
this.search_results = this.state.filtered.map(function(item, key) {
return (
// THESE onClicks DO NOT FIRE
<li key={item.id} onClick={(e) => _this.handleSelect(item.object)}>
<div className="name">{item.name}</div>
<small>{item.type}</small>
</li>
)
})
return (
<div className="search">
<input type="text" className="search-form" onChange={this.handleChange} placeholder="Search by client or advisor" />
<div className="results-container">
<ul className="results">
{this.search_results}
</ul>
// BUTTON HERE (in .results-container) DOES NOT FIRE
<button onClick={(e) => _this.handleSelect('hi')}>hi</button>
</div>
// BUTTON HERE (out of .results-container) FIRES
<button onClick={(e) => _this.handleSelect('hi')}>hi</button>
</div>
)
}
}
export default SearchForm
I have tried using just this vs. _this, binding/not binding the handleSelect function, passing and not passing event to it, passing and not passing any variables at all to it (and just having handleSelect console.log a 'firing'). I realize that the use of "this" within the map was a no go, thus the _this variable. Within that map, if I console.log(_this.handleSelect), it shows the function in the console.
Thank you in advance for taking a look!
Solved-- this ended up being an issue where the .results-container was hidden (via display: none;) and was only shown (via display: block) when the .results-container was focused on. Due to this no onClick events were firing on the items within .results-container.
Got around it by:
.search .results-container {
height: 0;
overflow: hidden;
}
input.search-form:focus + .results-container, .search .results-container:hover {
height: auto;
}
Thank you Acy for sending me in the right direction!
Related
How to push element inside useState array AND deleting said object in a dynamic matter using React hooks (useState)?
I'm most likely not googling this issue correctly, but after a lot of research I haven't figured out the issue here, so bare with me on this one.
The situation:
I have a wrapper JSX component which holds my React hook (useState). In this WrapperComponent I have the array state which holds the objects I loop over and generate the child components in the JSX code. I pass down my onChangeUpHandler which gets called every time I want to delete a child component from the array.
Wrapper component:
export const WrapperComponent = ({ component }) => {
// ID for component
const { odmParameter } = component;
const [wrappedComponentsArray, setWrappedComponentsArray] = useState([]);
const deleteChildComponent = (uuid) => {
// Logs to array "before" itsself
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5"},
{"uuid":"0ed3cc3-7cd-c647-25db-36ed78b5cbd8"]
*/
setWrappedComponentsArray(prevState => prevState.filter(item => item !== uuid));
// After
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5",{"uuid":"0ed3cc3-
7cd-c647-25db-36ed78b5cbd8"]
*/
};
const onChangeUpHandler = (event) => {
const { value } = event;
const { uuid } = event;
switch (value) {
case 'delete':
// This method gets hit
deleteChildComponent(uuid);
break;
default:
break;
}
};
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
onChangeOut: onChangeUpHandler,
};
setWrappedComponentsArray(wrappedComponentsArray => [...wrappedComponentsArray, objToAdd]);
// Have also tried this solution with no success
// setWrappedComponentsArray(wrappedComponentsArray.concat(objToAdd));
};
return (
<>
<div className='page-content'>
{/*Loop over useState array*/}
{
wrappedComponentsArray.length > 0 &&
<div>
{wrappedComponentsArray.map((props) => {
return <div className={'page-item'}>
<ChildComponent {...props} />
</div>;
})
}
</div>
}
{/*Add component btn*/}
{wrappedComponentsArray.length > 0 &&
<div className='page-button-container'>
<ButtonContainer
variant={'secondary'}
label={'Add new component'}
onClick={() => addOnClick()}
/>
</div>
}
</div>
</>
);
};
Child component:
export const ChildComponent = ({ uuid, onChangeOut }) => {
return (
<>
<div className={'row-box-item-wrapper'}>
<div className='row-box-item-input-container row-box-item-header'>
<Button
props={
type: 'delete',
info: 'Deletes the child component',
value: 'Delete',
uuid: uuid,
callback: onChangeOut
}
/>
</div>
<div>
{/* Displays generated uuid in the UI */}
{uuid}
</div>
</div>
</>
)
}
As you can see in my UI my adding logic works as expected (code not showing that the first element in the UI are not showing the delete button):
Here is my problem though:
Say I hit the add button on my WrapperComponent three times and adds three objects in my wrappedComponentsArray gets rendered in the UI via my mapping in the JSX in the WrapperComponent.
Then I hit the delete button on the third component and hit the deleteChildComponent() funtion in my parent component, where I console.log my wrappedComponentsArray from my useState.
The problem then occurs because I get this log:
(2) [{…}, {…}]
even though I know the array has three elements in it, and does not contain the third (and therefore get an undefined, when I try to filter it out, via the UUID key.
How do I solve this issue? Hope my code and explanation makes sense, and sorry if this question has already been posted, which I suspect it has.
You provided bad filter inside deleteChildComponent, rewrite to this:
setWrappedComponentsArray(prevState => prevState.filter(item => item.uuid !== uuid));
You did item !== uuid, instead of item.uuid !== uuid
Please try this, i hope this works
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item !== uuid));
};
After update
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item.uuid !== uuid)); // item replaced to item.uuid
};
Huge shoutout to #Jay Vaghasiya for the help.
Thanks to his expertise we managed to find the solution.
First of, I wasn't passing the uuid reference properly. The correct was, when making the objects, and pushing them to the array, we passed the uuid like this:
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
parentOdmParameter: odmParameter,
onChangeOut: function(el) { onChangeUpHandler(el, this.uuid)}
};
setWrappedComponentsArray([...wrappedComponentsArray, objToAdd]);
};
When calling to delete function the function that worked for us, was the following:
const deleteChildComponent = (uuid) => {
setWrappedComponentsArray(item => item.filter(__item => __item.uuid !== uuid)); // item replaced to item.uuid
};
I've tried to find a solution to this, but nothing seems to be working. What I'm trying to do is create a TreeView with a checkbox. When you select an item in the checkbox it appends a list, when you uncheck it, remove it from the list. This all works, but the problem I have when I collapse and expand a TreeItem, I lose the checked state. I tried solving this by checking my selected list but whenever the useEffect function runs, the child component doesn't have the correct parent state list.
I have the following parent component. This is for a form similar to this (https://www.youtube.com/watch?v=HuJDKp-9HHc)
export const Parent = () => {
const [data,setData] = useState({
name: "",
dataList : [],
// some other states
})
const handleListChange = (newObj) => {
//newObj : { field1 :"somestring",field2:"someotherString" }
setDataList(data => ({
...data,
dataList: data.actionData.concat(newObj)
}));
return (
{steps.current === 0 && <FirstPage //setting props}
....
{step.current == 3 && <TreeForm dataList={data.dataList} updateList={handleListChange}/>
)
}
The Tree component is a Material UI TreeView but customized to include a checkbox
Each Node is dynamically loaded from an API call due to the size of the data that is being passed back and forth. (The roots are loaded, then depending on which node you select, the child nodes are loaded at that time) .
My Tree class is
export default function Tree(props) {
useEffect(() => {
// call backend server to get roots
setRoots(resp)
})
return (
<TreeView >
Object.keys(root).map(key => (
<CustomTreeNode key={root.key} dataList={props.dataList} updateList={props.updateList}
)))}
</TreeView>
)
CustomTreeNode is defined as
export const CustomTreeNode = (props) => {
const [checked,setChecked] = useState(false)
const [childNodes,setChildNodes] = useState([])
async function handleExpand() {
//get children of current node from backend server
childList = []
for( var item in resp) {
childList.push(<CustomTreeNode dataList={props.dataList} updateList={props.updateList} />)
}
setChildNodes(childList)
}
const handleCheckboxClick () => {
if(!checked){
props.updateList(obj)
}
else{
//remove from list
}
setChecked(!checked)
}
// THIS IS THE ISSUE, props.dataList is NOT the updated list. This will work fine
// if I go to the next page/previous page and return here, because then it has the correct dataList.
useEffect(() => {
console.log("Tree Node Updating")
var isInList = props.dataList.find(function (el) {
return el.field === label
}) !== undefined;
if (isInList) {
setChecked(true);
} else {
setChecked(false)
}
}, [props.dataList])
return ( <TreeItem > {label} </TreeItem> )
}
You put props.data in the useEffect dependency array and not props.dataList so it does not update when props.dataList changes.
Edit: Your checked state is a state variable of the CustomTreeNode class. When a Tree is destroyed, that state variable is destroyed. You need to store your checked state in a higher component that is not destroyed, perhaps as a list of checked booleans.
I am trying to remove a child component by sending a function and an id and then calling that function
when a button in the child is clicked.
Note: The child is a class component, the parent a functional component
Here is the function defined in the parent:
const removeTable = (tableId) => {
const newArray = tables.filter((el) => (el.id !== tableId)
);
console.log(newArray);
setTables(newArray)
}
This is removing elements, but not the one I want. Instead of removing the element with the id I pass it, it keeps that number, starting from 0.
So when I click on the item with a id of 3, it only keeps 0-2. In this example it should keep 0-2 and 4-6. (The array is supposed to be 7 elements long, but somehow it is shortened (before the filter))
What I've tried and Discovered:
I was completely lost, so I decided to create a mock function without using the child:
const removeTabletest = () => {
const key = 1;
const testArr = [{id:1,op:"adsad"}, {id:2,op:"adsad"},{id:3,op:"adsad"} ];
const fml = testArr.filter( (el) => (el.id !== key));
console.log(fml)
}
This function works as I expect.
Finally I stumbled on the fact that when I console.log(tables) at the beginning of the function, I am not getting the same data as in React Dev Tools. The tables array is not the full array I expect.
But when I create another button that is called by the parent (instead of the child), then tables is logged correctly:
Any idea what is going on? or how else I can achieve this?
This sounds like a difficult approach to use with React. Generally what you want to do is just filter your array in the render method based on props or state. If you need a child element to modify what you filter by, like a button, you would pass a callback to the child. Here is an example:
class Table extends React.Component {
constructor(props) {
super(props);
this.state = { hiddenItems: [] };
}
hideItem = item => {
// to hide a row of the table we add it to the list
// of hidden items.
this.setState({ hiddenItems: [...this.state.hiddenItems, item] });
}
render() {
// Create the table element by filtering out hidden items
const table = this.props.items
.filter(item => !this.state.hiddenItems.includes(item))
.map(item => {
return (
<div key={item.id}>{item.contents}</div>
);
});
return (
<>
{table}
// the onClick function could also be passed to a
// child React object
<button onClick={() => this.hideItem(this.props.item[0])}>
Hide item 0
</button>
</>
);
}
}
Where this.props.items would look something like:
[{ id: 0, contents: 'blah blah'},
{ id: 1, contents: (<span>blah</span>)}]
Of course you can also have a function unhiding an item but hopefully this shows the general approach.
I'm working on a Notification feature in my app (pretty much like Facebook notifications).
When I click a button in the header navigation, the dropdown opens and shows the notification list. The notification has a Link (from react-router) in it.
What I need to do is to close the dropdown whenever a Link is clicked.
Here's roughly the hierarchy I currently have:
Header > Navigation > Button > Dropdown > List > Notification > Link
Since the dropdown functionality is used more that once, I've abstracted its behavior away into a HOC that uses render prop:
export default function withDropDown(ClickableElement) {
return class ClickableDropdown extends PureComponent {
static propTypes = {
children: PropTypes.func.isRequired,
showOnInit: PropTypes.bool,
};
static defaultProps = {
showOnInit: false,
};
state = {
show: !!this.props.showOnInit,
};
domRef = createRef();
componentDidMount() {
document.addEventListener('mousedown', this.handleGlobalClick);
}
toggle = show => {
this.setState({ show });
};
handleClick = () => this.toggle(true);
handleGlobalClick = event => {
if (this.domRef.current && !this.domRef.current.contains(event.target)) {
this.toggle(false);
}
};
render() {
const { children, ...props } = this.props;
return (
<Fragment>
<ClickableElement {...props} onClick={this.handleClick} />
{this.state.show && children(this.domRef)}
</Fragment>
);
}
};
}
The HOC above encloses the Button component, so I have:
const ButtonWithDropdown = withDropdown(Button);
class NotificationsHeaderDropdown extends PureComponent {
static propTypes = {
data: PropTypes.arrayOf(notification),
load: PropTypes.func,
};
static defaultProps = {
data: [],
load: () => {},
};
componentDidMount() {
this.props.load();
}
renderDropdown = ref => (
<Dropdown ref={ref}>
{data.length > 0 && <List items={this.props.data} />}
{data.length === 0 && <EmptyList />}
</Dropdown>
);
render() {
return (
<ButtonWithDropdown count={this.props.data.length}>
{this.renderDropdown}
</ButtonWithDropdown>
);
}
}
List and Notification are both dumb functional components, so I'm not posting their code here. Dropdown is pretty much the same, with the difference it uses ref forwarding.
What I really need is to call that .toggle() method from ClickableDropdown created by the HOC to be called whenever I click on a Link on the list.
Is there any way of doing this without passing that .toggle() method down the Button > Dropdown > List > Notification > Link subtree?
I'm using redux, but I'm not sure this is the kind of thing I'd put on the store.
Or should I handle this imperatively using the DOM API, by changing the implementation of handleGlobalClick from ClickableDropdown?
Edit:
I'm trying with the imperative approach, so I've changed the handleGlobalClick method:
const DISMISS_KEY = 'dropdown';
function contains(current, element) {
if (!current) {
return false;
}
return current.contains(element);
}
function isDismisser(dismissKey, current, element) {
if (!element || !contains(current, element)) {
return false;
}
const shouldDismiss = element.dataset.dismiss === dismissKey;
return shouldDismiss || isDismisser(dismissKey, current, element.parentNode);
}
// Then...
handleGlobalClick = event => {
const containsEventTarget = contains(this.domRef.current, event.target);
const shouldDismiss = isDismisser(
DISMISS_KEY,
this.domRef.current,
event.target
);
if (!containsEventTarget || shouldDismiss) {
this.toggle(false);
}
return true;
};
Then I changed the Link to include a data-dismiss property:
<Link
to={url}
data-dismiss="dropdown"
>
...
</Link>
Now the dropdown is closed, but I'm not redirected to the provided url anymore.
I tried to defer the execution of this.toggle(false) using requestAnimationFrame and setTimeout, but it didn't work either.
Solution:
Based on the answer by #streletss bellow, I came up with the following solution:
In order to be as generic as possible, I created a shouldHideOnUpdate prop in the ClickableDropdown dropdown component, whose Hindley-Milner-ish signature is:
shouldHideOnUpdate :: Props curr, Props prev => (curr, prev) -> Boolean
Here's the componentDidUpdate implementation:
componentDidUpdate(prevProps) {
if (this.props.shouldHideOnUpdate(this.props, prevProps)) {
this.toggle(false);
}
}
This way, I didn't need to use the withRouter HOC directly in my withDropdown HOC.
So, I lifted the responsibility of defining the condition for hiding the dropdown to the caller, which is my case is the Navigation component, where I did something like this:
const container = compose(withRouter, withDropdown);
const ButtonWithDropdown = container(Button);
function routeStateHasChanged(currentProps, prevProps) {
return currentProps.location.state !== prevProps.location.state;
}
// ... then
render() {
<ButtonWithDropdown shouldHideOnUpdate={routeStateHasChanged}>
{this.renderDropdown}
</ButtonWithDropdown>
}
It seems you could simply make use of withRouter HOC and check if this.props.location.pathname has changed when componentDidUpdate:
export default function withDropDown(ClickableElement) {
class ClickableDropdown extends Component {
// ...
componentDidUpdate(prevProps) {
if (this.props.location.pathname !== prevProps.location.pathname) {
this.toggle(false);
}
}
// ...
};
return withRouter(ClickableDropdown)
}
Is there any way of doing this without passing that .toggle() method down the Button > Dropdown > List > Notification > Link subtree?
In the question, you mention that you are using redux.So I assume that you store showOnInit in redux.We don't usually store a function in redux.In toggle function,I think you should dispatch an CHANGE_SHOW action to change the showOnInit in redux, then pass the show data not the function to the children component.Then after reducer dispatch,the react will change “show” automatically.
switch (action.type) {
case CHANGE_SHOW:
return Object.assign({}, state, {
showOnInit: action.text
})
...
default:
return state
}
Link element and data pass
Use the property in Link-to,not data-...Like this:
<Link
to={{
pathname: url,
state:{dismiss:"dropdown"}
}}
/>
And the state property will be found in this.props.location.
give context a little try(not recommend)
It may lead your project to instable and some other problems.(https://reactjs.org/docs/context.html#classcontexttype)
First,define context
const MyContext = React.createContext(defaultValue);
Second,define pass value
<MyContext.Provider value={this.toggle}>
Then,get the value in the nested component
<div value={this.context} />
Suppose that I have a component class which is responsible to change any number entered into textbox to text:
class NumbersToText extends Component {
onChange(event) {
const { target } = event;
const { value } = target;
if (hasNumbers(value)) {
target.value = numbersToText(value);
// HERE I NEED TO NOTIFY ABOUT CHANGES
}
}
render() {
return (
<span onChange={this.onChange}>
{this.props.children}
</span>
);
}
}
Now the usage would look something like this:
<NumbersToText>
<input onChange={this.saveValue}
</NumbersToText>
Let's say that all works, and the value gets changed to text.
Now the problem is that after I change numbers to text and assign that value to input onChange handlers are not executed again, thus saveValue is not called with updated value.
How should this problem be approached in order to trigger onChange handlers with new value?
I don't know exactly what you mean by numbers to text so I'll just assume you want to modify the value before calling the onChange function in the input, and also reflect that value in the input.
First of all, what you're doing will never work on React, React reflects internal virtual objects into the DOM, meaning you shloud not modify the DOM directly and instead you should modify this internal representantion (via setState, props) to reflect this change into the DOM.
There's also two types of inputs on React, controlled and uncontrolled. I will assume you want to use this on uncontrolled inputs.
The only possible solution I can see, is to transform the input using the React.cloneElement function adding a aditional step before calling the input's onChange callback.
Here's a possible implementation that will make the input uppercase.
class UpperCase extends React.Component {
constructor(props) {
super(props);
}
onChange(e, input, next) {
let value = input.value || '';
value = value.toUpperCase();
input.value = value;
next(value);
}
render() {
let childs = React.Children.map(this.props.children, child => {
let input = null; //Will take advantage of javascript's closures
let onChangeChild = child.props.onChange.bind(child);
return React.cloneElement(child, {
ref: ref => input = ref,
onChange: e => {
this.onChange(e, input, onChangeChild)
}
});
});
return (
<span>
{childs}
</span>
);
}
}
And you can use it like this:
<UpperCase>
<input onChange={(val) => console.log(val)}></input>
<textarea onChange={(val) => console.log(val)}></textarea>
</UpperCase>
Thanks to #tiagohngl I came up with a similar, but maybe a little less cluttered (without cloning elements) way:
class NumbersToText extends Component {
onChange(event) {
const { target } = event;
const { value } = target;
if (hasNumbers(value)) {
target.value = numbersToText(value);
this.childrenOnChange(event);
}
}
childrenOnChange(event) {
const { children } = this.props;
React.Children.forEach(children, child => {
if (child.props.onChange) {
child.props.onChange(event);
}
});
}
render() {
return (
<span onChange={this.onChange}>
{this.props.children}
</span>
);
}
}
export default class NumbersToText extends React.Component {
constructor(props) {
super(props)
this.onChange = this.onChange.bind(this);
}
componentWillMount() {
this.setState({ anyData: [] });
}
onChange(event) {
this.setState({anyData: event.target.value},
()=>{console.log("AnyData: "+this.state.anyData)});
// callback to console.log after setState is done
}
render() {
return (
<input type="text"
value={this.state.anyData}
onChange={this.onChange} />
);
}
}
As you mention that,
onChange is not called after changed value.
There are multiple possibilities.
onChange is not binded.
There are no state change in render method, so it will not re-render
make use of console.log() to trace the problem
I slightly ammend the code for illustration.
Hope it helps.
How react handle State Change (answer I posted before)