I have a function to render comments, in which each comment is stored as an object in an array. Comments can have reply comments, in which they have the exact same html and data to render, just their styling is different (via a CSS modifier class).
How can I make this function recursive? The renderReplies(comment.replies) calls a function that is the exact same as renderComments function, just without the mentioned function call renderReplies (as a reply to a reply is the exact same also in styling terms).
const renderComments = (comments) => {
commentsElement.innerHTML = '';
comments.forEach(comment => {
commentsElement.innerHTML += html;
// data-id attribute
const liElements = commentsElement.querySelectorAll('.comment');
const liElement = liElements.item(liElements.length - 1);
liElement.setAttribute('data-id', comment.id);
// author
liElement.querySelector('.comment__author').innerHTML = comment.user.username;
// avatar src & alt attributes
const avatar = liElement.querySelector('.comment__avatar');
avatar.setAttribute('src', comment.user.image.png);
avatar.setAttribute('alt', comment.user.username);
// time since posted
// content
const p = liElement.querySelector('.comment__text');
p.appendChild(document.createTextNode(comment.content));
// score
liElement.querySelector('.comment__score b').innerHTML = comment.score;
// replies
renderReplies(comment.replies);
});
};
Check whether there's a replies property. If it exists, call the function recursively. Since replies won't have replies of their own, you'll stop there.
const renderComments = (comments) => {
commentsElement.innerHTML = '';
comments.forEach(comment => {
commentsElement.innerHTML += html;
// data-id attribute
const liElements = commentsElement.querySelectorAll('.comment');
const liElement = liElements.item(liElements.length - 1);
liElement.setAttribute('data-id', comment.id);
// author
liElement.querySelector('.comment__author').innerHTML = comment.user.username;
// avatar src & alt attributes
const avatar = liElement.querySelector('.comment__avatar');
avatar.setAttribute('src', comment.user.image.png);
avatar.setAttribute('alt', comment.user.username);
// time since posted
// content
const p = liElement.querySelector('.comment__text');
p.appendChild(document.createTextNode(comment.content));
// score
liElement.querySelector('.comment__score b').innerHTML = comment.score;
// replies
if (comment.hasOwnProperty("replies")) {
renderComments(comment.replies);
}
});
};
Related
i know that the problem is that let todoList is an empty array, but i dont know how to solve it.
the id tags in my created html is so e can create a delete button later
heres my code:
const textArea = document.querySelector("textarea");
const button = document.querySelector("button");
const listContainer = document.querySelector(".list-container");
let id = 0;
let todoList = [];
button.onclick = function () {
const listItem = {
title: textArea.value,
};
todoList.push(listItem);
addToStorage(todoList);
const dataFromStorage = getFromStorage();
createHtml(dataFromStorage);
};
function addToStorage(items) {
const stringify = JSON.stringify(items);
localStorage.setItem("list", stringify);
}
function getFromStorage() {
const data = localStorage.getItem("list");
const unstrigified = JSON.parse(data);
return unstrigified;
}
const createHtml = (data) => {
id++;
listContainer.innerHTML = "";
data.forEach((item) => {
listContainer.innerHTML += `<div class="list-item" data-id=${id}><p>${item.title} </p><button class="remove" data-id=${id}>Delete</button></div>`;
});
};
The problem here is you just forgot to load the data from localStorage when the page loaded like this
window.onLoad = () => {
const dataFromStorage = getFromStorage();
if(dataFromStorage){
createHtml(dataFromStorage);
} else {
createHtml([]);
}
}
The problem in the code is as follows
Initially the todolist will be an empty array. so when you do the below
todoList.push(listItem);
// adding to local storage which will override the existing todos when page is refreshed
addToStorage(todoList);
// So when the below line is executed only the latest todo will be returned
const dataFromStorage = getFromStorage();
createHtml(dataFromStorage);
Fix:
Initialise the todos from localstorage instead of an empty array
let todoList = [];
// change it as below
let todoList = getFromStorage();
Now Modify the getFromStorage() as below
// If the data is present in the localStorage then return it, else return empty array
function getFromStorage() {
const data = localStorage.getItem("list");
if (!data) return [];
const unstrigified = JSON.parse(data);
return unstrigified;
}
Now when the page is loaded, we need to display the todos. Add the below lines of code
window.onload = function () {
createHtml(todoList);
};
That's it. This will fix the issue.
Few minor improvements can be made as well.
todoList.push(listItem);
addToStorage(todoList);
const dataFromStorage = getFromStorage(); // this line is not necessary, remove it
createHtml(dataFromStorage); // change this to createHtml(todoList)
Codepen
Thanks.
I'm trying to make a kind of catalog and once someone presses the "show more" button it should show the description from the corresponding data I got from a API/JSON file. I tried using e.target but I'm stuck at this point.
function getCocktailItemsSuccessHandler(data) {
for(item of data){
const cardDiv = document.createElement("div")
cardDiv.classList.add("card")
cocktailGallery.appendChild(cardDiv)
const nameCocktail = document.createElement("h2")
nameCocktail.classList.add("listName")
nameCocktail.innerText = item.name
cardDiv.appendChild(nameCocktail)
const img = document.createElement("img")
img.src = item.image
cardDiv.appendChild(img)
const detailsButton = document.createElement("button")
detailsButton.classList.add("detailsButton")
detailsButton.innerHTML = 'More'
detailsButton.addEventListener('click', detailsClickHandler)
cardDiv.appendChild(detailsButton)
}
}
function detailsClickHandler(e) {
let details = e.target
detailsContainer.innerHTML = item.
}
If I am understanding your structure, you can store the incoming data in a variable that you can reference after the fact. See the lines with the // <--- add this line and then the whole detailsClickHandler function.
Another option of course is to create a div and insert the description text in it, hide it with display:none and toggle it on with the button click. The way I've presented below is more dynamic, but not neccesarily better.
let theData = [] // <-- add this line
function getCocktailItemsSuccessHandler(data) {
theData = data; // <-- add this line
for (item of data) {
const cardDiv = document.createElement("div")
cardDiv.classList.add("card")
cardDiv.setAttribute('data-id', item.id); // <-- add this line
cocktailGallery.appendChild(cardDiv)
const nameCocktail = document.createElement("h2")
nameCocktail.classList.add("listName")
nameCocktail.innerText = item.name
cardDiv.appendChild(nameCocktail)
const img = document.createElement("img")
img.src = item.image
cardDiv.appendChild(img)
const detailsButton = document.createElement("button")
detailsButton.classList.add("detailsButton")
detailsButton.innerHTML = 'More'
detailsButton.addEventListener('click', detailsClickHandler)
cardDiv.appendChild(detailsButton)
}
}
function detailsClickHandler(e) {
let details = e.target
// get the relative id
let id = details.closest('.card').dataset.id;
// get the item from the object
let item = theData.filter(item => item.id.toString().trim() === id.toString().trim());
detailsContainer.innerHTML = item[0].description; // or whatever field the html is in
}
You can pass the item dynamically to the detailsClickHandler() function and then use it there to update details.
detailsButton.addEventListener('click', (event) => detailsClickHandler(event, item))
function detailsClickHandler(event, item) {
detailsContainer.innerHTML = item
}
Hoping you guys can help me, I have been playing around with brewdogs api called punk api.
I have built a search function but whenever the user searches for a word that returns multiple beers it prints them all at once.
Is there anyway to print one title, one tagline, one description, one abv, one pic and then repeat til all have been printed?
It is currently just printing every title in one div then every tag line in one div and so on.
I have also attached an image to show the results when searched.
function beerAsk(e) {
clear();
const beerage = document.getElementById("BeerInputSearch").value;
const url = api + beerage;
e.preventDefault()
fetch(url)
.then(reponse => {
return reponse.json()
})
.then(data => {
data.forEach((beer) => {
const theId = beer.id;
const theBeer = beer.name;
const theTagline = beer.tagline;
const theDescription = beer.description;
const theAbv = beer.abv;
const thePic = beer.image_url;
const nopic = document.getElementById('imageSearch').src = "https://images.punkapi.com/v2/keg.png"
function imgDisplay() {
if (thePic === null) {
return nopic;
} else {
return thePic;
}
}
// this is the bit that needs the work im guessing
searchBeer.innerHTML += theBeer
searchBeerTagline.innerHTML += theTagline
searchBeerAbv.innerHTML += "ABV: " + theAbv + "%"
searchBeerDescription.innerHTML += theDescription
document.getElementById('imageSearch').src = imgDisplay();
})
})
}
In your code, you asked to put all titles in the same div indeed:
// this is the bit that needs the work im guessing
searchBeer.innerHTML += theBeer
searchBeerTagline.innerHTML += theTagline
searchBeerAbv.innerHTML += "ABV: " + theAbv + "%"
searchBeerDescription.innerHTML += theDescription
Instead, you should use as many searchBeer as beer in data response. One popular technique is to use Template, like Reusable HTML components. There are some package for that. Here is a basic reusable component, let's say you have a search result div and at the bottom this component (HTML group of elements that are doing the same thing) in your HTML body:
<div id="search-result" ></div>
And later at the bottom:
<div class="beer-component" style="display:none;" >
<div class="beer-title" ></div>
<div class="beer-tagline" ></div>
<div class="beer-abv" ></div>
<div class="beer-description" ></div>
<div class="beer-image" ><img alt="beer" /></div>
</div>
Now you need to clone the component multiple times, add values to them and append each component in search result container.
EDIT --------------
When you submit a new search, the function codes will be interpreted again. So if you don't clear the search result container well, you will have your title to add up continuously because of +=, titleBeer.innerHTML += theBeer. You need to get rid of it and put data directly titleBeer.innerHTML = theBeer. Now the reason that might happening (cause I don't know what your function clear() does), is that when selecting component template:
const beerComponent = document.getElementsByClassName("beer-component")[0];
before clearing the result container, the DOM return the first beer component that reside in search result container, instead of going at the bottom to take the template instead. That 1st component already had data, hence, doing:
titleBeer.innerHTML += theBeer
the titles keep adding together, which is wrong, instead it should be:
titleBeer.innerHTML = theBeer
So you can just clear the search results before reselecting the Template component. Here is the final code in this case:
function beerAsk(e) {
// clear(); NOT SURE WHAT IT DOES, BUT I CLEAR it below
// new elements here
const resultContainer = document.getElementById("search-result");
// EDIT --- CLEAR Container first
resultContainer.innerHTML = "";
const beerComponent = document.getElementsByClassName("beer-component")[0];
const beerage = document.getElementById("BeerInputSearch").value;
const url = api + beerage;
e.preventDefault()
fetch(url)
.then(reponse => {
return reponse.json()
})
.then(data => {
data.forEach((beer) => {
const theId = beer.id;
const theBeer = beer.name;
const theTagline = beer.tagline;
const theDescription = beer.description;
const theAbv = beer.abv;
const thePic = beer.image_url;
const nopic = document.getElementById('imageSearch').src = "https://images.punkapi.com/v2/keg.png"
function imgDisplay() {
if (thePic === null) {
return nopic;
} else {
return thePic;
}
}
// this is the bit that needs the work im guessing
// CHANGES HAPPEN HERE
// Create a copy to be different DOM object from the original
let component = beerComponent.cloneNode(true);
let titleBeer = component.getElementsByClassName('beer-title')[0];
let taglineBeer = component.getElementsByClassName('beer-tagline')[0];
let abvBeer = component.getElementsByClassName('beer-abv')[0];
let descriptionBeer = component.getElementsByClassName('beer-description')[0];
let imgBeer = component.querySelector('.beer-image img'); // return one element
component.style = "display: block;";
// EDIT ---- change the += to =
titleBeer.innerHTML = theBeer
taglineBeer.innerHTML = theTagline
abvBeer.innerHTML = "ABV: " + theAbv + "%"
descriptionBeer.innerHTML += theDescription
imgBeer.src = imgDisplay();
imgBeer.alt = theBeer;
// Now append the component in Search Result container
resultContainer.appendNode(component);
})
})
}
And for the rest you would format the component with CSS to fit your design, or just use existing component HTML codes from a Template with CSS ready.
I've already asked that question but my explanation was pretty bad, so I decided to ask again with a better explanation and with actual code (I'll ask moderators to delete one of the posts). So let's consider the problem.
Following snippet represents rendering notes from array. However, during the adding note part, I mutate a state. So the question is: how can I add a new note in notes array without mutating? In other words, I want to remove replaceNotes and remain the same functionality. I know that it's possible to add notes without array at all, but I do need to update array with notes in due to the future reference. The ting is, in my original application I've got lists with notes, and while I switch between lists, I should get rendered notes that relies to the list I switch on. That's why I should keep the reference to notes array.
At the same time I'm wondering, would it be okay, if I just store notes in localStorage and then take notes from that data? Is it a good practice in functional programming?
const button = document.getElementById('button');
const notesContainer = document.querySelector('.notes');
const pipe = (f, g) => (...args) => f(g(...args));
let notes = [];
const createNote = (...fns) => fns.reduceRight(pipe);
const handleEvent = () =>
createNote(gatherContent, renderContent, replaceNotes)(notes);
function gatherContent(notes) {
const name = prompt('How do you want to name a note?');
return [...notes, { name }];
}
function renderContent(notes) {
function render(note) {
const noteEl = document.createElement('div');
noteEl.innerHTML = `<p>${note.name}</p>`;
notesContainer.append(noteEl);
}
notesContainer.innerHTML = '';
notes.map(render);
return notes;
}
const replaceNotes = newNotes => (notes = newNotes);
button.addEventListener('click', handleEvent);
<button id="button">Click me!</button>
<section class="notes"></section>
Here is how to create a simple task list app without mutating anything except for the DOM.
const button = document.getElementById("button");
const section = document.getElementById("notes");
const template = document.getElementById("template");
template.parentNode.removeChild(template);
const render = notes => {
button.onclick = event => {
const name = prompt("How do you want to name a note?");
render([...notes, { name }]);
};
while (section.lastChild) {
section.removeChild(section.lastChild);
}
for (const note of notes) {
const node = template.cloneNode(true);
node.firstChild.firstChild.nodeValue = note.name;
section.appendChild(node);
}
};
render([]);
<button id="button">Click me!</button>
<section id="notes"></section>
<div id="template"><p>name</p></div>
For a detailed explanation, read my previous answer. https://stackoverflow.com/a/58642199/783743
You can use this pattern with localStorage too.
const button = document.getElementById("button");
const section = document.getElementById("notes");
const template = document.getElementById("template");
template.parentNode.removeChild(template);
const render = notes => {
localStorage.setItem("notes", notes); // set notes
button.onclick = event => {
const name = prompt("How do you want to name a note?");
render([...notes, { name }]);
};
while (section.lastChild) {
section.removeChild(section.lastChild);
}
for (const note of notes) {
const node = template.cloneNode(true);
node.firstChild.firstChild.nodeValue = note.name;
section.appendChild(node);
}
};
render(localStorage.getItem("notes") || []); // get notes
Note that localStorage should only be used to save state that you want to use across sessions. It's not recommended to use localStorage as your application store. That would result in both bad performance and bad code structure.
I'm just starting to play around with Puppeteer (Headless Chrome) and Nodejs. I'm scraping some test sites, and things work great when all the values are present, but if the value is missing I get an error like:
Cannot read property 'src' of null (so in the code below, the first two passes might have all values, but the third pass, there is no picture, so it just errors out).
Before I was using if(!picture) continue; but I think it's not working now because of the for loop.
Any help would be greatly appreciated, thanks!
for (let i = 1; i <= 3; i++) {
//...Getting to correct page and scraping it three times
const result = await page.evaluate(() => {
let title = document.querySelector('h1').innerText;
let article = document.querySelector('.c-entry-content').innerText;
let picture = document.querySelector('.c-picture img').src;
if (!document.querySelector('.c-picture img').src) {
let picture = 'No Link'; } //throws error
let source = "The Verge";
let categories = "Tech";
if (!picture)
continue; //throws error
return {
title,
article,
picture,
source,
categories
}
});
}
let picture = document.querySelector('.c-picture img').src;
if (!document.querySelector('.c-picture img').src) {
let picture = 'No Link'; } //throws error
If there is no picture, then document.querySelector() returns null, which does not have a src property. You need to check that your query found an element before trying to read the src property.
Moving the null-check to the top of the function has the added benefit of saving unnecessary calculations when you are just going to bail out anyway.
async function scrape3() {
// ...
for (let i = 1; i <= 3; i++) {
//...Getting to correct page and scraping it three times
const result = await page.evaluate(() => {
const pictureElement = document.querySelector('.c-picture img');
if (!pictureElement) return null;
const picture = pictureElement.src;
const title = document.querySelector('h1').innerText;
const article = document.querySelector('.c-entry-content').innerText;
const source = "The Verge";
const categories = "Tech";
return {
title,
article,
picture,
source,
categories
}
});
if (!result) continue;
// ... do stuff with result
}
Answering comment question: "Is there a way just to skip anything blank, and return the rest?"
Yes. You just need to check the existence of each element that could be missing before trying to read a property off of it. In this case we can omit the early return since you're always interested in all the results.
async function scrape3() {
// ...
for (let i = 1; i <= 3; i++) {
const result = await page.evaluate(() => {
const img = document.querySelector('.c-picture img');
const h1 = document.querySelector('h1');
const content = document.querySelector('.c-entry-content');
const picture = img ? img.src : '';
const title = h1 ? h1.innerText : '';
const article = content ? content.innerText : '';
const source = "The Verge";
const categories = "Tech";
return {
title,
article,
picture,
source,
categories
}
});
// ...
}
}
Further thoughts
Since I'm still on this question, let me take this one step further, and refactor it a bit with some higher level techniques you might be interested in. Not sure if this is exactly what you are after, but it should give you some ideas about writing more maintainable code.
// Generic reusable helper to return an object property
// if object exists and has property, else a default value
//
// This is a curried function accepting one argument at a
// time and capturing each parameter in a closure.
//
const maybeGetProp = default => key => object =>
(object && object.hasOwnProperty(key)) ? object.key : default
// Pass in empty string as the default value
//
const getPropOrEmptyString = maybeGetProp('')
// Apply the second parameter, the property name, making 2
// slightly different functions which have a default value
// and a property name pre-loaded. Both functions only need
// an object passed in to return either the property if it
// exists or an empty string.
//
const maybeText = getPropOrEmptyString('innerText')
const maybeSrc = getPropOrEmptyString('src')
async function scrape3() {
// ...
// The _ parameter name is acknowledging that we expect a
// an argument passed in but saying we plan to ignore it.
//
const evaluate = _ => page.evaluate(() => {
// Attempt to retrieve the desired elements
//
const img = document.querySelector('.c-picture img');
const h1 = document.querySelector('h1')
const content = document.querySelector('.c-entry-content')
// Return the results, with empty string in
// place of any missing properties.
//
return {
title: maybeText(h1),
article: maybeText(article),
picture: maybeSrc(img),
source: 'The Verge',
categories: 'Tech'
}
}))
// Start with an empty array of length 3
//
const evaluations = Array(3).fill()
// Then map over that array ignoring the undefined
// input and return a promise for a page evaluation
//
.map(evaluate)
// All 3 scrapes are occuring concurrently. We'll
// wait for all of them to finish.
//
const results = await Promise.all(evaluations)
// Now we have an array of results, so we can
// continue using array methods to iterate over them
// or otherwise manipulate or transform them
//
results
.filter(result => result.title && result.picture)
.forEach(result => {
//
// Do something with each result
//
})
}
Try-catch worked for me:
try {
if (await page.$eval('element')!==null) {
const name = await page.$eval('element')
}
}catch(error){
name = ''
}