I'm trying to create an image gallery that has a variable number of inputs. I have successfully created an add button which will add a new element to my array that is in the state. However, when I click the button to remove an element from the array, it removes all elements except the first one. Could someone help me figure out where I'm going wrong here?
My initialization/add/remove logic in parent component:
const newImage = {
fileName: 'placeholder.png',
description: '',
}
const [galleryImages, setGalleryImages] = useState([newImage])
const addNewImage = () => {
setGalleryImages(galleryImages.concat(newImage))
}
const removeImage = (index) => {
setGalleryImages(galleryImages.splice(index, 1))
}
My image gallery component:
const ImageGallery = ({galleryImages, setGalleryImages, addNewImage, removeImage}) => {
console.log('gallery images:', galleryImages)
return(
galleryImages.map((image, index) => {
const fileId = 'image' + (index + 1) + 'File'
const descriptionId = 'image' + (index + 1) + 'Description'
return(
<Card key={index} style={{marginTop: '10px'}}>
<Card.Body>
<div style={{position: 'absolute', top:'5px', right:'5px'}}>
<IconButton aria-label="remove" color="secondary" onClick={() => removeImage(index)}>
<CancelIcon />
</IconButton>
</div>
<Card.Title>Image {index+1}</Card.Title>
<Form.Group>
<Form.File id={fileId} />
<Form.Label>Image Description</Form.Label>
<Form.Control id={descriptionId} type="text" placeholder="Image description..."/>
</Form.Group>
</Card.Body>
{ index === (galleryImages.length - 1) &&
<div style={{left: '0px', right:'0px', flex: 1, display: 'flex', justifyContent: 'center', alignItems: 'center', bottom: '-30px', position: 'absolute'}}>
<IconButton aria-label="add another image" onClick={() => addNewImage()}>
<AddCircleIcon style={{color: 'green', fontSize: 40, backgroundColor: 'white', borderRadius: '50%'}}/>
</IconButton>
</div>
}
</Card>
)
})
)
}
Splice mutates the array directly, which is generally disapproved in React.
While the recommended approach is using the filter method to remove, you can do it in this way if u want to use splice -
const removeImage = (index) => {
//create a new array here with the spread operator of the original array.
//Without this, react won't recognize the change and the child component won't be re-rendered!
const galleryImagesData = [...galleryImages];
galleryImagesData.splice(index, 1)
setGalleryImages(galleryImagesData)
}
Related
im trying to render a list and assign my SkillImageTapAction an index to use later but it seems only the last index from the list is saved in each case
This is the flat list
<View style={{ marginTop: 16, marginBottom: 48 }}>
<FlatList
data={matchedHelpers}
renderItem={matchedHelper => renderHelperCell(matchedHelper, props)}
keyExtractor={(matchedHelper: any) => matchedHelper.uid}
numColumns={1}
ListFooterComponent={<ListFooter />}
/>
</View>
And this is the render item
const renderHelperCell = (itemProps, props) => {
const { item } = itemProps;
const { index } = itemProps;
console.log("Index-->",index)
return (
<HelperCell matchedHelper={item} props={props} skillImageTapAction={() => {
setImages(item.matchedSkill.images);
console.log("Index2-->",index)
setTappedIndex(index)
setShowImageSlider(true);
}} />
);
};
I have a dynamically generated set of dropdowns and accordions that populate at render client-side (validated user purchases from db).
I'm running into an error that I'm sure comes from my menu anchorEl not knowing 'which' menu to open using anchorEl. The MUI documentation doesn't really cover multiple dynamic menus, so I'm unsure of how to manage which menu is open
Here is a pic that illustrates my use-case:
As you can see, the menu that gets anchored is actually the last rendered element. Every download button shows the last rendered menu. I've done research and I think I've whittled it down to the anchorEl and open props.
Here is my code. Keep in mind, the data structure is working as intended, so I've omitted it to keep it brief, and because it's coming from firebase, I'd have to completely recreate it here (and I think it's redundant).
The component:
import { useAuth } from '../contexts/AuthContext'
import { Accordion, AccordionSummary, AccordionDetails, Button, ButtonGroup, CircularProgress, ClickAwayListener, Grid, Menu, MenuItem, Typography } from '#material-ui/core'
import { ExpandMore as ExpandMoreIcon } from '#material-ui/icons'
import LoginForm from '../components/LoginForm'
import { motion } from 'framer-motion'
import { useEffect, useState } from 'react'
import { db, functions } from '../firebase'
import styles from '../styles/Account.module.scss'
export default function Account() {
const { currentUser } = useAuth()
const [userPurchases, setUserPurchases] = useState([])
const [anchorEl, setAnchorEl] = useState(null)
const [generatingURL, setGeneratingURL] = useState(false)
function openDownloads(e) {
setAnchorEl(prevState => (e.currentTarget))
}
function handleClose(e) {
setAnchorEl(prevState => null)
}
function generateLink(prefix, variationChoice, pack) {
console.log("pack from generate func", pack)
setGeneratingURL(true)
const variation = variationChoice ? `${variationChoice}/` : ''
console.log('link: ', `edit-elements/${prefix}/${variation}${pack}.zip`)
setGeneratingURL(false)
return
if (pack.downloads_remaining === 0) {
console.error("No more Downloads remaining")
setGeneratingURL(false)
handleClose()
return
}
handleClose()
const genLink = functions.httpsCallable('generatePresignedURL')
genLink({
fileName: pack,
variation: variation,
prefix: prefix
})
.then(res => {
console.log(JSON.stringify(res))
setGeneratingURL(false)
})
.catch(err => {
console.log(JSON.stringify(err))
setGeneratingURL(false)
})
}
useEffect(() => {
if (currentUser !== null) {
const fetchData = async () => {
// Grab user products_owned from customers collection for user UID
const results = await db.collection('customers').doc(currentUser.uid).get()
.then((response) => {
return response.data().products_owned
})
.catch(err => console.log(err))
Object.entries(results).map(([product, fields]) => {
// Grabbing each product document to get meta (title, prefix, image location, etc [so it's always current])
const productDoc = db.collection('products').doc(product).get()
.then(doc => {
const data = doc.data()
const productMeta = {
uid: product,
title: data.title,
main_image: data.main_image,
product_prefix: data.product_prefix,
variations: data.variations
}
// This is where we merge the meta with the customer purchase data for each product
setUserPurchases({
...userPurchases,
[product]: {
...fields,
...productMeta
}
})
})
.catch(err => {
console.error('Error retrieving purchases. Please refresh page to try again. Full error: ', JSON.stringify(err))
})
})
}
return fetchData()
}
}, [currentUser])
if (userPurchases.length === 0) {
return (
<CircularProgress />
)
}
return(
currentUser !== null && userPurchases !== null ?
<>
<p>Welcome, { currentUser.displayName || currentUser.email }!</p>
<Typography variant="h3" style={{marginBottom: '1em'}}>Purchased Products:</Typography>
{ userPurchases && Object.values(userPurchases).map((product) => {
const purchase_date = new Date(product.purchase_date.seconds * 1000).toLocaleDateString()
return (
<motion.div key={product.uid}>
<Accordion style={{backgroundColor: '#efefef'}}>
<AccordionSummary expandIcon={<ExpandMoreIcon style={{fontSize: "calc(2vw + 10px)"}}/>} aria-controls={`${product.title} accordion panel`}>
<Grid container direction="row" alignItems="center">
<Grid item xs={3}><img src={product.main_image} style={{ height: '100%', maxHeight: "200px", width: '100%', maxWidth: '150px' }}/></Grid>
<Grid item xs={6}><Typography variant="h6">{product.title}</Typography></Grid>
<Grid item xs={3}><Typography variant="body2"><b>Purchase Date:</b><br />{purchase_date}</Typography></Grid>
</Grid>
</AccordionSummary>
<AccordionDetails style={{backgroundColor: "#e5e5e5", borderTop: 'solid 6px #5e5e5e', padding: '0px'}}>
<Grid container direction="column" className={styles[`product-grid`]}>
{Object.entries(product.packs).map(([pack, downloads]) => {
// The pack object right now
return (
<Grid key={ `${pack}-container` } container direction="row" alignItems="center" justify="space-between" style={{padding: '2em 1em'}}>
<Grid item xs={4} style={{ textTransform: 'uppercase', backgroundColor: 'transparent' }}><Typography align="left" variant="subtitle2" style={{fontSize: 'calc(.5vw + 10px)'}}>{pack}</Typography></Grid>
<Grid item xs={4} style={{ backgroundColor: 'transparent' }}><Typography variant="subtitle2" style={{fontSize: "calc(.4vw + 10px)"}}>{`Remaining: ${downloads.downloads_remaining}`}</Typography></Grid>
<Grid item xs={4} style={{ backgroundColor: 'transparent' }}>
<ButtonGroup variant="contained" fullWidth >
<Button id={`${pack}-btn`} disabled={generatingURL} onClick={openDownloads} color='primary'>
<Typography variant="button" style={{fontSize: "calc(.4vw + 10px)"}} >{!generatingURL ? 'Downloads' : 'Processing'}</Typography>
</Button>
</ButtonGroup>
<ClickAwayListener key={`${product.product_prefix}-${pack}`} mouseEvent='onMouseDown' onClickAway={handleClose}>
<Menu anchorOrigin={{ vertical: 'top', horizontal: 'right' }} transformOrigin={{ vertical: 'top', horizontal: 'right' }} id={`${product}-variations`} open={Boolean(anchorEl)} anchorEl={anchorEl}>
{product.variations && <MenuItem onClick={() => generateLink(product.product_prefix, null, pack) }>{`Pack - ${pack}`}</MenuItem>}
{product.variations && Object.entries(product.variations).map(([variation, link]) => {
return (
<MenuItem key={`${product.product_prefix}-${variation}-${pack}`} onClick={() => generateLink(product.product_prefix, link, pack)}>{ variation }</MenuItem>
)
})}
</Menu>
</ClickAwayListener>
</Grid>
</Grid>
)}
)}
</Grid>
</AccordionDetails>
</Accordion>
</motion.div>
)
})
}
</>
:
<>
<p>No user Signed in</p>
<LoginForm />
</>
)
}
I think it also bears mentioning that I did check the rendered HTML, and the correct lists are there in order - It's just the last one assuming the state. Thanks in advance, and please let me know if I've missed something, or if I can clarify in any way. :)
i couldn't manage to have a menu dynamic,
instead i used the Collapse Panel example and there i manipulated with a property isOpen on every item of the array.
Check Cards Collapse Example
On the setIsOpen method you can change this bool prop:
const setIsOpen = (argNodeId: string) => {
const founded = tree.find(item => item.nodeId === argNodeId);
const items = [...tree];
if (founded) {
const index = tree.indexOf(founded);
founded.isOpen = !founded.isOpen;
items[index]=founded;
setTree(items);
}
};
<IconButton className={clsx(classes.expand, {
[classes.expandOpen]: node.isOpen,
})}
onClick={()=>setIsOpen(node.nodeId)}
aria-expanded={node.isOpen}
aria-label="show more"
>
<MoreVertIcon />
</IconButton>
</CardActions>
<Collapse in={node.isOpen} timeout="auto" unmountOnExit>
<CardContent>
<MenuItem onClick={handleClose}>{t("print")}</MenuItem>
<MenuItem onClick={handleClose}>{t("commodities_management.linkContainers")}</MenuItem>
<MenuItem onClick={handleClose}>{t("commodities_management.linkDetails")}</MenuItem>
</CardContent>
</Collapse>
I think this is the right solution for this: https://stackoverflow.com/a/59531513, change the anchorEl for every Menu element that you render. :D
This code belongs to TS react if you are using plain JS. Then remove the type.
import Menu from '#mui/material/Menu';
import MenuItem from '#mui/material/MenuItem';
import { useState } from 'react';
import { month } from '../../helper/Utilities';
function Company() {
const [anchorEl, setAnchorEl] = useState<HTMLElement[]>([]);
const handleClose = (event: any, idx: number) => {
let array = [...anchorEl];
array.splice(idx, 1);
setAnchorEl(array);
};
<div>
{month &&
month.map((val: any, ind: number) => {
return (
<div
key={val.id + 'w9348w344ndf allBankAndCardAccountOfClient'}
style={{ borderColor: ind === 0 ? '#007B55' : '#919EAB52' }}
>
<Menu
id='demo-positioned-menu'
aria-labelledby='demo-positioned-button'
anchorEl={anchorEl[ind]}
open={anchorEl[ind] ? true : false}
key={val.id + 'w9348w344ndf allBankAndCardAccountOfClient' + ind}
onClick={(event) => handleClose(event, ind)}
anchorOrigin={{
vertical: 'top',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
>
<MenuItem
key={val.id + 'w9348w344ndf allBankAndCardAccountOfClient' + ind}
onClick={(event) => handleClose(event, ind)}
style={{
display: ind === 0 ? 'none' : 'inline-block',
}}
>
<span
style={{
marginLeft: '.5em',
color: 'black',
background: 'inherit',
}}
>
Make Primary
</span>
</MenuItem>
<MenuItem onClick={(event) => handleClose(event, ind)}>
<span style={{ marginLeft: '.5em', color: 'black' }}>Edit</span>
</MenuItem>
<MenuItem
onClick={(event) => handleClose(event, ind)}
style={{
display: ind === 0 ? 'none' : 'inline-block',
}}
>
<span style={{ marginLeft: '.5em', color: 'red' }}>Delete</span>
</MenuItem>
</Menu>
</div>
);
})}
</div>;
}
export default Company;
I am new in React and trying to build dynamic form. It seems to work fine. The problem is when i type the field values, they are shown in screen, but the value property of Textinput remain null. I tried to explore all options, and it came down to async of setState. Since i am new i do not know how to make a call back function which can populate the value property of the dynamic form fields.
I have not inlcuded all the code, just what i thought would be relevant to avoid burden.
thanks
sal
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
InputArray: [],
view_data: {
id: 0,
data: null
},
step: 0,
TotalItem: [],
item:
{
id: 0,
Email: null,
Password: null,
Address: null
}
}
};
///onCHANGE FUNCTIONS
EnterValue1 = (e) => {
e.persist();
let item = { ...this.state.item };
item.Email= e.target.value;
this.setState({ item: item });
EnterValue2 = (e) => {
e.persist();
let item = { ...this.state.item };
item.Password = e.target.value;
this.setState({ item: item });
EnterValue3 = (e) => {
e.persist();
let item = { ...this.state.item };
item.Address = e.target.value;
this.setState({ item: item });
//Dynamic form
Inputs = () => {
return (
<View >
<TextInput
placeholder="enter email"
onBlur={this.focusHandler}
value={this.state.item.Email}
onChange={this.EnterValue1}
style={{ borderWidth: 2, borderColor: 'skyblue', margin: 20 }}
/>
<TextInput
placeholder="Password"
onBlur={this.focusHandler}
value={this.state.item.Password}
onChange={this.EnterValue2}
style={{ borderWidth: 2, borderColor: 'skyblue', margin: 20 }}
/>
<TextInput
placeholder="Address"
onBlur={this.focusHandler}
value={this.state.item.Address}
onChange={this.EnterValue3}
style={{ borderWidth: 2, borderColor: 'skyblue', margin: 20 }}
/>
</View>
)
};
// Render Method
render() {
return (
<View style={{ flex: 1, marginTop: 20 }}>
<ScrollView style={styles.scrollView} keyboardShouldPersistTaps='always' >
{this.state.InputArray.map((item, index) => (
//using highlight because it doenst pass on its effect to children, opacity does
<View key={index} onPress={() => this.viewPress(index)}>
<TouchableOpacity onPress={() => this.viewPress(index)}>
{item.data}
{this.state.step === 0 ?
<View style={styles.container}>
<View style={{ flex: 1 }} >
<Button type='button' style={{ flex: 1, backgroundColor: 'red' }} title="add" onPress={this.add} />
</View>
</View>
:
<View style={styles.container}>
<View style={{ flex: 1 }} >
<Button type='submit' style={{ flex: 1, backgroundColor: 'red' }} title="add" onPress={this.add} />
</View>
<View style={{ flex: 1 }}>
<Button type='button' style={{ flex: 1, backgroundColor: 'red' }} title="Remove" onPress={() => this.remove(index)} />
</View>
</View>
}
</TouchableOpacity>
</View>
))
}
</ScrollView>
<View style={styles.container}>
<View style={{ flex: 1 }}>
<Button type='submit' style={{ flex: 1, backgroundColor: 'blue' }} title="submit" onPress={this.submit} />
</View>
</View>
</View>
);
}
}
EnterValue3 = (e) => {
e.persist();
let item = { };
this.setState({ item: {...this.state.item, address: e.target.value });
}
Replace all your function with spread operator rather than directly assigning into the object.
Try this and check
EnterValue1 = (e) => {
e.persist();
this.setState({
item: {
...this.state.item,
Email: e.target.value,
}
});
}
Note: Your whole code may help much to debug your issue
Just in case for someone interested. This may not be the best solution. As this is my first project in react native.
Although i was not able to get the values prop using this.state, and they remained null. For my dynamic form, i made a function containing my Views/textinput with an index argument, provided by my map function(which iterates over an array that has length equal to number of forms added). I used onChageText method and in setState used a callback to save the typed values in an object with an id, that needs to be equivalent to the index of my dynamics mapped form. Using index of object of arrays, values=this.state.object[index],values, is saved in dynamic form.
It still did not populate the values prop, but it sure did maintain the typed content in the front end of the previous form when i add new form.
I'm using a React/MUI Popover inside a react-window List element and am unable to get the Popover to position correctly -- it always winds up in the top left corner of the window (the component is unable to perform a getBoundingClientRctd() on the anchor element [anchorEl in the docs]).
So to get around that problem temporarily, I decided to use the anchorPosition parameter which allows to set an absolute position -- in my case, just the middle of the window. That's not working either.
I've reviewed the values in Chrome DevTools and everything seems to be OK (i.e., I do get an anchorEl when I'm using that; I get valid positionLeft/Top values; etc...
Probably something really simple and hoping someone can point out what I did wrong.
Edited: Key elements of the solution
Row component must be defined outside of the containing component.
the <List> component has an itemData attribute which is used to pass custom data to Row.
Edited to add react-window List renderer.
Here's the basic setup:
Popover renderer
renderPopover(template, itemId) {
const { anchorEl, anchorId } = this.state;
const { classes } = this.props;
const open = Boolean(anchorEl) && anchorId === itemId;
const bgColor = '#babaef';
const { innerHeight, innerWidth } = window;
const positionLeft = innerWidth / 2;
const positionTop = innerHeight / 2;
console.log(`renderPopover: ${positionLeft} / ${positionTop}`);
<Popover
id="simple-popper"
open={open}
style={{ color: 'Black' }}
anchorEl={anchorEl}
onClose={event => this.handlePopoverClose(event)}
anchorPosition={{ left: {positionLeft}, top: {positionTop} }}
anchorReference="anchorPosition"
>
<Typography style={{ backgroundColor: bgColor }} className={classes.typography}>
{this.renderScheduleElements(template, itemId)}
</Typography>
</Popover>
);
}
Button element renderer
renderScheduleComponent(template, itemId) {
const { anchorEl, anchorId } = this.state;
const open = Boolean(anchorEl) && anchorId === itemId;
const { classes } = this.props;
const id = open ? 'simple-popper' : undefined;
return (
<Grid key={itemId} item>
<Paper className={classes.paper}>
<div style={{ padding: '4px' }}>
<Button
NO_ref={itemId}
NODE_ref={(node) => this.buttonRef = node}
id={itemId}
name={itemId}
aria-owns={id}
aria-haspopup="true"
variant="outlined"
color="primary"
style={{
fontWeight: 'bold',
padding: '8px',
margin: 'auto',
display: 'block',
width: '100%',
}}
onClick={event => this.handlePopoverClick(event, itemId)}
>
{template.templateName}
</Button>
{(this.renderPopover).call(this, template, itemId)}
</div>
</Paper>
</Grid>
);
}
Click event handler
handlePopoverClick(event, id) {
event.preventDefault();
console.log(`handlePopoverClick : ${event.currentTarget.name}`);
this.setState({
anchorEl: event.currentTarget,
anchorId: id,
});
}
react-window List renderer
renderScheduleColumn(columnData) {
const { classes } = this.props;
const { scheduleDate, scheduleTemplates } = columnData;
this.scheduleTemplates = scheduleTemplates;
const Row = ({ index, style }) => {
return (
<div className={index % 2 ? "ListItemOdd" : "ListItemEven"} style={style}>
{this.renderScheduleComponent(scheduleTemplates[index], `${scheduleDate}:${index}`)}
</div>
);
}
const { columnHeight, columnWidth } = this.state;
return (
<Grid id={scheduleDate} key={scheduleDate} item>
<Paper className={classes.paper}>
<div style={{ width: '100%', textAlign: 'center' }}>
<Typography variant="h6" style={{ padding: '24px', color: 'white', backgroundColor: '#3f51b5' }}>
{scheduleDate}
</Typography>
</div>
<List
className="List"
height={columnHeight}
itemCount={scheduleTemplates.length}
itemSize={50}
width={columnWidth}
>
{Row}
</List>
</Paper>
</Grid>
);
}
It looks like a similar problem as here: React Material-UI menu anchor broken by react-window list.
You should move the definition of your Row function out of renderScheduleColumn so that it is a consistent type. This will require moving/reworking renderScheduleComponent as well. You can use the itemData property on the List to pass information to the Row.
I have a screen with a button upon pressing which, a new view with a picker will be rendered. How do I set selectedValue for these pickers, if they all should be independent from each other?
I tried using an array and passing indices as an argument to the view generating function, but that doesn't seem to work.
import React from "react";
import { View, Picker, Button } from "react-native";
export default class SessionScreen extends React.Component {
state = {
externalData: ["Player1", "Player2", "Player3", "Player4"],
view: [],
selectedPlayers: []
};
async view(index) {
let players = this.state.externalData.map((s, i) => {
return <Picker.Item key={i} value={s} label={s} />;
});
let newVal = (
<View
key={this.state.selectedPlayers[index] + "view" + index}
style={{ flex: 1, flexDirection: "column", paddingTop: 60 }}
>
<View style={{ flexDirection: "row" }}>
<Picker
key={this.state.selectedPlayers[index] + "picker" + index}
selectedValue={this.state.selectedPlayers[index - 1]}
style={{ height: 50, width: 200 }}
onValueChange={itemValue =>
this.setState(prevState => ({
selectedPlayers: [...prevState.selectedPlayers, itemValue]
}))
}
>
{players}
</Picker>
</View>
</View>
);
await this.setState(prevState => ({
view: [...prevState.view, newVal]
}));
}
async addPlayer() {
await this.view(this.state.selectedPlayers.length);
}
render() {
var returnValue = [];
if (this.state.view.length > 0) returnValue = [...this.state.view];
returnValue.push(
<View
key={returnValue.length + 1}
style={{ flex: 1, flexDirection: "column", paddingTop: 100 }}
>
<Button
title="Add a player"
onPress={this.addPlayer.bind(this)}
accessibilityLabel="Add a new player to the table"
>
Add a player
</Button>
</View>
);
return returnValue;
}
}
When Add A Player button is pressed, a new Picker appears on the screen. When I pick a different value from the given items, it is still displaying 'Player1'. But when I press 'Add a Player' again, the value I had chosen in the first picker appears in the second one.
I am sure it's not the best approach to solving this problem and I am open to suggestions.
P.S. It's my third day trying to do something in react-native, or react at all.