Iteratively print JavaScript dictionary of objects into HTML divs - javascript

I'd appreciate some help with iteratively generating the beneath div based on the amount of items in a Javascript dictionary.
<div class="container" style="padding-bottom: 10px;">
<div class="dropdown" style="padding: 10px;">
<a href="#">TOP 3 PS5 HEADSETS<i class="fa fa-chevron-down"></i>
</a>
<ul>
<div id="links">
<center>
<p>3: ↓ INSERT TITLE FOR STEELSERIES ↓</p>
</center>
<div class="product">
<img src="img/products/h-steelseries.png">
<a class="link" href="INSERT LINK HERE">Read More</a>
</div>
<center>
<p>3: ↓ INSERT TITLE FOR OTHER↓</p>
</center>
<div class="product">
<img src="img/products/h-other.png">
<a class="link" href="INSERT LINK HERE">Read More</a>
</div>
</div>
</ul>
</div>
</div>
Beneath is the read.js file that contains the items in which I wish to generate the div class "product" for.
I'd really apprecaite any help with this.
var prod_obj = {
"headphone_products" : {
"title": "Steelseries",
"IMAGE": "h-steelseries.png",
"HREF" : "steelseries.html"
},
"other_products" : {
"title": "Other product",
"IMAGE": "h-other.png",
"HREF" : "other.html"
}
};
I have looked at other answers and couldn't find an example of a dictionary of object that was used to automatically generate divs. I intend on using this to list items on a website and would like to append objects to the dictionary and them to automatically generate a new div for each object once the script is executed.
Thank you for your time.

You can simply loop over the object and create the desired nodes inside the loop.
Here's a simpler version of the same.
var prod_obj = {
"headphone_products": {
"title": "Steelseries",
},
"other_products": {
"title": "Other product",
}
};
for (let keys in prod_obj) {
const div = document.createElement("div");
div.innerText = prod_obj[keys].title
document.body.appendChild(div)
}

You can use for-in loops and template literals to achieve what you want to achieve here.
const prod_obj = {
"headphone_products": {
"title": "Steelseries",
"image": "h-steelseries.png",
"href": "steelseries.html"
},
"other_products": {
"title": "Other product",
"image": "h-other.png",
"href": "other.html"
}
};
const div = document.getElementById('insertHere');
for (let products_key in prod_obj) {
let {title, image, href} = prod_obj[products_key];
let html = `<p>Title: ${title}, Image: ${image}, href: ${href}</p>`;
div.insertAdjacentHTML('beforeend', html);
}
<div id="insertHere">
</div>

What you describe sounds like a suitable candidate for a template which, according to the documentation on MDN says:
The HTML Content Template () element is a mechanism for
holding HTML that is not to be rendered immediately when a page is
loaded but may be instantiated subsequently during runtime using
JavaScript.
The following uses a simple class to load a new instance of the designated template for each product found within the source data ( what you refer to as a dictionary ). Once the template has been loaded from the shadows you can manipulate the contents as you wish. If you change the design of the template you change the design of the final layout. In the original HTML there is no span element surrounding the individual products but the way I wrote the template loader( for a specific job ) clones the first child element entirely - so a span will not affect layout unless styled specifically to do so.
class TemplateLoader{
constructor( id ){
this.id=id;
return this.create();
};
gettemplate(){
return document.querySelector( 'template[ data-id="'+this.id+'" ]' ) || false
};
clone(){
let tmpl=this.gettemplate();
return tmpl ? tmpl.content.firstElementChild.cloneNode( true ) : false;
};
gettarget(){
return document.querySelector( 'div[ id="'+this.id+'" ]' ) || false;
};
create(){
let tmpl=this.clone();
if( tmpl ){
let target=this.gettarget();
target.appendChild( tmpl );
return tmpl;
}
return false;
};
};
var prod_obj = {
'headphone_products' : {
'title': 'steelseries',
'image': 'h-steelseries.png',
'href' : 'steelseries.html'
},
'other_products' : {
'title': 'other product',
'image': 'h-other.png',
'href' : 'other.html'
},
'banana':{
'title':'curvy & yellow',
'image':'b-a-nana.png',
'href':'banana.html'
}
};
let id='links';
Object.keys( prod_obj ).forEach( cat => {
let data=prod_obj[ cat ];
let oTmpl=new TemplateLoader( id );
oTmpl.querySelector('center > p').textContent=data.title;
oTmpl.querySelector('div.product > img').src=['img/products',data.image].join('/');
oTmpl.querySelector('div.product > a.link').href=data.href;
});
<!-- regular HTML -->
<div class='container'>
<div class='dropdown'>
<a href='#'>TOP 3 PS5 HEADSETS<i class='fa fa-chevron-down'></i></a>
<ul>
<div id='links'>
<!-- items will be populated here -->
</div>
</ul>
</div>
</div>
<!-- our template that will be used to generate new content within the above, regular HTML' -->
<template data-id='links'>
<span>
<center>
<p></p>
</center>
<div class='product'>
<img />
<a class='link'>Read More</a>
</div>
</span>
</template>

