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
Related
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.
I have had trouble with a movie watch list project where i have two main pages (movie search page and watch list page) and one JS file they both share. I would like to be able to save a movie result after having searched for a movie on the first HTML page, and have it save to local storage and appear on the second HTML page.
I was able to create a watch list that lives on the same page as the movie search page and save selections temporarily, but when I refresh they disappeared. When I have tried saving to a separate page, I am unable to first fetch the ID where I want to insert the HTML data. I kind of understand this is because I am only able to load one page at at time, yet I have been unable to figure out what to do about it. I have also tried to use preventDefault to stop my second HTML page from refreshing in order to see if my data is saving, but i have been unsuccessful in this as well.
Here is my java script document which both pages share
function run(){
let ID
let movArr
document.querySelector("#search").addEventListener('click',async function(){
const movSearch = document.querySelector("#movieSearch").value
const res = await fetch(`https://www.omdbapi.com/?s=${movSearch}&type=Movie&apikey=3c746e2`)
const data = await res.json()
movArr = data.Search
html=""
for(let movie of movArr)
{
const rez = await fetch(`https://www.omdbapi.com/?i=${movie.imdbID}&type=Movie&apikey=3c746e2`)
const datatwo = await rez.json()
ID = datatwo.imdbID
html +=
` <div class="movieInput" style="display:flex;align-items:center">
<div>
<img src="${movie.Poster}" style="width:99px;margin:42px 0 0 44px">
</div>
<div></div>
<div style="margin:3px 0 0 21px">
<text style="font-weight:bold; font-size:18px">${movie.Title}</text>
<p>${datatwo.imdbRating}</p>
<div style="display:flex; gap:36px; font-size:12px">
<p>${datatwo.Runtime}</p> <p>${datatwo.Genre}</p> <p><i class="fa-solid fa-circle-plus" data-add="${datatwo.imdbID}"></i></p>
</div>
<div style="margin:-20px 0 0 0">
<p>${datatwo.Plot}</p>
</div>
</div>
</div>
<hr />`}
document.querySelector("#movies").innerHTML = html
}) }
function watchlist(){
localStorage.setItem( 'film' , JSON.stringify(html) )
document.querySelector('#moviesToo').innerHTML = JSON.parse(localStorage.getItem('film'))
}
function watch(){
let html
document.addEventListener('click', async function(e){
if(e.target.dataset.add){
console.log('clicked!')
const movie = e.target.dataset.add
const dez = await fetch(`https://www.omdbapi.com/?i=${movie}&type=Movie&apikey=3c746e2`)
const datathree = await dez.json()
ID = datathree.imdbID
html =
`
<div class="movieInput" style="display:flex;align-items:center">
<div>
<img src="${datathree.Poster}" style="width:99px;margin:42px 0 0 44px">
</div>
<div></div>
<div style="margin:3px 0 0 21px">
<text style="font-weight:bold; font-size:18px">${datathree.Title}</text>
<p>${datathree.imdbRating}</p>
<div style="display:flex; gap:36px; font-size:12px">
<p>${datathree.Runtime}</p> <p>${datathree.Genre}</p> <p><i class="fa-solid fa-circle-minus" data-add="${datathree.imdbID}"></i></p>
</div>
<div style="margin:-20px 0 0 0">
<p>${datathree.Plot}</p>
</div>
</div>
</div>
<hr />`}
return watchlist()
})
}
run()
watch()
hope you're okey.
I have a blog component which obtains posts from a DB via http request.
So, what i'm trying is, when the posts are in my component i want to filter them with a function.
I'm calling this function after get the posts in the subscribe method, but it doesn't works.
Function to filter the posts:
filtrarArticles(tipus)
{
var articles = document.getElementsByClassName("article") as HTMLCollectionOf<HTMLElement>;
for (var i = 0; i < articles.length; i++)
{
var tipusArticle = this.articles[i].categoria;
if (tipus == tipusArticle)
{
articles[i].style.display = "";
}
else
{
articles[i].style.display = "none";
}
}
}
HTML code that filters my function:
<div class="articles row w-100 align-self-end d-flex justify-content-center">
<div class="article col-xl-3 mb-md-0 mb-3" *ngFor="let article of articles; index as i" (click)="obrirModalArticle(article)">
<img class="imatgeAllargada d-block w-100" src="https://drive.google.com/uc?id={{article.img}}">
<br>
<h4>{{article.title_article}}</h4>
<p [innerHTML]="article.descripcio1"></p>
</div>
</div>
The articles array have one property which name is "categoria" an it's the key to filter it.
I call the filter function here:
this.articleService.obtenirArticles(this.idioma).subscribe(
res=>
{
this.articles = res;
this.filtrarArticles('generals');
},
error =>
{
alert("No s'han pogut obtenir els articles, intenta-ho més tard.");
});
this.translate.onLangChange.subscribe((event: LangChangeEvent) =>
{
this.idioma = event.lang;
this.articleService.obtenirArticles(this.idioma).subscribe(
res=>
{
this.articles = res;
},
error =>
{
alert("No s'han pogut obtenir els articles, intenta-ho més tard.");
}
)
});
This code is on init method. And I don't know why doesn't works?
It is considered a bad practice to access the DOM directly, one of the reasons being that you should not manipulate the template directly but rather let Angular do it for you. So what you should be doing instead is, create a model and let Angular know about it, bind it to the template and once it changes Angular will update the template for you.
Component
export class YourComponent {
allArticles; // Property to hold the result of the response
filteredArticles; // Property to hold what is going to be rendered on the template
...
ngOnInit() {
this.articleService.obtenirArticles(this.idioma).subscribe(res => {
this.allArticles = res; // Save the original response
this.filteredArticles = this.filtrarArticles('generals'); // Get the articles filtered and show this property on the template
},
error => {
alert("No s'han pogut obtenir els articles, intenta-ho més tard.");
});
this.translate.onLangChange.subscribe((event: LangChangeEvent) => {
this.idioma = event.lang;
this.articleService.obtenirArticles(this.idioma).subscribe(res => {
this.allArticles = res; // Save the original response
this.filteredArticles = [...this.allArticles]; // Clone the response array unfiltered
},
error => {
alert("No s'han pogut obtenir els articles, intenta-ho més tard.");
})
});
}
}
Filter function
filtrarArticles(tipus) {
// The filter method will return a copy of the array once it has been iterated over each item.
// If the condition is met, the item is going to be included in the resulting array, otherwise it won't.
return this.allArticles.filter(article => article.categoria === tipus);
}
Then in your template, you iterate over the filterArticles array, once that array change, Angular will update the template.
Template
<div class="articles row w-100 align-self-end d-flex justify-content-center">
<div class="article col-xl-3 mb-md-0 mb-3" *ngFor="let article of filteredArticles; index as i" (click)="obrirModalArticle(article)">
<img class="imatgeAllargada d-block w-100" src="https://drive.google.com/uc?id={{article.img}}">
<br>
<h4>{{article.title_article}}</h4>
<p [innerHTML]="article.descripcio1"></p>
</div>
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!
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!