Array is sometimes recreated, and in other times modified - javascript

I am trying to implement a multiple image file selector in my React app.
This is my input element:
<input
type="file"
multiple
onChange={handleImageChange}
/>
{renderPhotos(venueImages)}
These are the functions that are called when files are chosen:
const [venueImages, setVenueImages] = useState([]);`
const renderPhotos = source => {
console.log(source); ////////log 1
return source.map(photo => {
return <img src={photo} key={photo} />;
});
};
const handleImageChange = e => {
if (e.target.files) {
const filesArray = Array.from(e.target.files);
console.log(filesArray); ///////// log 2
filesArray.forEach(file => {
const tempUrl = URL.createObjectURL(file);
console.log(tempUrl); ////// log 3
setVenueImages([...venueImages, tempUrl]);
});
}
};
I call renderPhotos to show a preview off all the selected photos before uploading.
The issue I'm facing is as follow:
If I choose, for example, 5 photos, only 1 would end up being rendered on screen.
I've inserted console logs in handleImageChange, and what I get logged is confusing me even more.
The second log (I've numbered them in my code) prints an array of 5 files.
After from log 3 that I'll get 5 logs of the newly generated temporary URLs for each of the files.
But log 1, would only get printed once.
Now - if I'll click the input element to choose more files, I'll end up with another rendered image.
So basically everytime I choose images, no matter how many I've chosen, I'll only get one more image rendered.

The problem is that you are referencing the venueImages array in your setVenueImages call. Because the state update performed by setVenueImages is asynchronous, then you cannot be certain that venueImages contains all of the previous state updates.
The set state function can be passed a function that takes the old value, instead of just passing it the new value. This will ensure that your state updates are sequential. Replace your setVenueImages call with this:
setVenueImages(prevImages => [...prevImages, tempUrl]);
An additional change that I will suggest is to perform a concatenation of all images, instead of adding them one by one. This should be faster.
const handleImageChange = e => {
if (e.target.files) {
const filesArray = Array.from(e.target.files).map(file => URL.createObjectURL(file));
console.log(filesArray); ///////// log 2
setVenueImages(prevImages => prevImages.concat(filesArray));
}
};

That is happening because when you are saving the tempUrl, only one url is getting saved. Also do not set the state by adding images one by one.
Updated version of your handleImageChange function can be
const handleImageChange = e => {
if (e.target.files) {
const filesArray = Array.from(e.target.files);
const tempUrls = filesArray.map(file => URL.createObjectURL(file)))
setVenueImages([...venueImages, ...tempUrls])
}
};

Related

I want to prevent the localstorage from clear my objects on reload and adding new entries in my form

How do I prevent the local storage from being deleted after reload and insert a new entry from my input fields? I want to display the data that I saved in a list in HTML.
// when I add new items in my form and submit it, the old entries in local-storage gets deleted
let datas = [];
const addEntry = e => {
e.preventDefault();
let data = {
id: document.querySelector("#date").value,
situation: document.querySelector("#situation").value,
mood: document.querySelector("#mood").value,
special: document.querySelector("#special").value,
expectations: document.querySelector("#expectations").value,
fulfilled: document.querySelector("#fulfilled").value,
};
datas.push(data);
console.log(datas);
document.querySelector("form").reset();
localStorage.setItem("smokeEntries", JSON.stringify(datas));
};
let content;
//get object and save in array variable
const getEntriesFromLocalStorage = () => {
content = [JSON.parse(this.localStorage.getItem("smokeEntries"))];
};
document.addEventListener("DOMContentLoaded", () => {
document.querySelector("#submit").addEventListener("click", addEntry);
});
Personally I wouldn't try to keep two arrays (datas and content) in sync as it can cause inconsistency problems like this where you're using one to update and store information in state management and the other displayed visually are out of sync. I'd suggest combining them like this:
let content = [];
const addEntry = e => {
e.preventDefault();
let data = {
id: document.querySelector("#date").value,
situation: document.querySelector("#situation").value,
mood: document.querySelector("#mood").value,
special: document.querySelector("#special").value,
expectations: document.querySelector("#expectations").value,
fulfilled: document.querySelector("#fulfilled").value,
};
content.push(data);
document.querySelector("form").reset();
localStorage.setItem("smokeEntries", JSON.stringify(content));
};
const getEntriesFromLocalStorage = () => {
content = [JSON.parse(localStorage.getItem("smokeEntries"))];
};
document.addEventListener("DOMContentLoaded", () => {
document.querySelector("#submit").addEventListener("click", addEntry);
});
There also may be some problem with setting content (because we're wrapping it in brackets ([]) but it may already be an array -- should be pretty obvious after you run it, remove the brackets if it turns into nested arrays) but I haven't ran the code. Also note that for content to load "state" you'll need to call getEntriesFromLocalStorage() before using the state (or possibly trigger a re-render after).

Javascript Firebase getDownloadURL

I want to display several images on my webpage via javascript using Firebase Storage.
I use:
getDownloadURL(ref(storage, imageIndexPathRoot)).then((url) =>{
img.setAttribute('src', url);
The problem is that only the last image is displayed. If I have e.g. 5 pictures in my folder, the getDownload line with the imageIndexPathRoot is correct executed for all 5 images, but only at the last image the line img.setAttribute... is executed and unly this image was displayed on the webpage.
// Now we get the references of these images
listAll(listRef).then((res) => {
res.items.forEach(function(imageRef) {
// And finally display them
console.log(imageRef);
displayImage(imageRef);
});
}).catch((error) => {
// Handle any errors
console.log("Error 1");
});
function displayImage(imageRef) {
const img = document.getElementById('tierFoto');
img.src = imageRef.fullPath;
getDownloadURL(ref(storage, imageIndexPathRoot)).then((url) =>{
img.setAttribute('src', url);
})
.catch((error) => {
console.log(error);
});
}
}
Every time displayImage is called, it does:
const img = document.getElementById('tierFoto')
So it sets each image to the same HTML element, which explains why you only see the last of the images.
If you want to show a separate image each time you call displayImage, you'll need to get (or pass in) a different HTML element each time. How to do that, depends on your HTML structure.
For example, if your HTML has img elements with numbered IDs, you could do:
res.items.forEach(function(imageRef, i) { // 👈 get index from forEacj
displayImage(imageRef, i); // 👈 pass it to displayImage
});
And then
function displayImage(imageRef, index) { // get index from caller
const img = document.getElementById('tierFoto-'+index); // use index to look up element
...

How do I create an array from Promise results?

I'm using React to build a web app. At one point I have a list of ids, and I want to use those to retrieve a list of items from a database, get a list of metrics from each one, and then push those metrics to an array. My code so far is:
useEffect(() => {
const newMetrics = [];
currentItems.forEach((item) => {
const url = `items/listmetrics/${item.id}`;
Client.getData(url).then(async (metrics) => {
let promises = metrics.map((metricId: string) => {
// Get metric info
const urlMetric = `metrics/${metricId}`;
return Client.getData(urlMetric);
});
await Promise.all(promises).then((metrics: Array<any>) => {
metrics.forEach((metric: MetricModel) => {
const metricItem = {
id: metric.id,
metricName: metric.name
};
newMetrics.push(metricItem);
}
});
});
});
setMetrics(newMetrics);
});
}, [currentItems]);
where "metrics" is a state variable, set by setMetrics.
This appears to work ok, but when I try to access the resulting metrics array, it seems to be in the wrong format. If I try to read the value of metrics[0], it says it's undefined (although I know there are several items in metrics). Looking at it in the console, metrics looks like this:
However, normally the console shows arrays like this (this is a different variable, I'm just showing how it's listed with (2) [{...},{...}], whereas the one I've created shows as []):
I'm not confident with using Promise.all, so I suspect that that's where I've gone wrong, but I don't know how to fix it.

State objects and child component using sub values of object

I have a Dropzone that allows for multiple concurrent uploads and I want to show the progress for all uploads.
In my Dropzone component I have part of the state which is an array of uploads:
const [uploads, setUploads] = useState([])
Each element of the uploads array will be an upload object that has a few values, like so:
const uploading = {
id: 1,
files: <array of files>,
progress: 0
}
Once files/folders are dropped into the dropzone, the "uploading" object will be added to the "uploads state array" and the files will be sent to the backend API, which asynchronously uploads the files to the server.
The backend will periodically send a progress callback to the UI, to update the progress value of the correct element in the uploads state array (see progressCallback below)
What I am currently unable to achieve is to make sure the UI re-renders every time an object in the uploads array is being updated to show progress, so that I can show the progress of all uploads as they happen.
The UI Component looks like this:
export function UploaderDropzone(props) {
const [uploads, setUploads] = useState([])
const progressCallback = useCallback((progressObject, sessionContext, step) => {
const {uploadSessionParameters} = sessionContext
let uploading = {}
// let tmpArray = []
const tmpArray = [...uploads]
if (step === 'progress') {
const filtered = findUploadById(tmpArray, uploadSessionParameters.uploadSessionId)
uploading = filtered[0]
if (uploading) {
const itemIndex = tmpArray.indexOf(uploading)
tmpArray.splice(itemIndex, 1)
uploading.progress = progressObject.percentUpload
tmpArray.push(uploading)
setUploads(tmpArray)
// setUploads(prevUploads => [...prevUploads, uploading])
}
console.log('progress tmpArray = ' + JSON.stringify(tmpArray));
console.log('progress uploads = ' + JSON.stringify(uploads))
}
if (step === 'initialize') {
const uploadNumber = uploads.length + 1
uploading = {
uploadSessionId: uploadSessionParameters.uploadSessionId,
files: sessionContext.files,
uploadNumber: uploadNumber,
uploadName: `Upload #${uploadNumber}`,
sent: false,
progress: 0,
}
tmpArray.push(uploading)
setUploads(tmpArray)
console.log('initialize tmpArray = ' + JSON.stringify(tmpArray))
console.log('initialize uploads = ' + JSON.stringify(uploads))
}
}, [uploads])
const progressBars = uploads.map((upload) => {
return (
<Fragment>
<ProgessBar progress={upload.progress} />
</Fragment>
)
})
// ... more code here ... not required for understanding
return {
<Fragment>
<Dropzone
onDrop={
acceptedFiles => {
const filteredFiles = acceptedFiles.filter((file) =>
validateFile(file))
console.log("Filtered files" + filteredFiles)
if (filteredFiles.length > 0) {
setAlertMsg('')
}
else {
setAlertMsg('No files uploaded.')
}
// call to Node.js backend, passing it the progressCallback
startAsyncUploadSession(filteredFiles, progressCallback);
}
}
/>
{progressBars}
</Fragment>
}
}
The ProgressBar component is very simple:
export function ProgressBar(props) {
const {progress} = props
return (
<Fragment>
<p>`${progress}% uploaded ...`</p>
</Fragment>
)
}
Right now, this code doesn't even show the progress bar even though the uploads state array is constantly being updated in the progressCallback. Since I don't know the number of concurrent uploads that will be done, I cannot set the state in the higher order component and pass it as props, I need the child component (ProgressBar) to receive it as props from the multiple objects in the state array ... but I am clearly missing something ...
Any pointers ? Any hooks I could use to register the progress value of the objects in the uploads state array so that every time the backend updates us on the progress it is reflected in the UI ?
Edit: To include the partial fix suggested by #Robin Zigmond
Edit2: After some debugging, it seems to be a synchronization issue. So I need to add some code and details here.
When files are dropped into the Dropzone, its sends the files to the Node.js backend through a function call to a mock server, the call to startAsyncUploadSession(filteredFiles, progressCallback); in the onDrop event of the Dropzone (which uses the react-dropzone lib).
It would seem that when I call progressCallback later, the state is as it was on the first render, aka uploads state array is an empty array as it was when the files were dropped, not the updated one which contains the object added to uploads array in the 'initializing' step.
So amended question would be "How to make sure that the UI state is up to date when the progressCallback is called later on by the backend ?"
The problem is in your state updating code inside progressCallback. Here is the offending code, for reference:
const tmpArray = uploads
const itemIndex = tmpArray.indexOf(uploading)
tmpArray.splice(itemIndex, 1)
// HERE UPDATING ONE OF ITEM'S VALUES IN UPLOADS STATE ARRAY
uploading.progress = progressObject.percentUpload
tmpArray.push(uploading)
setUploads(tmpArray)
What this does is:
sets tmpArray to be a reference to the same object (uploads) as the current state
then mutates that array, first by splicing an element out, then pushing a new element on to it
At no point in step 2) does the reference change. So when you then call setUploads(tmpArray) - which might as well be setUploads(uploads) as those two variables are still references to the exact same array - React thinks you're setting the state to be exactly what it was, and therefore doesn't know to rerender.
That's the long way of explaining why you should never mutate state, as you are doing here. You need to update it immutably - that is, leave the old state alone, construct a new object/array, and pass that to the function that sets the new state.
There are a number of ways to do that here, but in your case it should be as simple as just making tmpArray a (shallow) *copy) of the current state. That is, change:
const tmpArray = uploads
to
const tmpArray = [...uploads]
Now tmpArray is a new reference, to an array holding the same values as before. Note that the copy is only "shallow", so the objects inside the array are still references to just one underlying object for each array element. But that doesn't seem to matter here, because you don't mutate those objects. If you try your code with this change, I believe it should start to work (or at least get you past this particular problem).

