Sum the width of all images - javascript

I am trying to get the sum of all image widths.
I tried to get the values in an array so that I could calculate the sum of them but still not working, but it should be something really simple indeed, all I want is the sum of the image width values.
componentDidMount() {
let images = document.querySelectorAll('#draggable img');
let widths = [];
images.forEach(each => {
each.addEventListener("load", () => {
widths.push(each.width)
});
});
console.log(widths); // this is logging the array!
const total = widths.reduce(function(a, b){ return a + b; });
console.log("total is : " + total ); // this is crashing!
}

Your widths Array might be empty (your are populating it with a load event) and your are calling reduce on it without initialValue.
This will cause an error, see Array.reduce ,
You could just do this:
widths.reduce((acc, width) => (acc + width), 0);
Update 1, Base on your Codepen and your comments.. The load event listener is not really neaded. There is a compatibily issue with IE < 9, which support attachEvent not addEventListener. I will suggest to use a timer with a recursive function.
sumWidths = () => {
const images = document.querySelectorAll('#draggable img');
let sum = 0;
images.forEach(({ width }) => {
if(!width){ // not width or width 0 means the image has not been fully loaded.
setTimeout(this.sumWidths, 500) // delay half second to allow image to load and try again;
return;
} else {
sum = sum + width;
}
});
// This function can be created on parent component
// or use state management library or what ever befit your needs.
saveImageWidths(sum); // Create this function
// Or save `sum` to the state of this component!!!!
this.setState(state => ({ ...state, widthsSum: sum }));
}
componentDidMount() {
this.sumWidths();
}
Update 2. Using load event listeer
Take a loot at your forked working codepen here
function totalImagesWidth() {
let reportWidth = 0;
let images = document.querySelectorAll('#imagesContainer img');
let imagesWidth = [];
images.forEach(each => {
each.addEventListener("load", () => {
imagesWidth.push(each.width);
if (imagesWidth.length === images.length) {
reportWidth = (imagesWidth.reduce((a, b) => { return a + b; }, 0));
showResult(reportWidth);
}
});
});
function showResult(reportWidth){
const results = document.createElement("p");
results.innerHTML = `
Images: ${images} <br />
Total images: ${images.length} <br />
<code>imagesWidth</code> length: ${imagesWidth.length} <br />
Images widths: ${imagesWidth.toString()} <br />
<b>SUM: ${reportWidth}</b>`;
document.body.appendChild(results);
console.log(imagesWidth);
}
}
totalImagesWidth()

You're trying to get the value of a variable that is supposed to be updated in the future. How about letting it be updated for an expected number of times (which is the total number of images) and then when it has, find the total.
I didn't run my code but am looking at something like
constructor() {
this.widths = [];
}
componentDidMount() {
let images = document.querySelectorAll('#draggable img');
images.forEach(each => {
each.addEventListener('load', () => {
widths.push(each.width);
if (widths.length === images.length) {
this.reportWidth(widths.reduce(function (a, b) { return a + b; }, 0));
}
});
});
}
reportWidth (width) {
console.log(`Width is finally found to be ${width}`);
}

let images = Array.from(document.querySelectorAll('#draggable img'));
reduce is not available on a DOM node collection. Make it an Array first.
Update:
Using your codepen: this logs the desired answer:
const images = Array.from(document.querySelectorAll('#imagesContainer img'));
const sumOfWidths = images.reduce((a, b) => a + b.width, 0);
console.log({sumOfWidths}); // => 600
This assumes the images have already loaded.
Here is a solution for waiting for them to load with promises, which worked in your codepen: (this is not meant to be a good example of using promises - just a simplistic example... consider adding reject handlers for instance)
function totalImagesWidth() {
const images = Array.from(document.querySelectorAll('#imagesContainer img'));
const loadHandler = img => new Promise(resolve => {
img.addEventListener("load", () => resolve({}));
});
Promise.all(images.map(loadHandler)).then(results);
function results() {
const sumOfWidths = images.reduce((a, b) => a + b.width, 0);
const results = document.createElement("p");
results.innerHTML = `
Images: ${images} <br />
Total images: ${images.length} <br />
<b>SUM: ${sumOfWidths}</b>`;
document.body.appendChild(results);
console.log(sumOfWidths);
}
}
totalImagesWidth()

