Problem using .find to manipulate an array of objects in javascript - javascript

I have been trying to get this solved for a while, but can't figure it out.
I received some products from an API and after deconstructing, I passed them into some HTML tags to perform DOM manipulation on them from JS like so...
displayProducts(products){
let result = '';
products.forEach(product => {
result += `
<article class="product">
<div class="img-container">
<img src=${product.image} alt="product" class="product-img">
<button class="bag-btn" data-id= ${product.id}>
<img src="./images/icons8_add_shopping_cart_100px.png"
width="16px" max-width= "18px" alt="add to cart"/>
Add to cart
</button>
</div>
<div class="goodsinfo">
<span class="description"> <img src="./images/icons8_eye_100px.png" class="view" data-class=${product.id}/>
</span>
<span class="titleprice">
<h3>${product.title}</h3>
<h4>#${product.price}</h4>
</span>
</div>
</article>
`
});
productsDOM.innerHTML = result;
}
Then I started manipulating, but I ran into a problem using the .find array method to match an id with the id from the array of objects even though they're exactly the same.
compileDescription(products){
console.log(products) // this works fine and displays 16 products which are 16 objects in arrays.
const eyeView = [...document.querySelectorAll('.view')];
eyeView.forEach(viewBtn=>{
viewBtn.addEventListener('click', (event)=>{
const btnId = event.target.dataset.class
console.log(btnId); //this works and shows the ID of the clicked button
const productFind = products.find(product=> product.id === btnId)
console.log(productFind); //returns undefined
})
})
}
What I want is that when a button with a particular id is clicked, the id is matched with an id of an object in the array of objects and the object is returned to me for manipulation.
Please help me guys. Thanks ahead.

Related

Javascript - Use array values dynamically in HTML

My end result is supposed to be a list of objects in html. Bootstrap behind this. I'd like for the list to be created dynamically so I don't have to manually create all the divs because I don't know how many there will be. Here's what I have.
I have an array similar to this:
activities =
[
{
"activityOwner": "Raymond Carlson",
"activityDesc": "Complete all the steps from Getting Started wizard"
},
{
"activityOwner": "Flopsy McDoogal",
"activityDesc": "Called interested in March fundraising Sponsorship"
},
{
"activityOwner": "Gary Busy",
"activityDesc": "Get approval for price quote"
}
]
This is the first part where I'm not sure what to do. I can assign the element ids individually for my html like this but what I'd like to do is count how many elements are in my array and create these for me. I won't know how many there are to make these manually. I'm sure there needs to be a loop but I couldn't figure it out.
document.getElementById('activityowner0').innerHTML = activities[0].activityOwner;
document.getElementById('activitydesc0').innerHTML = activities[0].activityDesc;
document.getElementById('activityowner1').innerHTML = activities[1].activityOwner;
document.getElementById('activitydesc1').innerHTML = activities[1].activityDesc;
document.getElementById('activityowner2').innerHTML = activities[2].activityOwner;
document.getElementById('activitydesc2').innerHTML = activities[2].activityDesc;
etc.
etc.
And then...once I have that part, I'd like to know how to create my html divs dynamically based on how many elements are in my array. Again, right now I don't know how many there are so I'm having to create a bunch of these and then have extras if I have too many.
<div class="container">
<div class="row"></div>
<div class="qa-message-list" id="wallmessages">
<br>
<div class="message-item" id="m0">
<div class="message-inner">
<div class="message-head clearfix">
<div class="user-detail">
<h5 class="handle">
<p id='activityowner0'></p>
</h5>
<div class="post-meta"></div>
</div>
</div>
<div class="qa-message-content">
<p id='activitydesc0'></p>
</div>
</div>
</div>
I know this is a big ask so just pointing me in the right direction would be very helpful. I hope my question was clear and I appreciate it.
One way for you to achieve this would be to loop through the objects in your activities array. From there you can use a HTML template to store the base HTML structure which you can clone and update with the values of each object before you append it to the DOM.
In addition, an important thing to note when generating repeated content in a loop: never use id attributes. You will either end up with duplicates, which is invalid as id need to be unique, or you'll end up with ugly code generating incremental/random id at runtime which is unnecessary. Use classes instead.
Here's a working example:
const activities = [{ "activityOwner": "Raymond Carlson", "activityDesc": "Complete all the steps from Getting Started wizard"}, {"activityOwner": "Flopsy McDoogal","activityDesc": "Called interested in March fundraising Sponsorship" }, { "activityOwner": "Gary Busy", "activityDesc": "Get approval for price quote" }]
const html = activities.map(obj => {
let item = document.querySelector('#template').innerHTML;
item = item.replace('{owner}', obj.activityOwner);
item = item.replace('{desc}', obj.activityDesc);
return item;
});
document.querySelector('#list').innerHTML = html.join('');
<div id="list"></div>
<template id="template">
<div class="container">
<div class="row"></div>
<div class="qa-message-list">
<div class="message-item">
<div class="message-inner">
<div class="message-head clearfix">
<div class="user-detail">
<h5 class="handle">
<p class="activityowner">{owner}</p>
</h5>
<div class="post-meta"></div>
</div>
</div>
<div class="qa-message-content">
<p class="activitydesc">{desc}</p>
</div>
</div>
</div>
</div>
</div>
</template>

