I am generating some div's and appending to the DOM with this function
//Run forEach method on newObj(cats from local storage) to populate and append template to the DOM
function getTheCats() {
//Limiting the results to 3. Probably better way to do this.
newObj.slice(0, 3).forEach(cat => {
const catEl = document.createElement('div')
catEl.classList.add('cat-detail')
catEl.innerHTML = `
<div class="img-id-container" id="pointer-control" onclick="getCatDeets()">
<img class='cat-image' src='${cat.thumbnail_url}' alt="Cat Pic"/>
<h3 class="id-left">Cat ${cat.id}</h3>
</div>
<p class="birthday-left">${cat.birthdate}</p>
`
mainLeft.appendChild(catEl)
})
}
getTheCats()
I am trying to log to console, some of the innerHTML when I click on one of the results.
I always get 'undefined' as a result. I know I am missing something, but I can't seem to figure out what. Any help would be greatly appreciated.
function myFunction(event) {
const clickedCat = event.target.nodeName;
console.log(clickedCat);
const details = clickedCat.innerHTML
console.log(details)
}
From David784 in the comments,
I unnecessarily added .nodeName to event.target
I replaced it with .innerHTML and I am able to retrieve the data I need.
function myFunction(event) {
const clickedCat = event.target.innerHTML;
console.log(clickedCat);
const details = clickedCat.innerHTML
console.log(details)
}
Related
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 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 was trying to get data from an API that gives me a list of movies and this is how I implemented the rendering of the results.
const getAllData = async () => {
const movieData = await getMovies()
const movieContainer = movieData.map((movie)=> {
const listItem = `
<li class="movie">
<img src="${movie.Poster}"></img>
<h1>
${movie.Title}
</h1>
<h2>Release Year: ${movie.Year}</h2>
<p>
<button onclick="getDetails('${movie.imdbID}')">
Click for more details
</button>
</p>
</li>
`;
return listItem
}).join('')
document.getElementById("movieList").innerHTML = movieContainer;
}
Here is the getDetails function that the button click calls.
function getDetails(id){
sessionStorage.setItem('movieId', id);
window.location = 'movie.html';
return false;}
This code works as intended. However, what I was having trouble with is that I want to implement this logic with event listeners instead of inline HTML event-handlers. How could I implement this?
Would I need to change anything drastic about my current code?
Create a <li> element instead of an HTML string, then select the button descendant using querySelector and you can use addEventListener:
const getAllData = async () => {
const movieData = await getMovies();
const ul = document.getElementById("movieList");
for (const movie of movieData) {
const li = ul.appendChild(document.createElement('li'));
li.className = 'movie';
li.innerHTML = `
<img src="${movie.Poster}"></img>
<h1>
${movie.Title}
</h1>
<h2>Release Year: ${movie.Year}</h2>
<p>
<button>
Click for more details
</button>
</p>
`;
li.querySelector('button').addEventListener('click', () => {
getDetails(movie.imdbID);
});
}
};
I'd also highly recommend against direct concatenation of external input into an HTML string, like with
<img src="${movie.Poster}"></img>
unless the input is absolutely trustworthy, since that can result in arbitrary code execution, which is a security risk. If you aren't doing it already, I'd suggest either verifying that the interpolated values are well formatted (for example, without anything which would result in HTML markup, like <script> tags), or assign the dynamic properties/attributes after the <li> has been populated with the base template.
Forgive me if this is redundant, I'm having trouble finding questions/answers that are using vanilla JS specifically.
I have a data object for store items I've created that I'm trying to display on the page but I only seem to be getting the first item in the array to appear, which lead me to believe I needed some sort of for loop related to the array length but I tried variations and I seem to be getting the same result (only the first item), or in some cases nothing at all.
I've logged out the HTML and the correct items are there in the console so its working and they are ready to go. I'm missing something.
feature = () => isFeatured.map(item => {
// console.log("imworking");
html = `
<img src="${item.image}" alt="">
<h2>${item.info}</h2>
<h3>${item.price}</h3>
<button>Add to Cart</button>
`
//console.log(html);
document.getElementById('featuredItem').innerHTML = html;
})
I don't think the problem is the HTML because the one item is displaying fine but here it is anyways
<div id="featuredItem"></div>
You're replacing the featuredItem's HTML every time the loop runs. Also, you shouldn't use map since you're not mapping to a new array; use forEach instead. Also, make sure to declare all variables with const (or let), to avoid implicitly creating global variables, which should be avoided:
const feature = () => isFeatured.forEach(item => {
// console.log("imworking");
const html = `
<img src="${item.image}" alt="">
<h2>${item.info}</h2>
<h3>${item.price}</h3>
<button>Add to Cart</button>
`;
//console.log(html);
document.getElementById('featuredItem').innerHTML += html;
});
feature();
But directly inserting variables into HTML markup like that is not all that safe. It would be better to explicitly create and append elements, assigning values to their textContent, like this:
const featuredItem = document.querySelector('#featuredItem');
const feature = () => isFeatured.forEach(({ image, info, price }) => {
featuredItem
.appendChild(document.createElement('img'))
.src = image;
featuredItem
.appendChild(document.createElement('h2'))
.textContent = info;
featuredItem
.appendChild(document.createElement('h3'))
.src = price;
featuredItem
.appendChild(document.createElement('button'))
.textContent = 'Add to Cart';
});
feature();
Hey sorry this is so late, hopefully you figured it out already! I ran out of room in the comment section so I'm throwing my response to #maxineheadroom down here. If I understand your question you want the img htags and buttons wrapped in a div? I would make a new function called creatItem or something.
const createItem = (item) => {
const wrapper = document.createElement('div');
wrapper.classList.add(`item_${item.id}`)
const img = document.createElement('img').src = item.img
const info = document.createElement('h2').textContent = item.info;
const price = document.createElement('h3').textContent = item.price;
const btn = document.createElement('button').textContent = 'Add to cart'
wrapper.appendChild(img);
wrapper.appendChild(info);
wrapper.appendChild(price);
wrapper.appendChild(btn);
return wrapper
}
then in your for loop you can just do
featuredItem.appendChild(createItem(item))
It is because you are setting the innerhtml, through looping the HTML changes as well as the innerhtml.
You should have a container for string HTML generated by loop.
feature = () => isFeatured.map(item => {
let markup = new Array();
html = `
<img src="${item.image}" alt="">
<h2>${item.info}</h2>
<h3>${item.price}</h3>
<button>Add to Cart</button>
`
markup.push(html);
html = markup.join("");
document.getElementById('featuredItem').innerHTML = html;
})
Also it can be than if you use appendchild rather than innerhtml.
I would like to use a javascript loop to create multiple HTML wrapper elements and insert JSON response API data into some of the elements (image, title, url, etc...).
Is this something I need to go line-by-line with?
<a class="scoreboard-video-outer-link" href="">
<div class="scoreboard-video--wrapper">
<div class="scoreboard-video--thumbnail">
<img src="http://via.placeholder.com/350x150">
</div>
<div class="scoreboard-video--info">
<div class="scoreboard-video--title">Pelicans # Bulls Postgame: E'Twaun Moore 10-8-17</div>
</div>
</div>
</a>
What I am trying:
var link = document.createElement('a');
document.getElementsByTagName("a")[0].setAttribute("class", "scoreboard-video-outer-link");
document.getElementsByTagName("a")[0].setAttribute("url", "google.com");
mainWrapper.appendChild(link);
var videoWrapper= document.createElement('div');
document.getElementsByTagName("div")[0].setAttribute("class", "scoreboard-video-outer-link");
link.appendChild(videoWrapper);
var videoThumbnailWrapper = document.createElement('div');
document.getElementsByTagName("div")[0].setAttribute("class", "scoreboard-video--thumbnail");
videoWrapper.appendChild(videoThumbnailWrapper);
var videoImage = document.createElement('img');
document.getElementsByTagName("img")[0].setAttribute("src", "url-of-image-from-api");
videoThumbnailWrapper.appendChild(videoImage);
Then I basically repeat that process for all nested HTML elements.
Create A-tag
Create class and href attributes for A-tag
Append class name and url to attributes
Append A-tag to main wrapper
Create DIV
Create class attributes for DIV
Append DIV to newly appended A-tag
I'd greatly appreciate it if you could enlighten me on the best way to do what I'm trying to explain here? Seems like it would get very messy.
Here's my answer. It's notated. In order to see the effects in the snippet you'll have to go into your developers console to either inspect the wrapper element or look at your developers console log.
We basically create some helper methods to easily create elements and append them to the DOM - it's really not as hard as it seems. This should also leave you in an easy place to append JSON retrieved Objects as properties to your elements!
Here's a Basic Version to give you the gist of what's happening and how to use it
//create element function
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
//append child function
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
//example:
//get wrapper div
let mainWrapper = document.getElementById("mainWrapper");
//create link and div
let link = create("a", { href:"google.com" });
let div = create("div", { id: "myDiv" });
//add link as a child to div, add the result to mainWrapper
ac(mainWrapper, ac(div, link));
//create element function
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
//append child function
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
//example:
//get wrapper div
let mainWrapper = document.getElementById("mainWrapper");
//create link and div
let link = create("a", { href:"google.com", textContent: "this text is a Link in the div" });
let div = create("div", { id: "myDiv", textContent: "this text is in the div! " });
//add link as a child to div, add the result to mainWrapper
ac(mainWrapper, ac(div, link));
div {
border: 3px solid black;
padding: 5px;
}
<div id="mainWrapper"></div>
Here is how to do specifically what you asked with more thoroughly notated code.
//get main wrapper
let mainWrapper = document.getElementById("mainWrapper");
//make a function to easily create elements
//function takes a tagName and an optional object for property values
//using Object.assign we can make tailored elements quickly.
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
//document.appendChild is great except
//it doesn't offer easy stackability
//The reason for this is that it always returns the appended child element
//we create a function that appends from Parent to Child
//and returns the compiled element(The Parent).
//Since we are ALWAYS returning the parent(regardles of if the child is specified)
//we can recursively call this function to great effect
//(you'll see this further down)
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
//these are the elements you wanted to append
//notice how easy it is to make them!
//FYI when adding classes directly to an HTMLElement
//the property to assign a value to is className -- NOT class
//this is a common mistake, so no big deal!
var link = create("a", {
className: "scoreboard-video-outer-link",
url: "google.com"
});
var videoWrapper = create("div", {
className: "scoreboard-video-outer-link"
});
var videoThumbnailWrapper = create("div", {
className: "scoreboard-video--thumbnail"
});
var videoImage = create("img", {
src: "url-of-image-from-api"
});
//here's where the recursion comes in:
ac(mainWrapper, ac(link, ac(videoWrapper, ac(videoThumbnailWrapper, videoImage))));
//keep in mind that it might be easiest to read the ac functions backwards
//the logic is this:
//Append videoImage to videoThumbnailWrapper
//Append (videoImage+videoThumbnailWrapper) to videoWrapper
//Append (videoWrapper+videoImage+videoThumbnailWrapper) to link
//Append (link+videoWrapper+videoImage+videoThumbnailWrapper) to mainWrapper
let mainWrapper = document.getElementById('mainWrapper');
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
var link = create("a", {
className: "scoreboard-video-outer-link",
url: "google.com"
});
var videoWrapper = create("div", {
className: "scoreboard-video-outer-link"
});
var videoThumbnailWrapper = create("div", {
className: "scoreboard-video--thumbnail"
});
var videoImage = create("img", {
src: "url-of-image-from-api"
});
ac(mainWrapper, ac(link, ac(videoWrapper, ac(videoThumbnailWrapper, videoImage))));
//pretty fancy.
//This is just to show the output in the log,
//feel free to just open up the developer console and look at the mainWrapper element.
console.dir(mainWrapper);
<div id="mainWrapper"></div>
Short version
Markup.js's loops.
Long version
You will find many solutions that work for this problem. But that may not be the point. The point is: is it right? And you may using the wrong tool for the problem.
I've worked with code that did similar things. I did not write it, but I had to work with it. You'll find that code like that quickly becomes very difficult to manage. You may think: "Oh, but I know what it's supposed to do. Once it's done, I won't change it."
Code falls into two categories:
Code you stop using and you therefore don't need to change.
Code you keep using and therefore that you will need to change.
So, "does it work?" is not the right question. There are many questions, but some of them are: "Will I be able to maintain this? Is it easy to read? If I change one part, does it only change the part I need to change or does it also change something else I don't mean to change?"
What I'm getting at here is that you should use a templating library. There are many for JavaScript.
In general, you should use a whole JavaScript application framework. There are three main ones nowadays:
ReactJS
Vue.js
Angular 2
For the sake of honesty, note I don't follow my own advice and still use Angular. (The original, not Angular 2.) But this is a steep learning curve. There are a lot of libraries that also include templating abilities.
But you've obviously got a whole project already set up and you want to just plug in a template into existing JavaScript code. You probably want a template language that does its thing and stays out of the way. When I started, I wanted that too. I used Markup.js . It's small, it's simple and it does what you want in this post.
https://github.com/adammark/Markup.js/
It's a first step. I think its loops feature are what you need. Start with that and work your way to a full framework in time.
Take a look at this - [underscore._template]
It is very tiny, and useful in this situation.
(https://www.npmjs.com/package/underscore.template).
const targetElement = document.querySelector('#target')
// Define your template
const template = UnderscoreTemplate(
'<a class="<%- link.className %>" href="<%- link.url %>">\
<div class="<%- wrapper.className %>">\
<div class="<%- thumbnail.className %>">\
<img src="<%- thumbnail.image %>">\
</div>\
<div class="<%- info.className %>">\
<div class="<%- info.title.className %>"><%- info.title.text %></div>\
</div>\
</div>\
</a>');
// Define values for template
const obj = {
link: {
className: 'scoreboard-video-outer-link',
url: '#someurl'
},
wrapper: {
className: 'scoreboard-video--wrapper'
},
thumbnail: {
className: 'scoreboard-video--thumbnail',
image: 'http://via.placeholder.com/350x150'
},
info: {
className: 'scoreboard-video--info',
title: {
className: 'scoreboard-video--title',
text: 'Pelicans # Bulls Postgame: E`Twaun Moore 10-8-17'
}
}
};
// Build template, and set innerHTML to output element.
targetElement.innerHTML = template(obj)
// And of course you can go into forEach loop here like
const arr = [obj, obj, obj]; // Create array from our object
arr.forEach(item => targetElement.innerHTML += template(item))
<script src="https://unpkg.com/underscore.template#0.1.7/dist/underscore.template.js"></script>
<div id="target">qq</div>