When your reduce happens, you cannot be sure the images are loaded because the event is asynchronous from your function. You need a mechanism to make sure they were all added. Like counting the number of images until the number of items in widths is the same as the number of images.
function whenTotalWidthReady(cb) {
let images = document.querySelectorAll('#draggable img');
if (images.length==0) cb(0);
let widths = [];
images.forEach(each => {
let img = new Image();
img.onload = function() {
widths.push(img.width);
if (widths.length==images.length) {
cb(widths.reduce(function(a, b){ return a + b; }););
}
}
img.src = each.src;
});
}
I don't know if creating intermediary Image objects is mandatory, but that is the way I know works. Also I do not think there is a problem with your reduce. As far as I know, the initial value is optional. But you still can pass "0" as suggested.
Now the way this version works is because it is all asynchronous, you pass a call back function as an argument. And this function will be called with the total width once images are ready.
function componentDidMount() {
whenTotalWidthReady(function(totalWidth) {
console.log("The total width of images is: " + totalWidth);
});
}

Use reduce like so:
const res = [...images].reduce((a, { width = 0 }) => a + width, 0);

Related

function called from useEffect rendering too fast and breaks, works correctly after re-render

I am using React-id-swiper to load images for products. Let's say I have 5 products.
I am trying to use a function to detect when every image is successfully loaded, then when they are, set the useState 'loading' to false, which will then display the images.
I am using a variable 'counter' to count how many images there are to load. Once 'counter' is more than or equal to the amount in the list (5), set 'loading' to FALSE and load the images.
However, I am noticing that when I console log 'counter' when running it from useEffect, it ends at '0' and renders 5 times. 'Loading' also remains true.
However, if I trigger a re-render, I see 1,2,3,4,5 in the console. 'Loading' then turns to false.
I suspect this is to do with useEffect being too fast, but I can't seem to figure it out. Can somebody help?
Code:
const { products } = useContext(WPContext);
const [productsList, setProductsList] = useState(products);
const [filteredList, setFilteredList] = useState(products);
const [loading, setLoading] = useState(false);
useEffect(() => {
loadImages(products);
}, []);
const loadImages = (allProducts) => {
let counter = 0;
setLoading(true);
const newProducts = allProducts.map((product) => {
const newImg = new Image();
newImg.src = "https://via.placeholder.com/450x630";
newImg.onload = function () {
counter += 1;
};
// At this point, if I console log 'counter', it returns as 0. If I trigger a re-render, it returns as 1,2,3,4,5
return {
...product,
acf: {
...product.acf,
product_image: {
...product.acf.product_image,
url: newImg.src,
},
},
};
});
handleLoading(newProducts, counter);
};
const handleLoading = (newProducts, counter) => {
if (counter >= filteredList.length) {
setLoading(false);
counter = 0;
setFilteredList(newProducts);
}
};
First weird thing is that you are calling the handleLoading() function before its even defined. This is probably not a problem but a weird practice.
You are also creating a new variable counter when you pass it in as an argument. You aren't actually reseting the counter that you want.
Also using onload is unnecessary and can cause weird behavior here since its not a synchronous operation but event based. As long as you set the img.src it should force it to load.
Try this:
const loadImages = (allProducts) => {
let counter = 0;
setLoading(true);
const newProducts = allProducts.map((product) => {
const newImg = new Image();
newImg.src = "https://via.placeholder.com/450x630";
counter += 1;
return { ...product, acf: { ...product.acf, product_image: { ...product.acf.product_image, url: newImg.src }}};
});
const handleLoading = (newProducts) => {
if (counter >= filteredList.length) {
setLoading(false);
counter = 0;
setFilteredList(newProducts);
}
};
handleLoading(newProducts);
};
useEffect(() => {
loadImages(products);
}, []);