The traditional way to add content to the DOM on-the-fly is to use a series of calls to createElmenent and appendChild (which is less error-prone than just inserting HTML strings). And you can loop through your data object's keys and extract the details you need to configure your new DOM elements. This script does both of these things in a function called updateDOM, which invokes the appendProductDetails function once per product.
I changed the hrefs to create functional (if arbitrary) links, and of course the images don't show up because they don't exist on StackOverflow's server. See the in-code comments for further explanation.
const currentProds = getProductsToShow();
updateDOM(currentProds);
function updateDOM(prod_obj) {
// Identifies parent div
const linksDiv = document.getElementById("links");
// Clears parent div
linksDiv.innerHTML = "";
// Loops through productName (keys) in prod_obj
const productNames = Object.keys(prod_obj);
for (let productName of productNames) {
// Gets details (inner object) for each product
const details_obj = prod_obj[productName];
// Creates, configures, and appends new elements for each product
appendProductDetails(linksDiv, details_obj);
}
}
function appendProductDetails(parentElement, detailsObject) {
const
// Gets local copies of values via "destructuring"
{ title, image, href } = detailsObject,
path = "img/products/", // Defines path to images
// Creates elements to add to the DOM
productDiv = document.createElement("div"),
titleP = document.createElement("p"),
img = document.createElement("img"),
anchor = document.createElement("a");
// Configures newly created elements
productDiv.classList.add("product");
titleP.textContent = title;
img.src = path + image;
img.alt = image;
anchor.classList.add("link");
anchor.href = href;
anchor.textContent = "Read More";
// Puts children into productDiv
productDiv.appendChild(titleP);
productDiv.appendChild(img);
productDiv.appendChild(anchor);
// Attaches everything to the DOM
parentElement.appendChild(productDiv);
}
// Provides demo data
function getProductsToShow() {
const productsObj = {
"headphone_products": {
"title": "Steelseries",
"image": "h-steelseries.png", // In img/products/
"href": "https://stackoverflow.com"
},
"other_products": {
"title": "Other product",
"image": "h-other.png",
"href": "https://eloquentjavascript.net/"
}
};
return productsObj;
}
.container{ width: 250px; text-align: center; }
.dropdown > a{ text-decoration: none; }
p{ margin: -0.1rem 0; font-size: 1.2rem; }
.product{ padding: 0.5rem ; }
.link{ margin-left: 1rem; }
<div class="container">
<div class="dropdown">
PS5 HEADSETS
<div id="links"></div>
</div>
</div>
(A more modern approach would be to repeatedly clone the contents of a template element and to use slot elements to insert corresponding product details into each new instance.)

Related

Retain order of elements using different selectors with puppeteer on a page with a flat structure

