How to add suggestion box to text input in React - reactjs

I have an input with an onChange attribute that calls a function that updates my prop to store the three suggestions that I want to display. These suggestions are stored in a string array. How can I display these values below the input like an autocomplete feature?
Currently using react typescript and bootstrap

This is how you should handle the dropdown suggestions. I am not very familiar with react bootstrap but here is the general idea:
import React, { useState, useRef, useEffect, Fragment } from "react";
const SuggestionsInput = () => {
const [ value, setValue ] = useState("");
const [ showDropdown, setShowDropdown ] = useState(false);
const wrapperRef = useRef(null);
const suggestions = "a, aa, b, bb, cc, d, dd, e, ee";
const suggestionArr = suggestions
.split(", ")
.filter(suggestion =>
suggestion.toLowerCase().includes(value.toLowerCase())
);
const changeHandler = e => {
setValue(e.target.value);
};
const handleClickOutside = e => {
if (wrapperRef.current && !wrapperRef.current.contains(e.target)) {
setShowDropdown(false);
}
};
// Attaching the previous event with UseEffect hook
useEffect(() => {
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
});
return (
<div className="input__wrapper" ref={wrapperRef}>
<input
type="text"
onChange={changeHandler}
onFocus={() => setShowDropdown(true)}
value={value}
/>
{showDropdown && (
<div className="suggestions__dropdown">
{suggestionArr && suggestionArr.length > 0 ? (
<Fragment>
{suggestionArr.map((suggestion, index) => (
<div
key={"suggestion_" + index}
className="suggestion__item"
onClick={() => setValue(suggestion)}
/>
))}
</Fragment>
) : (
<div className="no__suggestions">No suggestions found</div>
)}
</div>
)}
</div>
);
};
And you should add this css:
.input__wrapper {
position: relative;
}
.input__wrapper .suggestions__dropdown {
position: absolute;
top: 30px;
right: 0;
left: 0;
width: 100%;
height: 100px;
overflow-y: auto;
}

Related

How save style in the local storage

