This question already has answers here:
Wait for forEach with a promise inside to finish
(2 answers)
Closed 4 months ago.
Tried to get my head around this problem, but still couldn't(I feel like ELI5 would be appropriate approach for me when talking about Promises). I removed readMultipleFiles-function promise. Put Promise.all and tried async await, but the returned array is still empty. I'm completely lost and I would appreciate very much if you could point me my error(s) and maybe suggest some good YT lessons about these :/
Edited code and added all the required JS and HTML+CSS to create dropzone:
// Function to handle files user drag&drop to HTML page
const handleDrop = async (e) => {
console.log('1.0 handleDrop function starts... ');
// files are filelist-type
const dt = e.dataTransfer;
const files = dt.files;
let fileArray = [...files];
console.log('2. Starting to read files.');
let filesRead = await readMultipleFiles(fileArray);
// These gives still undefined
console.log('filesRead[1]: ',filesRead[1]);
console.log('filesRead: ',filesRead);
}
// Function to read multiple files in array. This should return array of files
const readMultipleFiles = async (fileArray) => {
console.log('\t2.1 readMultipleFiles working...');
return Promise.all(fileArray.map(f => {readFile(f)}));
}
// Function to read single file. This should return single file object:
// {filename: <somefilename>, content:<sometextcontent}
const readFile = (file) => {
console.log('\t2.2 readFile working...');
let myPromise = new Promise((resolve) => {
let singleFile;
// console.log('\t\t2.2.1 Reading single file...');
const reader = new FileReader();
reader.onload = (e) => {
// console.log('\t\t\t2.2.1 readFile -> file: ',e.target.result, 'typeof file:', typeof e.target.result);
fileContent = e.target.result;
fileName = file.name;
singleFile = {
'filename':file.name,
'content':e.target.result
};
// console.log('\t2.2.1 readFile --> fileRead: ', singleFile);
}
reader.readAsText(file);
resolve(singleFile);
})
return myPromise.then(res => {
// console.log('2.2.1.1 readfile -> myPromise.then res: ', res);
return res;
});
}
const initApp = () => {
const droparea = document.querySelector('.droparea');
const active = () => droparea.classList.add('green-border');
const inactive = () => droparea.classList.remove('green-border');
const prevents = (e) => e.preventDefault();
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(evtName => {
droparea.addEventListener(evtName, prevents);
});
['dragenter', 'dragover'].forEach(evtName => {
droparea.addEventListener(evtName, active);
});
['dragleave', 'drop'].forEach(evtName => {
droparea.addEventListener(evtName, inactive);
});
droparea.addEventListener('drop', handleDrop);
}
document.addEventListener("DOMContentLoaded", initApp);
HTML page for this:
<html>
<head>
<meta charset="utf-8">
<title>Page</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<link href="https://fonts.googleapis.com/css?family=Lato" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.2/css/all.min.css" integrity="sha512-1sCRPdkRXhBV2PBLUdRb4tMg1w2YPf37qatUFeS7zlBy7jJI8Lf4VHwWfZZfpXtYSLy85pkm9GaYVYMfw5BC1A==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.10/semantic.min.css">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div>
Page
</div>
<main>
<section class="droparea">
<i class="far fa-images"></i>
<p>Drop your files here</p>
</section>
</main>
<script type="text/javascript" src="index.js"></script>
CSS:
.hide {
display: none;
}
h3 {
margin: 0 auto;
padding-top: 10px;
}
.checkboxarea {
width: 99%;
overflow: hidden;
}
.checkboxitem {
width: 45%;
float: left;
}
.ui.segment {
border: 0;
box-shadow: none;
}
#drop_zone {
border: 5px solid blue;
width: 200px;
height: 100px;
}
.droparea {
margin: 1rem auto;
display: flex;
flex-direction: column;
justify-content: left;
align-items: center;
width: 384px;
max-width: 100%;
height: 160px;
border: 4px dashed grey;
border-radius: 15px;
}
.droparea i {
font-size: 3rem;
flex-grow: 1;
padding-top: 1rem;
}
.green-border {
border-color: green;
}
.selectResultData {
padding-right: 10px;
}
/* Style the button that is used to open and close the collapsible content */
.collapsible {
background-color: #eee;
color: #444;
cursor: pointer;
padding: 18px;
width: 100%;
border: none;
text-align: left;
outline: none;
font-size: 15px;
}
/* Add a background color to the button if it is clicked on (add the .active class with JS), and when you move the mouse over it (hover) */
.active, .collapsible:hover {
background-color: #ccc;
}
/* Style the collapsible content. Note: hidden by default */
.content {
padding: 0 18px;
display: none;
overflow: hidden;
background-color: #f1f1f1;
}
Original question:
I've been banging my head with this kind of problem for awhile now. I'm trying to use Javascript promises and async/await to fill array with object(txt-files dropped by user to HTML page) to be used later on my code but I cannot get around problem that array objects cannot be accessed although console.log shows array has objects. Seems to me that async/await is not waiting for the Array to be filled for some reason which I don't understand. Can you please help me with this?
//Code to handle user filedrops
const handleDrop = async (e) => {
console.log('1.0 handleDrop function starts... ');
// files are filelist-type
const dt = e.dataTransfer;
const files = dt.files;
let fileArray = [...files];
// Making sure files are in fileArray and they can be accessed
console.log('1.0 handleDrop -> fileArray.length',fileArray.length)
console.log('Files: ',fileArray);
console.log('2. Starting to read files.');
let filesRead = await readMultipleFiles(fileArray);
console.log('This log row should come after files have been read and saved to variable\nFiles read! filesRead: ', filesRead);
// **************************
// Why this gives undefined?
// **************************
console.log('filesRead[1]: ',filesRead[1]);
}
//Function to read Array of files and return them back in Array
const readMultipleFiles = (fileArray) => {
console.log('\t2.1 readMultipleFiles working...');
let myPromise = new Promise((resolve) => {
let filesRead = [];
fileArray.forEach(f => {
let file;
readFile(f).then(res => {
console.log('2.1.1 file returned by readFile', res, '\nf:',f);
file=res
return res;
}).then(res => filesRead.push(file));
});
resolve(filesRead);
})
return myPromise.then(res => res);
}
// Function to read single file and return it as an object
const readFile = (file) => {
console.log('\t2.2 readFile working...');
let myPromise = new Promise((resolve) => {
let singleFile;
console.log('\t\t2.2.1 Reading single file...');
const reader = new FileReader();
reader.onload = (e) => {
console.log('\t\t\t2.2.1 readFile -> file: ',e.target.result, 'typeof file:', typeof e.target.result);
fileContent = e.target.result;
fileName = file.name;
singleFile = {
'filename':file.name,
'content':e.target.result
};
console.log('\t2.2.1 readFile --> fileRead: ', singleFile);
resolve(singleFile);
}
reader.readAsText(file);
})
return myPromise.then(res => {
console.log('2.2.1.1 readfile -> myPromise.then res: ', res);
return res;
});
}
Your function readMultipleFiles returns a promise that resolves to an empty array because resolve is called before the asynchronous then callbacks have ran, and so filesRead is still [] when resolve(filesRead) is called.
Not your original problem, but it is an anti-pattern to create a promise with new Promise when you already have promises to work with (readFile(f)). In this case you probably want to get a promise from Promise.all
Here is how you can fix readMultipleFiles:
const readMultipleFiles = (fileArray) => {
console.log('\t2.1 readMultipleFiles working...');
return Promise.all(fileArray.map(readFile))
}
For simplicity sake, I didn't bother to have this line in the above correction:
console.log('2.1.1 file returned by readFile', res, '\nf:',f);
There is already a lot of debugging output there.
Related
I have an image uploader that I'm building that allows the user to remove images/ image previews prior to form submission. I seem to have a come across a side effect that a) I don't know what is causing it, and b) how to fix it.
When an image is deleted from the preview the image is removed via a click event and the remove() method on the image's parent figure element.
If a user then re-selects the same image (that has just been removed) from their computer with the file picker / input the image doesn't show on the preview the second time around? But, even more confusingly, if a completely different image is attached, and then the user tries to re-attach the original image that previously didn't show the second time, that image does then show again (I hope this makes sense).
I have no idea what is going on here?
Codepen: https://codepen.io/thechewy/pen/qBYYMYV
let dropZone = document.getElementById("zone"),
showSelectedImages = document.getElementById("show-selected-images"),
fileUploader = document.getElementById("upload-files");
dropZone.addEventListener("click", () => {
// assigns the dropzone to the hidden input element, when you click 'select files' it brings up a file picker window
fileUploader.click();
});
// Prevent browser default when draging over
dropZone.addEventListener("dragover", (e) => e.preventDefault());
// Prevent browser default when draging over
dropZone.addEventListener("drop", (e) => e.preventDefault());
fileUploader.addEventListener("change", (e) => {
// this function is further down but declared here and shows a thumbnail of the image
[...fileUploader.files].forEach(updateThumbnail);
console.log("fileUploader.files is: ", [...fileUploader.files]);
});
// updateThumbnail function
function updateThumbnail(file) {
if (file.type.startsWith("image/")) {
let uploadImageWrapper = document.createElement("figure"),
thumbnail = new Image(),
removeImage = `
<div class="remove-image jc-center flex"> Delete </div>
`;
// image thumbnail
thumbnail.classList.add("thumbnail");
thumbnail.src = URL.createObjectURL(file);
// appending elements
showSelectedImages.append(uploadImageWrapper); // <figure> element
uploadImageWrapper.append(thumbnail); // image thumbnail
uploadImageWrapper.insertAdjacentHTML("afterbegin", removeImage); // 'x' to remove image
// get the original width and height values of the thumbnail using the decode() method
thumbnail
.decode()
.then((response) => {
thumbWidth = thumbnail.naturalWidth;
thumbHeight = thumbnail.naturalHeight;
// typical front end image validations
if (thumbWidth * thumbHeight < 4000000) {
// output the error message
}
})
.catch((encodingError) => {
// Do something with the error.
});
// Delete images from the preview
document
.querySelectorAll("#show-selected-images .remove-image")
.forEach((i) => {
i.addEventListener("click", (e) => {
if (e.target) {
var deleteFigureEl = e.target.closest("figure");
// removes the image via removing it's parent element
deleteFigureEl.remove();
}
});
});
}
} // end of 'updateThumbnail' function
body {
margin: 0;
display: flex;
justify-content: center;
font-family: arial;
}
form {
width: 50%;
max-width: 600px;
}
.select-files {
padding: 1rem;
background: red;
cursor: pointer;
color: #fff;
font-weight: bold;
}
#show-selected-images {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-top: 2rem;
}
figure {
width: 100%;
margin: 0;
}
img {
display: block;
width: 100%;
height: auto;
}
.remove-image {
cursor: pointer;
padding: 1rem;
background: lightgrey;
}
<form id="upload-images-form" enctype="multipart/form-data" method="post">
<h1>Upload Your Images</h1>
<div id="zone">
<p class="select-files">SELECT FILES</p>
</div>
<div class="inner-wrapper">
<div class="upload-label-wrapper">
<input id="upload-files" style="display:none;" type="file" name="upload-files[]" multiple>
</div>
<button id="submit-images" oninput="updateThumbnail(this.files)">SUBMIT</button>
</div>
<div id="show-selected-images"></div>
</form>
let dropZone = document.getElementById("zone"),
showSelectedImages = document.getElementById("show-selected-images"),
fileUploader = document.getElementById("upload-files"),
files = [],
filesIndexes = new Set();
dropZone.addEventListener("click", () => {
// assigns the dropzone to the hidden input element, when you click 'select files' it brings up a file picker window
fileUploader.click();
});
// Prevent browser default when draging over
dropZone.addEventListener("dragover", (e) => e.preventDefault());
// Prevent browser default when draging over
dropZone.addEventListener("drop", (e) => e.preventDefault());
fileUploader.addEventListener("change", (e) => {
// this function is further down but declared here and shows a thumbnail of the image
[...fileUploader.files].forEach((file) => {
updateThumbnail(file);
let i = files.length;
while (filesIndexes.has(i)) {
i++;
}
files.push({ file, index: i });
filesIndexes.add(i);
});
fileUploader.value = "";
console.log("fileUploader.files is: ", [...fileUploader.files]);
});
// updateThumbnail function
function updateThumbnail(file) {
if (file.type.startsWith("image/")) {
let uploadImageWrapper = document.createElement("figure"),
thumbnail = new Image(),
removeImage = `
<div class="remove-image jc-center flex" index="${files.length}"> Delete </div>
`;
// image thumbnail
thumbnail.classList.add("thumbnail");
thumbnail.src = URL.createObjectURL(file);
// appending elements
showSelectedImages.append(uploadImageWrapper); // <figure> element
uploadImageWrapper.append(thumbnail); // image thumbnail
uploadImageWrapper.insertAdjacentHTML("afterbegin", removeImage); // 'x' to remove image
// get the original width and height values of the thumbnail using the decode() method
thumbnail
.decode()
.then((response) => {
thumbWidth = thumbnail.naturalWidth;
thumbHeight = thumbnail.naturalHeight;
// typical front end image validations
if (thumbWidth * thumbHeight < 4000000) {
// output the error message
}
})
.catch((encodingError) => {
// Do something with the error.
});
// Delete images from the preview
document
.querySelectorAll("#show-selected-images .remove-image")
.forEach((i) => {
i.onclick = (e) => {
console.log(e.currentTarget);
if (e.target) {
var deleteFigureEl = e.target.closest("figure");
let index = e.currentTarget.getAttribute("index");
files.forEach((obj, i) => {
if (obj.index == index) {
files.splice(i, 1);
filesIndexes.delete(Number(index));
}
});
// removes the image via removing it's parent element
deleteFigureEl.remove();
}
};
});
}
} // end of 'updateThumbnail' function
And then you should upload files like this
function upload(){
let formData = new FormData()
for (let index = 0; index < files.length; index++) {
formData.append(`file${files[index].index}`, files[index].file)
}
fetch("url", {method: "POST", body: formData})
}
I have a file uploader that shows preview images and it is currently set so if you click an image the image preview is deleted and the image is also effectively deleted from the FileList array before the form submission by creating a second array and setting the FileList to hold these values. All of this works OK.
The Context
Whilst setting this up and posting this as a question on StackOverflow in order to (theortically) keep the code simpler I removed the 'remove image' button and set it so the image was deleted both visually and from the FileList if you clicked the image itself. I'm now struggling to work out how to transfer this functionality so when the 'x' is clicked the same functionality happens.
In the current code if you click the image you get the expected behaviour.
I've now included code so that the image preview for each individual image is wrapped in a parent <figure> element with the 'x' .remove-image element included in this too. I've also included some commented out code at the bottom that if uncommented removes the <figure> element containing the image when the 'x' is clicked.
The Question
How do I get it so that instead of when the <img> is clicked the required functionality only happens when the 'x' is clicked?
I would think I need to somehow have the click event on .remove-image (the 'x') that uses the .closest method to go up to the figure element and down to the image i.e. removeImage.closest('figure').querySelector('.img') but I only want the required functionality when the 'x' is clicked, and not when either the 'x' or the image are clicked, which would be simple to fix.
Any help greatly appreciated.
Codepen: https://codepen.io/thechewy/pen/oNdrzjz
let attachFiles = document.getElementById("attach-files");
let previewWrapper = document.getElementById("show-selected-images");
let form = document.getElementById("upload-images-form");
let submitData = new DataTransfer();
attachFiles.addEventListener("change", (e) => {
const currentSubmitData = Array.from(submitData.files);
// For each addded file, add it to submitData if not already present
[...e.target.files].forEach((file) => {
if (currentSubmitData.every((currFile) => currFile.name !== file.name)) {
submitData.items.add(file);
}
});
// Sync attachFiles FileList with submitData FileList
attachFiles.files = submitData.files;
// Clear the previewWrapper before generating new previews
previewWrapper.replaceChildren();
// Generate a preview <img> for each selected file
[...submitData.files].forEach(showFiles);
});
function showFiles(file) {
let uploadImageWrapper = document.createElement("figure");
let previewImage = new Image();
let removeImage = `<div class="remove-image"> X </div>`;
// Set relevant <img> attributes
previewImage.dataset.name = file.name;
previewImage.classList.add("img");
previewImage.src = URL.createObjectURL(file);
// Adds click event listener to <img> preview (this needs moving to the .remove-image element)
previewImage.addEventListener("click", (e) => {
const target = e.currentTarget;
const name = target.dataset.name;
// Remove the clicked file from the submitData
[...submitData.files].forEach((file, idx) => {
if (file.name === name) {
submitData.items.remove(idx);
}
});
// Reset the attachFiles FileList
attachFiles.files = submitData.files;
// Remove the <img> node from the DOM
target.remove();
});
// Append <figure> and <img> preview node to DOM
previewWrapper.append(uploadImageWrapper); // <figure> element
uploadImageWrapper.append(previewImage); // <img>
uploadImageWrapper.insertAdjacentHTML('afterbegin', removeImage); // 'x' to remove the image
// // ===== Delete figure element that wraps the image =====
// document.querySelectorAll('#show-selected-images .remove-image').forEach(i => {
// i.addEventListener('click', (e) => {
// if (e.target) {
// let deleteFigureEl = e.target.closest('figure');
// // removes the image via removing it's parent element
// deleteFigureEl.remove();
// }
// })
// })
}
* {
position: relative;
}
form {
padding: 1rem 2rem;
width: 50%;
border: 1px solid;
}
input,
button {
display: block;
margin: 2rem 0;
}
.remove-image {
background: #000;
width: 30px;
height: 30px;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
top: 10px;
left: -10px;
position: absolute;
z-index: 2;
}
.img {
width: 200px;
height: 200px;
object-fit: cover;
margin: 0 1rem;
}
<form enctype="multipart/form-data" method="post" id="upload-images-form">
<input id="attach-files" type="file" name="attach-files[]" multiple>
<button name="submit" id="submit">SUBMIT</button>
<div id="show-selected-images"></div>
</form>
The two dynamic values in the click listener are the target (which is the previewImage itself) and the name (which is the same as the file.name already in scope). So, all you'd really need to do is change those variable references inside a removeImage event listener (and make removeImage an actual element, not just a string, so that .addEventListener can be called on it).
const removeImage = document.createElement('div');
removeImage.className = 'remove-image';
removeImage.textContent = ' X ';
removeImage.addEventListener('click', () => previewImage.click());
removeImage.addEventListener("click", () => {
const name = file.name;
[...submitData.files].forEach((file, idx) => {
if (file.name === name) {
submitData.items.remove(idx);
}
});
attachFiles.files = submitData.files;
previewImage.remove();
removeImage.remove();
});
let attachFiles = document.getElementById("attach-files");
let previewWrapper = document.getElementById("show-selected-images");
let form = document.getElementById("upload-images-form");
let submitData = new DataTransfer();
attachFiles.addEventListener("change", (e) => {
const currentSubmitData = Array.from(submitData.files);
// For each addded file, add it to submitData if not already present
[...e.target.files].forEach((file) => {
if (currentSubmitData.every((currFile) => currFile.name !== file.name)) {
submitData.items.add(file);
}
});
// Sync attachFiles FileList with submitData FileList
attachFiles.files = submitData.files;
// Clear the previewWrapper before generating new previews
previewWrapper.replaceChildren();
// Generate a preview <img> for each selected file
[...submitData.files].forEach(showFiles);
});
function showFiles(file) {
let uploadImageWrapper = document.createElement("figure");
let previewImage = new Image();
const removeImage = document.createElement('div');
removeImage.className = 'remove-image';
removeImage.textContent = ' X ';
removeImage.addEventListener('click', () => previewImage.click());
removeImage.addEventListener("click", () => {
const name = file.name;
[...submitData.files].forEach((file, idx) => {
if (file.name === name) {
submitData.items.remove(idx);
}
});
attachFiles.files = submitData.files;
previewImage.remove();
removeImage.remove();
});
// Set relevant <img> attributes
previewImage.classList.add("img");
previewImage.src = URL.createObjectURL(file);
// Append <figure> and <img> preview node to DOM
previewWrapper.append(uploadImageWrapper); // <figure> element
uploadImageWrapper.append(previewImage); // <img>
uploadImageWrapper.insertAdjacentElement('afterbegin', removeImage); // 'x' to remove the image
}
* {
position: relative;
}
form {
padding: 1rem 2rem;
width: 50%;
border: 1px solid;
}
input,
button {
display: block;
margin: 2rem 0;
}
.remove-image {
background: #000;
width: 30px;
height: 30px;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
top: 10px;
left: -10px;
position: absolute;
z-index: 2;
}
.img {
width: 200px;
height: 200px;
object-fit: cover;
margin: 0 1rem;
}
<form enctype="multipart/form-data" method="post" id="upload-images-form">
<input id="attach-files" type="file" name="attach-files[]" multiple>
<button name="submit" id="submit">SUBMIT</button>
<div id="show-selected-images"></div>
</form>
I'm trying to separate characters based on what house they belong to in the API (http://hp-api.herokuapp.com/api/characters)
I have tried using .filter and .map, but have been unable to achieve that goal, I don't know if this is the right place to ask for help understanding how to achieve my goal.
Here is the code:
const studentArray = [];
async function getStudents(url) {
const student = await fetch(url);
const jsondata = await student.json();
jsondata.forEach((student) => {
studentArray.push(student);
});
}
getStudents("http://hp-api.herokuapp.com/api/characters/students").then(() => {
});
<!DOCTYPE html>
<html lang="en">
<head>
<script src="/testing/script.js"></script>
<link rel="stylesheet" href="/testing/styles.css" />
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<main>
<div id="name" class="container"><button onclick="Gryffindor">Test</button></div>
<div id="name" class="container"><button onclick="Slytherin">Test</button></div>
<div id="name" class="container"><button onclick="Ravenclaw">Test</button></div>
<div id="name" class="container"><button onclick="Hufflepuff">Test</button></div>
</main>
</body>
</html>
I've just had a look here and there seems to already be an endpoint set up to get characters from particular houses if that's what you want to do?
E.g. fetch(http://hp-api.herokuapp.com/api/characters/house/gryffindor) will return an array of students in Gryffindor.
You could refactor your getStudents function to take the house as an argument and make a GET request to the http://hp-api.herokuapp.com/api/characters/house/:house endpoint, using a template literal.
Also your onClick isn't invoking any function. I suggest you have a look here for an example of how to use onClick
One possible approach, and especially for the chosen API, is to fetch all student-data exactly once.
From this data one can create an own index of house-specific student-lists. For the chosen API one would realize that there are actually students listed with no relation/link into any house. And the API itself does not provide students which are not associated with a house.
Thus, the task which does render the house-based filter-items can take this additional information (no house) as much into account as the option of displaying any student regardless of the house (all students).
The next step would initialize the filter handling which is the rendering of student-items from the currently chosen (click event) house-specific student-list.
It uses the technique of event delegation. Thus, there is a single listener subscribed to a single element which is the root-node of the students filter (instead of subscribing the listener again and again to each filter-item).
function emptyElementNode(elmNode) {
[...elmNode.childNodes].forEach(node => node.remove());
}
function createFilterItem(houseName) {
houseName = houseName.trim();
const elmItem = document.createElement('li');
const elmButton = document.createElement('button');
elmItem.dataset.filterLabel = houseName;
elmButton.textContent =
(houseName === '') && 'No House' || houseName;
elmItem.appendChild(elmButton);
return elmItem;
}
function createStudentItem(studentData) {
const elmItem = document.createElement('li');
const elmImage = document.createElement('img');
elmImage.src = studentData.image;
elmImage.alt = elmImage.title = studentData.name;
elmItem.appendChild(elmImage);
return elmItem;
}
function renderStudentsFilter(houseBasedIndex) {
const houseNameList = Object.keys(houseBasedIndex);
// console.log({ houseNameList });
const filterRoot = document
.querySelector('[data-students-filter]');
if (filterRoot) {
const filterListRoot = houseNameList
.reduce((rootNode, houseName) => {
rootNode
.appendChild(
createFilterItem(houseName)
);
return rootNode;
}, document.createElement('ul'));
const allStudentsFilterItem =
createFilterItem('All Students');
allStudentsFilterItem.dataset.filterLabel = 'all-students';
filterListRoot.appendChild(allStudentsFilterItem);
emptyElementNode(filterRoot);
filterRoot.appendChild(filterListRoot);
return filterRoot;
}
}
function renderStudentItems(studentList) {
const displayRoot = document
.querySelector('[data-student-list]');
if (displayRoot) {
const listRoot = studentList
.reduce((rootNode, studentData) => {
rootNode
.appendChild(
createStudentItem(studentData)
);
return rootNode;
}, document.createElement('ul'));
emptyElementNode(displayRoot);
displayRoot.appendChild(listRoot);
}
}
function handleStudentsFilterFromBoundIndex({ target }) {
const filterItem = target.closest('li[data-filter-label]')
const { dataset: { filterLabel } } = filterItem;
// console.log({ filterItem, filterLabel });
const studentList = this[filterLabel];
renderStudentItems(studentList);
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
}
function initializeStudentsFilterHandling(filterRoot, houseBasedIndex) {
filterRoot
.addEventListener(
'click',
handleStudentsFilterFromBoundIndex.bind(houseBasedIndex)
);
}
function createStudentsFilterByHouse(studentList) {
const houseBasedIndex = studentList
.reduce((index, student) => {
const houseName =
student.house?.trim() ?? '';
(index[houseName] ??= []).push(student);
return index;
}, {});
// console.log({
// studentList,
// houseBasedIndex,
// });
const filterRoot = renderStudentsFilter(houseBasedIndex);
houseBasedIndex['all-students'] = studentList;
initializeStudentsFilterHandling(filterRoot, houseBasedIndex);
}
async function main() {
const url = 'http://hp-api.herokuapp.com/api/characters/students';
const response = await fetch(url);
const studentList = await response.json();
createStudentsFilterByHouse(studentList);
}
main();
body {
margin: 32px 0 0 0!important;
}
ul {
list-style-type: none;
margin: 0;
padding: 0;
}
ul li {
display: inline-block;
margin: 0 3px 2px 3px;
}
ul li img {
float: left;
height: 120px;
width: auto;
}
ul li img[src=""] {
height: 98px;
width: auto;
padding: 10px 5px;
border: 1px dashed #aaa;
}
[data-students-filter] {
position: fixed;
left: 0;
top: 0;
}
<article data-students-overview>
<navigation data-students-filter>
<em>+++ initialize house based students filter +++</em>
</navigation>
<section data-student-list>
</section>
</article>
I have built a component that lets a user upload a file by either clicking on it, or dropping a file directly on it. Most of the time this works just fine, but I have noticed that it causes Google Chrome (Version 94.0.4606.81) to frequently freeze up completely, instead of opening the file picker. It will still let me press Ctrl+W to close the tab, but other tabs will also remain unresponsive to mouse clicks. I am forced to close the entire browser. I could not reproduce this in Microsoft Edge however.
const fileInput = document.querySelector("#fileInput");
const dropArea = document.querySelector("#dropArea");
const upload = document.querySelector("#upload");
function onDragBegin(e) {
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
dropArea.classList.add("dropArea-highlight");
upload.classList.add("upload-highlight");
}
function onDragEnd(e) {
e.preventDefault();
dropArea.classList.remove("dropArea-highlight");
upload.classList.remove("upload-highlight");
}
function onDrop(e) {
fileInput.files = e.dataTransfer.files;
fileInput.dispatchEvent(new Event("change"));
e.preventDefault();
}
async function onChangeFileInput() {
const { files } = this;
dropArea.classList.remove("dropArea-highlight");
upload.classList.remove("upload-highlight");
const url = "/api/har";
const formData = new FormData();
formData.append("harFile", files[0]);
fetch(url, {
method: "POST",
body: formData,
}).then(async (res) => {
if (!res.ok) {
const text = await res.text();
alert(text);
}
if (res.redirected) {
window.location.href = res.url;
}
});
}
dropArea.ondragover = onDragBegin;
dropArea.ondragenter = onDragBegin;
dropArea.ondragleave = onDragEnd;
dropArea.ondrop = onDrop;
fileInput.addEventListener("change", onChangeFileInput);
.upload {
border: 1px dashed var(--copytext-grey);
background-color: #e7eaef;
}
.upload:hover {
background-color: #e1e4ea;
border-style: dotted;
}
.upload-icon {
height: 64px;
filter: invert(36%) sepia(13%) saturate(381%) hue-rotate(171deg)
brightness(93%) contrast(91%);
margin-bottom: 16px;
}
#dropArea {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4rem 1rem;
}
#dropArea:hover {
cursor: pointer;
}
#dropArea h3 {
color: #555d66;
font-weight: bold;
}
#dropArea p {
color: #676f77;
}
#fileInput {
display: none;
}
.upload-highlight {
background-color: #e1e4ea;
border-style: dotted;
}
.dropArea-highlight {
cursor: pointer;
}
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="style.css">
<script src="upload.js" defer></script>
</head>
<body>
<div class="upload" id="upload">
<label for="fileInput">
<div id="dropArea">
<img src="upload.svg" class="upload-icon">
<h3>Drag and drop or click</h3>
<p>to upload your file</p>
<input type="file" name="harFile" id="fileInput" accept=".har">
</div>
</label>
</div>
</body>
</html>
I have already made a bugreport, but in the meantime I need to find a fix to stop the browser from freezing up. How?
Given this is not your code that's faulty, it will be hard to workaround that issue. (my guess would be some badly registered file the OS has troubles getting the metadata from, or some network drives (dropbox, icloud) that aren't connecting correctly).
The only workaround I can think of would be the new File System Access API, which hopefully would not trigger Chrome's issue (I think they shouldn't need the same metadata). But I am absolutely not sure this will do either so please let me know since I can't repro that issue myself.
Also, this API is currently only supported in Chromium based browsers, so you'd still need an <input type=file> fallback for other browsers.
Live demo as a glitch since this API isn't available in sandboxed iframes.
// we need to be handling a click event
document.querySelector("button").onclick = async () => {
if( !("showOpenFilePicker" in self) ) {
console.error("unsupported browser, should fallback to <input type=file>");
return;
}
const handles = await showOpenFilePicker();
const file = await handles[0].getFile();
// now you can handle the file as usual
document.getElementById( "log" ).textContent = await file.text();
};
soo, i am trying to use localstorage to store the classlist so it will remember whether or not something was added to their favorites list. i need to do this with JavaScript. The error provided comes from the console in my web browser (chrome)
The ERROR
main.js:99 Uncaught TypeError: Cannot read property 'value' of undefined
at storeFavo (main.js:99)
at HTMLButtonElement.favorites (main.js:71)
HTML
<!-- Showcase -->
<section class="showcase">
<div class="container grid ">
<div class="showcase-form card">
<h2>*Input Car Name*</h2>
<img alt="" src="#" >
<button id="favo" class="btn">Add to Favorites</button>
<button id="info" class="btn">More Info</button>
</div>
</div>
</section>
JavaScript
window.addEventListener('load', init);
const info = document.querySelector('#info');
const div1 = document.querySelector('.moreInfo');
const favo = document.querySelector('#favo');
let apiUrl = 'webservice/includes/actions.php';
let apiUrl2 = 'webservice/index.php';
let Favos = document.getElementById('favo').getElementsByClassName('favorited')[0];
function init() {
info.addEventListener('click', moreInfo);
favo.addEventListener('click', favorites);
if (typeof window.localStorage === "undefined") {
console.error('Local storage is not available in your browser');
return;
}
checkFromLocalStorage()
carList;
}
function carList() {
fetch(apiUrl)
.then((response) => {
if (!response.ok) {
throw new Error(response.statusText);
}
return response.json();
})
.then(getAjaxSuccessHandler)
.catch(getAjaxErrorHandler);
}
function getAjaxSuccessHandler(data) {
console.log(data);
}
function getAjaxErrorHandler(data) {
console.error(data);
}
// Generate Cards
// getCars.map((item)=>{
// return (
// <div>
// <p> {item.brand}</p>
// <p> {item.type}</p>
// </div>
// );
// }),
// More Info Button
function moreInfo() {
let title= document.createElement('h1');
title.innerHTML = 'Info';
div1.appendChild(title)
console.log("info?")
if (div1.style.display == 'block'){
div1.style.display = 'none';
} else {
div1.style.display = 'block';
}
}
// Add To Favorites Button
function favorites() {
console.log("favo")
if (favo.classList == 'btn') {
favo.classList.remove('btn')
favo.classList.add('favorited')
favo.innerHTML = 'Remove from favorites';
storeFavo()
}
else {
favo.classList.remove('favorited')
favo.classList.add('btn')
favo.innerHTML = 'Add to Favorites';
deleteClickHandler()
}
}
//Check
/**
* Is local storage is available on page load? Let's fill the form
*/
function checkFromLocalStorage() {
if (localStorage.getItem('favos') !== null) {
Favos.value = localStorage.getItem('favos');
}
}
//Store
/**
* After submitting the form, let's save the values in the local storage
*
* #param e
*/
function storeFavo(e) {
localStorage.setItem('favos', Favos.value);
localStorage['Favos'] = document.getElementById("favo")
}
//Delete
/**
* Make sure we clean up the local storage again
*
* #param e
*/
function deleteClickHandler(e) {
localStorage.removeItem('favos');
}
CSS
.favorited {
display: inline-block;
padding: 10px 30px;
cursor: pointer;
background: #470aed;
color: #ffffff;
border: none;
border-radius: 5px;
}
.btn {
display: inline-block;
padding: 10px 30px;
cursor: pointer;
background: var(--primary-color);
color: #ffffff;
border: none;
border-radius: 5px;
}
I will do my very best to respond fast and to provide any needed information if asked for.
Check the documentation of localStorage.setItem https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem, you can write only string values, but you are trying to write an Object value (https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementsByClassName). If you want to have some persistence fo your HTML element, you can save outerHTML https://developer.mozilla.org/ru/docs/Web/API/Element/outerHTML to the storage and parse it on load.