I have a page with a flat structure where the order the elements appear on the page defines the structure I need to maintain when saving the data.
<div class='parent'>
<h3 class="'group">Furniture</h3>
<h4 class="category">Office</h4>
<ul>
<li>Desk</li>
<li>Chairs</li>
</ul>
<h4 class="category">Home</h4>
<ul>
<li>Couches</li>
<li>Tables</li>
<li>Bookshelves</li>
</ul>
<h4 class="category">Outdoor</h4>
<ul>
<li>Shade Umbrellas</li>
</ul>
</div>
<h3 class="'group">Toys</h3>
<h4 class="category">Toddlers</h4>
<h4 class="category">Kids</h4>
<ul>
<li>Balls</li>
<li>Dolls</li>
</ul>
</div>
There are enough good query selectors for me to pull everything I need from the page, but I can't figure out how to tell puppeteer to organize categories by groups and li elements by categories.
-- added on edit --
Since posting this, I have put all of the HTML from the parent div into a variable using:
let allHTML = await page.evaluate(() => {
return Array.from(document.querySelectorAll('.parent')).map(el => el.innerHTML);
})
I am now working on slicing up the allHTML variable to get blocks of text binned by group and category. Then, I can use these blocks to recreate the organizational structure on the page.
This should work, but it is cumbersome. I'm hoping there is a more straightforward way to have puppeteer retain the order of elements selected with different query selectors allowing me to determine which instances of .category (or <h4>) come after each instance of .group (or <h3>)element.
The question is not well defined, so I make some assumptions:
build a nested object from group ==> category => list items
use group class to determine the group
use category class to determine the category
a category belongs to the previous group sibling
a list belongs to the previous category sibling
Solution:
const html = `<h3 class="group">Furniture</h3> <h4 class="category">Office</h4> <ul> <li>Desk</li> <li>Chairs</li> </ul>
<h4 class="category">Home</h4> <ul> <li>Couches</li> <li>Tables</li> <li>Bookshelves</li> </ul> <h4 class="category">Outdoor</h4> <ul> <li>Shade Umbrellas</li> </ul> </div> <h3 class="'group">Toys</h3> <h4 class="category">Toddlers</h4> <h4 class="category">Kids</h4> <ul> <li>Balls</li> <li>Dolls</li> </ul>`;
const el = document.createElement("div");
el.innerHTML = html;
let result = {};
let groupVal = '';
let categoryVal = '';
Array.from(el.children).forEach(e => {
if(e.classList.contains('group')) {
groupVal = e.innerText;
result[groupVal] = {};
} else if(e.classList.contains('category')) {
categoryVal = e.innerText;
result[groupVal][categoryVal] = {};
} else if(e.tagName === 'UL') {
result[groupVal][categoryVal] = Array.from(e.children).map(li => li.innerText);
}
});
console.log(JSON.stringify(result, null, ' '));
Output:
{
"Furniture": {
"Office": [
"Desk",
"Chairs"
],
"Home": [
"Couches",
"Tables",
"Bookshelves"
],
"Outdoor": [
"Shade Umbrellas"
],
"Toddlers": {},
"Kids": [
"Balls",
"Dolls"
]
}
}
Note: Instead of using .createElement() on html text you could get the inner HTML from the DOM (here done just to demo the solution)

What is the correct syntax for this JS loop including a lot of document.getElementById?