problem with naturalHeight and naturalWidth Javascript

I try to sort Images by landscape/portrait format therefor I first select all images on the page with the selectFunction() then I sort them with the function "hochquer" which triggers further actions.
my Problem:
One image shows up with a width and height of 0 in console.log() even though it is properly displayed on my page and has a actual width and height. If I reload the Page for a second time it works perfectly fine and I get a width and height for this Image.
can anyone help me with this?
//Screenshot of the console added
function selectFunction() {
img = document.querySelectorAll(".img");
let imgSelector = Array.from(img);
if (imgSelector % 2 != 0) {
imgSelector.push("even'er");
}
return imgSelector;
}
function hochquer(callback) {
for (let i = 0; i < selectFunction().length; i++) {
console.log(selectFunction()[i]);
let Width = selectFunction()[i].naturalWidth;
console.log(Width, "w");
let Height = selectFunction()[i].naturalHeight;
console.log(Height, "h");
let Ratio = Width / Height;
if (Ratio >= 1) {
//quer
querFormat.push(selectFunction()[i]);
} else {
querFormat.push(undefined);
}
if (Ratio < 1) {
//hoch
hochFormat.push(selectFunction()[i]);
} else {
hochFormat.push(undefined);
}
}
callback();
}
Update:
Screenshot of the console:
[1]: https://i.stack.imgur.com/Jr6P8.png
some more code I use which I think addresses what #54ka mentioned
function newImg() {
let createImg = [];
let imgSrc = [];
const imgContainer = document.querySelector(".gallery");
for (let i = 0; i < Gallery.length; i++) {
createImg.push(new Image());
imgSrc.push(Pfad + Gallery[i]);
createImg[i].setAttribute("src", imgSrc[i]);
createImg[i].setAttribute("class", "Img");
imgContainer.appendChild(createImg[i]);
function checkImage(path) {
return new Promise((resolve) => {
createImg[i].onload = () => resolve({ path, status: "ok" });
createImg[i].onerror = () => resolve({ path, status: "error" });
});
}
loadImg = function (...path) {
return Promise.all(path.map(checkImage));
};
}
loadImg(imgSrc).then(function (result) {
console.log(result);
hochquer(sort);
});
}
newImg();
Edit:
I have made the following changes basing on your comment below after executing previous code.
I'm now adding the event listeners to all images in imgLoadCallback( ) and created a callback function containing code from hochquer( ) function. Please try to run this code.
const imgLoadCallback = function(){
for (let i = 0; i < selectFunction().length; i++) {
console.log(selectFunction()[i]);
let Width = selectFunction()[i].naturalWidth;
console.log(Width, 'w');
let Height = selectFunction()[i].naturalHeight;
console.log(Height, 'h');
let Ratio = Width / Height;
if (Ratio >= 1) {
//quer
querFormat.push(selectFunction()[i]);
} else {
querFormat.push(undefined);
}
if (Ratio < 1) {
//hoch
hochFormat.push(selectFunction()[i]);
} else {
hochFormat.push(undefined);
}
}
}
function selectFunction() {
img = document.querySelectorAll(".img");
img.forEach(i => i.addEventListener('load', imgLoadCallback))
let imgSelector = Array.from(img);
if (imgSelector % 2 != 0) {
imgSelector.push("even'er");
}
return imgSelector;
}
function hochquer(callback) {
imgLoadCallback();
callback();
}
From before edit:
Like #54ka has mentioned in the comments, it is possible that the
image does not load immediately, usually stuff like loading images
from src etc happen in the WEB API Environment asynchronously while
execution , in order to not block the primary execution thread.
Remainder of your code is synchronous, including console.log( )s that
you are performing in order to log the width and height to the
console, so those happen pretty much immediately as soon as that
particular line of code is reached.
Therefore, what I have done in the below code is basically add an
event handler for each of the image tags and only when the image has
completed 'load' the below code inside the handler will be executed
(this is where we are printing the width and height to console now).
Now, this callback function that I have added will only execute after
image loads. So, with this, it will most likely print the correct
width and height to console. But I cannot be sure until we run it at
your end because you are calling a callback at the very end.
It was about a delay in the loading of the picture I fixed it with a new approach by using the onload event and code from the link #54ka provided.
comment of #54ka:
The reason is that when you first load, you take a size from a picture that has not yet been loaded. In the refresh browser, it lays it down and manages to show it before the script passes. To resolve this issue, add an "onload Event" such as: <body onload="myFunction()"> or try this link: Javascript - execute after all images have loaded

