this is written in JS
i cant seem to make the MovieDetails button work at all.
function searchMovie(query) {
const url = `https://imdb8.p.rapidapi.com/auto-complete?q=${query}`;
fetch(url, options)
.then(response => response.json())
.then(data => {
const list = data.d;
list.map((item) => { //makes a list of each individual movie from the data
const name = item.l; // holds the name of movie
const poster = item.i.imageUrl; // holds the poster, given by the data
const detail = item.id // holds the ttid of the movie
// below is what shows the poster, movie name, etc
const movie =
`
<body>
<div class="colmd3">
<div class = "well text-center">
<li><img src="${poster}">
<h2>${name}</h2>
</li>
<button type = "button" id = "MovieDetails" class="btn btn-primary" href="#">Movie Details</button>
<script>
document.getElementById('MovieDetails').addEventListener("click",myFunction);
function myFunction(){
console.log(detail)
}
</script>
</div>
</div>
</body>
`;
document.querySelector('.movies').innerHTML += movie; // returns the first element movies and poster to movie div
//console.log()
});
document.getElementById("errorMessage").innerHTML = "";
})
.catch((error) => {
document.getElementById("errorMessage").innerHTML = error;
});
// we should make a condition here for when a new item is placed here there will be a page refresh
// setTimeout(() => {
// location.reload(); }, 2000);
}
the function above will make an api call and then save the results into list, i can hold specific elements of the list in the three const's and const movie will output the movie poster, name and display it.
I want to make a button for each movie that when clicked will output the id of the movie which is held in const details.
But i cant figure out how to make it work, i have tried (button onclick = function ..) and (document.getElementById...) but it says that getElementById cant be null.
i know that this seems like a silly problem but i cant seem to figure how to make the button actually output something useful or any other way to make a button be mapped out to each api call.
You're heading in the right direction but there are a couple of pain-points with your code as the other commenters have indicated.
Your template string is adding a brand new body element to the page for each movie where there should just be one for the whole document. Nice idea to use a template string though - by far the simplest method to get new HTML on to the page.
Adding JS to the page dynamically like that is going to end up causing you all kinds of problems - probably too many to mention here, so I'll just skip to the good part.
First remove the body element from the template string, and perhaps tidy up the remaining HTML to be a little more semantic. I've used section here but, really, anything other than having lots of divs is a step in the right direction.
Second: event delegation. Element events "bubble up" the DOM. Instead of attaching a listener to every button we can add one listener to the movie list containing element, and have that catch and process events from its children.
(Note: in this example, instead of logging the details to the console, I'm adding the details to the HTML, and then allowing the button to toggle the element on/off.)
// Cache the movie list element, and attach a listener to it
const movieList = document.querySelector('.movielist');
movieList.addEventListener('click', handleClick);
// Demo data
const data=[{name:"movie1",poster:"img1",details:"Details for movie1."},{name:"movie2",poster:"img2",details:"Details for movie2."},{name:"movie3",poster:"img3",details:"Details for movie3."}];
// `map` over the data to produce your HTML using a
// template string as you've done in your code (no body element)
// Make sure you `join` up the array that `map` returns into a
// whole string once the iteration is complete.
const html = data.map(obj => {
return `
<section class="movie">
<header>${obj.name}</header>
<section class="details">${obj.details}</section>
<button type="button">Movie Details</button>
</section>
`;
}).join('');
// Insert that HTML on to the movie list element
movieList.insertAdjacentHTML('beforeend', html);
// This is the handler for the listener attached to the
// movie list. When that element detects an event from a button
// it finds button's previous element sibling (the section
// with the `.details` class), and, in this case, toggles a show
// class on/off
function handleClick(e) {
if (e.target.matches('button')) {
const details = e.target.previousElementSibling;
details.classList.toggle('show');
}
}
.movie { border: 1px solid #555; padding: 0.5em;}
.movie header { text-transform: uppercase; background-color: #efefef; }
.details { display: none; }
.show { display: block; }
button { margin-top: 0.5em; }
<section class="movielist"></section>
Additional documentation
insertAdjacentHTML
matches
classList
toggle
Related
I am fetching a list of posts from an API and displaying them on webpage. Now, there is a Delete button associated with each post which when clicked should remove the post.
index.html
<template id="single-post">
<li class="post-item">
<h2></h2>
<p></p>
<button>DELETE</button>
</li>
</template>
<ul class="posts"></ul>
app.js
const listElement = document.querySelector('.posts');
const postTemplate = document.getElementById('single-post');
const listOfPosts = await sendHttpRequest(
'GET',
'https://jsonplaceholder.typicode.com/posts'
);
// listOfPosts is already in parsed format
for (const post of listOfPosts) {
const postEl = document.importNode(postTemplate.content, true);
postEl.querySelector('h2').textContent = post.title.toUpperCase();
postEl.querySelector('p').textContent = post.body;
listElement.append(postEl);
const btn = postEl.querySelector('button');
console.log(btn, postEl);
btn.addEventListener('click', () => {
postEl.remove();
});
}
The above code only fetches first post only and throws
Uncaught (in promise) TypeError: Cannot read properties of null (reading 'addEventListener')
at HTMLButtonElement.fetchPosts
When I remove the Event Listener, the code works fine.
I guess this is something to do with importNode method since I have done similar things with createElement and they worked fine
EDIT
I did some little experimenting. The JSON post object returned by API also consisted of an id field. So, I basically added that id to each button that was being created.
Another thing is I used event delegation to remove() the li whose button is clicked.
And very surprisingly It works
const listElement = document.querySelector('.posts');
const postTemplate = document.getElementById('single-post');
const listOfPosts = await sendHttpRequest(
'GET',
'https://jsonplaceholder.typicode.com/posts'
);
// listOfPosts is already in parsed format
for (const post of listOfPosts) {
const postEl = document.importNode(postTemplate.content, true);
postEl.querySelector('h2').textContent = post.title.toUpperCase();
postEl.querySelector('p').textContent = post.body;
postEl.querySelector('button').id = post.id; // HERE
listElement.append(postEl);
}
// delete the li element
listElement.addEventListener('click', (event) => {
if(event.target.tagName === 'BUTTON') {
console.log(event.target);
event.target.parentElement.remove();
}
})
when clicked on first list post's DELETE button, it consoles
<button id="1">DELETE</button>
and removes that item.
This bascially proves that the button tag is certainly there since we are able to query select and set its id.
Strangely, when consoled it shows null.
Your code errors out here:
btn.addEventListener('click', () => {
postEl.remove();
});
The error message clarifies that btn is null, which means that postEl.querySelector('button') returned null, which means that there is no button tag inside postEl.
You will need to carefully look at the result of document.importNode(postTemplate.content, true) and see what it contains. You will see that it does not contain a button tag. So, either the button was not added, in which case you will need to adjust importNode, or, the button is not a button tag, but something else, like <input type="button" value="foo"> for example.
I have been working on a website that displays projects completed by students for their Senior Project. I wanted to be able to sort by project category to make it easier for the user. I added that functionality in and everything works well. I then decided since there are going to be multiple years worth of Senior projects on my website, I want to filter by year as well. However, I can't just add more buttons for years. For example, if I filtered by class of 2022, I wouldn't be able to filter by category within the class of 2022. More specifically, when I press 2022, it shows all of the students in that class. Then, if I press Creative Project for example, it goes back to showing every single Creative Project regardless of year instead of from the class of 2022. My initial thought process was to just make another function and div for the grad year buttons. However, that does not change anything. Here is my JavaScript code:
function filterProject(value){
let buttons = document.querySelectorAll(".button-value");
buttons.forEach((button) => {
if(value.toUpperCase() == button.innerText.toUpperCase()){
button.classList.add("active");
}else{
button.classList.remove("active");
}
});
let elements = document.querySelectorAll(".card");
elements.forEach((element) => {
if(value == "all"){
element.classList.remove("hide");
}
else{
//having a space messes it up, make it _
if(element.classList.contains(value.replace(" ", "_"))){
element.classList.remove("hide");
}
else{
element.classList.add("hide");
}
}
});
}
function filterProject2(value){
let buttons = document.querySelectorAll(".grad-button");
buttons.forEach((button) => {
if(value.toUpperCase() == button.innerText.toUpperCase()){
button.classList.add("active");
}else{
button.classList.remove("active");
}
});
let elements = document.querySelectorAll(".card");
elements.forEach((element) => {
if(value == "All Years"){
element.classList.remove("hide");
}
else{
//having a space messes it up, make it _
if(element.classList.contains(value.replace(" ", "_"))){
element.classList.remove("hide");
}
else{
element.classList.add("hide");
}
}
});
}
Here is the relevant HTML code as well:
<div id ="buttons">
<button class = "button-value" onclick="filterProject('all')">All</button>
<button class = "button-value" onclick="filterProject('Creative Project')">Creative Project</button>
<button class = "button-value" onclick="filterProject('Developing Voice')">Developing Voice</button>
<button class = "button-value" onclick="filterProject('Interdisciplinary Fusion')">Interdisciplinary Fusion</button>
<button class = "button-value" onclick="filterProject('Personal Writing')">Personal Writing</button>
<button class = "button-value" onclick="filterProject('Curriculum Designer')">Curriculum Designer</button>
<button class = "button-value" onclick="filterProject('Internship')">Internship</button>
</div>
<div id ="gradbuttons">
<button class = "grad-button" onclick="filterProject2('All Years')">All Years</button>
<button class = "grad-button" onclick="filterProject2('2021')">2021</button>
<button class = "grad-button" onclick="filterProject2('2022')">2022</button>
</div>
I know that I could just add another page that separates the projects by grad year and has different buttons for each year, but I want to use JavaScript and make the website cleaner. Any suggestions will help. Thanks!
I would use an object var filters = { "category": "", "year": "" }; as a state variable to store the current filter options. Since you do similar logic in both of your filter functions, I would combine them into a single function that accepts an additional parameter filterType which is either "category" or "year". Then in your filtering logic, you can update the current filter for the appropriate filter type and make sure every condition in your filters is met.
Instead of using a unique class to hide elements, you can use two different classes, for example category-hidden and year-hidden.
Inside category filter:
button.classList.add('category-hidden')
Inside year filter:
button.classList.add('year-hidden')
Then, in your css, you hide the elements that have both classes.
<style>
.category-hidden .year-hidden {
display: none;
}
</style>
EDIT
I've just realized that this approach won't work if you select only one filter. So, you'll need to add classes to the elements' container indicating which filters are currently active. For example, if you activate the year filter add the class year-filter-active.
<div id="container" class="year-filter-active">
...
elements
...
</div>
And in your css put the following rules:
<style>
div.year-filter-active .year-hidden, div.category-filter-active .category-hidden {
display: none;
}
</style>
I create directed graphs like the following from wikidata with the help of networkx and nxv. The result is an svg file which might be embedded in some html page.
Now I want that every node and every edge is "clickable", such that a user can add their comments to specific elements of the graph. I think this could be done with a modal dialog popping up. This dialog should know from which element it was triggered and it should send the content of the textarea to some url via a post request.
What would be the best way to achieve this?
Wrapped in a W3C standard Web Component (supported in all Modern Browsers) you can make it generic for any src="filename.svg"
Simple example: How to get SVG document data to be inserted into the DOM?
More complex example:
<graphviz-svg-annotator src="https://graphviz.org/Gallery/directed/fsm.svg">
</graphviz-svg-annotator>
The SVG is loaded with an async fetch
Nodes and Edges are clickable in this SO Snippet
add your own, better modal, window and saving to database
Try the SVGs from: https://graphviz.org/Gallery/directed/Genetic_Programming.html
<graphviz-svg-annotator src="fsm.svg"></graphviz-svg-annotator>
<graphviz-svg-annotator src="Linux_kernel_diagram.svg"></graphviz-svg-annotator>
<style>
svg .annotate { cursor:pointer }
</style>
<script>
customElements.define('graphviz-svg-annotator', class extends HTMLElement {
constructor() {
let loadSVG = async ( src , container = this.shadowRoot ) => {
container.innerHTML = `<style>:host { display:inline-block }
::slotted(svg) { width:100%;height:200px }
</style>
<slot name="svgonly">Loading ${src}</slot>`;
this.innerHTML = await(await fetch(src)).text(); // load full XML in lightDOM
let svg = this.querySelector("svg");
svg.slot = "svgonly"; // show only SVG part in shadowDOM slot
svg.querySelectorAll('g[id*="node"],g[id*="edge"]').forEach(g => {
let label = g.querySelector("text")?.innerHTML || "No label";
let shapes = g.querySelectorAll("*:not(title):not(text)");
let fill = (color = "none") => shapes.forEach(x => x.style.fill = color);
let prompt = "Please annotate: ID: " + g.id + " label: " + label;
g.classList.add("annotate");
g.onmouseenter = evt => fill("lightgreen");
g.onmouseleave = evt => fill();
g.onclick = evt => g.setAttribute("annotation", window.prompt(prompt));
})
}
super().attachShadow({ mode: 'open' });
loadSVG("//graphviz.org/Gallery/directed/"+this.getAttribute("src"));
}});
</script>
Detailed:
this.innerHTML = ... injects the full XML in the component ligthDOM
(because the element has shadowDOM, the lightDOM is not visible in the Browser)
But you only want the SVG part (graphviz XML has too much data)... and you don't want a screen flash; that is why I put the XML .. invisible.. in lightDOM
A shadowDOM <slot> is used to only reflect the <svg>
with this method the <svg> can still be styled from global CSS (see cursor:pointer)
With multiple SVGs on screen <g> ID values could conflict.
The complete SVG can be moved to shadowDOM with:
let svg = container.appendChild( this.querySelector("svg") );
But then you can't style the SVG with global CSS any more, because global CSS can't style shadowDOM
As far as I know, nxv generates a g element with class "node" for each node, all nested inside a graph g. So basically you could loop over all gs elements inside the main group and attach a click event listener on each one. (actually, depending of the desired behavior, you might want to attach the event listener to the shape inside the g, as done below. For the inside of the shape to be clickable, it has to be filled)
On click, it would update a form, to do several things: update its style to show it as a modal (when submitted, the form should go back to hiding), and update an hidden input with the text content of the clicked g.
Basically it would be something like that:
<svg>Your nxv output goes here</svg>
<form style="display: none;">
<input type="hidden" id="node_title">
<textarea></textarea>
<input type="submit" value="Send!">
</form>
<script>
const graph = document.querySelector("svg g");
const form = document.querySelector("form");
[...graph.querySelectorAll("g")].map(g => { //loop over each g element inside graph
if (g.getAttribute("class") == "node") { //filter for nodes
let target = "polygon";
if (g.querySelector("polygon") === null) {
target = "ellipse";
}
g.querySelector(target).addEventListener("click",() => {
const node_title = g.querySelector("text").innerHTML;
form.querySelector("#node_title").setAttribute("value", node_title);
form.setAttribute("style","display: block;");
});
}
});
const submitForm = async (e) => { //function for handling form submission
const endpoint = "path to your POST endpoint";
const body = {
source_node: form.querySelector("#node_title").value,
textarea: form.querySelector("textarea").value
}
e.preventDefault(); //prevent the default form submission behavior
let response = await fetch(endpoint, { method: "POST", body: JSON.stringify(body) });
// you might wanna do something with the server response
// if everything went ok, let's hide this form again & reset it
form.querySelector("#node_title").value = "";
form.querySelector("textarea").value = "";
form.setAttribute("style","display: none;");
}
form.addEventListener("submit",submitForm);
</script>
I was trying to get data from an API that gives me a list of movies and this is how I implemented the rendering of the results.
const getAllData = async () => {
const movieData = await getMovies()
const movieContainer = movieData.map((movie)=> {
const listItem = `
<li class="movie">
<img src="${movie.Poster}"></img>
<h1>
${movie.Title}
</h1>
<h2>Release Year: ${movie.Year}</h2>
<p>
<button onclick="getDetails('${movie.imdbID}')">
Click for more details
</button>
</p>
</li>
`;
return listItem
}).join('')
document.getElementById("movieList").innerHTML = movieContainer;
}
Here is the getDetails function that the button click calls.
function getDetails(id){
sessionStorage.setItem('movieId', id);
window.location = 'movie.html';
return false;}
This code works as intended. However, what I was having trouble with is that I want to implement this logic with event listeners instead of inline HTML event-handlers. How could I implement this?
Would I need to change anything drastic about my current code?
Create a <li> element instead of an HTML string, then select the button descendant using querySelector and you can use addEventListener:
const getAllData = async () => {
const movieData = await getMovies();
const ul = document.getElementById("movieList");
for (const movie of movieData) {
const li = ul.appendChild(document.createElement('li'));
li.className = 'movie';
li.innerHTML = `
<img src="${movie.Poster}"></img>
<h1>
${movie.Title}
</h1>
<h2>Release Year: ${movie.Year}</h2>
<p>
<button>
Click for more details
</button>
</p>
`;
li.querySelector('button').addEventListener('click', () => {
getDetails(movie.imdbID);
});
}
};
I'd also highly recommend against direct concatenation of external input into an HTML string, like with
<img src="${movie.Poster}"></img>
unless the input is absolutely trustworthy, since that can result in arbitrary code execution, which is a security risk. If you aren't doing it already, I'd suggest either verifying that the interpolated values are well formatted (for example, without anything which would result in HTML markup, like <script> tags), or assign the dynamic properties/attributes after the <li> has been populated with the base template.
I would like to use a javascript loop to create multiple HTML wrapper elements and insert JSON response API data into some of the elements (image, title, url, etc...).
Is this something I need to go line-by-line with?
<a class="scoreboard-video-outer-link" href="">
<div class="scoreboard-video--wrapper">
<div class="scoreboard-video--thumbnail">
<img src="http://via.placeholder.com/350x150">
</div>
<div class="scoreboard-video--info">
<div class="scoreboard-video--title">Pelicans # Bulls Postgame: E'Twaun Moore 10-8-17</div>
</div>
</div>
</a>
What I am trying:
var link = document.createElement('a');
document.getElementsByTagName("a")[0].setAttribute("class", "scoreboard-video-outer-link");
document.getElementsByTagName("a")[0].setAttribute("url", "google.com");
mainWrapper.appendChild(link);
var videoWrapper= document.createElement('div');
document.getElementsByTagName("div")[0].setAttribute("class", "scoreboard-video-outer-link");
link.appendChild(videoWrapper);
var videoThumbnailWrapper = document.createElement('div');
document.getElementsByTagName("div")[0].setAttribute("class", "scoreboard-video--thumbnail");
videoWrapper.appendChild(videoThumbnailWrapper);
var videoImage = document.createElement('img');
document.getElementsByTagName("img")[0].setAttribute("src", "url-of-image-from-api");
videoThumbnailWrapper.appendChild(videoImage);
Then I basically repeat that process for all nested HTML elements.
Create A-tag
Create class and href attributes for A-tag
Append class name and url to attributes
Append A-tag to main wrapper
Create DIV
Create class attributes for DIV
Append DIV to newly appended A-tag
I'd greatly appreciate it if you could enlighten me on the best way to do what I'm trying to explain here? Seems like it would get very messy.
Here's my answer. It's notated. In order to see the effects in the snippet you'll have to go into your developers console to either inspect the wrapper element or look at your developers console log.
We basically create some helper methods to easily create elements and append them to the DOM - it's really not as hard as it seems. This should also leave you in an easy place to append JSON retrieved Objects as properties to your elements!
Here's a Basic Version to give you the gist of what's happening and how to use it
//create element function
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
//append child function
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
//example:
//get wrapper div
let mainWrapper = document.getElementById("mainWrapper");
//create link and div
let link = create("a", { href:"google.com" });
let div = create("div", { id: "myDiv" });
//add link as a child to div, add the result to mainWrapper
ac(mainWrapper, ac(div, link));
//create element function
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
//append child function
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
//example:
//get wrapper div
let mainWrapper = document.getElementById("mainWrapper");
//create link and div
let link = create("a", { href:"google.com", textContent: "this text is a Link in the div" });
let div = create("div", { id: "myDiv", textContent: "this text is in the div! " });
//add link as a child to div, add the result to mainWrapper
ac(mainWrapper, ac(div, link));
div {
border: 3px solid black;
padding: 5px;
}
<div id="mainWrapper"></div>
Here is how to do specifically what you asked with more thoroughly notated code.
//get main wrapper
let mainWrapper = document.getElementById("mainWrapper");
//make a function to easily create elements
//function takes a tagName and an optional object for property values
//using Object.assign we can make tailored elements quickly.
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
//document.appendChild is great except
//it doesn't offer easy stackability
//The reason for this is that it always returns the appended child element
//we create a function that appends from Parent to Child
//and returns the compiled element(The Parent).
//Since we are ALWAYS returning the parent(regardles of if the child is specified)
//we can recursively call this function to great effect
//(you'll see this further down)
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
//these are the elements you wanted to append
//notice how easy it is to make them!
//FYI when adding classes directly to an HTMLElement
//the property to assign a value to is className -- NOT class
//this is a common mistake, so no big deal!
var link = create("a", {
className: "scoreboard-video-outer-link",
url: "google.com"
});
var videoWrapper = create("div", {
className: "scoreboard-video-outer-link"
});
var videoThumbnailWrapper = create("div", {
className: "scoreboard-video--thumbnail"
});
var videoImage = create("img", {
src: "url-of-image-from-api"
});
//here's where the recursion comes in:
ac(mainWrapper, ac(link, ac(videoWrapper, ac(videoThumbnailWrapper, videoImage))));
//keep in mind that it might be easiest to read the ac functions backwards
//the logic is this:
//Append videoImage to videoThumbnailWrapper
//Append (videoImage+videoThumbnailWrapper) to videoWrapper
//Append (videoWrapper+videoImage+videoThumbnailWrapper) to link
//Append (link+videoWrapper+videoImage+videoThumbnailWrapper) to mainWrapper
let mainWrapper = document.getElementById('mainWrapper');
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
var link = create("a", {
className: "scoreboard-video-outer-link",
url: "google.com"
});
var videoWrapper = create("div", {
className: "scoreboard-video-outer-link"
});
var videoThumbnailWrapper = create("div", {
className: "scoreboard-video--thumbnail"
});
var videoImage = create("img", {
src: "url-of-image-from-api"
});
ac(mainWrapper, ac(link, ac(videoWrapper, ac(videoThumbnailWrapper, videoImage))));
//pretty fancy.
//This is just to show the output in the log,
//feel free to just open up the developer console and look at the mainWrapper element.
console.dir(mainWrapper);
<div id="mainWrapper"></div>
Short version
Markup.js's loops.
Long version
You will find many solutions that work for this problem. But that may not be the point. The point is: is it right? And you may using the wrong tool for the problem.
I've worked with code that did similar things. I did not write it, but I had to work with it. You'll find that code like that quickly becomes very difficult to manage. You may think: "Oh, but I know what it's supposed to do. Once it's done, I won't change it."
Code falls into two categories:
Code you stop using and you therefore don't need to change.
Code you keep using and therefore that you will need to change.
So, "does it work?" is not the right question. There are many questions, but some of them are: "Will I be able to maintain this? Is it easy to read? If I change one part, does it only change the part I need to change or does it also change something else I don't mean to change?"
What I'm getting at here is that you should use a templating library. There are many for JavaScript.
In general, you should use a whole JavaScript application framework. There are three main ones nowadays:
ReactJS
Vue.js
Angular 2
For the sake of honesty, note I don't follow my own advice and still use Angular. (The original, not Angular 2.) But this is a steep learning curve. There are a lot of libraries that also include templating abilities.
But you've obviously got a whole project already set up and you want to just plug in a template into existing JavaScript code. You probably want a template language that does its thing and stays out of the way. When I started, I wanted that too. I used Markup.js . It's small, it's simple and it does what you want in this post.
https://github.com/adammark/Markup.js/
It's a first step. I think its loops feature are what you need. Start with that and work your way to a full framework in time.
Take a look at this - [underscore._template]
It is very tiny, and useful in this situation.
(https://www.npmjs.com/package/underscore.template).
const targetElement = document.querySelector('#target')
// Define your template
const template = UnderscoreTemplate(
'<a class="<%- link.className %>" href="<%- link.url %>">\
<div class="<%- wrapper.className %>">\
<div class="<%- thumbnail.className %>">\
<img src="<%- thumbnail.image %>">\
</div>\
<div class="<%- info.className %>">\
<div class="<%- info.title.className %>"><%- info.title.text %></div>\
</div>\
</div>\
</a>');
// Define values for template
const obj = {
link: {
className: 'scoreboard-video-outer-link',
url: '#someurl'
},
wrapper: {
className: 'scoreboard-video--wrapper'
},
thumbnail: {
className: 'scoreboard-video--thumbnail',
image: 'http://via.placeholder.com/350x150'
},
info: {
className: 'scoreboard-video--info',
title: {
className: 'scoreboard-video--title',
text: 'Pelicans # Bulls Postgame: E`Twaun Moore 10-8-17'
}
}
};
// Build template, and set innerHTML to output element.
targetElement.innerHTML = template(obj)
// And of course you can go into forEach loop here like
const arr = [obj, obj, obj]; // Create array from our object
arr.forEach(item => targetElement.innerHTML += template(item))
<script src="https://unpkg.com/underscore.template#0.1.7/dist/underscore.template.js"></script>
<div id="target">qq</div>