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();
})
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.
this is written in JS
i cant seem to make the MovieDetails button work at all.
function searchMovie(query) {
const url = `https://imdb8.p.rapidapi.com/auto-complete?q=${query}`;
fetch(url, options)
.then(response => response.json())
.then(data => {
const list = data.d;
list.map((item) => { //makes a list of each individual movie from the data
const name = item.l; // holds the name of movie
const poster = item.i.imageUrl; // holds the poster, given by the data
const detail = item.id // holds the ttid of the movie
// below is what shows the poster, movie name, etc
const movie =
`
<body>
<div class="colmd3">
<div class = "well text-center">
<li><img src="${poster}">
<h2>${name}</h2>
</li>
<button type = "button" id = "MovieDetails" class="btn btn-primary" href="#">Movie Details</button>
<script>
document.getElementById('MovieDetails').addEventListener("click",myFunction);
function myFunction(){
console.log(detail)
}
</script>
</div>
</div>
</body>
`;
document.querySelector('.movies').innerHTML += movie; // returns the first element movies and poster to movie div
//console.log()
});
document.getElementById("errorMessage").innerHTML = "";
})
.catch((error) => {
document.getElementById("errorMessage").innerHTML = error;
});
// we should make a condition here for when a new item is placed here there will be a page refresh
// setTimeout(() => {
// location.reload(); }, 2000);
}
the function above will make an api call and then save the results into list, i can hold specific elements of the list in the three const's and const movie will output the movie poster, name and display it.
I want to make a button for each movie that when clicked will output the id of the movie which is held in const details.
But i cant figure out how to make it work, i have tried (button onclick = function ..) and (document.getElementById...) but it says that getElementById cant be null.
i know that this seems like a silly problem but i cant seem to figure how to make the button actually output something useful or any other way to make a button be mapped out to each api call.
You're heading in the right direction but there are a couple of pain-points with your code as the other commenters have indicated.
Your template string is adding a brand new body element to the page for each movie where there should just be one for the whole document. Nice idea to use a template string though - by far the simplest method to get new HTML on to the page.
Adding JS to the page dynamically like that is going to end up causing you all kinds of problems - probably too many to mention here, so I'll just skip to the good part.
First remove the body element from the template string, and perhaps tidy up the remaining HTML to be a little more semantic. I've used section here but, really, anything other than having lots of divs is a step in the right direction.
Second: event delegation. Element events "bubble up" the DOM. Instead of attaching a listener to every button we can add one listener to the movie list containing element, and have that catch and process events from its children.
(Note: in this example, instead of logging the details to the console, I'm adding the details to the HTML, and then allowing the button to toggle the element on/off.)
// Cache the movie list element, and attach a listener to it
const movieList = document.querySelector('.movielist');
movieList.addEventListener('click', handleClick);
// Demo data
const data=[{name:"movie1",poster:"img1",details:"Details for movie1."},{name:"movie2",poster:"img2",details:"Details for movie2."},{name:"movie3",poster:"img3",details:"Details for movie3."}];
// `map` over the data to produce your HTML using a
// template string as you've done in your code (no body element)
// Make sure you `join` up the array that `map` returns into a
// whole string once the iteration is complete.
const html = data.map(obj => {
return `
<section class="movie">
<header>${obj.name}</header>
<section class="details">${obj.details}</section>
<button type="button">Movie Details</button>
</section>
`;
}).join('');
// Insert that HTML on to the movie list element
movieList.insertAdjacentHTML('beforeend', html);
// This is the handler for the listener attached to the
// movie list. When that element detects an event from a button
// it finds button's previous element sibling (the section
// with the `.details` class), and, in this case, toggles a show
// class on/off
function handleClick(e) {
if (e.target.matches('button')) {
const details = e.target.previousElementSibling;
details.classList.toggle('show');
}
}
.movie { border: 1px solid #555; padding: 0.5em;}
.movie header { text-transform: uppercase; background-color: #efefef; }
.details { display: none; }
.show { display: block; }
button { margin-top: 0.5em; }
<section class="movielist"></section>
Additional documentation
insertAdjacentHTML
matches
classList
toggle
I'm trying to get the Angular Drag and Drop tabs working with the SyncFusion component library. I have tried everything I can from the documentation but they still don't reorder.
Here's my HTML:
<div id="tab-container">
<div class="col-lg-8 content-wrapper control-section">
<ejs-tab id='draggableTab' #tabObj [allowDragAndDrop]='allowDragAndDrop' dragArea='#tab-container'
(created)='onTabCreate()' (onDragStart)='onTabDragStart($event)' (dragged)='onDraggedTab($event)'>
<e-tabitems>
<e-tabitem *ngFor="let item of headerText" [header]="item" [content]="contentTemplate"></e-tabitem>
</e-tabitems>
</ejs-tab>
</div>
</div>
And here are the relevant parts of the TypeScript file:
onTabCreate(): void {
const tabElement = document.getElementById("#draggableTab");
if (!isNullOrUndefined(tabElement)) {
tabElement.querySelector(".e-tab-header").classList.add("e-droppable");
tabElement.querySelector(".e-content").classList.add("tab-content");
}
}
onTabDragStart(args: DragEventArgs): void {
this.draggedItemHeader = <string> this.tabObj.items[args.index].header.text;
}
onDraggedTab(args: DragEventArgs): void {
const dragTabIndex: number = Array.prototype.indexOf.call(this.tabObj.element.querySelectorAll(".e-toolbar-item"), args.draggedItem);
const dropContainer: HTMLElement = <HTMLElement> args.target.closest("#TabContainer .content-wrapper .e-toolbar-item");
const dropNode: HTMLElement = <HTMLElement> args.target.closest("#TabContainer .content-wrapper .e-toolbar-item");
if (dropNode != null) {
args.cancel = true;
const dropIndex: number = Array.prototype.indexOf.call(dropContainer, dropNode);
console.log(dropIndex);
}
}
In the onDraggedTab method I am trying to get the index of where to place the tab, but can't figure it out. The dropNode is valid, and defined, but getting its index to swap it with the target (or the dragged tab) is not working.
You can get the dropped item index in dragged event using below customization. We have prepared a sample in which we have logged the dropped item index in the console which can be referred through the below link.
Sample: https://stackblitz.com/edit/angular-tab-drag-and-drop?file=app.component.ts
public onDraggedTab(args: DragEventArgs): void {
const dragTabIndex: number = Array.prototype.indexOf.call(
this.tabObj.element.querySelectorAll('.e-toolbar-item'),
args.draggedItem
);
const dropItem: HTMLElement = <HTMLElement>(
args.target.closest('#draggableTab .e-toolbar-item')
);
if (dropItem != null) {
//args.cancel = true;
let dropItemContainer: Element = args.target.closest('.e-toolbar-items');
const dropIndex: number =
dropItemContainer != null
? Array.prototype.slice
.call(dropItemContainer.querySelectorAll('.e-toolbar-item'))
.indexOf(dropItem)
: '';
console.log(dropIndex);
}
}
I have a side drawer where I'm showing the current cart products selected by the user. Initially, I have a <p> tag saying the cart is empty. However, I want to remove it if the cart has items inside. I'm using an OOP approach to design this page. See below the class I'm working with.
I tried to use an if statement to condition the <p> tag but this seems the wrong approach. Anyone has a better way to do this. See screenshot of the cart in the UI and code below:
class SideCartDrawer {
cartProducts = [];
constructor() {
this.productInCartEl = document.getElementById('item-cart-template');
}
addToCart(product) {
const updatedProducts = [...this.cartProducts];
updatedProducts.push(product);
this.cartProducts = updatedProducts;
this.renderCart();
}
renderCart() {
const cartListHook = document.getElementById('cart-items-list');
let cartEl = null;
if (this.cartProducts.length === 0) {
cartEl = '<h2>You Cart is Empty</h2>';
} else {
const productInCartElTemplate = document.importNode(
this.productInCartEl.content,
true
);
cartEl = productInCartElTemplate.querySelector('.cart-item');
for (let productInCart of this.cartProducts) {
cartEl.querySelector('h3').textContent = productInCart.productName;
cartEl.querySelector('p').textContent = `£ ${productInCart.price}`;
cartEl.querySelector('span').textContent = 1;
}
}
cartListHook.append(cartEl);
}
}
By the way, the <p> should reappear if the cart is back to empty :) !
With how your code is setup, you would want to reset the list on each render. You would do this by totally clearing out #cart-items-list. Here is a deletion method from this question
while (cartListHook.firstChild) {
cartListHook.removeChild(cartListHook.lastChild);
}
But you could use any method to delete the children of an HTML Node. To reiterate, you would put this right after getting the element by its id.
P.S. You probably want to put more code into your for loop, because it seems like it will only create cart-item element even if there are multiple items in this.cartProducts.
I have next code:
#foreach (var offer in Model.Packages)
{
#Html.Partial("Search/PackageOffer", new Primera.Site.WebUI.Models.ViewModels.Search.PackageOfferViewModel
{
Package = offer,
DisplayPricePerPerson = Model.DisplayPricePerPerson,
RoomsCount = Model.RoomsCount
})
}
I need to implement infinite scroll using js. How can I call render partial view on js and pass parameters on it?
As a very basic demo, you want to have a Partial View that is returning some sort of html to you. In my case this is just the next 10 numbers based on the the number submitted:
public IActionResult _InfiniteNumbers(int lastId)
{
var Ids = Enumerable.Range(lastId, 10);
return PartialView(Ids);
}
In a real world scenario this would be the next n entities of whatever - blogs, orderitems, comments.
The view for this is fairly straight forward as well:
#model IEnumerable<int>
#foreach (var number in Model)
{
<li data-number="#number">#number</li>
}
it just renders every number as a list item.
My main view then looks like this:
<h1>InfiniteNumbers</h1>
<ul id="partial-view-container">
</ul>
<input id="btnLoadMore" type="button" name="loadMore" value="Load More numbers" />
#section scripts {
<script>
$(document).ready(() => {
$("#btnLoadMore").click(() => { //the trigger
var ul = $("#partial-view-container"); //our container for all partial views
let lastId = 0; //the lastId displayed
let lastLi = $("#partial-view-container li:last-child"); //find the last number
if (lastLi.length !== 0) {
lastId = lastLi.attr("data-number"); //if we found something set the value
//lastId = +lastLi.data("number");
}
$.get(`/Home/_InfiniteNumbers?lastId=${lastId}`) //call our action method
.then((res) => {
ul.append(res); //append the html to the container
});
});
});
</script>
}
We have a <ul></ul> element that serves as our container for infinite loading. Since I am lazy, I am using a button as the trigger. In a more complex scenario, this would be an event waiting for the scrollbar to reach the bottom of the page or something similar. I'll leave this up to you.
We then query the list for its last <li> item and get its data-number attribute.
After this, we query our action method and get the next 10 items based on that number and finally just inject them into our view.