I have a React component to upload files. Below is part of the code that handles the file selection and once files are selected, it will display thumbnails.
I want to use RTL to test the file selection: clicking button which is EvidenceUploaderPanel, this opens the file selector input element, then choosing files.
This will also make the requests to upload the files as they are selected.
But I have no idea how to start.
function UploadScreen({
title,
maxNumberOfUploadFiles = 3,
acceptedFileTypes,
}: Props) {
const [documents, setDocuments] = useState<FileObject[]>([]);
const handleFileSelection = (files: FileList) => {
const documentsWithThumbnails = Array.from(files).map((file) => {
// here I also make a request to upload each file.
return {
file,
thumbnailURL: URL.createObjectURL(file),
name: file.name,
};
});
setDocuments((currentDocuments) => [...currentDocuments, ...documentsWithThumbnails]);
};
const inputRef = useRef(
(() => {
const element = document.createElement('input');
element.multiple = true;
element.type = 'file';
element.accept = acceptedFileTypes?.join(',') || IMAGE_MIME_TYPES.join(',');
element.addEventListener('change', () => {
if (element.files && documents.length + element.files.length < maxNumberOfUploadFiles) {
handleFileSelection(element.files);
element.value = '';
}
});
return element;
})(),
);
const handleOpenFileClicker = () => {
inputRef.current.click();
};
return (
<div>
<h2 className="container">{title}</h2>
{documents.length > 0 ? (
<section>
<div className="body-text">
Add files
</div>
<div className="thumbnail-container">
{documents.map((doc) => {
return (
<BaseThumbnail
src={doc?.thumbnailURL}
key={doc.name}
deleteAction={() => {
deleteDocument(doc.name);
}}
/>
);
})}
</div>
<Link onPress={handleOpenFileClicker}>
Add photos
</Link>
</section>
) : (
<section>
<div className="text">
Add files
</div>
<div className="upload-container" />
<EvidenceUploaderPanel
labelText="upload files"
openFilePicker={handleOpenFileClicker}
/>
</section>
)}
</div>
);
}
Have you thought about maybe using cypress for this?
They have a nice built in function that does exactly what you want and the setup Is really easy.
I’d recommend you using the cypress component testing for react, they have an entire page on their website’s docs explaining how to set it up. And than you can just mount the file selection component and use their cy.selectFile() method.
Good luck :)
Related
I made a custom file input in my app. It's working like a charm but when the file is uploaded, the custom file input is still showing the name of the file, which is a problem.
I tried to pass a state from parent component in order to reset the name displayed but, for some reason, the child prop does not update with the parent state and I don't know why.
Here's the custom file input :
export default function CustomInput({
disabler,
setUpperLevelFile,
previousName,
typeOfFiles,
lastInput,
reset,
}) {
const [fileUpload, setFileUpload] = useState(null);
useEffect(() => {
if (lastInput) {
setUpperLevelFile && setUpperLevelFile(fileUpload, lastInput);
} else {
setUpperLevelFile && setUpperLevelFile(fileUpload);
}
}, [fileUpload]);
useEffect(() => {
reset && setFileUpload(null);
console.log("custom input use effect : ", reset);
}, [reset]);
return (
<label className="customInputLabel">
<CustomButton
buttonInnerText="browse"
/>
<p>
{(fileUpload &&
`${fileUpload.name}, (${sumParser(fileUpload.size)})`) ||
(previousName && previousName) ||
"chose a file"}
</p>
<input
type="file"
name="realInput"
className="innerFileInput"
accept={typeOfFiles && typeOfFiles}
disabled={disabler && !disabler}
style={{ display: "none" }}
onChange={(e) => {
setFileUpload(e.target.files[0]);
}}
/>
</label>
);
}
And here is some of the parent code :
export default function ImportFiles(props){
...
const [resetInputs, setResetInputs] = useState(false);
const returningInputs = () => {
let stockInputs = [];
for (let i = 0; i < filesCounter; i++) {
stockInputs.push(
<CustomInput
key={`custom input ${i}`}
setUpperLevelFile={handlingInputChange}
lastInput={i === filesCounter - 1}
reset={resetInputs}
/>
);
}
setFilesInputs(stockInputs);
};
const handlingPostingFiles = () => {
postingFiles(uploadFiles, setUploadStatus);
setResetInputs(true);
};
useEffect(() => {
console.log("edit packages use effect : ", resetInputs);
}, [resetInputs]);
...
return(
...
{filesInputs}
...
)
The console.log in parent component shows that the state is updated but the one in CustomInput doesn't trigger after first render. So it's not updated.
After realizing the process - there should be a clean 🧼 🧽 phase:
so add this line after submitting form:
setFileUpload(null)
in the function handlingPostingFiles or postingFiles.
There is also option to hide this section with that condition: !resetInputs:
<p>
{((fileUpload && !resetInputs) &&
`${fileUpload.name}, (${sumParser(fileUpload.size)})`) ||
(previousName && previousName) ||
"chose a file"}
</p>
I am trying to get my form to upload several files, but once I upload the first one, I have no chance to load a second one. Any Idea what I am doing wrong?
This is my upload component:
import React, { Component } from 'react'
import * as RB from 'react-bootstrap'
import Button from 'components/Button/Button'
class uploadMob extends Component {
constructor(props) {
super(props)
this.state = {
files: [],
}
}
onFilesAdded = (e) => {
const filesArray = this.state.files
filesArray.push(e.target.files[0])
this.setState({ files: filesArray })
this.uploadFiles()
}
async uploadFiles() {
this.state.files.forEach((file) => {
this.sendRequest(file)
})
}
sendRequest(file) {
const { pdfUploadToState } = this.props
pdfUploadToState(file)
}
render() {
const files = this.state.files
return (
<RB.Form.Group>
<div className="upload-btn-wrapper">
<div className="Files">
{files.map((file, key) => {
return (
<div key={key} className="Row">
<span className="Filename">
{file.name}
</span>
</div>
)
})}
</div>
<Button size="sm" variant="light">
Dateien hochladen
</Button>
<input
type="file"
name="files"
id="files"
onChange={(e) => {
this.onFilesAdded(e)
}}
multiple
/>
</div>
</RB.Form.Group>
)
}
}
export default uploadMob
The first file is uploaded perfectly, but as mentioned, the button does not respond when trying to upload a second one.
Thanks for the help!
Your code seems correct but when you use input type file with multiple attribute you need to select multiple files and then hit upload button insted of selecting files one by one.
also replace
filesArray.push(e.target.files[0])
with
for (var i = 0; i < files.length; i++)
{
filesArray.push(e.target.files[i]);
}
to upload file one by one
replace
onFilesAdded = (e) =>
{
this.state.files.push(e.target.files[0])
this.uploadFiles()
}
hope this will help you
I have a Component that renders a list of elements using the map function. Each element is rendered with a delete and edit button. I have added the delete functionality, but I'm having problem with the edit one.
The functionality that I want is: click on edit item, replace H3 element (which is the title) with an input field and let the user update the name. I've tried replacing an element with another but this only works for the first element of the list, because I get the element with 'getElementById' I have tried doing it with querySelector, but that selects only the last element of the array.
I have no idea what to do. I know the issue is selecting the particular element at the right index. I use an id as a key but I don't know how to properly replace the html element. Any help will be vastly appreciated.
Here is where the map function renders the elements:
class Donut extends Component {
render(){
const {donuts, deleteDonut, editDonut} = this.props;
const donutsList = donuts.map((donut) => {
return <div key={donut.id} className="donut">
<div className="name">
<img src={donut.image} />
<div id="donut-name">
<h3 id="donut-title">{donut.name}</h3>
<p>{donut.date}</p>
</div>
</div>
<div className="price">
<p>{donut.price}</p>
<img src="img/edit.png" id={donut.id} onClick={()=>{editDonut(donut.id)}} />
<img src="img/delete.png" id={donut.id} onClick={() => {deleteDonut(donut.id)}} />
</div>
</div>
})
return (
<div>
{donutsList}
</div>
)
}
}
export default Donut
Try to avoid as much as possible directly manipulating DOM elements when you using React. In this case, you should use another approach:
Add a field to this class's state: editingDonutId
When you click in a donut, set the editingDonutId to corresponding id and when you finished it, reset the value.
In render function, inside the map, do a condition render to check if current rendering donut has same id with editingDonutId, if true, we render an input instead.
You are using react, not jquery, so do not use getElementById, try react solution.
This is my solution:
class Donut extends Component {
state = {
donutsState: {}
}
setDonutState: (id, value) => {
this.setState((preState) => {
const predonutState = preState.donutsState[id] || {}
return {
donutsState: {
...preState.donutsState,
[id]: {
...predonutState,
...value,
}
}
}
})
}
getDonutState: (id) => this.state.donutsState[id] || {};
render(){
const {donuts, deleteDonut, editDonut} = this.props;
const donutsList = donuts.map((donut) => {
const donutState = this.getDonutState(donut.id)
// when user input the name, save it in the state.
const onChange = (e) => {
this.setDonutState(donut.id, { value: e.target.value })
}
// when click edit, replace h3 with input.
const onEdit = () => {
this.setDonutState(donut.id, { eidt: true })
}
// when enter key, replace input with h3 and submit the name value.
const onKeyDown = (e) => {
if (e.key === 'Enter') {
this.setDonutState(donut.id, { eidt: false })
editDonut(donut.id, {
name: this.getDonutState(donut.id).value || donut.name,
})
}
}
return (
<div key={donut.id} className="donut">
<div className="name">
<img src={donut.image} />
<div id="donut-name">
{
donutState.edit
? <input id="edit-donut-title" value={donutState.value || donut.name} onChange={onChange} onKeyDown={onKeyDown} />
: <h3 id="donut-title">{donut.name}</h3>
}
<h3 id="donut-title">{donut.name}</h3>
<p>{donut.date}</p>
</div>
</div>
<div className="price">
<p>{donut.price}</p>
<img src="img/edit.png" id={donut.id} onClick={()=>{editDonut(donut.id)}} />
<img src="img/delete.png" id={donut.id} onClick={() => {deleteDonut(donut.id)}} />
</div>
</div>
)
})
return (
<div>
{donutsList}
</div>
)
}
}
export default Donut
I have a react component in which user can upload Image and he's also shown the preview of uploaded image. He can delete the image by clicking delete button corresponding to Image. I am using react-dropzone for it. Here's the code:
class UploadImage extends React.Component {
constructor(props) {
super(props);
this.onDrop = this.onDrop.bind(this);
this.deleteImage = this.deleteImage.bind(this);
this.state = {
filesToBeSent: [],
filesPreview: [],
printCount: 10,
};
}
onDrop(acceptedFiles, rejectedFiles) {
const filesToBeSent = this.state.filesToBeSent;
if (filesToBeSent.length < this.state.printCount) {
this.setState(prevState => ({
filesToBeSent: prevState.filesToBeSent.concat([{acceptedFiles}])
}));
console.log(filesToBeSent.length);
for (var i in filesToBeSent) {
console.log(filesToBeSent[i]);
this.setState(prevState => ({
filesPreview: prevState.filesPreview.concat([
<div>
<img src={filesToBeSent[i][0]}/>
<Button variant="fab" aria-label="Delete" onClick={(e) => this.deleteImage(e,i)}>
<DeleteIcon/>
</Button>
</div>
])
}));
}
} else {
alert("you have reached the limit of printing at a time")
}
}
deleteImage(e, id) {
console.log(id);
e.preventDefault();
this.setState({filesToBeSent: this.state.filesToBeSent.filter(function(fid) {
return fid !== id
})});
}
render() {
return(
<div>
<Dropzone onDrop={(files) => this.onDrop(files)}>
<div>
Upload your Property Images Here
</div>
</Dropzone>
{this.state.filesToBeSent.length > 0 ? (
<div>
<h2>
Uploading{this.state.filesToBeSent.length} files...
</h2>
</div>
) : null}
<div>
Files to be printed are: {this.state.filesPreview}
</div>
</div>
)
}
}
export default UploadImage;
My Question is my component is not re-rendering even after adding or removing an Image. Also, I've taken care of not mutating state arrays directly. Somebody, please help.
Try like this, I have used ES6
.
I'm implementing this library : https://github.com/felixrieseberg/React-Dropzone-Component
To trigger another component or element programmatically I can use refbut I got an error of photoUploadDropAreaElement is not a function using below code.
triggerUploadDialog(){
this.photoUploadDropAreaElement.click(); // doesn't work?
console.log(this.photoUploadDropAreaElement);
}
render() {
return(
<div onClick={this.triggerUploadDialog.bind(this)}>
<DropzoneComponent ref={dropArea => this.photoUploadDropAreaElement = dropArea} />
</div>
);
The result of DropzoneComponent look like this
What's wrong here? I just want to trigger a click to open the file dialog for user to select file to upload.
I'm using import * as Dropzone from 'react-dropzone'; via npm install react-dropzone --save-dev. Go here for the details.
This dropzone package allows you to, by default, click on the UI's dropzone to open the file dialog for user to select a file to upload.
Here is the code I used, which includes a 'Choose File' button as well as a 'Delete' button. *Note: multiple={false} disables the user's ability to choose multiple files. You can simply change it to true and the multiple file selection will be enabled.
import * as React from 'react';
import * as Dropzone from 'react-dropzone';
export interface FileUploadState { file: Array<File> }
export class FileUpload extends React.Component<{}, FileUploadState> {
constructor(props: any) {
super(props);
this.state = {
file: []
}
}
onDrop(droppedFile: any) {
if (droppedFile && droppedFile.preview) {
window.URL.revokeObjectURL(droppedFile.preview);
}
this.setState({
file: droppedFile
});
console.log(droppedFile);
}
onDelete() {
this.setState({
file: []
});
}
render() {
let dropzoneRef: any;
return (
<div>
<div>
<Dropzone ref={(node) => { dropzoneRef = node; }} onDrop={this.onDrop.bind(this)} multiple={false}>
<div className="text-center">Drag and drop your file here, or click here to select file to upload.</div>
</Dropzone>
<button type="button" className="button primary" onClick={(e) => {
e.preventDefault(); // --> without this onClick fires 3 times
dropzoneRef.open();
}}>
Choose File(s)
</button>
<button type="button" className="button alert" onClick={this.onDelete.bind(this)}>
Delete
</button>
</div>
<hr />
<div>
<h2>File(s) to be uploaded: {this.state.file.length} </h2>
<ul>
{
this.state.file.map(f => <li><code>{f.name}</code></li>)
}
</ul>
</div>
</div>
)
}
}
For anyone still looking, it looks like the library was updated to expose an open function.
https://github.com/react-dropzone/react-dropzone/commit/773eb660c7848dd1d67e6e13c7f7261eaaa9fd4d
You can use it this way via refs:
dropzoneRef: any;
onClickPickImage = () => {
if (this.dropzoneRef) {
this.dropzoneRef.open();
}
};
// When rendering your component, save a ref
<Dropzone
ref={(ref: any) => {
this.dropzoneRef = ref;
}}
onDrop={this.onDrop}
>
<div onClick={this.onClickPickImage}>Trigger manually</div>
</Dropzone>
Try like this. It's work for me
triggerUploadDialog () {
this.photoUploadDropAreaElement.dropzone.element.click()
}
Component
<div onClick={this.triggerUploadDialog.bind(this)}>
<DropzoneComponent ref={dropArea => this.photoUploadDropAreaElement = dropArea} />
</div>
My problem was NOT including the input element. When I did, it worked.