Firestore forEach returns one item - javascript

I'm creating a project / to-do appliction with firestore.
I want to return all the current projects where the active user has been assigned to.
In a console.log(doc.id, doc.data()), I get the two projects where he has been signed up for.
But when I want to show them both on the home screen, I only see one project.
I don't know what I'm doing wrong
Anyone that can help me?
My function:
const returnProjects = async () => {
const list = document.querySelector<HTMLDivElement>('#projectList');
const projects = query(collectionGroup(db, 'projects'));
const querySnapshot = await getDocs(projects);
querySnapshot.forEach((doc) => {
const deadline = doc.data().Deadline;
const fireBaseTime = new Date(
deadline.seconds * 1000 + deadline.nanoseconds / 1000000,
);
const formatOptions = {
format: 'dd MMM yy',
};
console.log(doc.id, '>', doc.data());
const newElemement = document.createElement('div');
if (list) list.appendChild(newElemement).setAttribute('class', 'projectCard');
const card = document.querySelector<HTMLDivElement>('.projectCard');
if (card) { card.innerHTML = `
<h4>${doc.id, doc.data().Name}</h4>
<p>${fireBaseTime.toLocaleDateString('eng-BE', formatOptions)}</p>
<span>3</span>
`; }
});
};
my Html:
<main>
<div id="dashboard" class="dashboard">
<div class='dashboardUtils'>
<h3 id="dashboardName"></h3>
<span id="currentDate"></span>
</div>
<button id="editDashboard" class="secondary-button"></button>
</div>
<div id='dashboardEdits-form' class="editOpen">
<form id='dashboardEdits' class='edit-form'>
<div id='practicalDisplayname'>
<label for='displayname' class='form-label'>Username</label>
<input type='text' class='form-input' id="displaynameInput" name='displayname'></input>
</div>
</form>
<button id="confirmEdits" class="secondary-button">Save edits</button>
</div>
<div id='amountMessage'>
<h1 id='amountProjects'></h1>
</div>
<div id='projectList'></div>
</main>
A screenshot:

The problem is that for each document in the results you do:
const card = document.querySelector<HTMLDivElement>('.projectCard');
if (card) { card.innerHTML = `
<h4>${doc.id, doc.data().Name}</h4>
<p>${fireBaseTime.toLocaleDateString('eng-BE', formatOptions)}</p>
<span>3</span>
`; }
While you're creating a new project card for each result, the querySelector always returns the first card for the HTML. From the MDN documentation on querySelector:
An Element object representing the first element in the document that matches
So for the second document, you're replacing the innerHTML that you set for the first document.
To solve the problem, since you already have a reference to the element you just generated, add the innerHTML to that instead of looking it up with a query selector:
querySnapshot.forEach((doc) => {
...
const newElement = document.createElement('div');
if (list) list.appendChild(newElemement).setAttribute('class', 'projectCard');
newElement.innerHTML = `
<h4>${doc.id, doc.data().Name}</h4>
<p>${fireBaseTime.toLocaleDateString('eng-BE', formatOptions)}</p>
<span>3</span>
`;
})

The problem you are facing here is caused by the following behavior:
https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild#:~:text=The%20appendChild()%20method%20of%20the%20Node%20interface%20adds%20a%20node%20to%20the%20end%20of%20the%20list%20of%20children%20of%20a%20specified%20parent%20node.%20If%20the%20given%20child%20is%20a%20reference%20to%20an%20existing%20node%20in%20the%20document%2C%20appendChild()%20moves%20it%20from%20its%20current%20position%20to%20the%20new%20position.
What this means is that appendChild will remove the node if already present in the DOM, which is what we are seeing here. This can be easily solved by creating a deep clone of the node first using cloneNode, before appending it to the target div.

Related

InnerHTML not displaying the content in string

I am trying to display two buttons with .innerHTML but it doesn't display the content put in the string.
The console is not showing any error and I've checked for typos but I haven't found anything.
Here's my HTML :
<div class="buttons">
<div class="buttons_inner" id="buttonShiny"></div>
</div>
And here's my JS :
const shiny = document.getElementById('buttonShiny');
const displayButtonShiny = (pokemon) => {
const shinyHTMLString = `
<button class="buttons_shiny" onclick="document.getElementById('pokemonSprite').src='https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/${pokemon.id}.png'">Shiny</button>
<button class="buttons_normal" onclick="document.getElementById('pokemonSprite').src='https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${pokemon.id}.png'">Normal</button>
`;
shiny.innerHTML = shinyHTMLString;
};
The content in the shinyHTMLString should be displayed in the #buttonShiny div but it doesn't work
The script tag to call the JS file is right before the closing body tag
Yes you declare the function but never use it. Add only displayButtonShiny("x") to your js code.
const shiny = document.getElementById('buttonShiny');
const displayButtonShiny = (pokemon) => {
const shinyHTMLString = `
<button class="buttons_shiny" onclick="document.getElementById('pokemonSprite').src='https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/${pokemon.id}.png'">Shiny</button>
<button class="buttons_normal" onclick="document.getElementById('pokemonSprite').src='https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${pokemon.id}.png'">Normal</button>
`;
shiny.innerHTML = shinyHTMLString;
};
displayButtonShiny("x"); // x = your pokemon
<div class="buttons">
<div class="buttons_inner" id="buttonShiny">btn1</div>
</div>

