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.
Related
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 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.
So in vanilla JS I made a class with a function that is suppose to add elements to the document but after those elements are added I also wanted to add a function that will remove selected elements. My problem is that when I try to query select the newly added elements it returns an empty array or node. can anyone give me a reason why and how to fix? Another side not if I got to the developer console I can select the new elements without a problem
class Stock_List {
constructor() {
// fetching the json file and building
this.getData();
}
/**
* this will read the json file, get the data needed
* and then build up the initial list on startup that was
* saved in the json file
*/
async getData() {
const response = await fetch('../stocks.json');
const data = await response.json();
for (const stock of data.stock_info) {
this.add_stock(stock);
}
}
/**
* this will add a stock to the front end
* #param {*} jsObject - A JavaScript Object of stock info
*/
add_stock(jsObject) {
let big_container = document.querySelector('.background');
// this statement fixes the background once a stock is added
big_container.style.position = 'sticky';
let stock_container = document.createElement('div');
stock_container.className = 'stock_container';
stock_container.id = jsObject['stock_ticker'];
// stock header being built
stock_container.innerHTML = `
<div class="stock_header">
<h2 class="stock_ticker">${jsObject['stock_ticker']}</h2>
<h2 class="price">${jsObject['price']}</h2>
<h2 class="percent_change">${jsObject['percent_change']}</h2>
<button>
<div class="line"></div>
</button>
</div>`;
// articles being built
for (let i = 0; i < jsObject['headers'].length; i++) {
stock_container.innerHTML += `
<div class="articles">
<h3>${jsObject['headers'][i]}</h3>
<p>
${jsObject['articles'][i]}
</p>`;
}
//closing off the div of the stock container
stock_container.innerHTML += `
</div>`;
big_container.appendChild(stock_container);
}
/*
removes a stock from the front end
and calls a function to remove it from the back end
*/
remove_stock(ticker) {
let removed_stock = document.querySelector(`#${ticker}`);
console.log(removed_stock);
}
}
You can fix this using event delegation. You need to add an event listener to the parent (in your case, big_container).
big_container.addEventListener('click', function(e) {
// Guard clause
if (!e.target.classList.contains('classname-of-the-ticker')) return;
clickedItem = e.target;
clickedItem.remove();
})
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'.
I'm rebuilding a todo list app and trying to work with Object Oriented Programming, which is new to me. I've got the task section built, but I am getting stuck on locating the "delete" buttons. When adding a new task, a font awesome icons show to the right. I'm trying to select them, but I am getting an empty nodelist each time the function runs:
Codepen: To Do List
To reproduce:
Add a task, and check the console. You'll see an empty nodelist.
What I've tried:
Right now, I'm trying to simply console.log the element. I'm running console.log(buttons) Each time the addTask() method runs.
Here's the full JS:
const submit = document.querySelector("#commit-task"),
results = document.querySelector("#task-results"),
input = document.querySelector("#input-task"),
buttons = document.querySelectorAll(".fa-times"); // These are what I'm trying to select
class Task {
constructor(task) {
this.taskText = task;
}
addTask() {
const text = input.value;
ui.clearInput();
const taskBody = `<div class="task">
<span>${text}</span>
<span>
<i class="fas fa-check" style="color: green;"></i>
<i class="fas fa-times" style="color: red;"></I> //This is the element I'm trying to select
</span>
</div>`;
results.innerHTML += taskBody;
console.log(buttons); //Here's where the Console.log statement is run
}
}
class UI {
clearInput() {
input.value = "";
input.focus();
}
}
const newTask = new Task();
const ui = new UI();
// Add Event Listeners:
submit.addEventListener("click", () => {
newTask.addTask(); //Here is when addTask() is run.
});
input.addEventListener("keyup", (e) => {
if (e.keyCode === 13) {
newTask.addTask();
}
});
Why does JavaScript think these buttons are not in the DOM? Thanks in advance.
document.querySelectorAll(".fa-times"); gets executed during the first assignment and as there are no icons during the time of initialization, buttons are equal to an empty NodeList.
In order check the current status you need to re run the query.
Just declare buttons as let buttons = document.querySelectorAll(".fa-times");
Then re-run the query and assign it's latest result to your buttons variable before logging it:
buttons = document.querySelectorAll(".fa-times");
console.log(buttons);