Conditionally hide the nth element of a v-for loop without modifying the array. vue 3 composition api search function

I have a ref variable (foxArticles ), which holds a list that contains 100 items. In a v-for loop i loop over each value. As a result, i have 100 values rendered on the page.
<template>
<div class="news_container">
<div
v-for="article in foxArticles"
v-bind:key="article"
class="article_single_cell"
>
<div
class="news_box shadow hover:bg-red-100 "
v-if="containsKeyword(article, keywordInput)"
>
<div class="news_box_right">
<div class="news_headline text-red-500">
<a :href="article.url" target="_blank">
{{ article.title }}
</a>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
const foxArticles = ref([]);
</script>
I also have a search function, which returns the value, if it includes the passed in keyword. The function is used in the child of the v-for loop.
<div class="search_input_container">
<input
type="text"
class="search_input"
v-model="keywordInput"
/>
</div>
<script>
const keywordInput = ref("");
function containsKeyword(article, keywordInput) {
if (article.title.toLowerCase().includes(keywordInput.toLowerCase())) {
return article;
}
}
</script>
The problem is, i can't use .slice() on the foxArticles array in the v-for loop, because that screws up the search functionality, as it returns only the values from the sliced range.
How can i have the access the all of the values of the array, while not rendering all 100 of returned articles on the initial load?
Any suggestions?
I think your approach will make it incredibly complex to achieve. It would be simpler to always iterate over some set, this set is either filtered based on a search-term, or it will be the first 100 items.
I'm not very familiar yet with the Vue 3 composition api so I'll demonstrate with a regular (vue 2) component.
<template>
<div class="news_container">
<div
v-for="article in matchingArticles"
v-bind:key="article"
class="article_single_cell"
>
... news_box ...
</div>
</div>
</template>
<script>
export default {
...
computed: {
matchingArticles() {
var articles = this.foxArticles;
if (this.keywordInput) {
articles = articles.filter(article => {
return this.containsKeyword(article, this.keywordInput)
})
} else {
// we will limit the result to 100
articles = articles.slice(0, 100);
}
// you may want to always limit results to 100
// but i'll leave that up to you.
return articles;
}
},
....
}
</script>
Another benefit is that the template does not need to worry about filtering results.
ok, so i kind of came up with another solution, for which you don't have to change the script part...
instead of having one v-for loop , you can make two of them, where each one is wrapped in a v-if statement div
The first v-if statement says, If the client has not used the search (keywordInput == ''), display articles in the range of (index, index)
The second one says = If the user has written something (keywordInput != ''), return those articles.
<template>
<div class="news_container">
<!-- if no search has been done -->
<div v-if="keywordInput == ''">
<div
v-for="article in foxArticles.slice(0, 4)"
v-bind:key="article"
class="article_single_cell"
>
<div class="news_box shadow hover:bg-red-100 ">
<div class="news_box_right">
<div class="news_headline text-red-500">
<a :href="article.url" target="_blank">
{{ article.title }}
</a>
</div>
</div>
</div>
</div>
</div>
<!-- if searched something -->
<div v-else-if="keywordInput != ''">
<div
v-for="article in foxArticles"
v-bind:key="article"
class="article_single_cell"
>
<div
class="news_box shadow hover:bg-red-100 "
v-if="containsKeyword(article, keywordInput) && keywordInput != ''"
>
<div class="news_box_right">
<div class="news_headline text-red-500">
<a :href="article.url" target="_blank">
{{ article.title }}
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
im not sure how this impacts performance tho, but that's a problem for another day

