How to create a searchable componenent in React - javascript

Hi so I have some data that I want to search and filter through using hooks. I am able to get the initial list loaded but I don't know how to search the array and if the input text matches text in the array then display the array text that matches the input text. I tried using filter and stuff but am just confused as to how to put it all together. The data I need I can get by using props.data.map((e) => e.name)
import React, { useState } from "react";
import CharacterCard from "./CharacterCard";
import CharacterList from "./CharacterList";
export default function SearchForm(props) {
console.log(props.data.map((e) => e.name));
let hi = []
props.data.map((e) => hi.push(e.name))
const [search, setSearch] = useState('');
const check = () => {
if(search === ''){
return <CharacterCard data={props.data} />
} else if (hi.indexOf(search) === -1 ){
return <CharacterCard data={props.data} />
} else {
return <h1>{I want this to be anything that matches the input bot value}</h1>
}
}
let handleChange =(e) => {
setSearch(e.target.value)
}
return (
<section className="search-form">
<input type="text" onChange={handleChange} value={search} />
{check()}
</section>
);
}

You're on the right track, you just need to modify your check function. This example will render a list of all names that have a partial match in the beginning of the name:
const check = () => {
// get array of all names in `props.data`
const names = props.data.map(e => e.name);
// this will filter out any names that do not start with the content of `search`
// leaving us with an array of matching names
const searchMatches = names.filter(name => name.startsWith(search));
// do something with the results `searchMatches`
// (this example returns a list of names)
return <ul>{searchMatches.map(name => <li key={name}>{name}</li><ul>
})

Related

ReactJS - Can i save specific rendered mapped data, to a variable?

I have data mapped in a component like this:
import React from "react";
import { useState } from "react";
import { useEffect } from "react";
import { get } from "lodash";
const Products = ({ data }) => {
return (
data.map((item, index) = > (
<div id={index}>
<img src={item.img} /> <br />
{item.name} <br />
{get(moreData, `[${item.name.toLowerCase()}].info[0]`)}
{get(moreData, `[${item.name.toLowerCase()}].info[1]`)}
{get(moreData, `[${item.name.toLowerCase()}].info[2]`)}
</div>
I want to be able to store this data:
{item.name}
{get(moreData, `[${item.name.toLowerCase()}].info[0]`)}
{get(moreData, `[${item.name.toLowerCase()}].info[1]`)}
{get(moreData, `[${item.name.toLowerCase()}].info[2]`)}
in a string, like string = {item.name},{moreData.item.name.toLowerCase().info[0]},...//etc
However you cannot declare variables inside of a component (as far as i know, still new to this).
I've tried .concat() - after each line and .push() with array instead of string:
{item.name} <br />
{dataString.concat(item.name)}
{dataArr.push(item.name)}
{get(moreData, `[${item.name.toLowerCase()}].info[0]`)}
{get(moreData, `[${item.name.toLowerCase()}].info[1]`)}
{get(moreData, `[${item.name.toLowerCase()}].info[2]`)}
I was going to use DOM, but i've been told it's bad practice to use DOM in react.
I've also tried using state in the same way:
const [dataString, setDataString] = useState("");
...
{item.name}
{setDataString((dataString += item.name))}
But nothing seems to work as intended for me, and i'm out of ideas.
Edit:
I want to be able to copy the 'string/text' to clipboard eventually. So it can be imported to another site. Their required format is Item1, item1-info1, item1-info2, item1-info3, item2, item2-info1, item2-info2, item2-info3...etc
here is an example of how you can use the data object outside of the mapped objects.
also an example how to convert the data to an string in the format required.
because I don't know the structure of the data object of yours I just created an one, take this code and change it to the data structure
const data = [
{
name: "name1",
info1: "info1-1",
info2: "info1-2",
info3: "info1-3",
},
{
name: "name2",
info1: "info2-1",
info2: "info2-2",
info3: "info2-3",
},
{
name: "name3",
info1: "info3-1",
info2: "info3-2",
info3: "info3-3",
},
];
change this function to one that fits your needs.
const getStringOfData = (data) => {
let stringArray = [];
data.map((item) => {
stringArray.push(item.name);
stringArray.push(item.info1);
stringArray.push(item.info2);
stringArray.push(item.info3);
});
let stringResult = stringArray.join(",");
return stringResult;
};
useEffect(() => {
console.log("onMounte with useEffect");
let stringResult = getStringOfData(data)
console.log(stringResult);
}, []);
you can also call this function on onClick function depend in your requirement , the data object is available almost everywhere in your component
Continuing our discussion from the comments, it looks like there are two different things you are trying to do here:
display the UI to the user in a certain way
to be able to copy the information in a specific stringified format
I would advice to split these two behaviors, because they are totally unrelated to each other in terms of logic.
Start by aligning the information in your components how you need it.
const Product = ({ item, index }) => {
// do whatever you need to do with the information
// to display it correctly
const identifier = name.toLowerCase()
const infos = (moreData[identifier] && moreData[identifier].info) || {}
return (
<div>
{ info[0] && <p> info[0] </p>}
{ info[1] && <p> info[1] </p>}
{ info[1] && <p> info[2] </p>}
</div>
)
}
const Products = ({ data }) => {
const onClick = useCallback(
async () => {
// parse data and copy it using a separate
// util method.
const dataToString = /* actual implementation of the formatting to a string representation */
copyTextToClipBoard(dataToString)
},
[data]
)
return (
<div>
<button onClick={onClick} />
<div>
data.map((item, index) => (
<Product item={item} index={index} key={item.id} />
))
</div>
</div>
)
};
Now this would be your UI, the copyToClipboard method, is a bit of a hack, and looks like so:
const copyTextToClipBoard = (text) => {
const element = document.createElement('textarea');
element.style = { display: 'none' }; // eslint-disable-line
element.value = text;
document.body.appendChild(element);
element.select();
document.execCommand('copy');
document.body.removeChild(element);
};

How to add function to "X" icon in Material UI Searchbar?

I have the following code:
import SearchBar from "material-ui-search-bar";
const data = [
{
name: "Jane"
},
{
name: "Mark"
},
{
name: "Jason"
}
];
export default function App() {
const [results, setResults] = useState();
const filteredResults = data.filter((item) => {
return Object.keys(item)?.some((key) => {
return item[key].includes(results?.toLowerCase());
});
});
return (
<div className="App">
<SearchBar
value={results}
onChange={(value) => setResults(value)}
placeholder="Please enter name..."
/>
{filteredResults.map((item) => {
return <li>{item.name}</li>;
})}
</div>
);
}
codesandbox
when I delete name from search bar using delete keyboard all names from data are displayed below the search bar, but if I click X button, it clears the search bar but doesn't display all names. Is there a way to add a function to this X button so when I click it, it acts the same way as delete keyboard?
You can pass a function to the onCancelSearch prop to reset the results state variable.
<SearchBar
value={results}
onChange={(value) => setResults(value)}
onCancelSearch={() => setResults('')}
/>
Suggestions
It's better to initialize results with an empty string. You can now remove the ? in results?.toLowerCase() since results will never be nullish (undefined or null).
const [results, setResults] = useState('')
You should pass the key prop to the li element. You can add an id property to the items array to use as the key or use the item index.
{
filteredResults.map((item) => (
<li key={item.id}>{item.name}</li>
))
}
And there are a couple of issues with the filtering logic.
You're converting the search query to lowercase but not the name. In your example, if you search for 'ja', nothing would show up even though matches exist (Jane and Jason).
filteredResults will throw an error if any of the object values do not have the includes method (You can reproduce the issue by adding a numeric id field to the array items). You could fix it by using a searchableKeys array to only perform the search in specific fields.
const searchableKeys = ['name']
const filteredResults = data.filter((item) =>
searchableKeys.some((key) =>
item[key].toLowerCase().includes(results.toLowerCase())
)
)
I would recommend renaming results to query or searchQuery for clarity.
Hello upon checking your problem, the reason why its remapping the list on delete key (in keyboard) because it triggers the onChange event of the searchBar to have the same event as the searchBar, i've tried it on my end and it seems that this solution can be solve your issue
<SearchBar
value={results}
onChange={(value) => setResults(value)}
placeholder="Please enter name..."
closeIcon={<button onClick={() => setResults("")}>clear</button>}
/>
the closeIcon props - overrides the close icon and its methods..
here is the documentation that i check material-ui-search-bar
here also the replicated/solved code-sandbox

React usestate array length varies depending on item i click

im trying to build a list you can add and delete components from.
Adding works but when i try to delete an item, every item that comes after that also gets deleted.
I found that the length of the use state array i use varies depending on which item i click delete on.
const Alerting = () => {
const [Alerts, setAlert] = useState([]);
const AddAlertingChoice = () => {
const addedAlerts = Alerts => [...Alerts, <AlertingCoinChoice coins={coins} id={new Date().getUTCMilliseconds()}];
setAlert(addedAlerts);
}
const DeleteAlertingChoice = id =>{
console.log("alerts "+Alerts.length) //This length always displays the item index-1?
const removedArr = [...Alerts].filter(alert => alert.props.id != id)
setAlert(removedArr)
}
return (
<>
<AlertingContainer>
<CoinChoiceContainer>
{Alerts.map((item, i) => (
item
))}
</CoinChoiceContainer>
<AddAlertButton onClick={AddAlertingChoice}>+</AddAlertButton>
</AlertingContainer>
</>
)
The items
const AlertingCoinChoice = ({coins, id, DeleteAlertingChoice}) => {
return (
<>
<AlertingCoin>
<CoinSelect id={'SelectCoin'}>
<OptionCoin value='' disabled selected>Select your option</OptionCoin>
</CoinSelect>
<ThresholdInput id={'LowerThresholdInput'} type='number'
pattern='^-?[0-9]\d*\.?\d*$'/>
<ThresholdInput id={'UpperThresholdInput'} type='number'
pattern='^-?[0-9]\d*\.?\d*$'/>
<SaveButton id={'AlertSaveAndEdit'} onClick={ClickSaveButton}>Save</SaveButton>
<DeleteAlertButton onClick={() => {DeleteAlertingChoice(id)}}>X</DeleteAlertButton>
</AlertingCoin>
</>
)
why cant it just delete the item i pass with the id parameter?
It sounds like you're only passing down the DeleteAlertingChoice when initially putting the new <AlertingCoinChoice into state, so when it gets called, the length is the length it was when that component was created, and not the length the current state is.
This also causes the problem that the DeleteAlertingChoice that a component closes over will only have the Alerts it closes over from the time when that one component was created - it won't have the data from further alerts.
These problems are all caused by one thing: the fact that you put the components into state. Don't put components into state, instead transform state into components only when rendering.
const Alerting = () => {
const [alerts, setAlerts] = useState([]);
const AddAlertingChoice = () => {
setAlerts([
...alerts,
{ coins, id: Date.now() }
]);
}
const DeleteAlertingChoice = id => {
console.log("alerts " + alerts.length);
setAlerts(alerts.filter(alert => alert.id !== id));
}
return (
<AlertingContainer>
<CoinChoiceContainer>
{Alerts.map((alertData) => (
<AlertingCoinChoice {...alertData} DeleteAlertingChoice={DeleteAlertingChoice} />
))}
</CoinChoiceContainer>
<AddAlertButton onClick={AddAlertingChoice}>+</AddAlertButton>
</AlertingContainer>
);
};
You also don't need fragments <> </> when you're already only rendering a single top-level JSX item.

React Hooks: State is resetting to empty array even if I use the spread operator, prevState, etc

Simplified Code Sample right here
WORDS:
In short: My items state is resetting to [] with each NEW checkbox clicked and I dont understand why. But instead I want to use the spread operator and useState hooks to push an new item into the array so it's an array of objects.
Current behavior in detail: I'm creating an object and setting it in state using all (and I mean ALL) manner of React useState hooks like this: setItems((prevState) => [...prevState, { [evt.target.value]: evt.target.checked }]); As I check one item it's added and items becomes an array of objects (it being added over and over again is not the problem; I'll add a check for that later). BUT Here's the problem: when I click a NEW checkbox the items array is set back to [] and isnt concatenated with the prev items—even though I'm using prevState, spread operator, an arrow func as a wrapper, and all that jazz.
Desired behavior: Every time I check a checkbox, I want to update items [] to push a new object into it, which represents all items that have ever been checked. Before you say anything about duplicating: I'll add the check to see if an item is already in the array, and just update it if so. And before I add all items to cart, I'll strip all objects with checked = false states.
Can you help me understand what react lifecycle fundamentals I'm missing here; why is this happening? And how can I fix it?
CODE:
Where this is happening:
Simplified version of InputComponent
const InputComponent = ({ type, itemId, handleSearchQuery, onSubmit }) => {
const [items, setItems] = useState([]);
const captureInput = (evt) => {
if (evt.target.type === 'checkbox') {
setItems((prevState) => [...prevState, { [evt.target.value]: evt.target.checked }]);
}
};
const renderCheckbox = () => {
return (
<form>
<input type={type} name={itemId} value={itemId} onChange={setItem} />
<input name={itemId} type='submit' value='Add to Cart' />
</form>
);
};
return (
<div className='input-bar'>
{renderCheckbox()}
</div>
);
};
export default InputComponent;
Where this component is used:
import React from 'react';
import InputComponent from './InputComponent';
import './ResultsRenderer.css';
function ResultsRenderer({ data }) {
const renderListings = () => {
let listings = data ? data.Search : null;
return listings
? listings.map((item) => {
return (
<div className='cart-row'>
<InputComponent type='checkbox' className='cart-checkbox' itemId={item.imdbID} />
<div key={item.imdbID} className={item.imdbID}>
<img src={`${item.Poster}`} alt={item.Title} />
<div>
Title<em>{item.Title}</em>
</div>
<div>{item.Year}</div>
<em>{item.imdbID}</em>
</div>
</div>
);
})
: null;
};
return <>{renderListings()}</>;
}
export default ResultsRenderer;
items state is doing its job perfectly fine, you misunderstood the situation.
you're using items state inside InputComponent and for each listings item there is one InputComponent and each one have their own items, I think you meant to use items state inside ResultsRenderer Component to chase all selected items.
here is the changes you need to do:
const InputComponent = ({ type, itemId, setItems }) => {
const captureInput = (evt) => {
if (evt.target.type === "checkbox") {
setItems((prevState) => [
...prevState,
{ [evt.target.value]: evt.target.checked }
]);
}
};
return (
<div className="input-bar">
<form>
<input
type={type}
name={itemId}
value={itemId}
onChange={captureInput}
/>
<input name={itemId} type="submit" value="Add to Cart" />
</form>
</div>
);
};
export default InputComponent;
function ResultsRenderer() {
const [items, setItems] = useState([]);
useEffect(() => {
console.log(items);
}, [items]);
const renderListings = () => {
let listings = [
{ itemId: 1, title: "Hello" },
{ itemId: 2, title: "World" }
];
return listings
? listings.map((item) => {
return (
<div className="cart-row">
<InputComponent
type="checkbox"
className="cart-checkbox"
itemId={item.itemId}
setItems={setItems}
/>
<div key={item.itemId} className={item.itemId}>
<div>
Title<em>{item.Title}</em>
</div>
</div>
</div>
);
})
: null;
};
return <>{renderListings()}</>;
}
and here is the working demo: https://codesandbox.io/s/boring-cookies-t0g4e?file=/src/InputComponent.jsx

Adding, deleting, and editing input values dynamically with Reactjs

I have a UserLists component where the user types into an input and adds the value onto the bottom of the screen.
The input value is added into the whitelist state. The state is then mapped and creates more inputs where the user can decide to delete said input or edit it.
I am having trouble deleting the inputs. I thought I could delete each input individually by splicing the state, but my implementation of the deleteItem deletes multiple inputs when any single one of them is clicked.
I also cannot edit any of the inputs because their value is set by my addItem function.
import React, { useEffect, useState } from "react";
export const UserLists = () => {
const [whitelist, setWhiteList] = useState([]);
const addItem = () => {
let newValue = document.getElementById("whiteList").value;
setWhiteList([...whitelist, newValue]);
};
useEffect(() => {
console.log(whitelist, "item changed");
}, [whitelist]);
const deleteItem = (index) => {
let test = whitelist.splice(index, 1);
setWhiteList(test);
console.log("index:", index);
};
const editItem = () => {};
return (
<div>
<h2>WhiteList</h2>
<input id="whiteList" type="text" />
<button onClick={addItem}>Add</button>
{whitelist.map((item, index) => {
return (
<div>
<input type="text" value={item} onChange={editItem} />
<button onClick={() => deleteItem(index)}>Delete</button>
<p>{index}</p>
</div>
);
})}
</div>
);
};
How can I revise my code to successfully individually delete and edit inputs?
My codesandbox
You need to change your editItem and deleteItem functions in order to make your edit and delete functionality work properly. Here's a code sandbox link to the solution to your problem:
https://codesandbox.io/s/whitelist-forked-y51w8
Don't do:
let test = whitelist.slice(index, 1);
setWhiteList(test);
Do this instead:
whitelist.splice(index, 1);
setWhiteList([...whitelist]);

Categories

Resources