How to add scroll into listItem of list on search - javascript

I have a List which contains ListItem and a search bar. Search functionality is working well as per the requirement and its highlighting the ListItem. Now i am trying to add the scroll so that the List gets scroll to first occurrence of the 'includes' if the search item is not available on visible window but not able to implement it. Tried const ref = React.createRef();
How to get it working?
Here is the codesandbox link

You can easily achieve this one using vanilla js.
First just store the index of all matching ListItem in a state:
const getMatchingListItems = React.useCallback(() => {
return [...messages]
.map(({ id, primary, secondary, person }, i) => {
if (secondary.includes(searchTranscript) && searchTranscript !== "") {
return i;
}
})
.filter((elmt) => elmt !== undefined);
}, [searchTranscript]);
const [matchingListItems, setMatchingListItems] = useState(getMatchingListItems());
Use it to define which className to use:
<div
className={
matchingListItems.includes(i) ? classes.searchHighLight : ""
}
And finally use the useEffect hook to scroll to the first matching LisItem when matchingListItems is updated:
React.useEffect(() => {
setMatchingListItems(getMatchingListItems());
}, [getMatchingListItems, setMatchingListItems]);
React.useEffect(() => {
const firstListMatch = matchingListItems[0];
if (firstListMatch) {
const firstLi = document.querySelector(
`ul div:nth-child(${firstListMatch})`
);
firstLi.scrollIntoView();
} else {
document.querySelector("ul").scrollIntoView();
}
}, [matchingListItems]);
You can try it using this codesandbox.

Related

React Native, State Changes, but JSX Conditional Rendering Does not Updated UI

Hello and thank you for your time in advance!
I am struggling with a small issue that I have not encountered before, with React not rendering an UI element based on a check function. Basically, what I am trying to make, is a multiple selection filter menu, where when an option is clicked, the dot next to it changes to red.
For this purpose I append the value of each option to an array, and using array.sort, verify when it is to be added (value is pushed to FilterList) and removed (value is popped from FilterList)
The checkfilter function operates normally, and when logging the state, indeed it works as intended with the array being updated.
However, the JSX code running the checkfilter to render the extra red dot inside the bigger circle, unfortunately does not.
Additionally, when the screen is refreshed, the UI is updated normally, with every option clicked, now showing the aforementioned red dot.
Why is this happening? I have tried several hooks, JSX approaches, using imported components and more that I can't even remember, yet the UI will not update oddly.
Below you can find a snippet of the code. Please bear in mind this is a render function for a flatlist component
const checkFilter = useCallback((element) => {
return filterList?.some((el: any) => (el == element))
}, [filterList])
const removeFilter = useCallback((cat) => {
let temparr = filterList
var index = temparr?.indexOf(cat);
if (index > -1) {
temparr?.splice(index, 1);
}
setFilterList(temparr)
}, [filterList])
const addFilter = useCallback((cat) => {
let temparr = filterList;
temparr.push(cat);
setFilterList(temparr)
}, [filterList])
const renderFilter = useCallback((item) => {
return (
<Pressable
onPress={() => {
checkFilter(item?.item) ? removeFilter(item?.item) : addFilter(item?.item);
console.log(filterList)
}}
style={styles.modalOptionWrapper}
>
<Text style={styles.modalOptionTitle(checkFilter)}>{item?.item}</Text>
<View style={styles.modalOptionRowRight}>
<View style={styles.radioBtn}>
{checkFilter(item?.item) ?
<View style={styles.radioBtnBullet} />
:
null
}
</View>
</View>
</Pressable>
)
}, [filterList])
This may not be correct answer but try this. When I say simple basic codes, like this.
const ListItems = () => {
const [filterList, setFilterList] = React.useState([]); // must be array
const checkFilter = filterList?.some((el) => el === element);
const removeFilter = useCallback(
(cat) => {
// not updating new state, just useing previous state
setFilterList((prev) => [...prev].filter((el) => el !== cat));
// used spread iterator
},
[] // no need dependency
);
const addFilter = useCallback((cat) => {
// used spread indicator
setFilterList((prev) => [...prev, cat]);
}, []);
const renderFilter = useCallback((item) => {
// just checking our codes is right with basic elements
// it may be 'undefined'
return <Text>{JSON.stringify(item)}</Text>;
}, []);
return filterList.map(renderFilter);
};

React | Adding and deleting object in React Hooks (useState)

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
};

React child component not re-rendering on updated parent state

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.

React - conditional/message