Cannot read property 'map' of undefined even after API fetched correctly in console in Async

Hi I am trying to create a API based news search webapp using JS. So here's the function where I am getting the error
async function fetchUsers(searchTerm, searchLimit, sortBy){
//RETURNS PROMISES
let url=`some api parameters searchTerm, SearchLimit and sorby is passed`;
const res=await fetch(url);
const data=await res.json();
const article = [];
document.getElementById("container").innerHTML=`
${data.article.map(function(post){
//NEWSCARD
return(`
<ul id="news-article" style="list-style-type:none;">
<li class="article">
<div class="card mb-2">
<img class="article-image" class="card-img-top" src="${post.urlToimage}" alt="Card image cap">
<div class="card-body">
<h2 class="article-title" class="card-title">${post.title}</h2>
<p class="article-description" class="card-text">${truncateString(post.selftext, 100)}</p>
<a class="article-link" href="${post.url}" target="_blank
" class="btn btn-primary">Read More</a>
<hr>
<span class="article-author" class="badge badge-secondary">Subreddit: ${post.subreddit}</span>
<span class="badge badge-dark">Score: ${post.score}</span>
</div>
</div>
</li>
</ul>
`)
}
.join('')
)}} fetchUsers();
The main error is observed at ${data.article.map(function(post){
Could anyone suggest what could be the possible reason for it ?
Thanks
The only way to narrow down and find the issue is to first write the code in clean way.
I would suggest breakdown your code in manageable parts and try debug features in browser with debugger or console.log() works really well.
There are even better options like TDD where you can write your code and test it with the help of code to get more confidence with tools like jest
NOTE: pseudo code
function getPosts() {
return posts
}
function renderPost(post) {
return `<div>${post}</div> `
}
function render() {
const posts = getPosts()
console.info('post -> ', posts)
const postsMarkup = posts.map((post) => renderPost(post)).join('');
console.info('postsMarkup -> ', postsMarkup)
document.getElementById('container').innerHTML = postMarkup
}
What is happening is that the document.getElementByid section is being called asynchronously, perhaps at a time when the data has not returned anything, or when the article variable is not even declared, you can try this:
async function getData() {
const res = await fetch(url);
const data = await res.json();
populateList(data);
}
function populateList(article) {
const innerHTML = article.map(post => `
<ul id="news-article" style="list-style-type:none;">
<li class="article">
<div class="card mb-2">
<img class="article-image" class="card-img-top" src="${post.urlToimage}" alt="Card image cap">
<div class="card-body">
<h2 class="article-title" class="card-title">${post.title}</h2>
<p class="article-description" class="card-text">${truncateString(post.selftext, 100)}</p>
<a class="article-link" href="${post.url}" target="_blank" class="btn btn-primary">Read More</a>
<hr>
<span class="article-author" class="badge badge-secondary">Subreddit: ${post.subreddit}</span>
<span class="badge badge-dark">Score: ${post.score}</span>
</div>
</div>
</li>
</ul>
`);
document.getElementById('container').innerHTML = innerHTML;
}
The issue (from observing the error in the console) is that your code is attempting to access a property that doesn't exist.
Without access to the exact data that you're using, it'd be impossible to inform you of what the exact problem is. However, here are some steps I'd take to diagnose the issue:
Set up a breakpoint (debugger) or a console.log(data) right after data is assigned so that you can observe it.
Either click through the data variable in your browser's dev tools, or check the assumptions your code is making programmatically.
In terms of programmatically checking your code's assumptions:
Verify that data is an object by running typeof data should return object
Verify that data does in fact have a property article by running Object(data).hasOwnProperty('article')
Verify that data.article is an array by running Array.isArray(data.article)
Verify that each article within data has a selfText, subreddit, score (using step 2)
My guess is you'll find a misalignment between your code and the data in this process. If not, I'd like to know where you land.
You can also do this without the async and await keywords, and just use a callback within a .then statement, like this:
function fetchUsers(searchTerm, searchLimit, sortBy){
let url=`some url based on params`;
fetch(url)
.then( res => res.json() )
.then( data => populateList(data) )
}
function populateList(data) {
const innerHTML = data.articles.map(post => `
<ul id="news-article" style="list-style-type:none;">
<li class="article">
<div class="card mb-2">
<img class="article-image" class="card-img-top" src="${post.urlToimage}" alt="Card image cap">
<div class="card-body">
<h2 class="article-title" class="card-title">${post.title}</h2>
<p class="article-description" class="card-text">${truncateString(post.selftext, 100)}</p>
<a class="article-link" href="${post.url}" target="_blank" class="btn btn-primary">Read More</a>
<hr>
<span class="article-author" class="badge badge-secondary">Subreddit: ${post.subreddit}</span>
<span class="badge badge-dark">Score: ${post.score}</span>
</div>
</div>
</li>
</ul>
`);
document.getElementById('container').innerHTML = innerHTML;
}
fetchUsers(searchTerm, searchLimit, sortBy);
Also, hijacking Misir Jafarov's comment, the array in your data object is called articles, but you're trying to map from article. I feel like you'd still have problems without this typo, but that will definitely hang you up. Thanks #Misir Jafarov

Vue JS send data from v-for on button click

I'm new to Vue JS, and i'm having a little problem
i'm looping through an array and i have a button inside the div that i'm looping with
the idea is to get the data of the specified data after the click event
for example let's say i have this array numbers: [1,2,3,4,5] and i'm looping through it like this
<div v-for="number in numbers">
<p>{{ number }}</p>
<button v-on:click="getTheSelectedOne"> Get The Value </button>
</div>
i tried doing so
<button v-on:click="getTheValueOfTheSelectedOne(number)"> Get The Value </button>
but i got an error,
how can i achieve such a result ?
<div v-for="number in numbers">
Should be:
<div v-for="(number, index) in numbers" :key="index">
The following:
<button v-on:click="getTheSelectedOne"> Get The Value </button>
Should be:
<button v-on:click="getTheSelectedOne(number)"> Get The Value </button>
And you must have that method defined:
methods: {
getTheSelectedOne (number) {
// then number will be the number
console.log(number)
}
}

jQuery script works just for one button on the product catalog

I have a product catalog and I want to save on localstorage the products selected by the user.
The jquery script only gets the first product on each page on each click... it simply ignores the rest of the products, and the console prints the same object.
Here is my HTML+TWIG code
{% for products in pagination %}
<div class="product col-sm-6 col-lg-6 col-md-6 hero-feature">
<div id="{{attribute(products ,'id')}}" class="product thumbnail">
<img id="prodImage" class="img-responsive img-rounded" src="{{attribute(products ,'image')}}" style="width:150px;height:150px" >
<div class="product caption">
<h4 id="prodPrice" class="product pull-right"><b>{{ attribute (products, 'price') }}Lei</b></h4>
<h4 id="prodName" style="height:100px;width:200px;">
<a id="prodLink"
href="{{ attribute (products, 'affiliatelink') }}"
target="_blank">{{attribute ( products, 'name') }}</br></a>
</h4>
</div>
<div class="add-to-cart" class="product" >
<button id="buttonProd" class="oneButton btn btn-danger " value="Save" type="button">Adauga</button>
</div>
</div>
</div>
{% endfor %}
Here is the jquery script
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js"></script>
<script type='text/javascript' defer="defer">
$(document).ready(function() {
var prodId = $('.thumbnail').attr("id");
$("#buttonProd").on("click", 'button', function(event){
event.preventDefault();
var products =
{
prodID :$('.thumbnail').attr("id"),
prodName :$('#prodName').text(),
prodPrice :$('#prodPrice').text(),
prodImage :$('#prodImage').attr('src'),
prodLink :$('#prodLink').attr('href')
};
localStorage.setItem('products-' + prodId, JSON.stringify(products));
var retrievedObject = JSON.parse(localStorage.getItem('products-' + prodId));
console.log('retrievedObject: ', retrievedObject);
});
});
</script>
How can I make the script take each product proprieties on click. Thank you in advance.
In JQuery, the assumption is made that all ID's will be unique. Since you're repeating "#buttonProd", JQuery will only select the first one to bind the action to. If you want to bind to multiple elements, you'll either have to give each button a unique ID or use some other selector to attach your jQuery functionality.
From the documentation for the ID Selector:
Calling jQuery() (or $()) with an id selector as its argument will return a jQuery object containing a collection of either zero or one DOM element.
As the other answers have eluded to you should be using a different selector. I recommend simply adding a descriptive class to each element you wish to grab data from.
<div class="products">
<div class="product">
<div class="productName">First Product</div>
<div class="productPrice">5.00</div>
</div>
<div class="product">
<div class="productName">Second Product</div>
<div class="productPrice">4.00</div>
</div>
<button id="buttonProduct">Log Product Info</button>
</div>
If you notice in the above HTML each div that contains a product's name or a product's price shares the same class productName and productPrice respectively. In addition each container class for each product has the same class as well: product.
This will allow us to utilize the JQuery class selector $(".") to iterate over each product container. We do this using the .each() function. We use the .find() function to locate productName and productPrice in each iteration of the loop.
$(document).ready(function() {
$("#buttonProduct").click(function(){
var products = [];
// Notice the dot in $(".product") that is the class selector
// the .each iterates over every element that matches the preceding selector
$(".product").each(function(){
products.push({
// The .find() selects an element inside $(this)
// that matches the parameter (either .productName or .productPrice
productName : $(this) .find('.productName').html(),
productPrice : $(this).find('.productPrice').html()
});
});
console.log(products);
});
});
For a working example of this check out this jsfiddle. (I noticed you had console.log() in your code so that's where I output the result.)
You want to save the product in localStorage on clicking of corresponding button right?
for that instead of binding click event via jquery, put it in html and move the click code to a function saveProduct()
HTML:
<button id="buttonProd" class="oneButton btn btn-danger " value="Save" type="button" onclick="saveProduct({{attribute(products ,'id')}})">Adauga</button>
JS:
function saveProduct(event, prod_id){
event.preventDefault();
var products =
{
prodID :prod_id,
prodName :$('#'+prod_id+' #prodName').text(),
prodPrice :$('#'+prod_id+' #prodPrice').text(),
prodImage :$('#'+prod_id+' #prodImage').attr('src'),
prodLink :$('#'+prod_id+' #prodLink').attr('href')
};
localStorage.setItem('products-' + prod_id, JSON.stringify(products));
var retrievedObject = JSON.parse(localStorage.getItem('products-' + prod_id));
console.log('retrievedObject: ', retrievedObject);
});
}
You need to change #buttonProd from an id to a class. An id is only supposed to appear once on a page, so jQuery will only apply it to one. Change it to a class in your markup and your script and it should work fine. Same for prodName, prodPrice, prodImage, and prodLink. Anything that will be going inside the loop needs to be a class, and any id should be unique, like you have {{attribute(products ,'id')}}

Categories

Resources