React setTimeout with Loop

I am pulling documents from Firebase, running calculations on them and separating the results into an array. I have an event listener in place to update the array with new data as it is populated.
I am using setTimeout to loop through an array which works perfectly with the initial data load, but occasionally, when the array is updated with new information, the setTimeout glitches and either begins looping through from the beginning rather than continuing the loop, or creates a visual issue where the loop doubles.
Everything lives inside of a useEffect to ensure that data changes are only mapped when the listener finds new data. I am wondering if I need to find a way to get the setTimeout outside of this effect? Is there something I'm missing to avoid this issue?
const TeamDetails = (props) => {
const [teamState, setTeamState] = useState(props.pushData)
const [slide, setSlide] = useState(0)
useEffect(() => {
setTeamState(props.pushData)
}, [props.pushData])
useEffect(()=> {
const teams = teamState.filter(x => x.regData.onTeam !== "null" && x.regData.onTeam !== undefined)
const listTeams = [...new Set(teams.map((x) => x.regData.onTeam).sort())];
const allTeamData = () => {
let array = []
listTeams.forEach((doc) => {
//ALL CALCULATIONS HAPPEN HERE
}
array.push(state)
})
return array
}
function SetData() {
var data = allTeamData()[slide];
//THIS FUNCTION BREAKS DOWN THE ARRAY INTO INDIVIDUAL HTML ELEMENTS
}
SetData()
setTimeout(() => {
if (slide === (allTeamData().length - 1)) {
setSlide(0);
}
if (slide !== (allTeamData().length - 1)) {
setSlide(slide + 1);
}
SetData();
console.log(slide)
}, 8000)
}, [teamState, slide]);

Monitoring array for changes

I want to monitor a product on my own site for changes. I have stored all available sizes of the product in an array and I want to console.log once a new size gets added to the array.
I'm doing that just for practice hence why I don't want to monitor data source.
for (let product of products) {
main(product)
};
function main(product) {
request({
url: product.url
}, (err, res, body) => {
if (res.statusCode == 200) {
const $ = cheerio.load(body);
const availableSizes = []; //this stores all available sizes
$('.size_box').each((i, el) => {
let size = $(el).text()
availableSizes.push(size) //adds each size to availableSizes
})
} else {
console.log('Error ' + err)
console.log('Retrying in ' + (config.delay/1000) + ' second/s')
setTimeout(() => {
main(product)
}, config.delay)
};
});
};
Basically what I want to achieve/add to the code would be this:
if (<no sizes added>) {
setTimeout(() => {
main(product)
}, config.delay)
} else {
console.log('New size added ' + size)
}
But like I said, I'm not sure how to check for specific changes to the array without just spamming all available sizes all the time.
If there's a better way of monitoring sizes for changes, feel free to suggest it, just storing all sizes in an array was the first thing that came into my mind.
His is a bit of a hack, but unfortunately I do not know of a better way to do this. You can amend to the push function being called by over riding the push function on definition. See below:
var myArr = [];
myArr.push = (...args)=>{
logOnPush(args);
Array.prototype.push.apply(myArr, args);
}
function logOnPush(res){
var container = document.getElementById("displayContainer");
container.innerText = container.innerText+'\n'+'Items added: ' + res;
console.log('Items added: ', res);
}
function addToArray(){
myArr.push(Math.round(Math.random() * 10));
var container = document.getElementById("displayContainer");
container.innerText = myArr.join(', ');
}
document.getElementById("addBtn").addEventListener('click', () => {
addToArray();
});
<button id="addBtn">Add to array</button>
<p id="displayContainer"></p>
<p id="consoleContainer"></p>
You could create a custom class, with its data being updated with a custom push method. This class would act exactly as an Array object (you could even extend the Array class), but you can then add your custom behavior
Take a look at this page for an example: https://github.com/wesbos/es6-articles/blob/master/54%20-%20Extending%20Arrays%20with%20Classes%20for%20Custom%20Collections.md

