I was trying to test a few novice tricks from a project tutorial. Wanted to create a small scale task app and ran into a weird problem. The last document.addEventListener below should theoretically call the closest element with the class name of ".name" should be detected since its in the same parent div with the button. However it is returning NULL. Am I applying the .closest() method wrong?
The event listener detects the button after everytime a task is created. Not sure why it returns NULL when, after creating the task via addTaskButton, the task with the class name of ".name". I even tried to create a data attribute id based off of the taskName itself to see if it'll detect, but still NULL / erroring.
const list = []
const container = document.querySelector('.container');
const itemContainer = document.querySelector('.item');
const addTaskButton = document.querySelector('.add-task');
const taskInput = document.querySelector('#task-name');
function renderTasks(){
itemContainer.innerHTML = ''
list.forEach(task => {
const itemElement = document.createElement('div')
itemElement.innerHTML = `
<div class="name">
${task.taskName}
</div>
<button class="retrieval">Retrieve ID</button>
`
itemElement.dataset.itemName = task.taskName
itemContainer.appendChild(itemElement);
})
}
addTaskButton.addEventListener('click', (e)=>{
e.preventDefault();
list.push({ taskName: taskInput.value})
renderTasks()
})
document.addEventListener('click', (e)=>{
if(e.target.matches('.retrieval')){
const taskName = e.target.closest('.name');
console.log(taskName)
}
})
Ok, I double checked the mdn article it says:
closestElement is the Element which is the closest ancestor of the
selected element. It may be null.
That means it only looks for parents and parents of parents and so on, not 'siblings'.
Related
I'm creating a library Web application that allows you to click a button that brings up a form to add a book via title, author, pages, and if you've read it or not. Each form input gets added to a "card" in the document via createElement/appendChild and also gets added to the myLibrary array via a constructor function. Here's my script:
const modal = document.getElementById("myModal");
const btn = document.getElementById("newBook");
const modalBtn = document.getElementById("modal-btn");
const title = document.getElementById("title");
const author = document.getElementById("author");
const pages = document.getElementById("pages");
const haveRead = document.getElementById("have-read");
const span = document.getElementsByClassName("close");
const cards = document.getElementById("cards");
let myLibrary = [];
// Book Constructor
function Book(title, author, pages, haveRead) {
(this.title = title),
(this.author = author),
(this.pages = pages),
(this.haveRead = haveRead);
if (alreadyInLibrary(title)) {
return alert("Sorry, it looks like this book is already in your library");
}
addBooKToLibrary(this);
}
// Adds book to array
const addBooKToLibrary = function (book) {
myLibrary.push(book);
};
const book1 = new Book("Harry Potter", "J.K. Rowling", 123, "Unread");
const book2 = new Book("LotR", "J.R.R. Tolkien", 4214, "Read");
const book3 = new Book("No Country for Old Men", "Cormac McCarthy", 575, "Unread");
// Creates books for each card in the DOM
createCard = function () {
cards.innerHTML = "";
myLibrary.forEach((book) => {
let html = `<div class="card"><p>${book.title}</p><p>${book.author}</p><p>${book.pages}</p><p>${book.haveRead}</p><button class="remove-btn" onclick="deleteBook(this)">Delete</div>`;
cards.innerHTML += html;
});
};
// Checks the array for already registered books
function alreadyInLibrary(title) {
return myLibrary.some(function (el) {
return el.title === title;
});
}
modalBtn.addEventListener("click", function (event) {
event.preventDefault();
const book = new Book(title.value, author.value, pages.value, haveRead.value);
modal.style.display = "none";
createCard();
});
I've added a "Delete" button to each book's card that calls a function to remove itself from the document:
function deleteBook(el) {
const element = el;
element.parentNode.remove();
}
However, the book stays in the array even after the card is deleted, and I can't figure out how to implement a function that deletes the object from the array if it's not found in the document.
I've tried adding a unique ID to each book object in the myLibrary array to target the object with to delete it from the array, but couldn't get that to work. I've tried looping through the array and using an if statement to see if myLibrary.title === book.title, else remove it from the array, but that's not working either.
Here's a working snippet.
Notes
It is considered good practice to separate your JS and HTML, here that means removing the onclick()s in your HTML, and replacing them with addEventListeners in your JS.
When a button is clicked, we need to identify the book it represents. You are already using title to uniquely identify a book in alreadyInLibrary(), so we'll use that. Let's add a class to the p that displays the title so we can do that: <p class='title'>...</p>. Now we can search with .getElementsByClassName('title') to get the p, and here's how to get the text of an element.
But how to find the title of the specific button that was clicked? We need to find the parent card, and then the title inside that. There are a few options:
If we start from the button that was clicked, we can find the closest parent .card, and then find the .title on that card. Here's an example of how to find the parent: Find the closest ancestor element that has a specific class, and here's an example of how to find our title element inside the parent card: Get element inside element by class and ID - JavaScript
Alternatively, we can add a click handler to the card, instead of the button. Inside an event handler this refers to the element which the event happened to, so in this case that would be the whole card. So we can search the clicked card form the title using the same example linked above: Get element inside element by class and ID - JavaScript.
I've gone with the 2nd option in the code below, but either are fine.
let myLibrary = [];
const cards = document.querySelectorAll(".card");
// Book Constructor
function Book(title, author, pages, haveRead) {
(this.title = title),
(this.author = author),
(this.pages = pages),
(this.haveRead = haveRead);
addBooKToLibrary(this);
}
// Adds book to array
const addBooKToLibrary = function (book) {
myLibrary.push(book);
};
const book1 = new Book("Harry Potter", "J.K. Rowling", 123, "Unread");
const book2 = new Book("LotR", "J.R.R. Tolkien", 4214, "Read");
const book3 = new Book("No Country for Old Men", "Cormac McCarthy", 575, "Unread");
// We want to add an event handler for each card. cards is a nodelist,
// we need an array to iterate over:
// https://stackoverflow.com/questions/12330086/how-to-loop-through-selected-elements-with-document-queryselectorall
Array.from(cards).forEach(function (card) {
// Add event handler for each card
card.addEventListener("click", function (event) {
// Since the handler is for the card, we need to ignore clicks
// everywhere except directly on buttons:
// https://stackoverflow.com/questions/49680484/how-to-add-one-event-listener-for-all-buttons
if (event.target.nodeName !== 'BUTTON') {
return;
}
// Find the title of the book being deleted by searching inside
// the card that registered this click
// https://stackoverflow.com/questions/7815374/get-element-inside-element-by-class-and-id-javascript
// https://stackoverflow.com/questions/6743912/how-to-get-the-pure-text-without-html-element-using-javascript
let p = this.getElementsByClassName('title')[0];
let title = p.textContent;
// console.log(title);
// Find the index of array element for this book
// https://stackoverflow.com/questions/7364150/find-object-by-id-in-an-array-of-javascript-objects
let index = myLibrary.findIndex(x => x.title === title);
// Now remove this book from the array
// https://stackoverflow.com/questions/5767325/how-can-i-remove-a-specific-item-from-an-array
myLibrary.splice(index, 1);
// Just for debugging, show it really is removed from myLibrary
console.dir(myLibrary);
// And remove it from the page
this.remove();
});
});
.card {
border: 1px solid black;
}
<div id="cards">
<div class="card">
<p class='title'>Harry Potter</p>
<p>J.K. Rowling</p>
<p>123</p>
<p>Unread</p>
<button class="remove-btn">Delete</button>
</div>
<div class="card">
<p class='title'>LotR</p>
<p>J.R.R. Tolkien</p>
<p>4214</p>
<p>Read</p>
<button class="remove-btn">Delete</button>
</div>
<div class="card">
<p class='title'>No Country for Old Men</p>
<p>Cormac McCarthy</p>
<p>575</p>
<p>Unread</p>
<button class="remove-btn">Delete</button>
</div>
</div>
You can use the data- attribute to store the title and then delete the book by it.
To do this, you will need to add a data-title attribute to a card like so
let html = `<div class="card" data-title="${book.title}"><p>${book.title}</p><p>${book.author}</p><p>${book.pages}</p><p>${book.haveRead}</p><button class="remove-btn" onclick="deleteBook(this)">Delete</div>`;
and then read the data-title attribute in your delete function:
function deleteBook(el) {
// removing book by title
const bookTitle = el.getAttribute("data-title");
myLibrary = myLibrary.filter((book) => book.title !== bookTitle);
const element = el;
element.parentNode.remove();
}
Please let me know if this helps.
I have a very basic question which I just cant seem to work it out
I just want to basically create a function that will showDetails when I click in an object from a list which is called "pokemonList"
Nothing I do creates a function to what I need to do which is the object having a response after being clicked:
the part of the code in the question is:
.
`function eventListener(button, pokemon) {
button.addEventListener("click", function() {
showDetails(pokemon);
});
}
///////////this following part is incorrect, it needs to make the ''showDetails'' be responsive and ///////////show the pokemon(object) name. Please help me as I'm stuck and I've tried many things
**function showDetails(pokemon) {
eventListener("button");
console.log(pokemon);
}**
///////////////
pokemonList.forEach(function(pokemon) {
let pokemonList = document.querySelector(".pokemon-list");
let listItem = document.createElement("li");
let button = document.createElement("button");
button.innerText = pokemon.name;
button.classList.add("stylez");
listItem.appendChild(button);
pokemonList.appendChild(listItem);
});`
At the moment showDetails calls eventListener which calls showDetails which calls eventListener etc - which is bad.
So ideally you want to
Cache the list element first.
Using event delegation assign one listener to that element that can catch events from its child elements when they're fired and "bubble up" the DOM.
To minimise the adverse affects of repeatedly updating the DOM within your loop create a document fragment which you can append new elements, and which - post loop - you can append to the list element.
showDetails will handle the events from the list listener. In this example it checks to see if the child element that fired the event is a button, and then logs the text content of the button - but obviously you can change that to update a separate element etc.
// Cache the list element, and add a listener to it.
const list = document.querySelector('.pokemon-list');
list.addEventListener('click', showDetails);
const pokemonList = [
{ name: 'Bob' },
{ name: 'Ron' },
{ name: 'Sue' },
{ name: 'Jane' }
];
// Check that the child element that fired
// the event is a button, and log its text content
function showDetails(e) {
if (e.target.matches('button')) {
console.log(e.target.textContent);
}
}
// Create a document fragment
const frag = document.createDocumentFragment();
// Loop over the array and for each object
// add create a list item, and a button, append the button
// to the list item, and then the list item to the fragment
pokemonList.forEach(pokemon => {
const listItem = document.createElement('li');
const button = document.createElement('button');
button.textContent = pokemon.name;
button.classList.add('stylez');
listItem.appendChild(button);
frag.appendChild(listItem);
});
// Finally append the fragment of list items
// to the cached list element
list.appendChild(frag);
.stylez { background-color: lightgreen; }
<ul class="pokemon-list" />
I am just stuck on getting everything to work in my todo list. Where I am at the moment is that when a new todo is added I need one button to toggle that it is completed, and the other button to remove the item. It seems as though I am forgetting or not understanding the proper methodology to allow both buttons to be able to target the item. I can only get one to work and not both.
I am still new JavaScript and all this, I am trying to complete this exercise for my coding bootcamp and have been stuck on this todo list for over a week now.
Sorry if I am not formatting this correctly, first time posting here and based on how difficult this is going, I am going to be getting a lot of practice.
todoForm.addEventListener("submit", function(event) {
event.preventDefault();
let newTodos = document.createElement("li")
newTodos.innerText = document.querySelector("#add-el").value
newTodos.classList.add("item")
todoUl.appendChild(newTodos);
let completedBtn = document.createElement("button");
completedBtn.innerText = "✔️"
completedBtn.classList.add("completed-Btn");
completedBtn.type = "button"
newTodos.appendChild(completedBtn)
console.log(completedBtn)
let deleteBtn = document.createElement("button");
deleteBtn.innerText = "🗑️"
deleteBtn.classList.add("delete-Btn");
deleteBtn.type = "button"
newTodos.appendChild(deleteBtn)
console.log(deleteBtn)
newTodos.addEventListener("click", function() {
newTodos.style.textDecoration = "line-through"
})
newTodos.addEventListener("click", function() {
newTodos.removeChild()
})
//can not get the delete button to work
todoForm.reset()
console.log(newTodos)
})
Got some help from my mentor, so with the help of the first answer posted I changed the targets for the event listener after we realized that I was not targeting the parent for the removeChild, nor was I passing in an argument.
deleteBtn.addEventListener("click", function() {
console.log(deleteBtn)
todoUl.removeChild(newTodos)
})
You're just setting the event listeners on the wrong element.
newTodos.addEventListener("click", function() {
newTodos.style.textDecoration = "line-through"
})
newTodos.addEventListener("click", function() {
newTodos.removeChild()
})
You're setting both listeners on the newTodos element instead of the individual buttons, like so:
completedBtn.addEventListener("click", function() {
newTodos.style.textDecoration = "line-through"
})
deleteBtn.addEventListener("click", function() {
newTodos.remove()
})
Also, you need to call newTodos.remove() instead of newTodos.removeChild()
I am fetching a list of posts from an API and displaying them on webpage. Now, there is a Delete button associated with each post which when clicked should remove the post.
index.html
<template id="single-post">
<li class="post-item">
<h2></h2>
<p></p>
<button>DELETE</button>
</li>
</template>
<ul class="posts"></ul>
app.js
const listElement = document.querySelector('.posts');
const postTemplate = document.getElementById('single-post');
const listOfPosts = await sendHttpRequest(
'GET',
'https://jsonplaceholder.typicode.com/posts'
);
// listOfPosts is already in parsed format
for (const post of listOfPosts) {
const postEl = document.importNode(postTemplate.content, true);
postEl.querySelector('h2').textContent = post.title.toUpperCase();
postEl.querySelector('p').textContent = post.body;
listElement.append(postEl);
const btn = postEl.querySelector('button');
console.log(btn, postEl);
btn.addEventListener('click', () => {
postEl.remove();
});
}
The above code only fetches first post only and throws
Uncaught (in promise) TypeError: Cannot read properties of null (reading 'addEventListener')
at HTMLButtonElement.fetchPosts
When I remove the Event Listener, the code works fine.
I guess this is something to do with importNode method since I have done similar things with createElement and they worked fine
EDIT
I did some little experimenting. The JSON post object returned by API also consisted of an id field. So, I basically added that id to each button that was being created.
Another thing is I used event delegation to remove() the li whose button is clicked.
And very surprisingly It works
const listElement = document.querySelector('.posts');
const postTemplate = document.getElementById('single-post');
const listOfPosts = await sendHttpRequest(
'GET',
'https://jsonplaceholder.typicode.com/posts'
);
// listOfPosts is already in parsed format
for (const post of listOfPosts) {
const postEl = document.importNode(postTemplate.content, true);
postEl.querySelector('h2').textContent = post.title.toUpperCase();
postEl.querySelector('p').textContent = post.body;
postEl.querySelector('button').id = post.id; // HERE
listElement.append(postEl);
}
// delete the li element
listElement.addEventListener('click', (event) => {
if(event.target.tagName === 'BUTTON') {
console.log(event.target);
event.target.parentElement.remove();
}
})
when clicked on first list post's DELETE button, it consoles
<button id="1">DELETE</button>
and removes that item.
This bascially proves that the button tag is certainly there since we are able to query select and set its id.
Strangely, when consoled it shows null.
Your code errors out here:
btn.addEventListener('click', () => {
postEl.remove();
});
The error message clarifies that btn is null, which means that postEl.querySelector('button') returned null, which means that there is no button tag inside postEl.
You will need to carefully look at the result of document.importNode(postTemplate.content, true) and see what it contains. You will see that it does not contain a button tag. So, either the button was not added, in which case you will need to adjust importNode, or, the button is not a button tag, but something else, like <input type="button" value="foo"> for example.
I am trying to add a click event listener to a div that is dynamically generated after page load but I can't seem to get the event to register. I am following the instructions found in this answer however, it is not working for me.
In my ngOnInit() I have a combineLatest():
combineLatest([this.params$, this.user$]).subscribe(([params, user]: [Params, User]) => {
this.artistId = parseInt(params['artist']);
this.user = user;
if (this.artistId) {
this.artistProfileGQL.watch({
id: this.artistId
}).valueChanges.subscribe((response: ApolloQueryResult<ArtistProfileQuery>) => {
this.artist = response.data.artist;
this.initElements(); // WHERE I CALL TO INITIALIZE DYNAMIC DOM ELEMENTS
});
})
In this block, I call initElements() which is where I create certain DOM elements. I've included most of them below. Essentially, I have a header element, and inside this header element, I create a followBtn, that looks like this (i removed the title, followers, elements etc from the code for brevity). I added comments in caps for the most relevant lines:
initElements() {
const parentElement = this.el.nativeElement;
this.header = parentElement.querySelector('ion-header');
// Create image overlay
this.imageOverlay = this.renderer.createElement('div');
this.renderer.addClass(this.imageOverlay, 'image-overlay');
this.colorOverlay = this.renderer.createElement('div');
this.renderer.addClass(this.colorOverlay, 'color-overlay');
this.colorOverlay.appendChild(this.imageOverlay);
this.header.appendChild(this.colorOverlay);
var artistHeader = this.renderer.createElement('div');
this.renderer.addClass(artistHeader, 'artist-header');
// HERES WHERE I CREATE MY BUTTON ELEMENT
this.followBtn = this.renderer.createElement('div');
this.renderer.addClass(this.followBtn, "follow-btn");
var followText = this.renderer.createText('FOLLOW');
this.renderer.appendChild(this.followBtn, followText);
this.renderer.appendChild(artistHeader, this.followBtn);
this.renderer.appendChild(this.imageOverlay, artistHeader);
// HERES WHERE I CREATE MY LISTENER
this.followButtonListener = this.renderer.listen(this.followBtn, 'click', (event) => {
console.log(event);
});
}
However, when I click on the element, I don't get anything printed to my console. If I change the target of the listener to a DOM element, the click listener works. What am I doing wrong?