I have the following code and need to add two extra things to it but I'm stuck and not sure how to do it.
I need to add:
If there are no products in the category a NotFound component will show a message.
By typing 'all' in the input we should be able to see the entire list of products again from all the categories.
Ideally I'm looking for the simplest solution as I'm currently learning React. Thanks!
Main Component
import React from 'react';
import Item from './components/Item';
class App extends React.Component {
state = {
items: [
{
title: "The Spice Girls",
price: 10,
category: "Pop",
quantity: 1,
},
{
title: "Beethoven",
price: 5,
category: "Classical",
quantity: 1,
},
{
title: "Bob Marley",
price: 15,
category: "Reggae",
quantity: 1,
}
],
category: " ",
filtered: [],
}
handleChange = e => {
this.setState({category: e.target.value},()=>console.log(this.state.category));
}
handleClick = (event) => {
event.preventDefault()
var newList = this.state.items;
var filteredItems = newList.filter(item => item.category === this.state.category)
this.setState({filtered: filteredItems})
}
render () {
let show;
if(this.state.category !== " "){
show = this.state.filtered.map((item, i) => <Item key = {i} cd={item}/>)
}else{
show = this.state.items.map( (item,i) => <Item key = {i} cd={item}/>)
}
return (
<div>
<h1 className = "title">CD</h1>
<h2>Search music below:</h2>
<form>
Music style: <input onChange = {this.handleChange}></input>
<button onClick = {this.handleClick}>Search</button>
</form>
{show}
</div>
)
}
}
export default App;
Item Component
import React from 'react';
class Item extends React.Component {
render () {
return (
<div className = "items">
<div className = "item">
<h3>{this.props.cd.title}</h3>
<div className = "price">Price: {this.props.cd.price}€</div>
<div className = "quantity">Quantity: {this.props.cd.quantity}</div>
<div className = "category">Category: {this.props.cd.category}</div>
</div>
</div>
)
}
}
export default Item;
First of all some suggested changes before I answer your question
There are a few things which confused me much when analysing your code, so will share them with you, as if in the future you work on teams, it would be handy if other people can understand your code.
You have an on change event for the text box which is different to the event for the search button next to it. Users would expect it to be the same and so that's really confusing.
You have 2 lists of items essentially, a raw and unfiltered and you switch between which 2 to present on the screen. Sometimes you need to have a raw set and that's fine, but perhaps make sure that the only ones which are presented as such is just either the state.items or the state.filtered. I would probably expect the state.filtered
Make your search case insensitive, e.g. pop should match Pop
Answer to your question'ss
If there are no products in the category a NotFound component will show a message.
For this I would first modify your show logic to work on the same filtered list just change your event functions to manipulate the filtered list and untouch the items one.
add another condition perhaps for when there are no cases
if (this.state.filtered) {
show = this.state.filtered.map((item, i) => <Item key={i} cd={item} />);
} else {
show = <h1>NoneFound</h1>;
}
By typing 'all' in the input we should be able to see the entire list of products again from all the categories.
handleClick = event => {
event.preventDefault();
var { category } = this.state;
var newList = this.state.items;
var filteredItems;
if ([" ", "All"].some(t => t === category)) {
filteredItems = newList;
} else {
filteredItems = newList.filter(
item => item.category.toLowerCase() === this.state.category.toLowerCase()
);
}
this.setState({ filtered: filteredItems });
};
My souloution to this was to modify your onClick event to correctly manipulated the filtered list.
You can see my full soloution to this on my codesandbox here

Dynamic className in map not changing after update

I'm trying to update my react className when the active changes in the sites variable which is mapped to loop through the items.
What happens is that the className 'inactive' does not go away if the active status changes to true or visa versa.
Code:
// Context: this code is inside of the component
const [sites, setSites] = useState([]); <--- Updated dynamically with fetch()
const changeActive = (id) => {
const tmpSites = sites;
for (const s in tmpSites) {
if (tmpSites[s].id === id) {
tmpSites[s].active = !Boolean(tmpSites[s].active);
}
}
setSites(tmpSites);
};
return (
{sites.length ? sites.map((item, i) => {
return (
<tr className={`${!Boolean(item.active) ? 'inactive' : ''}`} key={item.id}>
// inbetween data
</tr>
)
}) : null}
)
You need to create a copy of the sites array and make changes to the copy and then set it in state. Never mutate state directly as it might not cause a re-render as we are updating the state with the same object reference.
const changeActive = (id) => {
const tmpSites = [...sites];
for (const s in tmpSites) {
if (tmpSites[s].id === id) {
tmpSites[s].active = !Boolean(tmpSites[s].active);
}
}
setSites(tmpSites);
};
Because you are mutating the original sites Object and not cloning it before making the changes, the useState ("setSites") does not actually re-renders the component because it cannot compare previous Object to current, because they are the same.
You must do a deep-clone of the sites Array of Objects:
const changeActive = (id) => {
setSites(sites => {
sites.map(site => ({ // ← first-level clone
...site // ← second-level clone
active: site.id === id ? !site.active : site.active
}))
})
}
It is imperative to use the setSites function that returns the current state and then you can reliably deep-clone it.

Categories

Resources