PDFMake loop using canvas data doesn't keep correct order

I am using the following code to render DOM elements as canvas data and then make a PDF with them.
I have had to create a loop for this because if it was all one image it is impossible to correctly place the data over multiple pages.
$scope.pdfMaker = function() {
var content = [];
var pdfElement = document.getElementsByClassName("pdfElement");
for (var i = pdfElement.length - 1; i >= 0; i--) {
html2canvas(pdfElement[i], {
onrendered: function(canvas) {
var data = canvas.toDataURL();
content.push({
image: data,
width: 500,
margin: [0, 5]
});
console.log(canvas);
}
});
}
var docDefinition = {
content: content
};
setTimeout(function() {
pdfMake.createPdf(docDefinition).download("Score_Details.pdf");
}, 10000);
}
Issues:
I have that nasty 10 second timeout to allow time for processing, how can I restructure my code to allow the PDF to be made after all canvas data has been performed.
My canvas elements are becoming mixed up when they are converted, the correct order is essential. How can I maintain the order of the DOM elements?
It seems that html2canvas returns a promise:
$scope.pdfMaker = function() {
var promises = [];
var pdfElements = document.getElementsByClassName("pdfElement");
for (var i = 0; i < pdfElements.length; i++) {
promises.push(
html2canvas(pdfElements[i]).then(function(canvas) {
console.log('finished one!');
return {
image: canvas.toDataURL(),
width: 500,
margin: [0, 5]
};
})
);
}
console.log('done calling');
$q.all(promises).then(function(content) {
console.log('all done');
pdfMake.createPdf({ content: content }).download("Score_Details.pdf");
console.log('download one');
}, function(err) {
console.error(err);
})
}
To get your code to work just requires a few mods.
The problem from your description is that the rendering time for html2canvas varies so that they come in an undetermined order. If you make the code inside the for loop a function and pass the index to it, the function will close over the argument variable, you can use that index in the onrendered callback because (that will also close over the index) to place the rendered html in the correct position of the content array.
To know when you have completed all the rendering and can convert to pdf, keep a count of the number of html2canvas renderer you have started. When the onrendered callback is called reduce the count by one. When the count is zero you know all the documents have been rendered so you can then call createPdf.
Below are the changes
$scope.pdfMaker = function() {
var i,pdfElement,content,count,docDefinition;
function makeCanvas(index){ // closure ensures that index is unique inside this function.
count += 1; // count the new render request
html2canvas(pdfElement[index], {
onrendered: function(canvas) { // this function closes over index
content[index] = { // add the canvas content to the correct index
image: canvas.toDataURL(),
width: 500,
margin: [0, 5]
};
count -= 1; // reduce count
if(count === 0){ // is count 0, if so then all done
// render the pdf and download
pdfMake.createPdf(docDefinition).download("Score_Details.pdf");
}
}
});
}
// get elements
pdfElement = document.getElementsByClassName("pdfElement");
content = []; // create content array
count = 0; // set render request counter
docDefinition = {content: content}; //
for (i = pdfElement.length - 1; i >= 0; i--) { //do the loop thing
makeCanvas(i); // call the makeCanvas function with the index
}
}
The only thing I was not sure of was the order you wanted the pdfElements to appear. The changes will place the rendered content into the content array in the same order as document.getElementsByClassName("pdfElement"); gets them.
If you want the documents in the revers order call content.reverse() when the count is back to zero and you are ready to create the pdf.
Closure is a very powerful feature of Javascript. If you are unsure of how closure works then a review is well worth the time.
MDN closures is as good a place as any to start and there are plenty of resources on the net to learn from.

Categories