I am making a tournament table on HTML and JS. I have a couple of hundred of « match boxes » to which I want to assign a range of different datas (such as a logo/flag, name of the team, score and more).
How could I write a loop that would execute the following potential thousands of lines :
document.getElementById("matchBox1_team1").innerHTML = teams[1].name;
document.getElementById("matchBox1_flag1").innerHTML = teams[1].flag;
document.getElementById("matchBox1_team2").innerHTML = teams[2].name;
document.getElementById("matchBox1_flag2").innerHTML = teams[2].flag;
document.getElementById("matchBox2_team1").innerHTML = teams[3].name;
document.getElementById("matchBox2_flag1").innerHTML = teams[3].flag;
document.getElementById("matchBox2_team2").innerHTML = teams[4].name;
document.getElementById("matchBox2_flag2").innerHTML = teams[4].flag;
etc…
something like this but I havent got the syntax right :
var j=1;
for (i=0;i<1000;i+=2){
document.getElementById("matchBox("j")_team1").innerHTML = teams[i].name;
document.getElementById("matchBox("j")_flag1").innerHTML = teams[i].flag;
document.getElementById("matchBox("j")_team2").innerHTML = teams[i+1].name;
document.getElementById("matchBox("j")_flag2").innerHTML = teams[i+1].flag;
j++}
Thank you!
It really depends on your method of retrieving data. If you are using ajax then create table rows with js and append those rows to the table so you will only have to get only a single element.
var matchesTable = document.getElementById('matches');
function addMatch(name,flag){
var row = document.createElement('tr');
var nameColumn = document.createElement('th');
nameColumn.innerText = name;
var flagColumn = document.createElement('th');
flagColumn.innerText = flag;
row.append(nameColumn);
row.append(flagColumn);
matchesTable.append(row);
}
Wherever you get that data just call addMatch function and pass all the details as parameters and create th elements for all those details and append to that table.
Else you will need thousands of those rows already created for you to get them and then add those details.
Using id attributes with sequential numbers is a code smell: it is bad practice. Instead you should use classes where their sequence follows from their natural order in the DOM.
For instance:
let teams = [
{ name: "New York Knicks", flag: "flagged" },
{ name: "Toronto Raptors", flag: "cleared" },
{ name: "Sacramento Kings", flag: "cleared" },
{ name: "Miami Hear", flag: "pending" }
];
let teamElems = document.querySelectorAll(".matchBox .team");
let flagElems = document.querySelectorAll(".matchBox .flag");
teams.forEach(({name, flag}, i) => {
teamElems[i].textContent = name;
flagElems[i].textContent = flag;
});
.matchBox { border: 1px solid }
<div class="matchBox">
<div class="team"></div>
<div class="flag"></div>
</div>
<div class="matchBox">
<div class="team"></div>
<div class="flag"></div>
</div>
<div class="matchBox">
<div class="team"></div>
<div class="flag"></div>
</div>
<div class="matchBox">
<div class="team"></div>
<div class="flag"></div>
</div>
You should use appendChild method into the for loop you have created.
Also one hint is you have to create the element (createElement) which you want to insert in the for loop and insert it in the table you have created using appendChild.
You can refer to syntax on w3cschool

Generating dynamic html cards from a javascript array

