I am new to ReactJS and I am trying to create a search feature with react by fetching data from multiple API. Below is the Search.js file. I tried so many times to make it functions and make the results appear live while typing. I however keep on getting this error message TypeError: values.map is not a function. Where am I going wrong and how do I fix it?
function Search() {
const [input, setInput] = useState("");
const [results, setResults] = useState([]);
const urls = [
'https://jsonplaceholder.typicode.com/posts/1/comments',
'https://jsonplaceholder.typicode.com/comments?postId=1'
]
Promise.all(urls.map(url => fetch(url)
.then((values) => Promise.all(values.map(value => value.json())))
.then((response) => response.json())
.then((data) => setResults(data))
.catch(error => console.log('There was a problem!', error))
), [])
const handleChange = (e) => {
e.preventDefault();
setInput(e.target.value);
}
if (input.length > 0) {
results.filter((i) => {
return i.name.toLowerCase().match(input);
})
}
return ( <
div className = "search"
htmlFor = "search-input" >
<
input type = "text"
name = "query"
value = {
input
}
id = "search"
placeholder = "Search"
onChange = {
handleChange
}
/> {
results.map((result, index) => {
return ( <
div className = "results"
key = {
index
} >
<
h2 > {
result.name
} < /h2> <
p > {
result.body
} < /p> {
result
} <
/div>
)
})
} </div>
)
}
.search {
position: relative;
left: 12.78%;
right: 26.67%;
top: 3%;
bottom: 92.97%;
}
.search input {
/* position: absolute; */
width: 40%;
height: 43px;
right: 384px;
margin-top: 50px;
top: 1.56%;
bottom: 92.97%;
background: rgba(0, 31, 51, 0.02);
border: 1px solid black;
border-radius: 50px;
float: left;
outline: none;
font-family: 'Montserrat', sans-serif;
font-style: normal;
font-weight: normal;
font-size: 16px;
line-height: 20px;
/* identical to box height */
display: flex;
align-items: center;
/* Dark */
color: #001F33;
}
/* Search Icon */
input#search {
background-repeat: no-repeat;
text-indent: 50px;
background-size: 18px;
background-position: 30px 15px;
}
input#search:focus {
background-image: none;
text-indent: 0px
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Issue
The response you get from fetch(url) is just the one single response, so there's nothing to map.
The data fetching occurs in the function body of the component, so when working this causes render looping since each render cycle fetches data and updates state.
The input.length > 0 filtering before the return does nothing since the returned filtered array isn't saved, and it also incorrectly searches for sub-strings.
Attempt to render result object in the render function, objects are invalid JSX
Solution
Skip the .then((values) => Promise.all(values.map((value) => value.json()))) step and just move on to accessing the JSON data.
Move the data fetching into a mounting useEffect hook so it's run only once.
Move the filter function inline in the render function and use string.prototype.includes to search.
Based on other properties rendered and what is left on the result object I'll assume you probably wanted to render the email property.
Code:
function Search() {
const [input, setInput] = useState("");
const [results, setResults] = useState([]);
useEffect(() => {
const urls = [
"https://jsonplaceholder.typicode.com/posts/1/comments",
"https://jsonplaceholder.typicode.com/comments?postId=1"
];
Promise.all(
urls.map((url) =>
fetch(url)
.then((response) => response.json())
.then((data) => setResults(data))
.catch((error) => console.log("There was a problem!", error))
),
[]
);
}, []);
const handleChange = (e) => {
e.preventDefault();
setInput(e.target.value.toLowerCase());
};
return (
<div className="search" htmlFor="search-input">
<input
type="text"
name="query"
value={input}
id="search"
placeholder="Search"
onChange={handleChange}
/>
{results
.filter((i) => i.name.toLowerCase().includes(input))
.map((result, index) => {
return (
<div className="results" key={index}>
<h2>{result.name}</h2>
<p>{result.body}</p>
{result.email}
</div>
);
})}
</div>
);
}
Related
I'm trying to map some json elements to a Table (Customers.jsx using ) but can't see to figure out the correct way. How to insert my Fetch Method into Customers.jsx correctly? especially map renderBody part and the bodyData={/The JsonData/}
Fetch method
componentDidMount() {
this.refershList();
}
async refershList() {
const cookies = new Cookies();
await fetch('https://xxxxxxxxxxxxxxxxx/Customers', {
headers: { Authorization: `Bearer ${cookies.get('userToken')}` }
})
.then(response => response.json())
.then(data => {
this.setState({ deps: data });
});
}
Customers.jsx
import React from 'react'
import Table from '../components/table/Table'
const customerTableHead = [
'',
'name',
'email',
'phone',
'total orders',
'total spend',
'location'
]
const renderHead = (item, index) => <th key={index}>{item}</th>
const renderBody = (item, index) => (
<tr key={index}>
<td>{item.id}</td>
<td>{item.name}</td>
<td>{item.email}</td>
<td>{item.phone}</td>
<td>{item.total_orders}</td>
<td>{item.total_spend}</td>
<td>{item.location}</td>
</tr>
)
const Customers = () => {
return (
<div>
<h2 className="page-header">
customers
</h2>
<div className="row">
<div className="col-12">
<div className="card">
<div className="card__body">
<Table
limit='10'
headData={customerTableHead}
renderHead={(item, index) => renderHead(item, index)}
bodyData={/*The JsonData*/}
renderBody={(item, index) => renderBody(item, index)}
/>
</div>
</div>
</div>
</div>
</div>
)
}
export default Customers
API JSON data
[
{
"id":1,
"name":"Brittan Rois",
"email":"brois0#unicef.org",
"location":"Bator",
"phone":"+62 745 807 7685",
"total_spend":"$557248.44",
"total_orders":24011
},
{
"id":2,
"name":"Matthew Junifer",
"email":"mjunifer1#buzzfeed.com",
"location":"Bromma",
"phone":"+46 993 722 3008",
"total_spend":"$599864.94",
"total_orders":60195
},
{
"id":3,
"name":"Finlay Baylay",
"email":"fbaylay2#purevolume.com",
"location":"Atalaia",
"phone":"+55 232 355 3569",
"total_spend":"$171337.47",
"total_orders":96328
},
{
"id":4,
"name":"Beryle Monelli",
"email":"bmonelli3#amazonaws.com",
"location":"Martingança",
"phone":"+351 734 876 8127",
"total_spend":"$335862.78",
"total_orders":78768
}
]
Table.jsx
import React, {useState} from 'react'
import './table.css'
const Table = props => {
const initDataShow = props.limit && props.bodyData ? props.bodyData.slice(0, Number(props.limit)) : props.bodyData
const [dataShow, setDataShow] = useState(initDataShow)
let pages = 1
let range = []
if (props.limit !== undefined) {
let page = Math.floor(props.bodyData.length / Number(props.limit))
pages = props.bodyData.length % Number(props.limit) === 0 ? page : page + 1
range = [...Array(pages).keys()]
}
const [currPage, setCurrPage] = useState(0)
const selectPage = page => {
const start = Number(props.limit) * page
const end = start + Number(props.limit)
setDataShow(props.bodyData.slice(start, end))
setCurrPage(page)
}
return (
<div>
<div className="table-wrapper">
<table>
{
props.headData && props.renderHead ? (
<thead>
<tr>
{
props.headData.map((item, index) => props.renderHead(item, index))
}
</tr>
</thead>
) : null
}
{
props.bodyData && props.renderBody ? (
<tbody>
{
dataShow.map((item, index) => props.renderBody(item, index))
}
</tbody>
) : null
}
</table>
</div>
{
pages > 1 ? (
<div className="table__pagination">
{
range.map((item, index) => (
<div key={index} className={`table__pagination-item ${currPage === index ? 'active' : ''}`} onClick={() => selectPage(index)}>
{item + 1}
</div>
))
}
</div>
) : null
}
</div>
)
}
export default Table
Table.css
.table-wrapper {
overflow-y: auto;
}
table {
width: 100%;
min-width: 400px;
border-spacing: 0;
}
thead {
background-color: var(--second-bg);
}
tr {
text-align: left;
}
th,
td {
text-transform: capitalize;
padding: 15px 10px;
}
tbody > tr:hover {
background-color: var(--main-color);
color: var(--txt-white);
}
.table__pagination {
display: flex;
width: 100%;
justify-content: flex-end;
align-items: center;
margin-top: 20px;
}
.table__pagination-item ~ .table__pagination-item {
margin-left: 10px;
}
.table__pagination-item {
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.table__pagination-item.active,
.table__pagination-item.active:hover {
background-color: var(--main-color);
color: var(--txt-white);
font-weight: 600;
}
.table__pagination-item:hover {
color: var(--txt-white);
background-color: var(--second-color);
}
To handle this case you can use the useEffect hook inside the component. This hook lets you execute a function when the component mounts and whenever any variable defined in its dependencies array changes. Like this:
const Customers = () => {
//...
const [deps, setDeps] = useState();
refreshList() {
const cookies = new Cookies();
fetch('https://xxxxxxxxxxxxxxxxx/Customers', {
headers: { Authorization: `Bearer ${cookies.get('userToken')}` }
}).then(response => response.json()).
then(data => {
setDeps(data);
});
}
//This effect will call refreshList() when the component is mounted
useEffect(() => {
refreshList();
}, []); //The empty array tells that this will be executed only when the component mounts
//...
}
UPDATE: This is how you may use it in your component itself
//...
const Customers = () => {
const [data, setData] = useState();
refreshList() {
const cookies = new Cookies();
fetch('https://xxxxxxxxxxxxxxxxx/Customers', {
headers: { Authorization: `Bearer ${cookies.get('userToken')}` }
}).then(response => response.json()).
then(data => {
setData(data);
});
}
useEffect(() => {
refreshList();
}, []);
return (
<div>
<h2 className="page-header">
customers
</h2>
<div className="row">
<div className="col-12">
<div className="card">
<div className="card__body">
<Table
limit='10'
headData={customerTableHead}
renderHead={(item, index) => renderHead(item, index)}
bodyData={data}
renderBody={(item, index) => renderBody(item, index)}
/>
</div>
</div>
</div>
</div>
</div>
)
}
If i write a text korean in ingreInput and use space twice, it will automatically create a dot
like this 안.
However, this happens only when i use Korean.
what should i do ? if i want to resolve this problem?
this is my code
(Upload.js)
const IngreInput = styled.TextInput`
width: 150px;
height: 50px;
border:1px solid #A9A9A9;
margin:5px;
border-radius: 50px;
color: #292929;
text-align: center;
`;
const Upload = ({}) => {
const [ingre1, onChangeIngre1, setIngre1] = useInput('');
}
return (
<IngreInput
placeholder="ingre1"
value={ingre1}
onChange={onChangeIngre1}
/>
)
(useInput.js)
export default (initValue = null) => {
const [value, setValue] = useState(initValue);
const handler = useCallback((e) => {
setValue(e.nativeEvent.text);
}, []);
return [value, handler, setValue];
};
I am working on a progress bar (Eventually..) and I want to stop the animation (calling cancelAnimationRequest) when reaching a certain value (10, 100, ..., N) and reset it to 0.
However, with my current code, it resets to 0 but keeps running indefinitely. I think I might have something wrong in this part of the code:
setCount((prevCount) => {
console.log('requestRef.current', requestRef.current, prevCount);
if (prevCount < 10) return prevCount + deltaTime * 0.001;
// Trying to cancel the animation here and reset to 0:
cancelAnimationFrame(requestRef.current);
return 0;
});
This is the whole example:
const Counter = () => {
const [count, setCount] = React.useState(0);
// Use useRef for mutable variables that we want to persist
// without triggering a re-render on their change:
const requestRef = React.useRef();
const previousTimeRef = React.useRef();
const animate = (time) => {
if (previousTimeRef.current != undefined) {
const deltaTime = time - previousTimeRef.current;
// Pass on a function to the setter of the state
// to make sure we always have the latest state:
setCount((prevCount) => {
console.log('requestRef.current', requestRef.current, prevCount);
if (prevCount < 10) return prevCount + deltaTime * 0.001;
// Trying to cancel the animation here and reset to 0:
cancelAnimationFrame(requestRef.current);
return 0;
});
}
previousTimeRef.current = time;
requestRef.current = requestAnimationFrame(animate);
}
React.useEffect(() => {
requestRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(requestRef.current);
}, []);
return <div>{ Math.round(count) }</div>;
}
ReactDOM.render(<Counter />, document.getElementById('app'));
html {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
body {
font-size: 60px;
font-weight: 700;
font-family: 'Roboto Mono', monospace;
color: #5D9199;
background-color: #A3E3ED;
}
.as-console-wrapper {
max-height: 66px !important;
}
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
Code pen: https://codepen.io/fr-nevin/pen/RwrLmPd
The main problem with your code is that you are trying to cancel an update that has already been executed. Instead, you can just avoid requesting that last update that you don't need. You can see the problem and a simple solution for that below:
const Counter = () => {
const [count, setCount] = React.useState(0);
const requestRef = React.useRef();
const previousTimeRef = React.useRef(0);
const animate = React.useCallback((time) => {
console.log(' RUN:', requestRef.current);
setCount((prevCount) => {
const deltaTime = time - previousTimeRef.current;
const nextCount = prevCount + deltaTime * 0.001;
// We add 1 to the limit value to make sure the last valid value is
// also displayed for one whole "frame":
if (nextCount >= 11) {
console.log(' CANCEL:', requestRef.current, '(this won\'t work as inteneded)');
// This won't work:
// cancelAnimationFrame(requestRef.current);
// Instead, let's use this Ref to avoid calling `requestAnimationFrame` again:
requestRef.current = null;
}
return nextCount >= 11 ? 0 : nextCount;
});
// If we have already reached the limit value, don't call `requestAnimationFrame` again:
if (requestRef.current !== null) {
previousTimeRef.current = time;
requestRef.current = requestAnimationFrame(animate);
console.log('- SCHEDULE:', requestRef.current);
}
}, []);
React.useEffect(() => {
requestRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(requestRef.current);
}, []);
// This floors the value:
// See https://stackoverflow.com/questions/7487977/using-bitwise-or-0-to-floor-a-number.
return (<div>{ count | 0 } / 10</div>);
};
ReactDOM.render(<Counter />, document.getElementById('app'));
html {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
body {
font-size: 60px;
font-weight: 700;
font-family: 'Roboto Mono', monospace;
color: #5D9199;
background-color: #A3E3ED;
}
.as-console-wrapper {
max-height: 66px !important;
}
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
In any case, you are also updating the state many more times than actually needed, which you can avoid by using refs and the timestamp (time) provided by requestAnimationFrame to keep track of the current and next/target counter values. You are still going to call the requestAnimationFrame update function the same number of times, but you will only update the state (setCount(...)) once you know the change is going to be reflected in the UI.
const Counter = ({ max = 10, rate = 0.001, location }) => {
const limit = max + 1;
const [count, setCount] = React.useState(0);
const t0Ref = React.useRef(Date.now());
const requestRef = React.useRef();
const targetValueRef = React.useRef(1);
const animate = React.useCallback(() => {
// No need to keep track of the previous time, store initial time instead. Note we can't
// use the time param provided by requestAnimationFrame to the callback, as that one won't
// be reset when the `location` changes:
const time = Date.now() - t0Ref.current;
const nextValue = time * rate;
if (nextValue >= limit) {
console.log('Reset to 0');
setCount(0);
return;
}
const targetValue = targetValueRef.current;
if (nextValue >= targetValue) {
console.log(`Update ${ targetValue - 1 } -> ${ nextValue | 0 }`);
setCount(targetValue);
targetValueRef.current = targetValue + 1;
}
requestRef.current = requestAnimationFrame(animate);
}, []);
React.useEffect(() => {
requestRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(requestRef.current);
}, []);
React.useEffect(() => {
// Reset counter if `location` changes, but there's no need to call `cancelAnimationFrame` .
setCount(0);
t0Ref.current = Date.now();
targetValueRef.current = 1;
}, [location]);
return (<div className="counter">{ count } / { max }</div>);
};
const App = () => {
const [fakeLocation, setFakeLocation] = React.useState('/');
const handleButtonClicked = React.useCallback(() => {
setFakeLocation(`/${ Math.random().toString(36).slice(2) }`);
}, []);
return (<div>
<span className="location">Fake Location: { fakeLocation }</span>
<Counter max={ 10 } location={ fakeLocation } />
<button className="button" onClick={ handleButtonClicked }>Update Parent</button>
</div>);
};
ReactDOM.render(<App />, document.getElementById('app'));
html {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
body {
font-family: 'Roboto Mono', monospace;
color: #5D9199;
background-color: #A3E3ED;
}
.location {
font-size: 16px;
}
.counter {
font-size: 60px;
font-weight: 700;
}
.button {
border: 2px solid #5D9199;
padding: 8px;
margin: 0;
font-family: 'Roboto Mono', monospace;
color: #5D9199;
background: transparent;
outline: none;
}
.as-console-wrapper {
max-height: 66px !important;
}
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
Okay - I'm having an issue with my react app onChange attribute lagging by one keystroke.
I am pretty sure this has to do with this code block running before the state is being updated.
if (hex.length === 3) {
let newColor = hex.join('');
let newColors = [...colors, newColor];
setColors(newColors);
setHex([]);
}
I tried moving the above block to a useEffect hook (as such) so that it would run when the value of hex changes to remedy this.
useEffect(() => {
if (hex.length === 3) {
let newColor = hex.join('');
let newColors = [...colors, newColor];
setColors(newColors);
setHex([]);
}
}, hex)
This did not work as expected, and I'm still facing the same issue. The goal is when an input is received, if the length of the total ASCII input is 3 characters or more, it will convert that text ([61, 61, 61] for example) into hex string of 6 characters, which will eventually be converted into a color hex code.
All of my code is as follows.
import TextInput from './components/TextInput';
import Swatch from './components/Swatch';
import './App.css';
function App() {
const [colors, setColors] = useState([]);
const [hex, setHex] = useState([]);
const [text, setText] = useState();
const convertToHex = (e) => {
const inputText = e.target.value;
setText(inputText);
for (let n = 0, l = inputText.length; n < l; n++) {
let newHex = Number(inputText.charCodeAt(n)).toString(16);
let newHexArr = [...hex, newHex];
setHex(newHexArr);
}
if (hex.length === 3) {
let newColor = hex.join('');
let newColors = [...colors, newColor];
setColors(newColors);
setHex([]);
}
};
return (
<div className='App'>
<h1 id='title'>Color Palette Generator</h1>
<TextInput func={convertToHex} />
<Swatch color='#55444' />
</div>
);
}
export default App;
How is the TextInput component managing its state (the input's value)?
I suspect you might have a problem there, as changing that TextInput component with a simple controlled input:
<input type="text" onChange={ handleOnChange } value={ text } />
And using your existing code as handleOnChange works as expected:
const App = () => {
const [colors, setColors] = React.useState([]);
const [hex, setHex] = React.useState([]);
const [text, setText] = React.useState('');
const handleOnChange = (e) => {
const inputText = e.target.value;
// I thought your problem was related to `text`...:
setText(inputText);
for (let n = 0, l = inputText.length; n < l; n++) {
let newHex = Number(inputText.charCodeAt(n)).toString(16);
let newHexArr = [...hex, newHex];
setHex(newHexArr);
}
if (hex.length === 3) {
// But looking at the comments it looks like it's related to `colors`, so as
// pointed out already you should be using the functional version of `setState`
// to make sure you are using the most recent value of `colors` when updating
// them here:
setColors(prevColors => [...prevColors, hex.join('')]);
setHex([]);
}
};
return (
<div className="app">
<h1 className="title">Color Palette Generator</h1>
<input className="input" type="text" onChange={ handleOnChange } value={ text } />
<pre>{ hex.join('') }</pre>
<pre>{ colors.join(', ') }</pre>
</div>
);
}
ReactDOM.render(<App />, document.querySelector('#app'));
body {
font-family: monospace;
margin: 0;
}
.app {
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
.title {
display: flex;
margin: 32px 0;
}
.input {
margin: 0 4px;
padding: 8px;
border: 2px solid black;
background: transparent;
border-radius: 2px;
}
.as-console-wrapper {
max-height: 45px !important;
}
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
In any case, there are a few other things that could be improved on that code, such as using a single useState, using useCallback, adding a separate <button> to push colors to the state or actually doing the RGB to HEX conversion:
const App = () => {
const [state, setState] = React.useState({
value: '',
hex: '',
colors: [],
});
const handleInputChange = React.useCallback((e) => {
const nextValue = e.target.value || '';
const nextHex = nextValue
.trim()
.replace(/[^0-9,]/g, '')
.split(',')
.map(x => `0${ parseInt(x).toString(16) }`.slice(-2))
.join('')
.toUpperCase();
setState((prevState) => ({
value: nextValue,
hex: nextHex,
colors: prevState.colors,
}));
}, []);
const handleButtonClick = React.useCallback(() => {
setState((prevState) => {
return prevState.hex.length === 3 || prevState.hex.length === 6 ? {
value: '',
hex: '',
colors: [...prevState.colors, prevState.hex],
} : prevState;
});
}, []);
return (
<div className="app">
<h1 className="title">Color Palette Generator</h1>
<input
type="text"
className="input"
value={ state.value }
onChange={ handleInputChange } />
<input
type="text"
className="input"
value={ `#${ state.hex }` }
readOnly />
<button className="button" onClick={ handleButtonClick }>
Add color
</button>
<pre className="input">{ state.colors.join('\n') }</pre>
</div>
);
}
ReactDOM.render(<App />, document.querySelector('#app'));
body {
font-family: monospace;
margin: 0;
}
.app {
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
.title {
display: flex;
margin: 32px 0;
}
.input,
.button {
margin: 8px 0 0;
padding: 8px;
border: 2px solid black;
background: transparent;
border-radius: 2px;
width: 50vw;
box-sizing: border-box;
}
.button {
cursor: pointer;
}
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
I am trying to create star rating where the functionality has to be following:
In read mode, the stars are shown as per average (should support 100%
i.e 5 or 96% i.e 4.6) in write mode, the user can only rate 1, 1.5, 2,
2.5 etc not 2.6
The read mode is working as expected but is having problem with write mode.
The problem in write mode is I cannot update the rating with non-decimal value from 1 to 5 and also half value like 1.5, 2.5, 3.5 etc. On hovering how do i decide if my mouse pointer is in the full star or half of star? Can anyone look at this, please?
I have created a sandbox for showing the demo
Here it is
https://codesandbox.io/s/9l6kmnw7vw
The code is as follow
UPDATED CODE
// #flow
import React from "react";
import styled, { css } from "styled-components";
const StyledIcon = styled.i`
display: inline-block;
width: 12px;
overflow: hidden;
direction: ${props => props.direction && props.direction};
${props => props.css && css(...props.css)};
`;
const StyledRating = styled.div`
unicode-bidi: bidi-override;
font-size: 25px;
height: 25px;
width: 125px;
margin: 0 auto;
position: relative;
padding: 0;
text-shadow: 0px 1px 0 #a2a2a2;
color: grey;
`;
const TopStyledRating = styled.div`
padding: 0;
position: absolute;
z-index: 1;
display: block;
top: 0;
left: 0;
overflow: hidden;
${props => props.css && css(...props.css)};
width: ${props => props.width && props.width};
`;
const BottomStyledRating = styled.div`
padding: 0;
display: block;
z-index: 0;
`;
class Rating extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
rating: this.props.rating || null,
// eslint-disable-next-line
temp_rating: null
};
}
handleMouseover(rating) {
console.log("rating", rating);
this.setState(prev => ({
rating,
// eslint-disable-next-line
temp_rating: prev.rating
}));
}
handleMouseout() {
this.setState(prev => ({
rating: prev.temp_rating
}));
}
rate(rating) {
this.setState({
rating,
// eslint-disable-next-line
temp_rating: rating
});
}
calculateWidth = value => {
const { total } = this.props;
const { rating } = this.state;
return Math.floor((rating / total) * 100).toFixed(2) + "%";
};
render() {
const { disabled, isReadonly } = this.props;
const { rating } = this.state;
const topStars = [];
const bottomStars = [];
const writableStars = [];
console.log("rating", rating);
// eslint-disable-next-line
if (isReadonly) {
for (let i = 0; i < 5; i++) {
topStars.push(<span>★</span>);
}
for (let i = 0; i < 5; i++) {
bottomStars.push(<span>★</span>);
}
} else {
// eslint-disable-next-line
for (let i = 0; i < 10; i++) {
let klass = "star_border";
if (rating >= i && rating !== null) {
klass = "star";
}
writableStars.push(
<StyledIcon
direction={i % 2 === 0 ? "ltr" : "rtl"}
className="material-icons"
css={this.props.css}
onMouseOver={() => !disabled && this.handleMouseover(i)}
onFocus={() => !disabled && this.handleMouseover(i)}
onClick={() => !disabled && this.rate(i)}
onMouseOut={() => !disabled && this.handleMouseout()}
onBlur={() => !disabled && this.handleMouseout()}
>
{klass}
</StyledIcon>
);
}
}
return (
<React.Fragment>
{isReadonly ? (
<StyledRating>
<TopStyledRating
css={this.props.css}
width={this.calculateWidth(this.props.rating)}
>
{topStars}
</TopStyledRating>
<BottomStyledRating>{bottomStars}</BottomStyledRating>
</StyledRating>
) : (
<React.Fragment>
{rating}
{writableStars}
</React.Fragment>
)}
</React.Fragment>
);
}
}
Rating.defaultProps = {
css: "",
disabled: false
};
export default Rating;
Now the writable stars is separately done to show the stars status when hovering and clicking but when I am supplying rating as 5 it is filling the third stars instead of 5th.
I think your current problem seems to be with where your mouse event is set, as you are handling it on the individual stars, they disappear, and trigger a mouseout event, causing this constant switch in visibility.
I would rather set the detection of the rating on the outer div, and then track where the mouse is in relation to the div, and set the width of the writable stars according to that.
I tried to make a sample from scratch, that shows how you could handle the changes from the outer div. I am sure the formula I used can be simplified still, but okay, this was just to demonstrate how it can work.
const { Component } = React;
const getRating = x => (parseInt(x / 20) * 20 + (x % 20 >= 13 ? 20 : x % 20 >= 7 ? 10 : 0));
class Rating extends Component {
constructor() {
super();
this.state = {
appliedRating: '86%'
};
this.setParentElement = this.setParentElement.bind( this );
this.handleMouseOver = this.handleMouseOver.bind( this );
this.applyRating = this.applyRating.bind( this );
this.reset = this.reset.bind( this );
this.stopReset = this.stopReset.bind( this );
}
stopReset() {
clearTimeout( this.resetTimeout );
}
setParentElement(e) {
this.parentElement = e;
}
handleMouseOver(e) {
this.stopReset();
if (e.currentTarget !== this.parentElement) {
return;
}
const targetRating = getRating(e.clientX - this.parentElement.offsetLeft);
if (this.state.setRating !== targetRating) {
this.setState({
setRating: targetRating
});
}
}
applyRating(e) {
this.setState({
currentRating: this.state.setRating
});
}
reset(e) {
this.resetTimeout = setTimeout(() => this.setState( { setRating: null } ), 50 );
}
renderStars( width, ...classes ) {
return (
<div
onMouseEnter={this.stopReset}
className={ ['flex-rating', ...classes].join(' ')}
style={{width}}>
<span onMouseEnter={this.stopReset} className="star">★</span>
<span onMouseEnter={this.stopReset} className="star">★</span>
<span onMouseEnter={this.stopReset} className="star">★</span>
<span onMouseEnter={this.stopReset} className="star">★</span>
<span onMouseEnter={this.stopReset} className="star">★</span>
</div>
);
}
renderFixed() {
return this.renderStars('100%', 'fixed');
}
renderReadOnlyRating() {
const { appliedRating } = this.state;
return this.renderStars( appliedRating, 'readonly' );
}
renderWriteRating() {
let { setRating, currentRating } = this.state;
if (setRating === 0) {
setRating = '0%';
}
if (currentRating === undefined) {
currentRating = '100%';
}
return this.renderStars( setRating || currentRating, 'writable' );
}
render() {
return (
<div>
<div
ref={ this.setParentElement }
className="rating"
onMouseMove={ this.handleMouseOver }
onMouseOut={ this.reset }
onClick={ this.applyRating }>
{ this.renderFixed() }
{ this.renderReadOnlyRating() }
{ this.renderWriteRating() }
</div>
<div>Current rating: { ( ( this.state.currentRating || 0 ) / 20) }</div>
</div>
);
}
}
ReactDOM.render( <Rating />, document.getElementById('container') );
body { margin: 50px; }
.rating {
font-family: 'Courier new';
font-size: 16px;
position: relative;
display: inline-block;
width: 100px;
height: 25px;
align-items: flex-start;
justify-content: center;
align-content: center;
background-color: white;
}
.flex-rating {
position: absolute;
top: 0;
left: 0;
display: flex;
height: 100%;
overflow: hidden;
cursor: pointer;
}
.fixed {
color: black;
font-size: 1.1em;
font-weight: bold;
}
.readonly {
color: silver;
font-weight: bold;
}
.writable {
color: blue;
background-color: rgba(100, 100, 100, .5);
}
.star {
text-align: center;
width: 20px;
max-width: 20px;
min-width: 20px;
}
<script id="react" src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.2/react.js"></script>
<script id="react-dom" src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/15.6.2/react-dom.js"></script>
<div id="container"></div>