I am in the middle of implementing delete functionality to an image uploader. My error message is Cannot read property 'id' of undefined. I don't think I am assigning an id correctly and my approach is wrong to the task at hand.
Tried passing the object into the function and using JavaScripts delete API to delete the specific file. Not best practice to mutate the state directly rather to copy the state, modify then paste the new state.
Tried using the name as the indentifier but does not work. I am recommended to delete by an id or key.
import * as React from "react"
import Dropzone, { DropFilesEventHandler } from "react-dropzone"
import FaIcon from "components/FaIcon"
import { PrimaryButton } from "components/Buttons"
interface ImageFile extends File {
preview?: string
id: any
}
class ImageUpload extends React.Component {
state = {
files: []
}
onDrop: DropFilesEventHandler = (files: ImageFile[]) => {
files.map(file => Object.assign(file, {
preview: URL.createObjectURL(file)
}))
this.setState({ files: [...this.state.files, ...files] })
}
deleteImage = (file: ImageFile, index: number) => {
console.log(file, index)
// this.setState({
// files: this.state.files.filter(file => file.name !== file),
// });
}
render() {
const files = this.state.files.map((file: ImageFile, index: number) => (
console.log(file),
(
<div key={index}>
<div>
<img src={file.preview} />
<div onClick={() => this.deleteImage(file, index)}>
<FaIcon icon="plus" />
</div>
</div>
</div>
)
))
return (
<div>
<div>
<h2>Upload Images</h2>
</div>
<form>
<div>
{files}
<Dropzone onDrop={this.onDrop} accept="image/*">
<FaIcon icon="plus"/>
</Dropzone>
</div>
<div>
<label>Category</label>
<div>
<input
type="text"
placeholder={"please enter / select..."}
value={this.state.categoryInput}
onChange={(e) => this.categoryInputValue(e)}
/>
</div>
<PrimaryButton>Upload</PrimaryButton>
</div>
</form>
</div >
)
}
}
export default ImageUpload
I expect the file to be removed from the array. Actual result is nothing happens and error message appears.
You could remove the object with an index value
deleteImage = (file, index) => {
const myNewFiles = [...this.state.files]; // copy of Original State
myNewFiles.splice(index, 1);
this.setState({
files: myNewFiles
});
};
and i've also made a sandbox example
https://codesandbox.io/embed/0krqwkokw
deleteImage = (file_id) => {
const { files } = this.state;
this.setState({
files: files.filter(file => file.id !== file_id),
});
}
You should write this as you are passing file id into the deleteImage function.
Related
So I'm doing a list in which you can add items. When you add them you have two options:
Delete the whole list
Delete a specific item.
But for some reason the "handeDelete" button is not working. Can somebody tell me what did I write wrong in the code?
The link to CodeSandbox is:
codesandbox
import { useState } from "react";
import uuid from "react-uuid";
export default function ItemList() {
const [items, setItems] = useState({ item: "" });
const [groceryList, setGroceryList] = useState([]);
function handleChange(value, type) {
setItems((prev) => {
return { ...prev, [type]: value };
});
}
function handleSubmit(e) {
e.preventDefault();
const newItem = { ...items, id: uuid() };
setGroceryList([...groceryList, newItem]);
setItems({ item: "" });
}
function handleDelete(id) {
setGroceryList(groceryList.filter((items) => items.id !== id));
}
return (
<>
<form autoComplete="off" onSubmit={handleSubmit}>
<input
type="text"
name="item"
id="item"
value={items.item}
onChange={(e) => handleChange(e.target.value, "item")}
/>
</form>
{groceryList.map((list) => {
return (
<div key={list.id}>
<ul>
<li> {list.item}</li>
</ul>
<button onClick={(id) => handleDelete()}>Delete</button>
</div>
);
})}
<button onClick={() => setGroceryList([])}>Clear</button>
</>
);
}
Your delete button definition is wrong:
<button onClick={() => handleDelete(list.id)}>Delete</button>
the parameter you are receiving from the click event is not the id. Since you are not working with the event args itselfy you can safely ignore it. The second mistake was, that you are not passing the id itself to your handleDelete function.
For learning purposes, humor yourself and print the event to the console, while developing:
<button onClick={(evt) => {
console.log(evt)
handleDelete(list.id)
}}>
Delete
</button>
This will show you, that the parameter, that you named id (and I renamend to evt), is in fact reacts Synthetic Event: https://reactjs.org/docs/events.html
I am building an image upload form using Next.js/React.js, and I want the user to be able to assign a tag to each upload. I also want to show the image preview using 'URL.createObjectURL'. The uploading works fine, but on the upload page where I try to iterate through the list of images to show the preview and show an input box to assign the tag, none of this is showing. I cannot work out why.
The code:
import { useState } from "react";
import Head from 'next/head'
import Layout, { siteTitle } from '../../components/layout'
import Image from 'next/image'
export default function PrivatePage(props) {
const [images, setImages] = useState([])
const [imageURLS, setImageURLS] = useState([])
const [tag, setTag] = useState(null)
const uploadImageToClient = (event) => {
var imageList = images
var urlList = imageURLS
if (event.target.files && event.target.files[0]) {
imageList.push(event.target.files[0]);
urlList.push(URL.createObjectURL(event.target.files[0]))
setImages(imageList);
setImageURLS(urlList);
}
};
const uploadTagToClient = (event) => {
if (event.target.value) {
const i = event.target.value;
setTag(i);
}
};
const uploadToServer = async (event) => {
const body = new FormData()
images.map((file, index) => {
body.append(`file${index}`, file);
});
body.append("tag", tag)
const response = await fetch("/api/file", {
method: "POST",
body
});
};
return (
<Layout home>
<Head>
<title>{siteTitle}</title>
</Head>
<div className="container">
<div className="row">
<h4>Select Images</h4>
<div className="col">
<input type="file" className="btn btn-outline-success-inverse" onChange={uploadImageToClient} />
<input type="file" className="btn btn-outline-success-inverse" onChange={uploadImageToClient} />
</div>
<div className="col">
</div>
<button
className="btn btn-outline-success-inverse"
type="submit"
onClick={uploadToServer}
>
Send to server
</button>
{images.map((file, index) => {
return (
<div class="row ">
<lead>{file.name}</lead>
<input type="text" onChange={uploadTagToClient} />
<image src={imageURLS[index]}/>
</div>)
})}
</div>
</div>
</Layout>
);
}
To clarify, nothing inside images.map is showing when I select images.
The issue happens because you're mutating the arrays you have in state with imageList.push and urlList.push which React doesn't pick up. This means state doesn't actually get updated and a re-render doesn't occur.
To fix it, rather than mutating those state arrays you need to create new ones when updating them.
const uploadImageToClient = (event) => {
if (event.target.files && event.target.files[0]) {
setImages((imageList) => [...imageList, event.target.files[0]]);
setImageURLS((urlList) => [
...urlList,
URL.createObjectURL(event.target.files[0])
]);
}
};
Unrelated to the main issue, you have several minor issues inside the render part of your component, specifically inside images.map.
You need to set a key prop on the outer <div> element;
The <lead> element doesn't exist, and needs to be replaced with a valid element;
The <image> element also doesn't exist, you probably meant <img> or <Image> (from next/image).
You can handle image upload with multiple image preview with following code.
const handleFile = (e) => {
setMessage("");
let file = e.target.files;
for (let i = 0; i < file.length; i++) {
const fileType = file[i]['type'];
const validImageTypes = ['image/gif', 'image/jpeg', 'image/png'];
if (validImageTypes.includes(fileType)) {
setFile([...files,file[i]]);
} else {
setMessage("only images accepted");
}
}
};
Follow this snippet https://bbbootstrap.com/snippets/multiple-image-upload-preview-and-remove-92816546
I'm trying to create a function that renders an array of links and i want to create a text input and a button that adds value from input in the array. I got the links saved in the state in the object that looks like this:
sourceLinks: {
0: "https://www.w3schools.com/html/"
1: "https://www.apachefriends.org/docs/"
2: "https://docs.moodle.org/38/en/Windows_installation_using_XAMPP"
}
I've managed to render the links like this:
renderLinks() {
let sessionLinks = this.state.sessionLinks;
let links = [];
Object.values(sessionLinks).map((link) => {
links.push(<div className="column">
<span>
<InputPreview inputValue={link} classes="w-300" />
</span>
</div>)
})
return links;
}
InputPreview is the component i use for displaying links. I'm tryin to add a text input and a button bellow the rendered links that adds the value to the array, and an icon next to every link that removes it from an array. I'm trying to do it all in one function renderLinks() and then call it in render. I know i have to push and slice items from an array and update the state but i'm strugling cause i just started learning react. Please help :)
You can add and render links with below code.
import React from "react";
class ItemList extends React.Component {
state = {
links: ["item1"],
newItem: ""
};
submit(e, newLink) {
e.preventDefault();
let updatedLinks = this.state.links;
updatedLinks.push(newLink);
this.setState({ links: updatedLinks });
}
render() {
return (
<React.Fragment>
<ul>
{this.state.links?.map((link, i) => (
<li key={i}>
<p>{link}</p>
</li>
))}
</ul>
<form onSubmit={(e) => this.submit(e, this.state.newItem)}>
<input
type="text"
value={this.state.newItem}
onChange={(e) => this.setState({ newItem: e.target.value })}
/>
<button type="submit">ADD</button>
</form>
</React.Fragment>
);
}
}
export default ItemList;
Let me know for further clarificaton.
This is a example with functional components and hooks
import React, { useState } from 'react';
const sourceLinks = [
'https://www.w3schools.com/html/',
'https://www.apachefriends.org/docs/',
'https://docs.moodle.org/38/en/Windows_installation_using_XAMPP',
];
export const ListLinks = () => {
const [links, setLinks] = useState(sourceLinks);
const [newLink, setNewLink] = useState('');
const handleAdd = () => {
setLinks(links => [...links, newLink]);
};
const handleChangeNewLink = e => {
const { value } = e.target;
setNewLink(value);
};
return (
<div>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<input type='text' value={newLink} onChange={handleChangeNewLink} />
<button onClick={handleAdd}>Add</button>
</div>
<br />
{links.map((link, index) => (
<p key={index}>{link}</p>
))}
</div>
);
};
This is the result:
Lastly, read the documentation, managing the state is essential.
I have been attempting to toggle a class on click so that when I click on one of the mapped items in my Tasks component, I add the 'complete' class and put a line through that item (crossing items off of a todo list). However with my current code set up, when I click on one element to add the class, all the other elements get crossed out as well and vice versa.
Here is my current setup. The class 'complete' is what will add a line through one of the mapped items in the Tasks component.
import { Container, Row} from 'react-bootstrap';
import {Link} from 'react-router-dom';
import axios from 'axios';
const List = (props) =>{
return(
<div>
<Link style={{textDecoration:'none'}} to={`/lists/${props.listId}`} > <p className="list-item">{props.item}</p></Link>
</div>
)
}
const Tasks = (props) =>{
return(
<div onClick={props.onClick} className={props.className} >
<div className='task-item' >
<p >{props.item}</p>
</div>
</div>
)
}
export default class Display extends Component {
constructor(props){
super(props)
this.onCompletedTask = this.onCompletedTask.bind(this);
this.state = {
list: [],
tasks:[],
complete:false
}
}
componentWillUpdate(nextProps){
axios.get(`http://localhost:8080/lists/${this.props.match.params.listId}`)
.then(response =>{
this.setState({
tasks:response.data
})
})
}
componentDidMount(){
axios.get('http://localhost:8080/lists')
.then(response=>{
this.setState({
list:response.data
})
})
.catch(error =>{
console.log(error)
});
}
onCompletedTask(item){
this.setState({ complete: !this.state.complete});
}
listCollection(){
return(
this.state.list.map(item=>{
return(<List item = {item.title} listId={item._id} key = {item._id} />)
})
)
}
taskCollection(){
return(
this.state.tasks.map((item, index) =>{
return(<Tasks onClick = {()=>this.onCompletedTask(item)} className={this.state.complete ? 'complete': ''} item={item.task} key={index}/>)
})
)
}
render() {
return (
<div id='main' >
<Container>
<Row>
<div className="sidebar">
<h1 style={{fontSize:"25pt"}}>Lists</h1>
<div className="list-menu">
{this.listCollection()}
</div>
<form action='/new-list' method='GET'>
<div style={{textAlign:'center'}}>
<button className='list-button' style={{fontSize:'12pt', borderRadius:'5px'}}>
+ New List
</button>
</div>
</form>
</div>
<div className='tasks'>
<h1 style={{fontSize:'25pt'}}>Tasks</h1>
{this.taskCollection()}
<form action={`/lists/${this.props.match.params.listId}/new-task`} method='GET'>
<button className='task-button'>
+
</button>
</form>
</div>
</Row>
</Container>
</div>
)
}
}
Your state holds only a single completed value, which OFC toggle all tasks. You could instead store a map of completed tasks.
this.state = {
list: [],
tasks: [],
complete: {}, // <--- use empty object as simple map object
}
Update onCompletedTask to store some uniquely identifying property of a task, like an id field
onCompletedTask(item){
this.setState(prevState => ({
completed: {
...prevState.completed, // <--- spread existing completed state
[item.id]: !prevState.completed[item.id] // <--- toggle value
},
}));
}
Update. taskCollection to check the completed map by id
taskCollection = () => {
const { completed, tasks } = this.state;
return tasks.map((item, index) => (
<Tasks
onClick={() => this.onCompletedTask(item)}
className={completed[item.id] ? "complete" : ""} // <--- check completed[item.id]
item={item.task}
key={index}
/>
))
};
I am developing an application in React. I have a parent element wherein I have a child component(Photo) which needs to rendered multiple times(7 in below case). So I have used .map and allPhotos variable is rendered in return function.
Parent Component :
handlePhotos = (event, isSingleMulti, photoIndex) => {
console.log("Upload Photo", event.target.files, photoIndex, isSingleMulti);
}
openFileDialog(isSingleMulti, photoIndex) {
isSingleMulti === 'M' ? document.getElementById('multi-photo-input').click() : document.getElementById('single-photo-input').click();
}
let photosTemp = [1,2,3,4,5,6,7];
let allPhotos = photosTemp.map(ele => {
return <Photo key={ele} photoBoxID={ele} openFileDialog={this.openFileDialog} handlePhotos={this.handlePhotos} onDeletePhoto={this.onDeletePhoto}/>
});
In my Child component (Photo) I have a div, onClick of div I call openFileDialog which internally calls click of hidden input element(#single-photo-input). onChange of input element I call handlePhotos. Both of these functions handlePhotos and openFileDialog are defined in my parent and passed to Child (Photo) as a prop.
Now what I need is that when onChange method handlePhotos is called, I want to return each Photo photoBoxID value. Basically, I want to check which Photo component was clicked. But every time I get value as 1 instead of respective 1,2,3 etc. What wrong am I doing?
Child Component :
const UploadImage = (props) => {
console.log(props.photoBoxID);
return (
<div className="photo-root">
<div className="photo-inner-container" onClick={() => props.openFileDialog('S')}>
<span className="inner-text">+</span>
<form encType="multipart/form-data" id="single-photo-form">
<input type="file" name="file" id="single-photo-input" className="hide" accept="image/jpg, image/jpeg, image/png"
onChange={(event) => props.handlePhotos(event, 'S', props.photoBoxID)}/>
</form>
</div>
</div>
)
}
class Photo extends React.Component {
render() {
return (
true === true ? <UploadImage {...this.props}/> : <ImagePreview {...this.props}/>
)
}
}
In open dialog, you are getting element by id but you don't have unique ids so you can append index to your input ids and pass index to openDialog function and click that particular input only.
Parent component
handlePhotos = (event, isSingleMulti, photoIndex) => {
console.log("Upload Photo", event.target.files, photoIndex, isSingleMulti);
}
openFileDialog(isSingleMulti, photoIndex) {
isSingleMulti === 'M' ? document.getElementById(`multi-photo-input-${photoIndex}`).click() : document.getElementById(`single-photo-input-${photoIndex}`).click(); //appended index here
}
let photosTemp = [1,2,3,4,5,6,7];
let allPhotos = photosTemp.map(ele => {
return <Photo key={ele} photoBoxID={ele} openFileDialog={this.openFileDialog} handlePhotos={this.handlePhotos} onDeletePhoto={this.onDeletePhoto}/>
});
in your photo.js
const UploadImage = (props) => {
console.log(props.photoBoxID);
return (
<div className="photo-root">
<div className="photo-inner-container" onClick={() => props.openFileDialog('S', props.photoBoxID)}>
<span className="inner-text">+</span>
<form encType="multipart/form-data" id={`single-photo-form-${props.photoBoxID}`}>
<input type="file" name="file" id={`single-photo-input-${props.photoBoxID}`} className="hide" accept="image/jpg, image/jpeg, image/png"
onChange={(event) => props.handlePhotos(event, 'S', props.photoBoxID)}/>
</form>
</div>
</div>
)
}
class Photo extends React.Component {
render() {
return (
true === true ? <UploadImage {...this.props}/> : <ImagePreview {...this.props}/>
)
}
}
Child Component :
// as is
<div className="photo-inner-container" onClick={() => props.openFileDialog('S')}>
// to be
<div className="photo-inner-container" onClick={(event) => props.openFileDialog(event, 'S')}>
Parent Component :
// as is
openFileDialog(isSingleMulti, photoIndex) {
isSingleMulti === 'M' ? document.getElementById('multi-photo-input').click() : document.getElementById('single-photo-input').click();
}
// to be
openFileDialog = (event, isSingleMulti) => {
isSingleMulti === 'M' ? document.getElementById('multi-photo-input').click() : event.currentTarget.childNodes[1].children[0].click();
}
Hmmm...
I tried to find another way to catch the exact input a client clicks lol