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.
Related
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);
}
});
};
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 want to rerun the search or populate the results as if the user never left.
So I visit the page, search for cats and get some results based on cats. I close the tab, come back later, and rather than find myself on a fresh page that has no results, I’m provided with the results from my last search. I am only allowed to use Javascript and local storage to achieve this. Here is my JS file:
var search = document.getElementById('searchTerm');
if(!localStorage.getItem('input')) {
populateStorage();
}
// To set the event to listen to
const form = document.querySelector('.js-search-form');
// To listen when user hits submit or press enter
form.addEventListener('submit', (e) => {
// Start of the query string
const base = `https://api.themoviedb.org/3/search/movie?include_adult=false&page=1&`;
// Extension of the query string
const searchTerm = document.querySelector('#searchTerm').value;
const language = `en-US&`;
const api_key = `7ab3cb18b4ad4a07bbd8bb01acfa7091`;
// The complete build of the url
const url = `${base}query=${searchTerm}&language=${language}api_key=${api_key}`;
// The get method
const option = {
method: 'GET'
}
// Stops the default action
e.preventDefault()
fetch(url)
//Parse data
.then( response => {
if (response.ok) {
return response.json()
} else {
throw response
}
})
// Do something with data
.then( data => {
console.log(data)
// Output results
let resultElement = '';
if(data.results.length > 1) {
resultElement += `
<h2><span>Results for ${searchTerm}</span></h2>
<section class="js-search-results clearfix">
`;
if(data.results) {
data.results.forEach(function(results){
resultElement += `<article class="item">
<div class="container">
<img src="https://image.tmdb.org/t/p/w500${results.poster_path}"/>
<div class="content">`;
if(results.title.length > 17) {
resultElement += `<h3>${results.title.substr(0,17)}...</h3>`;
} else {
resultElement += `<h3>${results.title}</h3>`;
}
resultElement += `<p>Released: ${results.release_date}</p>`;
resultElement += `</div>
</div>
</article>`;
});
resultElement += `</section>`;
populateStorage()
} else {
resultElement += '<p class="no-results">No results</p>';
}
document.querySelector('.js-search-results').innerHTML = resultElement;
}
})
.catch(err => {
console.log(err)
})
})
function populateStorage() {
localStorage.setItem('searchTerm', document.getElementById('searchTerm').value);
}
EDIT -
You are calling localStorage.getItem('input'); but you are saving the local storage item as searchTerm please make these names consistent with one another
you either need to change localStorage.getItem('input')
TO
localStorage.getItem('searchTerm');
OR
change localStorage.setItem('searchTerm', document.getElementById('searchTerm').value)
TO
localStorage.setItem('input', document.getElementById('searchTerm').value)
-Edit-
Assuming that this is wrapped in a document ready function, update your initial check of storage to look like this...
var search = document.getElementById('searchTerm');
//This assumes that you corrected your getItem to reference 'searchTerm'
if(!localStorage.getItem('searchTerm')) {
populateStorage();
}
else {
search.value = localStorage.getItem('searchTerm');
}
Your code never makes a call to actually populate that field so this should fix that.
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 = ''
}
can anyone tell me where i'm going wrong?
I'm desperately trying to get my jquery carousel to re-render with the results of a filtered array - but really hitting issues today.
I grab the carouselId from its div with wrapper
i get the api array of objects & store it in moviesArray.
Then the key thing is that when someone has typed in the input, it re-renders the carousel based on that newly filtered array.
function populateCarousel() {
var moviesArray = [];
var tempResultsArr = [];
fetch('http://localhost:3000/getdata')
.then((res) => res.json())
.then((json) => {
moviesArray = json;
// ===== if NO searchTerm is entered ====
// THIS ONE WORKS
if (typeof searchTerm === 'undefined') {
moviesArray.map((each) => {
var eachTile = document.createElement("a");
var imageForEachTile = document.createElement("img");
imageForEachTile.setAttribute('src', each.poster);
eachTile.setAttribute("class", "carousel-item thumbnail-item");
eachTile.setAttribute("href", "#linkToSomeWhere");
eachTile.innerHTML = each.title;
eachTile.appendChild(imageForEachTile);
wrapper.appendChild(eachTile);
})
} else {
// ===== if a searchTerm *IS* entered ====
// THIS ONE DOESN'T WORK!
while (wrapper.firstChild) {
wrapper.removeChild(wrapper.firstChild);
}
function checkWords(searchTerm, arr) {
let st = searchTerm.toLowerCase();
return arr.filter(each => each.title.toLowerCase().indexOf(st) === 0);
}
let newArr = checkWords(searchTerm, moviesArray);
newArr.map((each) => {
var eachTile = document.createElement("a");
var imageForEachTile = document.createElement("img");
imageForEachTile.setAttribute('src', each.poster);
eachTile.setAttribute("class", "carousel-item thumbnail-item");
eachTile.setAttribute("href", "#linkToSomeWhere");
eachTile.innerHTML = each.title;
eachTile.appendChild(imageForEachTile);
wrapper.appendChild(eachTile);
})
// this logs out correctly & when i go into the html
// the correct searchresults are there, a and img tags.
// but they dont show.
console.log('wrapper: ', wrapper);
}
})
}
you notice the .map((each)... is identical on both sides of the IF/ELSE.
It does create the elements and attributes correctly, and does append them because a) i can log them out but b) i can actually inspect the HTMl in chrome and see them there.
But why aren't they actually showing?