I have such a project. Here I want the button border save in the local storage.The buttons are divided into categories. For example when you refresh the page after selecting a sports button, the border of the button disappears. I want save btn border in the localstorage. I saved the categories in memory, but I can't make the border of the selected button.How can I fix it?
import React, { useEffect, useState } from "react";
import SpinnerLoad from './components/SpinnerLoad'
import NewsItem from "./components/NewsItem";
import Category from "./components/data/Category"
const App = () => {
const [state, setState] = useState([]);
const [loading, setLoading] = useState(false)
const [selected, setSelected] = useState('');
const fetchValue = (category, index) => {
localStorage.setItem("category", category);
localStorage.setItem("selected", index);
fetch(`https://inshorts-api.herokuapp.com/news?category=${category}`)
.then(res => res.json())
.then(res => {
setState(res.data)
setLoading(true)
})
.catch((error) => console.log(error))
setLoading(false);
};
const CategoryButton = ({ category, i }) => (
// passing index --> i to the fetch Value
<button onClick={() =>{ fetchValue(category,i) ; setSelected(i)} }
style={{border : selected === i ? '1px solid red' : null}} >{category}</button>
);
useEffect(() => {
let categoryValue = localStorage.getItem("category") || "all";
fetchValue(categoryValue)
const select = localStorage.getItem("selected") || "";
setSelected(select);
}, []);
return (
<>
<div className="header-bg">
<h1 className="mb-3">News</h1>
<div className="btns ">
{Category.map((value,i) => {
return <CategoryButton category={value} i={i}/>;
})}
</div>
</div>
<div className="news">
<div className="container">
<div className="row">
{
!loading
? <SpinnerLoad />
:
state.map((data, index) => {
return (
<NewsItem
imageUrl={data.imageUrl}
author={data.author}
title={data.title}
content={data.content}
date={data.date}
key={data.id}
/>
);
})
}
</div>
</div>
</div>
</>
);
};
export default App;
According to the code looks like you want to display data specific to a category set when the user clicks on the category buttons. and after the click, the correct data is rendered and the current category button receives a change in its style highlighting it is the current state.
I don't understand why you need to store anything in a client's localstorage,
I would not recommend storing too much in localStorage as it is limited and is used by different sites a user visits, I only store authentication tokens in localstorage and I believe that is the norm.
I've tried to create the effect you want without the need to store in local storage
import React, { useState, useCallback, useEffect } from "react";
import ReactDOM from "react-dom";
import { cat } from "../categories.js";
import { news } from "../news.js";
function Example() {
const [state, setState] = useState([]);
const [loading, setLoading] = useState(false);
const [selected, setSelected] = useState(null);
useEffect(() => {
function fetchFunction() {
setLoading(true);
for (let i = 0; i < news.length; i++) {
if (news[i].id === selected) {
const current = news[i].c;
setState(current);
}
}
setLoading(false);
}
fetchFunction();
}, [selected]);
return (
<>
<ol
style={{
width: "50%",
listStyle: "none",
display: "flex",
justifyContent: "space-between"
}}
>
{cat.map((item, index) => {
return (
<li key={index}>
<button
style={{ border: selected === item.id && "none" }}
onClick={() => {
setSelected(item.id);
}}
>
{item.name}
</button>
</li>
);
})}
</ol>
<section style={{ width: "100%", height: "70%" }}>
{state.map((item, index) => {
return (
<div
key={index}
style={{
width: "30%",
height: "30%",
background: "red",
display: "flex",
alignItems: "center",
justifyContent: "center",
margin: "1% 0 2% 0"
}}
>
{item.name}
</div>
);
})}
</section>
</>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<Example />, rootElement);
You can save the selectedIndex in localStorage and retrieve it in the useEffect..
const CategoryButton = ({ category, i }) => (
// passing index --> i to the fetch Value
// setting selected as string instead of index for type checking
<button onClick={() =>{ fetchValue(category,i) ; setSelected(`${i}`)} }
style={{border : selected === `${i}` ? '1px solid red' : null}} >{category}</button>
);
const fetchValue = (category, index) => {
localStorage.setItem("category", category);
localStorage.setItem("selected", index);
// ...
}
useEffect(() => {
const select = localStorage.getItem("selected") || "";
// passing selectedIndex to the fetchValue, otherwise it becomes
//undefined..
fetchValue(categoryValue,select)
setSelected(select);
},[])

Cannot display a number of input fields given a value

I'm trying to display fields based on the value of a props so let's say my props value = 2 then I want to display 2 inputs but I can't manage to get it work.
This is what I tried
const [numberOfFields, setNumberOfFields] = useState(0);
const [loadFields, setloadFields] = useState([]);
const addField = () => {
return loadFields.map((tier) => {
<div>
<p style={{color:'black'}}>Tier {tier + 1}</p>
<InputNumber />
</div>
})
}
const onPropsValueLoaded = (value) => {
let tmp = value
setNumberOfFields(tmp);
if (numberOfFields > 0) {
const generateArrays = Array.from(value).keys()
setloadFields(generateArrays);
} else {
setloadFields([]);
}
}
useEffect(() => {
onPropsValueLoaded(props.numberOfTiers);
}, [])
return (
<>
<Button type="primary" onClick={showModal}>
Buy tickets
</Button>
<Modal
title="Buy ticket"
visible={visible}
onOk={handleOk}
confirmLoading={confirmLoading}
onCancel={handleCancel}
>
<p style={{ color: 'black' }}>{props.numberOfTiers}</p>
{loadFields.length ? (
<div>{addField()}</div>
) : null}
<p style={{ color: 'black' }}>Total price: </p>
</Modal>
</>
);
so here props.NumberOfTiers = 2 so I want 2 input fields to be displayed but right now none are displayed even though loadFields.length is not null
I am displaying this inside a modal (even though I don't think it changes anything).
I am doing this when I load the page that's why I am using the useEffect(), because if I use a field and update this onChange it works nicely.
EDIT:
I changed the onPropsValueLoaded() function
const generateArrays = Array.from({length : tmp}, (v,k) => k)
instead of
const generateArrays = Array.from(value).keys()
There are couple of things you should fix in here,
First, you need to return div in addField function to render the inputs.
Second, you should move your function onPropsValueLoaded inside useEffect or use useCallback to prevent effect change on each render.
Third, your method of creating array using Array.from is not correct syntax which should be Array.from(Array(number).keys()).
So the working code should be , I also made a sample here
import React, { useState, useEffect } from "react";
import "./styles.css";
export default function App() {
const [numberOfFields, setNumberOfFields] = useState(0);
const [loadFields, setloadFields] = useState([]);
const addField = () => {
return loadFields.map((tier) => {
return (
<div key={tier}>
<p style={{ color: "black" }}>Tier {tier + 1}</p>
<input type="text" />
</div>
);
});
};
useEffect(() => {
let tmp = 2; // tier number
setNumberOfFields(tmp);
if (numberOfFields > 0) {
const generateArrays = Array.from(Array(tmp).keys());
setloadFields(generateArrays);
} else {
setloadFields([]);
}
}, [numberOfFields]);
return (
<>
<button type="button">Buy tickets</button>
<p style={{ color: "black" }}>2</p>
{loadFields.length ? <div>{addField()}</div> : null}
<p style={{ color: "black" }}>Total price: </p>
</>
);
}

Array not resetting after changing query text

React beginner here. I'm learning how to make a simple scrolling website. What happens is that when I type for example "hello" the first results will show up correctly, however if I type something else, the results will be added at the end of the array, and if I want to see them, I will have to scroll down. I want it to show the results as I am typing all while "resetting" the array every time.
Here's the code:
import React, { Component } from 'react';
import axios from 'axios';
import styled from 'styled-components';
const SearchStickerContainer = styled.div`
width: 100%;
height: 70vh;
overflow: scroll;
position: absolute;
top: 20%;
`;
const SearchLabel = styled.label`
font-size: 25px;
font-weight: bold;
`;
function SearchStickers() {
const [photos, setPhotos] = React.useState([]);
const [page, setPage] = React.useState(1);
const [bottom, setBottom] = React.useState(false);
const [query, setQuery] = React.useState(null);
const [search, setSearch] = React.useState('');
React.useEffect(() => {
querySearch(null, page);
}, [])
const querySearch = async (queryString, page) => {
let config = {
headers: {
apiKey: '823bb74a52fb44f8590c87b3dfd8c4e8'
}
}
if (bottom === true) {
setPage(page + 1);
}
if (queryString !== null) {
await axios
.get(`/v1/search?userId=9937&q=${queryString}&lang=en&pageNumber=${page}&limit=20`, config)
.then(res => {
if (res.data.body.stickerList !== null) {
setPhotos([...photos, ...res.data.body.stickerList]);
}
});
}
}
const searchBar = () => {
return (
<form>
<SearchLabel>
<span>search</span>
</SearchLabel>
<input
type="text"
id="search"
placeholder="search stickers"
onChange={(e) => [setQuery(e.target.value), querySearch(e.target.value)]}
value={query}
/>
</form>
)
}
const handleScroll = (e) => {
const bottom = e.target.scrollHeight - e.target.scrollTop === e.target.clientHeight;
setBottom(true);
if (bottom) {
querySearch(query, page);
setBottom(false);
}
}
return (
<SearchStickerContainer onScroll={handleScroll}>
{searchBar()}
<div>
<div style={{ minHeight: "100px" }}>
{photos && photos.map(user => (
<img src={user.stickerImg} height="200px" width="200px" />
))}
</div>
{bottom === true ? <div> Loading ... </div> : null}
</div>
</SearchStickerContainer>
);
}
export default SearchStickers;
I suspect the issue comes from:
setPhotos([...photos, ...res.data.body.packageList]);
No matter how else I change it, the issue will remain the same (or get different issues, like the scrolling disappearing).
If you are not wanting to always append search results to the existing state, then instead of
setPhotos([...photos, ...res.data.body.stickerList]);
just fully replace the photos state
setPhotos(res.data.body.stickerList);

react-big-calendar: How make a popup with onSelectEvent

<DragAndDropCalendar
selectable
localizer={localizer}
events={events}
style={{ height: 1550 }}
onSelectSlot={(e) => handleSelect(e)}
onSelectEvent={(e) => handleSelectedEvent(e)}
/>
here's the function:
function handleSelectedEvent (e) {
<div className="modal">
{console.log(e)}
</div>
}
The issue:
The modal wont show up, it does show in console log but then I tried to put it in a modal, it just does not render. I have tried react-responsive-modal and also other bootstrap modals but it just does not render.
import React, { useState} from 'react'
function Calendar() {
const [selectedEvent, setSelectedEvent] = useState(undefined)
const [modalState, setModalState] = useState(false)
const handleSelectedEvent = (event) => {
setSelectedEvent(event)
setModalState(true)
}
const Modal = () => {
return (
<div className={`modal-${modalState == true ? 'show' : 'hide'}`}>
// Here you define your modal, what you want it to contain.
// Event title for example will be accessible via 'selectedEvent.title'
</div>
)
}
return (
<div>
{selectedEvent && <Modal />}
<Calendar
selectable
localizer={localizer}
events={events}
style={{ height: 1550 }}
onSelectSlot={(e) => handleSelect(e)}
onSelectEvent={(e) => handleSelectedEvent(e)}
/>
</div>
)
}
And then, in css, you have to do:
.modal-show {
display: block;
}
.modal-hide {
display: none;
}

How do I return an input tag from a function?

I have a button on my webpage, and I want an input tag to appear, whenever the user clicks that button. I earlier tried something like this:
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [showInput, setShowInput] = useState(false);
const handleClick = () => setShowInput(true);
return (
<div className="App">
<button onClick={handleClick}>Click me</button>
{showInput ? <input type="text" /> : ""}
</div>
);
}
But this only worked once. I want it to add an input tag whenever the user clicks that button. How do I do so?
Instead of maintaining the number of input elements in the state, i suggest that you maintain an object in the state that is initially empty. Once the button is clicked to add an input, you could update the object with a key-value pair that represents the new input element.
State after adding one input could like as shown below:
{
input1: { value: '' }
}
Similarly, as more inputs are added, more objects will be added in the state.
This will allow your input elements to be controlled components and will allow you to handle the onChange event with only one event handler function.
Demo
let counter = 1;
function App() {
const [inputs, setInputs] = React.useState({});
const handleClick = () => {
const inputName = "input" + counter++;
const inputObj = { value: "" };
setInputs({ ...inputs, [inputName]: inputObj });
};
const handleChange = (event) => {
const { name, value } = event.target;
setInputs({ ...inputs, [name]: { ...inputs[name], value } });
};
return (
<div className="App">
<button onClick={handleClick}>Add Input</button>
<div className="inputContainer">
{Object.keys(inputs).map((inputName) => {
const { value } = inputs[inputName];
return (
<input
key={inputName}
name={inputName}
value={value}
onChange={handleChange}
placeholder={inputName}
/>
);
})}
</div>
</div>
);
}
ReactDOM.render(<App/>, document.querySelector('#root'));
.App {
font-family: sans-serif;
text-align: center;
}
.inputContainer {
display: flex;
flex-direction: column;
max-width: 300px;
margin: 10px auto;
}
input {
margin: 5px;
padding: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Make showInput a number that defaults to 0.
Have handleClick increment that number instead of just setting true.
Outside the return expression, create an array. With a for loop, push inputs (until you reach the number specified) into the array.
Replace the line where you add the input to the JSX with that array.
Something like ...
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [inputs, setInputs] = useState([]);
const handleClick = () => setInputs([...inputs, ""]);
return (
<div className="App">
<button onClick={handleClick}>Click me</button>
{inputs.map(i => <input type="text"/>)}
</div>
);
}
Now you can also store your input values into your inputs state for further processing.
I leave formatting up to you ... !
import React, { useState } from "react";
export default function App() {
const initialValue = [{ value: "first input" }];
const [userInputs, setUserInputs] = useState(initialValue);
const handleClick = () => {
const updatedInputs = [...userInputs, { value: "new input"}]
setUserInputs(updatedInputs);
}
return (
<div className="App">
<button onClick={handleClick}>Click me</button>
{userInputs.map((el, i) => (
<input type="text" value={el.value} />
))}
</div>
);
}
All of the implementation above is correct, But I also have my own implementation.
import React, { useState, Fragment } from "react";
export default function App() {
const [showInputs, setInputs] = useState([]);
const handleClick = () => {
setInputs((prev) => {
const i = prev.length + 1;
return [
...prev,
<Fragment key={i}>
<input type="text" />
<br />
</Fragment>
];
});
};
return (
<div className="App">
<button onClick={handleClick}>Click me</button>
<br />
{showInputs}
</div>
);
}

Resources