Apendchild only apending once in ForEach method

function addOrgsToCard(Data) {
const orgs = document.getElementById("orgs");
const org = document.createElement("div");
org.classList.add("org");
Data.forEach((organisation)=>{
org.innerHTML = `
<div class="org">
<img src="${organisation.avatar_url}" alt="" />
<span>${organisation.login}</span>
</div>
`;
console.log(organisation.login);
orgs.appendChild(org);
});
}
This is the function to add organization details in a div of a profile card but the problem is apendchild method is appending only one organisation detail (the last one from the API call) ,Can anyone help me solve this issue?
org.innerHTML = overwrites the previous value.
So every time you loop over the data the previous elements get removed.
You should move the creation of your org element inside the forEach loop:
function addOrgsToCard(Data) {
const orgs = document.getElementById("orgs");
Data.forEach((organisation)=>{
const org = document.createElement("div");
org.classList.add("org");
org.innerHTML = `
<div class="org">
<img src="${organisation.avatar_url}" alt="" />
<span>${organisation.login}</span>
</div>
`;
console.log(organisation.login);
orgs.appendChild(org);
});
}
Try using only "append"(I am not sure if this works).

Firebase Paginate

I made the code below referring to the pagination document of FIREBASE.
( https://firebase.google.com/docs/firestore/query-data/query-cursors#web-v8_3 )
I know that 'limit(3)' prints 3 documents, but I don't know how to use the 'next' and 'last' variables.
What I want to implement is to show three of my posts per page and move to the next page when the button is pressed.
Since I just started web development, everything is difficult. please help me
var first = db.collection('product').orderBy('date').limit(3);
var paginate = first.get().then((snapshot)=>{
snapshot.forEach((doc) => {
console.log(doc.id);
var summary = doc.data().content.slice(0,50);
var template = `<div class="product">
<div class="thumbnail" style="background-image: url('${doc.data().image}')"></div>
<div class="flex-grow-1 p-4">
<h5 class="title">${doc.data().title}</h5>
<p class="date">${doc.data().date.toDate()}</p>
<p class = "summary">${summary}</p>
</div>
</div>
<hr>`;
$('.container').append(template);
})
You can try this function:
let lastDocSnap = null
async function getNextPage(lastDoc) {
let ref = db.collection('product').orderBy('date')
// Check if there is previos snapshot available
if (lastDoc) ref = ref.startAfter(lastDoc)
const snapshot = await ref.limit(3).get()
// Set new last snapshot
lastDocSnapshot = snapshot.docs[snapshot.size - 1]
// return data
return snapshot.docs.map(d => d.data())
}
While calling this function, pass the lastDocSnap as a param:
getNextPage().then((docs) => {
docs.forEach((doc) => {
// doc itself is the data of document here
// Hence .data() is removed from original code
console.log(doc.id);
var summary = doc.content.slice(0,50);
var template = `<div class="product">
<div class="thumbnail" style="background-image: url('${doc.image}')"></div>
<div class="flex-grow-1 p-4">
<h5 class="title">${doc.title}</h5>
<p class="date">${doc.date.toDate()}</p>
<p class = "summary">${summary}</p>
</div>
</div>
<hr>`;
$('.container').append(template);
})
})
Call this function at page load (as lastDocSnap will be null, it'll fetch first 3 docs). Call the same function when user clicks 'next' but this time as we have lastDocSnap, the startAfter method will be added to the query. This essentially means the query will first order the document by date and then fetch 3 documents after the document you pass in startAfter

Problems getting data from Firestore to display in DOM

I'm creating a project with the use of Firebase and I'm having some issues with getting the data inside of my database to display in the DOM due to error "setupFoodGroup is not defined"...This is likely due to a rookie coding error, but I can't seem to work out where I've gone wrong.
Current code as per below:
// Get data from database
db.collection('foodgroups').get().then(snapshot => {
setupFoodGroup(snapshot.docs);
});
// Setting Up Food Lists
const foodList = document.querySelector('.foodGroups');
const setupFoodGroup = (data) => {
let html = '';
data.forEach(doc => {
const group = doc.data();
const li = `
<li>
<div class="card bg-light mb-3" style="max-width: 20rem;">
<div class="card-body">
<h4 class="card-title">${group.title}</h4>
<p class="card-text">${group.content}</p>
</>
</div>
</li>
`;
html += li
})
foodGroups.innerHTML = html;
};
I'm then wanting to get them displaying inside the following HTML on a display page
<div class="card-body">
<ul class="foodGroups">
</ul>
</div>
Any points would be very much appreciated.
Cheers!

Get id of an element from template strings/literals

I am trying to get the button below with addEventListener. How ever it returns null. The html is rendered from js using template string. So what I am trying to achieve is to addEventListener to delete button inside the template string.
// This is the template string
data.forEach(doc => {
checkin = doc.data();
card = `
<div class="carousel-item fieldId" data-id="${doc.id}">
<div class="col-12">
<div class="card checkCard" style="margin: 0 auto;">
<img src="${checkin.photo}" class="card-img-top" alt="...">
<button type="submit" class="btn center delete">
<i class="material-icons" style="font-size: 1em;">delete_forever</i>
</button>
<div class="card-body">
<h5 class="card-title">${checkin.title}</h5>
<p class="card-text">${checkin.description}</p>
</div>
</div>
</div>
</div>
`;
html += card;
});
checkinList.innerHTML = html;
//This is the delete button
const deleteContent = document.querySelector('.delete');
deleteContent.addEventListener('click', (e) => {
// get current document ID
console.log('hmm')
e.stopPropagation();
let id = $('.carousel-item').attr('data-id')
db.collection("checkin").doc(id).delete().then(() => {
console.log(id + "successfully deleted!");
$('.carousel-item').attr('data-id')
}).catch(function (error) {
console.error("Error removing document: ", error);
});
});
And this is the erro from the console
Uncaught TypeError: Cannot read property 'addEventListener' of null
at index.js:123
(anonymous) # index.js:123
Hopefully I've outlined all the changes I made to get this to work:
As was pointed out in comments, IDs must be unique. Classes are generally better to use as JavaScript (and CSS) hooks. Many people now use a js- prefix for these to help follow the logic from HTML -> JS when maintaining code so I would suggest this.
document.querySelector('.delete') will only get a single element - you need querySelectorAll here, since you will have a delete button for each item.
$('.carousel-item') is (I'm assuming) a jQuery function call. This will get all .carousel-item elements in the document and .attr('data-id') will get the attribute of only the first one. If you wanted to use jQuery here, you would want to go up the DOM from the button element like $(e.target).parent('.carousel-item'). But, since the other code isn't using jQuery, it would be more consistent to use e.target.closest('.js-carousel-item'), imo. Then, to get data-id, we can easily use element.dataset.id.
Don't use globals for this like sao suggested
I'm not sure if data in your example came from a call to db.collection('checkin').get(), but if it was a Promise in your code, instead of the snapshot itself, you would run into problems if your delete button code wasn't nested in the then() callback.
This is more of an optional side-note, but your code could become more readable by refactoring it to use async/await.
I've included a working snippet based on your example below to demonstrate:
;(() => {
// My attempt at a quick mock of Firebase to make this work as a snippet
const db = {
_collections: {
'checkin': [
{
id: 1,
photo: 'https://images.unsplash.com/photo-1508138221679-760a23a2285b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1267&q=80',
title: 'airplane w/ trees',
description: 'airplane on ground surrounded with trees',
}, {
id: 2,
photo: 'https://images.unsplash.com/photo-1485550409059-9afb054cada4?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=701&q=80',
title: 'head to head',
description: 'minifigure head lot',
},
],
},
collection(key) {
const c = this._collections[key];
const doc = (id) => ({
id,
data() {
return c.find(o => o.id === id);
},
async delete() {
const idx = c.findIndex(o => o.id === id);
c.splice(idx, 1);
},
});
return {
doc,
async get() {
return c.map(o => o.id).map(doc);
},
};
},
}
const render = () => {
db.collection('checkin').get().then(snapshot => {
const cards = snapshot.map(doc => {
const checkin = doc.data();
return `
<div class="js-carousel-item carousel-item fieldId" data-id="${doc.id}">
<div class="col-12">
<div class="card checkCard" style="margin: 0 auto;">
<img src="${checkin.photo}" class="card-img-top" alt="...">
<button class="js-delete" type="submit" class="btn center">
<i class="material-icons" style="font-size: 1em;">delete_forever</i>
</button>
<div class="card-body">
<h5 class="card-title">${checkin.title}</h5>
<p class="card-text">${checkin.description}</p>
</div>
</div>
</div>
</div>`;
});
document.querySelector('.js-checkin-list').innerHTML = cards.join('');
// This is the delete button
const deleteButtons = document.querySelectorAll('.js-delete');
const deleteHandler = (e) => {
e.stopPropagation();
const el = e.target.closest('.js-carousel-item');
const id = +el.dataset.id;
db.collection('checkin').doc(id).delete().then(() => {
console.log(`${id} successfully deleted!`);
}).catch((error) => {
console.error('Error removing document: ', error);
})
render();
}
deleteButtons.forEach(
btn => btn.addEventListener('click', deleteHandler)
);
});
};
render();
})();
<div class="js-checkin-list carousel"></div>
use onclick inline in your string
for example
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="parentDiv"></div>
</body>
<script>
let element = `<div id="childDiv" onclick="myFunction()">click here</div>`;
document.getElementById("parentDiv").innerHTML = element;
function myFunction() {
console.log("works every time");
}
</script>
</html>
now the child div gets inserted into the parent and is has an onclick event listener
if you want it to loop, don't loop the addEventListener in JS, loop it in the template literal, in other words, just add this in your string not an extra function.
i just tested it and it works..every...time
have fun!

Categories

Resources