Get id of an element from template strings/literals - javascript

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!

Related

Firestore forEach returns one item

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.

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

Angular - Execute function after http request

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>

Javascript Issue with aria-hidden popup

I have this ajax code that gets a res.json(event) from the server and then creates an object based on the value received.
Here is part of that code:
html += `<div class="card-header" id="headingOne-${i}">` +
`<div class="event-time"><time class="published" datetime="2017-03-24T18:18">${data[i].events.targetReminder} | ${data[i].events.targetAmPM}</time><div class="more"> <svg class="olymp-three-dots-icon"><use xlink:href="svg-icons/sprites/icons.svg#olymp-three-dots-icon"></use> </svg><ul class="more-dropdown"><li>Mark as Completed</li> <li>Delete Event </li></ul></div></div>` +
`<h5 class="mb-0 title"><a href="" data-toggle="collapse" data-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne" >${data[i].events.title}<i class="fa fa-angle-down" aria-hidden="true"></i>` +
` <span class="event-status-icon" data-toggle="modal" data-target="#public-event"><svg class="olymp-calendar-icon" data-toggle="tooltip" data-placement="top" id ="uncomplet-${i}"data-original-title="UNCOMPLETED"><use xlink:href="svg-icons/sprites/icons.svg#olymp-calendar-icon"></use></svg></span></a></h5></div>` +
`<div id="collapseOne" class="collapse show" aria-labelledby="headingOne" data-parent="#headingOne-${i}"><div class="card-body" id="${data[i].events._id}" onClick="reply_click()">${data[i].events.caption}</div><div class="place inline-items"><svg class="olymp-add-a-place-icon"><use xlink:href="svg-icons/sprites/icons.svg#olymp-add-a-place-icon"></use></svg><span>${data[i].events.location}</span></div></div></div>`;
And here is the output of the code:
The aria-hidden that I'm talking about is this one
<div class="modal fade" id="public-event" tabindex="-1" role="dialog" aria-labelledby="public-event" aria-hidden="true">
What I have tried and already know it's not a proper solution is here
Code test
I created a script that change aria-hidden => true or false but I don't know how to link it with my box
<script type="text/javascript">
function reply_click()
{
document.getElementById('public-event').setAttribute('aria-hidden', 'false');
}
</script>
another failed try:
<script type="text/javascript">
function reply_click()
{
alert('this function is called')
$(`#mark-${i}`).onclick = function() {
document.getElementById('public-event').setAttribute('aria-hidden', 'false');
};
}
</script>
I also added in the HTML this function onClick="reply_click()" but nothing is happening. I only get the alert('this function is called')
Could you suggest me an idea, please?
By looking at your question and discussion on the comment section I think that you are trying to add click event on your dynamically generated div section(html part) and open the pop up modal.
We can achieve that by using $('#id' OR '.class').modal('show') in jquery.
So why don't you add a class any where inside your div section of html and bind a click function using jquery. Suppose you have added a class name showModal on the very first div after card-header like, div="card-header showModal"
$('#eventCard').on('click', '.showModal', function(){
$('#public-event').modal('show');
});
We cannot directly use $('.showModal').click...... because document structure is change after appending the html section after #eventCard which was initially was not present. Hpe this works.
could this be your problem ? you create an item dynamically with javascript, but the item you want to select, click event, is not actually created at that time. Once the item you want to export is created, you can select it and want to make the changes you want.
udpated: I added a snippet of what I meant. in order for me to select the H1 tag that occurs after I click on the button, I have to write a function that will occur after it occurs.
const button = document.querySelector(".clickme");
const container = document.querySelector(".container");
button.addEventListener("click", () => {
container.innerHTML += `
<h1 class = "change-modal"> ı cant select this</h1>
<div class="modal fade" id="public-event" tabindex="-1" role="dialog" aria-labelledby="public-event"
aria-hidden="true"> `
})
const changeModal = document.querySelector(".change-modal")
console.log(changeModal)
<button class="clickme">Click me</button>
<div class="container" style="background-color: red;">
</div>
<h1 class="change-modal">ı select this</h1>
updated2 :
for (let i = 0; i < data.length; i++) {
html += // your html
$("eventCard").append(html);
//after this, you can select the item and type the function because the items are created here.
}
Updated 3!!! : I explained how to do things with javascript without using jquery. Hopefully you know what I mean.
async getData() { // fetch operations using javascript
const data = await fetch(url); // your api url
const jsonToData = await data.json(); // here you can Request api and obtain the data
return jsonToData;
}
getData().then((data) => {
console.log(data) // ıts probably an array.
for (let data = 0; data < jsonToData.length; data++) {
// data operations, what if you want to
html += // you printed document items,
}
})
.then(() => {
//!!! IMPORTANT!!! this is where you need to perform the operation of selecting the element function. you can also write a function that can work for the code here, but I've written it one by one for now.
const clickedElement = //type whatever element you want to click on. !!
clickedElement.addEventListener("click", function() {
const elementToChange = document.getElementById(".public-event");
elementToChange.setAttribute('aria-hidden', 'false');
})
})
.catch(err => console.log(err))

I wanna attach a tooltip to ngx-newsticker events array "for each item"

I have a news bar and I used ngx-newsticker, I need to attach a tooltip for each item in the news bar. However, the plugin takes an array of string as shown below.
I tried to loop throw the array but it doesn't work:
<div class="M_announcementDiv" *ngIf="announcementsArrInTicker && announcementsArrInTicker.length>0">
<div class="container">
<div placement="bottom" [ngbTooltip]="announcementsArrInTicker">
<ngx-newsticker title="{{'ANNOUNCEMENTS'|translate}}" [events]="announcementsArrInTicker" [interval]="interval"></ngx-newsticker>
</div>
</div>
</div>
I need to push the current item to the tooltip in this line instead of pushing
the whole array.
<div placement="bottom" [ngbTooltip]="announcementsArrInTicker">
and this is the ts function that retrieves the data:
getGeneralAnnouncements() {
// this.isAuthenticated = this.authService.isAuthenticated();
this.isAuthenticated =false;
this.newsService.searchGeneralAnnouncements(this.searchModel, this.isAuthenticated).subscribe(res => {
this.announcementArr = res;
res.announcements.map((announcement) => {
if(this.translate.currentLang=="ar"){
this.announcementsArrInTicker.push(announcement.bodyAr);
}else{
this.announcementsArrInTicker.push(announcement.bodyEn);
}
});
}, error => {
// this.businessException = errorsUtility.getBusinessException(error); //this.notificationService.showNotification(this.businessException.message,
NotificationType.Error);
})
}

Categories

Resources