I would like to display some card elements in HTML. I would like to get the variables of the card element from a javascript array.(Such as title etc..).
The card element number will also depend on the Javascript array size. I have looked at other questions such as generating dynamic tables but as I have a long customized html code block, it doesn't work.
This is for a website that I am building. I already got the variables from an api end point with a HTTP get request but am unable to display as I wish to. I have looked at many similar questions but can't find the answer that I am looking for.
This is the script for getting the variables with the HTTP get request
<script>
const request = new XMLHttpRequest();
request.open('GET', 'api/seminars', true);
request.onload = function() {
// Begin accessing JSON data here
const data = JSON.parse(this.response);
if (request.status >= 200 && request.status < 400) {
data.forEach(resultArray => {
document.getElementById('output').innerHTML = resultArray.name;
document.getElementById('description').innerHTML =
resultArray.description;
document.getElementById('date').innerHTML = resultArray.date;
});
} else {
console.log('error');
}
};
request.send();
</script>
HTML CODE :
<div id="accordion">
<div class="card">
<div class="card-header" id="headingOne">
<h5 class="mb-0">
<button class="btn btn-link" data-toggle="collapse" data- target="#collapseOne" aria-expanded="true"
aria-controls="collapseOne">
</button>
</h5>
</div>
<div id="collapseOne" class="collapse show" aria-labelledby="headingOne" data-parent="#accordion">
<div class="card-body">
<h5 id="name"></h5>
<p id="description"></p>
<p id="description"></p>
...
</div>
</div>
</div>
</div>
And I have continued html code from here on not relevant ..
If there are 3 objects in my array, I would like to create 3 different cards and display name,description.. attributes. But my code only creates one card and displays last object's attributes.
You code never really "creates" elements based on your API call - It just updates (ie, overwrites) the existing dom elements by updating the innerHTML of fixed elements referenced by their IDs.
If my interpretation of your code is correct, you should be only seeing the LAST item in your API result. There are also some other weird stuff going on like duplicate IDs which Im guessing are typos
To fix this, create a new div.card-body for each item your API returns and append it to your container
const apiResult = [{
title: "title1",
description: "desc1",
output: "out1"
}, {
title: "title2",
description: "desc2",
output: "out2"
}, {
title: "title3",
description: "desc3",
output: "out3"
}];
const container = document.getElementById('accordion');
apiResult.forEach((result, idx) => {
// Create card element
const card = document.createElement('div');
card.classList = 'card-body';
// Construct card content
const content = `
<div class="card">
<div class="card-header" id="heading-${idx}">
<h5 class="mb-0">
<button class="btn btn-link" data-toggle="collapse" data-target="#collapse-${idx}" aria-expanded="true" aria-controls="collapse-${idx}">
</button>
</h5>
</div>
<div id="collapse-${idx}" class="collapse show" aria-labelledby="heading-${idx}" data-parent="#accordion">
<div class="card-body">
<h5>${result.title}</h5>
<p>${result.description}</p>
<p>${result.output}</p>
...
</div>
</div>
</div>
`;
// Append newyly created card element to the container
container.innerHTML += content;
})
.card {
padding: 1rem;
border: 1px solid black;
margin: 1rem;
}
<div id="accordion">
</div>
Note:
While this works, it's not very scalable. There are some great templating libraries out there with much more advanced interpolation features that you can consider if you have to do something like this in many places (and maintain it)
jsRender
Underscore Templates
Handlebars
UI frameworks like Vue, React, Angular wrap templating with binding to data models and handle auto updating the DOM for you to make things like this easier. Worth investigating if you have a lot of dynamically updating parts on your webpage
Suppose you have already queried the API, a way of doing it could be:
// Supposing you already have queried the API and have your data
let data = [
{name: 'name0', description: 'description', date: 'XX/XX/XXXX'},
{name: 'name1', description: 'description', date: 'XX/XX/XXXX'},
{name: 'name2', description: 'description', date: 'XX/XX/XXXX'},
]
data.forEach(res => {
let card = document.createElement("div");
let name = document.createTextNode('Name:' + res.name + ', ');
card.appendChild(name);
let description = document.createTextNode('Description:' + res.description + ', ');
card.appendChild(description);
let date = document.createTextNode('date:' + res.date);
card.appendChild(date);
let container = document.querySelector("#container");
container.appendChild(card);
});
<!-- At one point where you want to generate the cards you put this container -->
<div id="container"></div>
Yours was not working since you updated always the same elements instead of appending new ones :)
You are overwriting the description's innerHTML with every repetition of your for loop.
Change this
document.getElementById('output').innerHTML = resultArray.name;
document.getElementById('description').innerHTML = resultArray.description;
document.getElementById('date').innerHTML = resultArray.date;
to this
var element = document.createElement('div'); // create new div
var name = document.createElement('h4');
name.innerHTML = resultArray.name;
element.appendChild(name);
var description = document.createElement('p');
description.innerHTML = resultArray.description;
element.appendChild(description);
var date = document.createElement('span');
date.innerHTML = resultArray.date;
element.appendChild(date);
document.body.appendChild(element);

Refer to object in second column