How to connect loop data to pdfgeneratorapi with wix corvid?

I'm generating PDF by using https://pdfgeneratorapi.com/.
Now I can show data one by one using this code.Can any one give me suggestion how can show all data with loop or any other way?
This below photos showing my template from pdfgenerator .
This is the code I'm using to generate PDF
let communicationWay1=[
{0:"dim"},
{1:"kal"}
];
let cstomerExpence1=[
{0:"dim"},
{1:"kal"}
];
let title="test";
let names="test";
let phone="test";
let email="test";
let maritalStatus="test";
let city="test";
let other="test";
const result = await wixData.query(collection)
.eq('main_user_email', $w('#mainE').text)
.find()
.then( (results) => {
if (results.totalCount>0) {
count=1;
// title=results.items[1].title;
names=results.items[0].names;
email=results.items[0].emial;
phone=results.items[0].phone;
maritalStatus=results.items[0].maritalStatus;
city=results.items[0].city;
other=results.items[0].cousterExpenses_other;
title=results.items[0].title;
communicationWay=results.items[0].communicationWay;
cstomerExpence=results.items[0].cstomerExpence;
}
if (results.totalCount>1) {
names1=results.items[1].names;
email1=results.items[1].emial;
phone1=results.items[1].phone;
maritalStatus1=results.items[1].maritalStatus;
city1=results.items[1].city;
other1=results.items[1].cousterExpenses_other;
title1=results.items[1].title;
communicationWay1=results.items[1].communicationWay;
cstomerExpence1=results.items[1].cstomerExpence;
}
} )
.catch( (err) => {
console.log(err);
} );
// Add your code for this event here:
const pdfUrl = await getPdfUrl
({title,names,email,phone,city,maritalStatus,other,communicationWay,cstomerExpence,title1,
names1,email1,phone1,city1,maritalStatus1,other1,communicationWay1,cstomerExpence1
});
if (count===0) { $w("#text21").show();}
else{ $w("#downloadButton").link=wixLocation.to(pdfUrl);}
BELOW CODE IS BACKEND CODE/JSW CODE.
Also I want to open pdf in new tab. I know "_blank" method can be used to open a new tab.But I'm not sure how to add it with the url
import PDFGeneratorAPI from 'pdf-generator-api'
const apiKey = 'MYKEY';
const apiSecret = 'MYAPISECRET';
const baseUrl = 'https://us1.pdfgeneratorapi.com/api/v3/';
const workspace = "HELLO#gmail.com";
const templateID = "MYTEMPLATEID";
let Client = new PDFGeneratorAPI(apiKey, apiSecret)
Client.setBaseUrl(baseUrl)
Client.setWorkspace(workspace)
export async function getPdfUrl(data) {
const {response} = await Client.output(templateID, data, undefined, undefined, {output: 'url'})
return response
}
Just put it in a while loop with a boolean condition.
You can create a variable, for example allShowed, and set its value to False. After that, create another variable, for example numberOfDataToShow, and set it as the number of elements you want to display. Then create a counter, countShowed, initialized with 0 as its value.
Now create a while loop: while allShowed value is False, you loop (and add data).
Everytime a piece of your data is showed, you increment the value of countShowed (and set it to go on adding/showing data). When countShowed will have the exact same value of numberOfDataToShow, set allShowed to True. The loop will interrupt and all your data will be showed.
You would need to use the Container or Table component in PDF Generator API to iterate over a list of items. As #JustCallMeA said you need to send an array of items. PDF Generator API now has an official Wix Velo (previously Corvid) tutorial with a demo page: https://support.pdfgeneratorapi.com/en/article/how-to-integrate-with-wix-velo-13s8135

Categories