I am new to Javascript and I try to add an event listener for each button on every card, but the code make the last card (button) only have the event 'click' so is there's any way to make it happen with innerHTML card
this is the code:
let tracksRender = (track) => {
track.forEach(element => {
//this the card that will add the button for
let card = `<div class="card">
<div class="image">
<img class="image_img" src="${element.artwork_url || 'http://lorempixel.com/100/100/abstract/'}">
</div >
<div class="content">
<div class="header">
${element.title}
</div>
</div>
</div >`;
//here i add the card to DOM
let searchResults = document.querySelector('.js-search-results');
searchResults.innerHTML += card;
// store the content of the button
let inBtn = `<i class="add icon"></i>
<span>Add to playlist</span>`;
// created button container
let btn = document.createElement("div");
btn.classList.add("ui", "bottom", "attached", "button", "js-button");
// added the content of the button
btn.innerHTML += inBtn;
// here i add the the event Listener to the button
btn.addEventListener('click', () => {
console.log("click");
});
//here i add the button to the last card have been created
searchResults.querySelector(".card:last-child").append(btn);
});
}
and the structure:
<!doctype html>
<html>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>SoundCloud Player</title>
<link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css'>
<link rel='stylesheet' href='styles/main.css'>
<style></style>
</head>
<body id="soundcloud-player">
<div class="ui container col">
<div class="col">
<div class="main">
<div class="js-search-results search-results ui cards">
</div>
</div>
</div>
</div>
<script src="https://connect.soundcloud.com/sdk/sdk-3.3.2.js"></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.js'></script>
<script src="javascript/main.js"></script>
</body>
</html>
Fiddle: https://jsfiddle.net/y73bstju/7/
but it will add the event to the last card only
It might help to create elements instead of appending innerHTML:
let tracksRender = (track) => {
// Select the results here, so you wont have to repeat it
const searchResults = document.querySelector('.js-search-results');
track.forEach(element => {
// Create the card, give it its class and innerHTML
const card = document.createElement('div');
card.className = 'card';
card.innerHTML = `<div class="image">
<img class="image_img" src="${element.artwork_url || 'http://lorempixel.com/100/100/abstract/'}">
</div >
<div class="content">
<div class="header">
${element.title}
</div>
</div>`;
// Created the button, give its classes and innerHTML
const btn = document.createElement('div');
btn.className = 'ui bottom attached button js-button';
btn.innerHTML = '<i class="add icon"></i><span>Add to playlist</span>';
// Add the event listener
btn.addEventListener('click', () => {
console.log('click');
});
// Append the button to the created card
card.appendChild(btn);
// Add the card to the results
searchResults.appendChild(card);
});
}
I agree with you that theoretically the object should have the event, but the behavior we experience is that whenever another write happens at the relevant section of the DOM, the event handler is lost, which is the reason the last element has the click event. So, let's write first into the DOM and only when we are done with that should we add the event listeners, like:
let SoundCloudAPI = {};
SoundCloudAPI.init = () => {
SC.initialize({ client_id: 'cd9be64eeb32d1741c17cb39e41d254d' });
};
SoundCloudAPI.init();
SoundCloudAPI.getTrack = (inputVlue) => {
SC.get('/tracks', {
q: inputVlue
}).then((tracks) => {
console.log(tracks);
SoundCloudAPI.renderTracks(tracks);
});
}
SoundCloudAPI.getTrack("alan walker");
SoundCloudAPI.renderTracks = (track) => {
track.forEach(element => {
//this the card that will add the button for
let card = `<div class="card">
<div class="image">
<img class="image_img" src="${element.artwork_url || 'http://lorempixel.com/100/100/abstract/'}">
</div >
<div class="content">
<div class="header">
${element.title}
</div>
</div>
</div >`;
//here i add the card to DOM
let searchResults = document.querySelector('.js-search-results');
searchResults.innerHTML += card;
// store the content of the button
let inBtn = `<i class="add icon"></i>
<span>Add to playlist</span>`;
// created button container
let btn = document.createElement("div");
btn.classList.add("ui", "bottom", "attached", "button", "js-button", "fresh");
// added the content of the button
btn.innerHTML += inBtn;
//here i add the button to the last card have been created
searchResults.querySelector(".card:last-child").append(btn);
});
for (let btn of document.querySelectorAll('.ui.attached.button.js-button.fresh')) {
// here i add the the event Listener to the button
btn.addEventListener('click', () => {
console.log("click");
});
}
}
Fiddle: https://jsfiddle.net/r84um9pt/
I think:
you're doing:
let searchResults = document.querySelector('.js-search-results');
searchResults.innerHTML += card;
Serializing again and again in your div "searchResults"
innerHTML ~erases~ listeners, and probably it is ~erasing~ your previous listeners. Besides, i don't remember where i read that innerHTML script code cannot run (for security purposes)
From https://medium.com/#kevinchi118/innerhtml-vs-createelement-appendchild-3da39275a694
using innerHTML reparses and recreates all DOM nodes inside the div
element and is less efficient than simply appending a new element to
the div. In the above cases, createElement is the more performant
choice.
Be careful:
Using "append" again and again uses badly browser resources, redrawing many times.
But, you can append in a documentFragment and append it to the div -js-search-results
DocumentFragment:
https://developer.mozilla.org/es/docs/Web/API/DocumentFragment
Welcome in the community. In you're code you're adding many classes to button. You can add event Listener to any one of the unique class name which is specifically applied on button element only.
You can replace:
btn.addEventListener('click', () => {
console.log('click');
});
with:
document.querySelectorAll('.js-button').forEach(el=>{
el.addEventListener('click', (event) => {
event.preventDefault();
console.log("click");
});
});
Also, it would be good if you add eventListener out of the loop in which you are appending elements.
Related
This question already has answers here:
Event binding on dynamically created elements?
(23 answers)
Closed last year.
I am trying to create a todolist type webpage and I ran into this problem. Basically when I change or update the list element, the list does not get updated and is still looping through the first 2 buttons in its default html
index.html:
<body>
<script src="script.js" defer></script>
<link href="styles.css">
<div id="essentials"></div>
<h2>ToDoList</h2>
<input id="essentialInput">
<button id="addTask">Add Task</button>
</div>
<list id="list">
<h3 id="0">something epic <button class="deleteButton" id="0">Delete</button></h3>
<h3 id="1">Something else <button class="deleteButton" id="1">Delete</button></h3>
</list>
</body>
script.js:
const essentialInput = document.getElementById('essentialInput')
const addTaskButton = document.getElementById('addTask')
const list = document.getElementById('list')
let button = document.querySelectorAll('.deleteButton')
addTaskButton.addEventListener('click', function() {
text = essentialInput.value
if(text == null) return "Nothing in input field"
children = list.childElementCount
list.innerHTML += `<h3 id=${children}>${text} <button class="deleteButton" id=${children}>Delete</button></h3>`
})
for(let i = 0; i<button.length; i++) {
button[i].addEventListener('click', function() {
console.log(button[i].id)
})
}
I know the issue but I dont know how to fix it. I've tried to put the button eventListeners in a change event listener for when the list changes but then nothing happens until I click a delete button.
The problem is that using the += operator on an element's innerHTML is NOT just concatenating the new HTML... It wholy replaces it with the result.
That is why you were loosing the listeners already set on the two first <h3>.
Then, the new <h3> never had any listeners on them.
So the solution for this case is to register the event listener on the static parent on behalf of its childrens... And take advantage of event bubbling. That is called event delegation.
const essentialInput = document.getElementById('essentialInput')
const addTaskButton = document.getElementById('addTask')
const list = document.getElementById('list')
let button = document.querySelectorAll('.deleteButton')
addTaskButton.addEventListener('click', function() {
text = essentialInput.value
if(text == null) return "Nothing in input field"
children = list.childElementCount
list.innerHTML += `<h3 id=${children}>${text} <button class="deleteButton" id=${children}>Delete</button></h3>`
})
// Add the event listener on the static parent
list.addEventListener("click", function(e){
if(e.target.classList.contains("deleteButton")){
console.clear()
console.log(e.target.id)
}
})
<body>
<script src="script.js" defer></script>
<link href="styles.css">
<div id="essentials"></div>
<h2>ToDoList</h2>
<input id="essentialInput">
<button id="addTask">Add Task</button>
</div>
<list id="list">
<h3 id="0">something epic <button class="deleteButton" id="0">Delete</button></h3>
<h3 id="1">Something else <button class="deleteButton" id="1">Delete</button></h3>
</list>
</body>
I'm learning JavaScript and this is a practice scenario for me.
What I have already is a button that clones content, and within that content that has been cloned, there is a button to remove it.
When I click the button that prompts you to remove the content, it removes the first set of content.
What I want to happen is when you click the button that prompts you to remove the content, it removes the content related to that button and nothing else.
This is the CodePen link.
https://codepen.io/JosephChunta/pen/YzwwgvQ
Here is the code.
function addContent() {
var itm = document.getElementById("newContent");
var cln = itm.cloneNode(true);
document.getElementById("placeToStoreContent").appendChild(cln);
}
function removeContent() {
var x = document.getElementById("content").parentNode.remove();
}
// This is for debug purposes to see which content is which
document.getElementById('orderContent')
.addEventListener('click', function(e) {
const orderedNumber = document.querySelectorAll('.thisIsContent');
let i = 1;
for (p of orderedNumber) {
p.innerText = '' + (i++);
}
});
.contentThatShouldBeHidden {
display: none;
}
<div id="placeToStoreContent">
</div>
<button id="orderContent" onclick="addContent()">Add Content</button>
<div class="contentThatShouldBeHidden">
<div id="newContent">
<div id="content">
<p class="thisIsContent">This is a prompt</p>
<button onclick="removeContent()">Remove this</button>
<hr />
</div>
</div>
</div>
When you'r trying to remove by ID, it takes the first ID it finds.
To remove the correct content, send this onclick.
<button onclick="removeContent(this)">Remove this</button>
And handle it in your function:
function removeContent(el) {
el.parentNode.remove();
}
Example:
function addContent() {
var itm = document.getElementById("newContent");
var cln = itm.cloneNode(true);
document.getElementById("placeToStoreContent").appendChild(cln);
}
function removeContent(el) {
el.parentNode.remove();
}
// This is for debug purposes to see which content is which
document.getElementById('orderContent')
.addEventListener('click', function(e) {
const orderedNumber = document.querySelectorAll('.thisIsContent');
let i = 1;
for (p of orderedNumber) {
p.innerText = '' + (i++);
}
});
.contentThatShouldBeHidden { display: none; }
<div id="placeToStoreContent">
</div>
<button id="orderContent" onclick="addContent()">Add Content</button>
<div class="contentThatShouldBeHidden">
<div id="newContent">
<div id="content">
<p class="thisIsContent">This is a prompt</p>
<button onclick="removeContent(this)">Remove this</button>
<hr />
</div>
</div>
</div>
In your remove button, do this:
<!-- The "this" keyword is a reference to the button element itself -->
<button onclick="removeContent(this)">Remove this</button>
And in your javascript:
function removeContent(element) {
element.parentNode.remove();
}
I have a question about targetting other element than it pointing the element clicked in the following code.
https://codepen.io/jotnajoa/pen/gOpQdzV
I don't get this part below.
const addClassToStrings = addClassToEach(strings, className);
btn.addEventListener('click', (el) => {
if (addClassToStrings.next().done)
el.target.classList.add(className);
});
If the addClassToEach event is tied into the button,
I assumed .classList.add(className) event would apply to the button itself.
Because it is targetting it.
I guess el.target..... this part does something.
Can anyone help me to understand this part?
DEMO:
const strings = document.querySelectorAll('.string');
const btn = document.querySelector('#btn');
const className = 'darker';
function* addClassToEach(elements, className) {
for (const el of Array.from(elements))
yield el.classList.add(className);
}
const addClassToStrings = addClassToEach(strings, className);
btn.addEventListener('click', (el) => {
if (addClassToStrings.next().done)
el.target.classList.add(className);
});
.string{width:50vmin;height:8vmin;margin:4vmin;transition:.3s ease;background:#90ee90}.string.darker{background:#343434;filter:blur(3vmin)}#btn{position:absolute;padding:5vmin 10vmin;color:#d6d6d6;font-size:8vmin;font-family:Roboto,sans-serif;user-select:none;background:#3749a2;border-radius:5%;top:5vmin;left:80vmin;transition:.3s ease;cursor:pointer}#btn:active,#btn:hover{border-radius:50%;box-shadow:1vmin 1vmin 1vmin grey}#btn.darker{border-radius:0;background:#343434;filter:blur(3vmin)}
<div class="string"></div>
<div class="string"></div>
<div class="string"></div>
<div class="string"></div>
<div class="string"></div>
<div class="string"></div>
<div class="string"></div>
<div class="string"></div>
<div id="btn">Click me</div>
I have a menu list and when you click on one of the items it should show the content block relevant to that item using vanilla JavaScript (No jQuery)
When it opens the content block for that specific item the other content blocks should be hidden.
HTML Menu Items:
<div class="our-clients__categories">
<h4>Technology</h4>
<h4>Retail</h4>
<h4>Finance</h4>
</div>
Content Blocks
<div class="company-brands" data-brand="technology">
<h1>Technology</h1>
</div>
<div class="company-brands" data-brand="retail">
<h1>retail</h1>
</div>
<div class="company-brands" data-brand="finance">
<h1>finance</h1>
</div>
So e.g. when you click on Technology it must show the content block for Technology and so on.
Any help to get started would be great! Thank you
const toogleLinks = document.querySelectorAll('.js--company-toggle');
const toogleBlocks = document.querySelectorAll('.js--company-item');
// Loop through all links
Array.from(toogleLinks).forEach(link => {
// add click event
link.addEventListener('click', function(event) {
// Hide all blocks
Array.from(toogleBlocks).forEach(item => item.classList.add('js--company-item--hidden'));
// Get target block
const target = this.getAttribute('href');
// Show target block
document.querySelector(target).classList.remove('js--company-item--hidden');
}, false);
});
.js--company-item--hidden {
display: none;
}
<div class="our-clients__categories">
<h4>Technology</h4>
<h4>Retail</h4>
<h4>Finance</h4>
</div>
<div class="company-brands js--company-item" id="technology">
<h1>Technology</h1>
</div>
<div class="company-brands js--company-item js--company-item--hidden" id="retail">
<h1>retail</h1>
</div>
<div class="company-brands js--company-item js--company-item--hidden" id="finance">
<h1>finance</h1>
</div>
Or the same with data-attribute
const toogleLinks = document.querySelectorAll('[data-toggle]');
// Loop through all links
Array.from(toogleLinks).forEach(link => {
// get tooble elements
const elementsSelector = link.getAttribute('data-toggle')
const elements = document.querySelectorAll(elementsSelector);
// add click event
link.addEventListener('click', function(event) {
// Hide all blocks
Array.from(elements).forEach(item => item.setAttribute('hidden', true));
// Get target block
const target = this.getAttribute('href');
// Show target block
document.querySelector(target).removeAttribute('hidden')
}, false);
});
<div class="our-clients__categories">
<h4>Technology</h4>
<h4>Retail</h4>
<h4>Finance</h4>
</div>
<div class="company-brands" id="technology">
<h1>Technology</h1>
</div>
<div class="company-brands" id="retail" hidden>
<h1>retail</h1>
</div>
<div class="company-brands" id="finance" hidden>
<h1>finance</h1>
</div>
I am creating a news feed with VueJS and I have run into a bit of a problem with rendering the content. The API I am using sadly I am unable to change to suit my need properly at this time. The API gives me all the content already in HTML tags and it can also include images and lists and all the other basics. What I want to do is create a "read more" section which will render the first 20 words if just the text of the first "p" tag and stop there.
Does anyone know a quick and efficient way of doing this with JS?
My current display VueJS render is the following:
<div v-for="news_item in news_items">
<div v-bind:class="{ 'col-md-4': display}">
<div class="card">
<div class="header">
<h2>
{{news_item.title}} <small>{{news_item.subtitle}}</small>
</h2>
</div>
<div class="body" style="padding-top: 0">
<div class="row" style="margin-right: -20px; margin-left: -20px;">
<div class="col-md-12"
style="padding-left: 0px; padding-right: 0px;">
<img :src="news_item['thumbnail']"
class="img-responsive smaller-img" alt=""
style=" margin: 0 auto; max-height: 250px;">
</div>
</div>
<div class="row">
<div class="col-md-12">
<div v-html="news_item.content"></div>
</div>
</div>
</div>
</div>
</div>
</div>
This is the perfect time to use a directive:
https://v2.vuejs.org/v2/guide/custom-directive.html
See the codepen here: https://codepen.io/huntleth/pen/GOXaLo
Using the trim directive, you can change the content of the element. In the example above, it will show the first 5 words followed by an ellipsis.
If you're just after a pure js solution, this should do it:
var resultString = str.split(' ').slice(0, 20).join(" ");
You could use the trim directive and search the el for any p tags, and then change their content accordingly.
You don't appear to have tried anything yet, so I'll just give you these pointers. If you run into specific problems, ask again.
Make a component
The component should receive the html as a prop
The component should have a data item to control whether it is expanded
The component should have a computed that gets the first 20 words of the first paragraph tag. You can use textContent to get text from an HTML node.
The computed is the most likely part to pose a challenge. It will look something like this
blurb() {
const div = document.createElement('div');
div.innerHTML = this.content; // this.content is the prop
const firstP = div.querySelector('p');
const text = firstP.textContent;
const match = text.match(/(\S+\s*){0,20}/);
return match[0];
}
Rough implementation, Pure Js approach
document.getElementById("addContent").onclick = display;
document.getElementById("ellipsAnchor").onclick = hideEllipsis;
function display() {
document.getElementById("instruction").classList+= " hide";
let content = document.getElementById("inputbox").value;
if(content.length > 30) {
let sliced = content.slice(30);
let unsliced = content.substring(0,29);
let spantag = document.createElement("span");
spantag.className = "toReplace hide"
let text = document.createTextNode(sliced);
spantag.appendChild(text);
let spantag1 = document.createElement("span");
let text1 = document.createTextNode(unsliced);
spantag1.appendChild(text1);
let contentTag =document.getElementById("content");
contentTag.appendChild(spantag1)
contentTag.appendChild(spantag)
document.getElementById("ellipsis").classList -= "hide";
}
}
function hideEllipsis(){
document.getElementById("ellipsis").classList += " hide";
document.querySelectorAll("span.hide")[0].classList -= " hide"
}
.hide {
display : none;
}
<textarea type="text" id="inputbox"></textarea>
<button id="addContent">
Show content
</button>
<div id="content">
</div>
<div class="hide" id="ellipsis">
Read More..
</div>
<div id="instruction">
Type more than 30 characters and click show content
</div>
You can write a vue directive to solve this.
Set max-height to the div.
count the words and append "Read more.." link to the content.
Add a click event to 'read more' to expand the DIV to full height.
For example see this codepen
let handler = ""
Vue.directive("viewmore", {
inserted: function (el, binding){
let maxlines = binding.value
let lineheight = parseFloat(getComputedStyle(el).lineHeight)
let paddingtop = parseFloat(getComputedStyle(el).paddingTop)
let lines = (el.clientHeight) / lineheight ;
let maxheight = (lineheight * maxlines) + paddingtop + (lineheight/5)
if(lines>maxlines){
el.classList.add('vmore')
el.style.maxHeight = maxheight + 'px'
el.addEventListener('click', handler = ()=> {
el.style.maxHeight = ""
el.scrollIntoView({behavior: "smooth"})
el.removeEventListener('click', handler)
el.classList.remove('vmore')
})
}
},
unbind: function (el, binding) {
el.removeEventListener('click', handler)
handler = ""
}
});
https://codepen.io/dagalti/pen/vPOZaB .
it works based on the lines in the content.
Code : https://gist.github.com/dagalti/c8fc86cb791a51fe24e5dc647507c4a3
Expanding on the answers by tom_h and Roy J, here's what I'm using in my vue application to make the ellipsis clickable:
Vue.component("ellipsis", {
template: "#ellipsis-template",
props: ['content'],
data: function() {
return {
wordLength: 3, // default number of words to truncate
showAll: false
}
}
});
<script type="text/x-template" id="ellipsis-template">
<span v-if="content.split(' ').length>wordLength && showAll">{{content}}
(less)
</span>
<span v-else-if="content.split(' ').length>wordLength && !showAll">
{{content.split(" ").slice(0,wordLength).join(" ")}}
...
</span>
<span v-else>{{content}}</span>
</script>
To call it:
<ellipsis :content="someData"></ellipsis>