I am trying to refer to object in second column (object link_icon) and based on .json data, I am assigning new url.
Structure of my html:
<div class="details">
<h1 id="title">title</h1>
<h3 id="author">
author
</h3>
</div>
<a href="https://www.google.pl/">
<div><i class="link_icon"></i></div>
</a>
$(function(){
var url = 'img_and_data/' + randomNumbers[bgImgIndex] +'.json';
var title = $("#title");// the id of the heading
var author = $("#author a");// id of author
var link_icon = $(".link_icon a"); // <------- problem probably is here
$.getJSON(url, function (data) {
title.text(toTitleCase(data.title));
author.text(data.copyright);
author.attr("href", data.url);
link_icon.attr("href", data.permalink);
});
}
Here is my full code:
https://jsfiddle.net/cvv0sen8/8/
How should I refer to link_icon to modify href?
Could you help me?
JSON:
{
"title": "example title",
"copyright": "example author",
"permalink": "https://www.bing.com/",
"url": "http://www.yahoo.com"
}
var link_icon = $(".link_icon a");
Your selector here means "every a tag which has a tag with class link_icon as a parent". Problem is your html document doesn't contain any a tag in link_icon: your selector will return nothing.
You may change your html like this:
<div><a class="link_a" href="#"><i class="link_icon"></i></a></div>
And your js:
var link_a = $(".link_a");
// ...
link_a.attr("href", data.permalink);

determining which item of a list was clicked- javascript

I have a winJS list which is full of images. When a user clicks on an image, they are being sent to a second page. Is there any way of being able to determine which image the user clicked on page 1 (each image, when clicked, goes to the same page 2 but with a custom div). Here is my list declared and then I am push items to it:
var names_Array = [];
var names_List = new WinJS.Binding.List(names_Array);
var idPL;
names_List.push({ name: "man 1", image: "image/man1.png", ClientID: "1111" });
names_List.push({ name: "man 2 ", image: "image/man2.png", ClientID: "2222" });
idPL = document.getElementById("names_List");
idPL.addEventListener("iteminvoked", onclickItem);
function onclickItem(e) {
console.log("Item clicked");
}
A the minute I am populating the div based on which item was last pushed to the list, but I need this to be more flexible and be able to select the first item (even after a second one has been added)
EDIT: I AM NOW GETTING AN ERROR 'Unable to get property 'addEventListener' of undefined or null reference'. I THOUGHT I had defined it in my code above
To get the detail of which item is clicked in winjs listview we can use itemInvoke event of the Winjs listview.
<div id="itemsList" data-win-control="WinJS.Binding.Template" style="display:none;">
<div data-win-bind="className: type">
<img src="#" style="width: 100px; height: 100px;"
data-win-bind="alt: title; src: picture" />
<div>
<h4 data-win-bind="innerText: title" style="width:100px; height:20px;"></h4>
</div>
<div>
<img id="like"src="images/like.png" class="win-interactive" data-win-bind="alt:title; onclick: LikeClick;" />
</div>
</div>
</div>
<div id="UserListView" data-win-control="WinJS.UI.ListView" style="border-top: 5px solid #000; min-width:500px;"
data-win-options="{
selectionMode:'multi',
itemTemplate:select('#itemsList'),
layout:{
type:WinJS.UI.GridLayout}}"
>
</div>
then in the js
UserListView.addEventListener("iteminvoked", ItemInvoke);
function ItemInvoke(evt) {
var name=evt.detail.itemPromise._value.data.title;
var picturePath=evt.detail.itemPromise._value.data.picture;
}
Here in this listview we are only displaying the image and title of the user.
You can reformat your image urls to include:
A hash: image.html#man1.png
A query string: image.html?image=man1.png
And then use those to extract information from the URL using JavaScript for when the 2nd page is loaded.
Either assign an eventListener globally and check which was clicked and do something, or assign an eventListener per item and do whatever you require.
UPDATE: I am unable to demonstrate using WinJS, but this example should suffice.
HTML
<ul id="names_List"></ul>
Javascript
var names_Array = [],
idPL = document.getElementById("names_List");
names_Array.push({
name: "man 1",
image: "image/man1.png",
ClientID: "1111"
});
names_Array.push({
name: "man 2 ",
image: "image/man2.png",
ClientID: "2222"
});
names_Array.forEach(function (item) {
var li = document.createElement("li"),
img = document.createElement("img");
img.name = item.name;
img.src = item.img;
li.appendChild(img);
idPL.appendChild(li);
});
function onclickItem(e) {
if (e.target.tagName === "IMG") {
console.log("Item clicked", e.target.name);
}
}
idPL.addEventListener("click", onclickItem, false);
On jsfiddle
The best and easiest way you can do this is by declaring a namespace with the value of the image inside an eventlistener for selectionchanged, like this:
listview.addEventListener("selectionchanged", function (item) {
listview.selection.getItems().done(function (items) {
WinJS.Namespace.define("imageInformation", {
seletedImage: items[0].data.image,
});
}, false);
After the namespace is declared, you can access it on the other page:
var path = imageInformation.selectedImage;

Categories

Resources