I am still learning React and am trying to parse text from an API call into json objects while displaying progress in a progress bar.
Below, Home.js uses useEffect hook to
call getData that grabs (lots of) API text data
call parseText to parse returned text
call parseSeriesId to parse returned results
set seriesIdParts
Finally, seriesIdParts is passed to Occupation.js as props. That works fine and working code is displayed below.
My problem is that I want to make the progress bar disappear when parseText is finished. I found a post that did it using a ternary operator, so trying that I changed this:
<ProgressBar percentage={percentage}/>
to this:
{percentage >= 100 ? console.log("Done.") : <ProgressBar percentage={percentage}/>
and now the progressBar disappears, but it no longer populates green. I can log the percentage value just fine in both ProgressBar.js and Filler.js, so it seems to be that the Filler width element is not picking up the percentage value increment:
<div className="filler" style={{ width: `${percentage}%`}}>
How do I fix this?
App.js
import React from 'react';
import './App.css';
import Home from './components/Home';
function App() {
return (
<div className="App">
<Home />
</div>
);
}
export default App;
Home.js
import React from 'react';
import { useState, useEffect } from 'react';
import Occupation from './Occupation';
import ProgressBar from './ProgressBar';
const Home = () => {
const [seriesIdParts, setSeriesIdParts] = useState([]);
const [percentage, setPercentage] = useState(0);
async function getData(url) {
const response = await fetch(url);
if( response.status !== 200 ) {
throw new Error('Problem calling ' + url);
}
return response.text();
}
function parseSeriesId(data) {
let results = new Set()
data.forEach(function(value) {
let areaCode = value.seriesId.substring(4, 11);
let industry = value.seriesId.substring(11, 17);
let occupation = value.seriesId.substring(17, 23);
let dataType = value.seriesId.substring(23, value.seriesId.length);
let seriesIdVal = value.value;
if( dataType === '04') {
let result = {areaCode: areaCode, industry: industry, occupation: occupation, dataType: dataType, seriesIdVal: seriesIdVal};
results.add(result);
}
})
return results;
}
useEffect(() => {
getData('https://download.bls.gov/pub/time.series/oe/oe.data.0.Current')
.then(data => {
const parseText = function(data) {
data = data.split('\n');
let localNum = 1090;
let results = new Set();
let intervalAmount = 100 / localNum;
for( let i=1; i<=localNum; i++ ) {
setPercentage((i * intervalAmount));
let line = data[i];
if( line !== "") {
line = line.split('\t');
let seriesIdValue = line[0].trim();
let valueValue = line[3].trim();
if (valueValue !== '-') {
results.add({'seriesId': seriesIdValue, 'value': valueValue});
}
}
}
return results;
}
return parseText(data, 100);
})
.then(data => {
return parseSeriesId(data);
})
.then(data => {
setSeriesIdParts(data);
})
.catch(err => console.log(err));
}, [percentage]);
return(
<div className="homeComponent">
<label id="parsetext" >
Parsing Text...
<ProgressBar percentage={percentage}/>
</label>
{seriesIdParts && <Occupation seriesIdParts={seriesIdParts}/>}
</div>
);
}
export default Home;
ProgressBar.js
import React, { useState } from 'react';
import Filler from './Filler';
const ProgressBar = ({percentage}) => {
// console.log('Pbar: ', percentage); <== Displays fine!
return (
<div className="progressbar">
<Filler percentage={percentage}/>
</div>
);
}
export default ProgressBar;
Filler.js
import React from 'react';
const Filler = ({percentage}) => {
return (
<div className="filler" style={{ width: `${percentage}%`}}>
{console.log('filler: ' + percentage)}. <== Displays fine!
</div>
);
}
export default Filler;
Occupation.js
import React from 'react';
import { useState, useEffect } from 'react';
const Occupation = ({seriesIdParts}) => {
console.log('seriesIdParts in Occupation:', seriesIdParts.size) <== Displays fine!
//other code...
};
export default Occupation;
App.css
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
#media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
#keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.progressbar {
margin: 4em auto;
width: 80%;
height: 2em;
background-color: lightgray;
border-radius: 30px;
}
.filler {
height: inherit;
width: 0px;
background-color: green;
border-radius: inherit;
}
#parsetext {
text-align: 'center';
margin: 4em 0 0;
}
UPDATE: If there is a better way to make the progress bar disappear, I'm open to suggestions!
What if you set the height of the filler class to like 20px instead of inherit just to try?
.filler {
height: inherit;
width: 0px;
background-color: green;
border-radius: inherit;
}
I tried to test just that div and I couldn't see it with "inherit"
Related
I am trying to make a notification toast component. And I want it to be removed after 2 seconds (not-shown on the screed) Although, it is removed (not-shown on the screen via top:-100 argument), the component is getting rendered infinitely. You can see it from the console.log's I have placed inside the component and inside the useEffect call with setTimeout.
My expectation is that setTimeout should run setShowState after 2 seconds and then useEffect should do the cleanup and remove the timer. So everything is back to normal until showState changes.
import React, {useEffect, useState} from 'react'
import I18n from '../../i18n'
import styled from 'styled-components'
import {createGlobalStyle} from 'styled-components'
import {useSelector} from 'react-redux'
const NotificationStyle = createGlobalStyle`
#media (max-width: 500px) {
.notification_mssg {
left: 10px;
}
}
`
const Container = styled.div`
color: white;
position: fixed;
top: ${(props) => props.top}px;
right: 16px;
z-index: 2000;
transition: top 0.5s ease;
`
const NoticitactionIcon = styled.div`
float: left;
font-size: 27px;
width: 40px;
height: 40px;
text-align: center;
`
const NotificationMessage = styled.span`
padding: 10px;
line-height: 40px;
`
function NotificationAlertRoot(props) {
const create_notification = useSelector((state) => state.notifications.create_notification)
const {message, code} = create_notification.success_info
const [showState, setShowState] = useState({top: -100, msg: message, bgColor: '#444'})
// show notification
useEffect(() => {
setShowState({top: 96, msg: I18n.t(message), bgColor: backgroundColor(code)})
}, [message, code])
console.log('amIrendered', showState) // although showState doesn't change, component is getting rendered infinitely :/
// hide notification after 2 seconds
useEffect(() => {
const timerId = setTimeout(() => {
setShowState({
top: -100,
msg: '',
bgColor: `#ffffff00`,
})
console.log("timerId", timerId) // I see timerId is changing so the problem most probably in this useEffect call.
}, 2000)
return () => {
clearTimeout(timerId)
}
}, [showState])
const notificationIcon = (bgColor) => {
switch (bgColor) {
case '#32c786':
return (
<NoticitactionIcon style={{background: '#2aa872'}}>
<i className="zmdi zmdi-info" />
</NoticitactionIcon>
)
case '#ffc721':
return (
<NoticitactionIcon style={{background: '#fabb00'}}>
<i className="zmdi zmdi-alert-triangle" />
</NoticitactionIcon>
)
case '#ff6b68':
return (
<NoticitactionIcon style={{background: '#ff4642'}}>
<i className="zmdi zmdi-alert-circle" />
</NoticitactionIcon>
)
default:
return <span></span>
}
}
function backgroundColor(code) {
switch (Math.floor(code / 100)) {
case 2:
return '#32c786'
case 3:
return '#ffc721'
case 4:
return '#ff6b68'
case 5:
return '#ff6b68'
default:
return '#444'
}
}
return (
<React.Fragment>
<NotificationStyle />
<Container
className="notification_mssg"
top={showState.top}
style={{background: showState.bgColor}}
>
{notificationIcon(showState.bgColor)}
<NotificationMessage>{showState.msg}</NotificationMessage>
</Container>
</React.Fragment>
)
}
export default NotificationAlertRoot
Do you have an idea what is wrong above?
I guess the problem comes from your dependency array. Your useEffect is dependent on showState and each time, you are calling setShowState in your useEffect when setShowState is called showState changes and then again, your useEffect
gets invoked(it is dependent on ShowState), and again setShowState is called and ...
infinity loop!
I found the root of the problem. Sometimes you are too focused on something and you forget the little details of useEffect. It is always dangerous to provide objects as dependency arrays to useEffect. The dependency array values should be simple values. So now I introduced a new state (flag, setFlag) with just boolean values and I make the second useEffect just to follow that simple value. Everything is working just fine now.
import React, {useEffect, useState} from 'react'
import I18n from '../../i18n'
import styled from 'styled-components'
import {createGlobalStyle} from 'styled-components'
import {useSelector} from 'react-redux'
const NotificationStyle = createGlobalStyle`
#media (max-width: 500px) {
.notification_mssg {
left: 10px;
}
}
`
const Container = styled.div`
color: white;
position: fixed;
top: ${(props) => props.top}px;
right: 16px;
z-index: 2000;
transition: top 0.5s ease;
`
const NotificationIcon = styled.div`
float: left;
font-size: 27px;
width: 40px;
height: 40px;
text-align: center;
`
const NotificationMessage = styled.span`
padding: 10px;
line-height: 40px;
`
function NotificationAlertRoot(props) {
const create_notification = useSelector((state) => state.notifications.create_notification)
const {message, code} = create_notification.success_info
const [showState, setShowState] = useState({top: -100, msg: message, bgColor: '#444'})
const [flag, setFlag] = useState(false) // when you follow the showState at the second useEffect you have an infinite loop. Because it is an object.
// show notification
useEffect(() => {
setShowState({top: 96, msg: I18n.t(message), bgColor: backgroundColor(code)})
setFlag(true)
}, [message, code])
// hide notification after 2 seconds
useEffect(() => {
const timerId = setTimeout(() => {
setShowState({top: -100,msg: '', bgColor: `#ffffff00`})
setFlag(false)
}, 2000)
return () => {
clearTimeout(timerId)
}
}, [flag]) // showState
const notificationIcon = (bgColor) => {
switch (bgColor) {
case '#32c786':
return (
<NotificationIcon style={{background: '#2aa872'}}>
<i className="zmdi zmdi-info" />
</NotificationIcon>
)
case '#ffc721':
return (
<NotificationIcon style={{background: '#fabb00'}}>
<i className="zmdi zmdi-alert-triangle" />
</NotificationIcon>
)
case '#ff6b68':
return (
<NotificationIcon style={{background: '#ff4642'}}>
<i className="zmdi zmdi-alert-circle" />
</NotificationIcon>
)
default:
return <span></span>
}
}
const backgroundColor = (code) => {
switch (Math.floor(code / 100)) {
case 2:
return '#32c786'
case 3:
return '#ffc721'
case 4:
return '#ff6b68'
case 5:
return '#ff6b68'
default:
return '#444'
}
}
return (
<React.Fragment>
<NotificationStyle />
<Container
className="notification_mssg"
top={showState.top}
style={{background: showState.bgColor}}
>
{notificationIcon(showState.bgColor)}
<NotificationMessage>{showState.msg}</NotificationMessage>
</Container>
</React.Fragment>
)
}
export default NotificationAlertRoot
The element works when using an external node module but not when using a locally downloaded node module but I can't work out why?
here's the Swipi-cards library:
https://github.com/riolcrt/swipi-cards/blob/master/demo/index.html
My code so far(working) but when I use local node module as source it doesn't any fixes?
import React, { useState, useEffect } from 'react';
import Data from '../Data/webApps_data';
function Webapps() {
const [loading_animation, setloading_animation] = useState(false);
const [arrayChecker, set_arrayChecker] = useState(0);
useEffect(() => {
if (loading_animation === false) {
setTimeout(() => {
setloading_animation(!loading_animation);
console.log()
}, 100);
}
const script = document.createElement('script');
script.src = "https://unpkg.com/swipi-cards#1.0.0/dist/swipi-cards/swipi-cards.js";
script.async = true;
document.body.appendChild(script);
return () => {
document.body.removeChild(script);
}
}, [loading_animation]);
const arrayLimiter = () => {
if (arrayChecker < (Data.length - 1)) {
set_arrayChecker(arrayChecker + 1)
} else {
set_arrayChecker(0)
}
console.log(Data);
};
const filteredData = Data[arrayChecker];
const textTransition = loading_animation ? 'text_transition ease' : 'text_transition';
const elementTransition = loading_animation ? 'element_transition ease' : 'element_transition';
//swipicard script
return (
<div className='webAppStyles'>
<rg-swipi-cards stack-offset-y="0.3" class='hydrated'>
<rg-swipi-card left-color='green' right-color='green' class='hydrated'>
<p>test1</p>
</rg-swipi-card>
<rg-swipi-card left-color='green' right-color='green' class='hydrated'>
<p>test2</p>
</rg-swipi-card>
</rg-swipi-cards>
</div>
)
}
scss styles:
rg-swipi-cards {
display: flex;
align-self: center;
background: chocolate;
width: 350px !important;
height: 400px !important;
align-items: center;
justify-content: center;
rg-swipi-card {
width: 100%;
position: absolute;
transition: all 0.5s ease-in-out 0s;
z-index: 4;
opacity: 1.33333;
}
p {
text-align: center;
}
}
ok so using this as the source works but I don't know why this must mean the node module doesn't work any ideas?
script.src = "https://unpkg.com/swipi-cards#1.0.0/dist/swipi-cards/swipi-cards.js";
I have different "cards" that on click onClick I want their margin-left property to be modified
To do that I use useState, for which I have only one state that is an object that stores the states for all the cards
The below example code shows the problem, but a simplified version that doesn't have a component <Type> and that uses a simple elements array works as expected
So, if I need to use a structure like the one below, how could I keep the transition effect?
Example code
https://codesandbox.io/s/keen-shadow-2v16s?fontsize=14&hidenavigation=1&theme=dark
import React, { useState } from "react";
import styled from "#emotion/styled";
export default function App() {
const [userTap, setUserTap] = useState({});
const elements1 = [...Array(5)];
const elements2 = [...Array(3)];
const Type = ({ list }) =>
list.map((el, i) => (
<Ingredient
key={"draggable" + i}
onClick={e => {
e.stopPropagation();
e.preventDefault();
userTap[i] = userTap[i] ? 0 : 1;
setUserTap({ ...userTap }); // create a new ref to provoke the rerender
return;
}}
userTap={userTap[i]}
>
<div>item</div>
</Ingredient>
));
return (
<>
<Type list={elements1} />
<Type list={elements2} />
</>
);
}
const Ingredient = styled.li`
list-style: none;
cursor: pointer;
margin: 5px;
padding: 5px;
background: #ccc;
border-radius: 3px;
width: 50px;
margin-left: ${props => (props.userTap ? "100px" : "15px")};
transition: all 0.2s ease-in;
`;
The only thing needed to be done, as #larz suggested in the comments, is to move the useState to the last component, as shown below
https://codesandbox.io/s/affectionate-hawking-5p81d?fontsize=14&hidenavigation=1&theme=dark
import React, { useState } from "react";
import styled from "#emotion/styled";
export default function App() {
const elements1 = [...Array(5)];
const elements2 = [...Array(3)];
const Type = ({ list, type }) => {
const [userTap, setUserTap] = useState({});
return list.map((el, i) => (
<Ingredient
key={"draggable" + i}
onClick={e => {
e.stopPropagation();
e.preventDefault();
userTap[type + i] = userTap[type + i] ? 0 : 1;
setUserTap({ ...userTap }); // create a new ref to provoke the rerender
return;
}}
userTap={userTap[type + i]}
>
<div>item</div>
</Ingredient>
));
};
return (
<>
<Type list={elements1} type="one" />
<Type list={elements2} type="two" />
</>
);
}
const Ingredient = styled.li`
list-style: none;
cursor: pointer;
margin: 5px;
padding: 5px;
background: #ccc;
border-radius: 3px;
width: 50px;
margin-left: ${props => (props.userTap ? "100px" : "15px")};
transition: all 0.2s ease-in;
`;
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>
I'm currently working on an image uploader component in React. Everything works fine but the deleting method. I've read a couple of articles on how to update arrays/objects and the idea of immutable state. Here's what I've tried:
.filter()
.slice()
.splice() (I doubt this would work as it modifies the original array)
And I always got this error no matter what I tried:
Warning: Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.
And this is my code:
ImageUploader.js
import React, { Component } from 'react';
import styled from 'styled-components';
import FileUploadButton from '../FileUploadButton';
import ImagePreviewer from './ImagePreviewer';
import {
Typography,
Button
} from '#material-ui/core';
import theme from '../../../theme';
import uuidv5 from 'uuid/v5';
const StyledPreviewerContainer = styled.div`
display: flex;
margin: ${theme.spacing.unit}px 0;
overflow: hidden;
overflow-x: auto;
`;
export default class ImageUploader extends Component {
state = {
uploadedImages: []
}
updateImages = e => {
const { uploadedImages } = this.state,
files = [...e.target.files],
inexistentImages = files.filter(image => uploadedImages.indexOf(image) === -1);
this.setState(prevState => ({
uploadedImages: [...prevState.uploadedImages, ...inexistentImages]
}));
this.props.onChange(e);
}
removeImages = image => {
const { uploadedImages } = this.state,
imageIndex = uploadedImages.indexOf(image);
this.setState(prevState => ({
uploadedImages: prevState.uploadedImages.filter((image, index) => index !== imageIndex)
}));
};
render() {
const {
className,
label,
id,
multiple,
name,
onBlur
} = this.props, {
uploadedImages
} = this.state;
return (
<div className={className}>
<Typography>
{label}
</Typography>
<StyledPreviewerContainer>
{uploadedImages.map(image =>
<ImagePreviewer
src={URL.createObjectURL(image)}
image={image}
removeImages={this.removeImages}
key={uuidv5(image.name, uuidv5.URL)}
/>
)}
</StyledPreviewerContainer>
<FileUploadButton
id={id}
multiple={multiple}
name={name}
onChange={this.updateImages}
onBlur={onBlur}
/>
<Button>
Delete all
</Button>
</div>
);
}
}
ImagePreviewer.js
import React, { Component } from 'react';
import styled from 'styled-components';
import AnimatedImageActions from './AnimatedImageActions';
import { ClickAwayListener } from '#material-ui/core';
import theme from '../../../theme';
const StyledImagePreviewer = styled.div`
height: 128px;
position: relative;
user-select: none;
cursor: pointer;
&:not(:last-child) {
margin-right: ${theme.spacing.unit * 2}px;
}
`;
const StyledImage = styled.img`
height: 100%;
`;
export default class ImagePreviewer extends Component {
state = {
actionsOpened: false
};
openActions = () => {
this.setState({
actionsOpened: true
});
};
closeActions = () => {
this.setState({
actionsOpened: false
});
};
render() {
const {
actionsOpened
} = this.state,
{
src,
image,
removeImages
} = this.props;
return (
<ClickAwayListener onClickAway={this.closeActions}>
<StyledImagePreviewer onClick={this.openActions}>
<StyledImage src={src} />
<AnimatedImageActions
actionsOpened={actionsOpened}
image={image}
removeImages={removeImages}
/>
</StyledImagePreviewer>
</ClickAwayListener>
);
}
}
AnimatedImageActions.js
import React from 'react';
import styled from 'styled-components';
import { Button } from '#material-ui/core';
import { Delete as DeleteIcon } from '#material-ui/icons';
import { fade } from '#material-ui/core/styles/colorManipulator';
import theme from '../../../theme';
import {
Motion,
spring
} from 'react-motion';
const StyledImageActions = styled.div`
position: absolute;
top: 0;
left: 0;
color: ${theme.palette.common.white};
background-color: ${fade(theme.palette.common.black, 0.4)};
width: 100%;
height: 100%;
display: flex;
`;
const StyledImageActionsInner = styled.div`
margin: auto;
`;
const StyledDeleteIcon = styled(DeleteIcon)`
margin-right: ${theme.spacing.unit}px;
`;
const AnimatedImageActions = ({ actionsOpened, removeImages, image }) =>
<Motion
defaultStyle={{
scale: 0
}}
style={{
scale: spring(actionsOpened ? 1 : 0, {
stiffness: 250
})
}}
>
{({ scale }) =>
<StyledImageActions style={{
transform: `scale(${scale})`
}}>
<StyledImageActionsInner>
<Button
color="inherit"
onClick={removeImages(image)}
>
<StyledDeleteIcon />
Delete
</Button>
</StyledImageActionsInner>
</StyledImageActions>
}
</Motion>
;
export default AnimatedImageActions
Any help would be greatly appreciated!
Could it be that onClick={removeImages(image)} should be onClick={()=>removeImages(image)}?
Otherwise, removeImages is calling setState in AnimatedImageActions's render pass.