AddEventListener event fires only once - javascript

I am using Django templating engine and JavaScript. My HTML looks like this
<p class="content-card__address">{{ z.formatted_address|truncatewords:6 }}</p>
<div class="content-card-inner">
<p class="content-card__review">Отзывы ({{ z.post_relate.all.count }})</p>
<p class="content-card__phone">{{ z.international_phone_number }}</p>
<div class="div-shaddow"></div>
<p class="content-card__text">Показать</p>
</div>
Cards with text to be generated on the backend using a template engine. My JavaScript code only works on the first card and I need it to work on all cards. With JavaScript I add a class to the div elements. Here is my JavaScript
let call = document.querySelector('.content-card__text');
let divShadow = document.querySelector('.div-shaddow');
call.addEventListener('click', clickCall)
function clickCall() {
call.classList.add('visually-hidden');
divShadow.classList.add('visually-hidden');
}

This code returns you the first element in the DOM and you add click handlers only for it
document.querySelector('.content-card__text')
It will work for you:
const buttons = document.querySelectorAll('.content-card__text');
buttons.forEach(button => {
button.addEventListener('click', clickCall);
});
But please also note that you need to take this into account when working with .divShadow if this element is not alone on the page
Update: example based on your comment
const buttons = document.querySelectorAll('.content-card__text');
const divShadow = document.querySelectorAll('.div-shaddow');
buttons.forEach((button, index) => {
button.addEventListener('click', () => clickCall(index));
});
function clickCall(index) {
buttons[index].classList.add('visually-hidden');
divShadow[index].classList.add('visually-hidden');
}

Related

Can a javascript variable be made local to a specific html element?

As a novice Javascript programmer, I'd like to create an html document presenting a feature very similar to the "reveal spoiler" used extensively in the Stack Exchange sites.
My document therefore has a few <div> elements, each of which has an onClick event listner which, when clicked, should reveal a hiddent text.
I already know that this can be accomplished, e.g., by
<div onclick="this.innerHTML='Revealed text'"> Click to reveal </div>
However, I would like the text to be revealed to be initially stored in a variable, say txt, which will be used when the element is clicked, as in:
<div onclick="this.innerHTML=txt"> Click to reveal </div>
Since there will be many such <div> elements, I certainly cannot store the text to be revealed in a global variable. My question is then:
Can I declare a variable that is local to a specific html element?
Yes you can. HTML elements are essentially just Javascript Objects with properties/keys and values. So you could add a key and a value to an HTML element object.
But you have to add it to the dataset object that sits inside the element, like this:
element.dataset.txt = 'This is a value' // Just like a JS object
A working example of what you want could look like this:
function addVariable() {
const myElement = document.querySelector('div')
myElement.dataset.txt = 'This is the extended data'
}
function showExtendedText(event) {
const currentElement = event.currentTarget
currentElement.innerHTML += currentElement.dataset.txt
}
addVariable() // Calling this one immediately to add variables on initial load
<div onclick="showExtendedText(event)">Click to see more </div>
Or you could do it by adding the variable as a data-txt attribute right onto the element itself, in which case you don't even need the addVariable() function:
function showExtendedText(event) {
const currentElement = event.currentTarget
currentElement.innerHTML += currentElement.dataset.txt
}
<div onclick="showExtendedText(event)" data-txt="This is the extended data">Click to see more </div>
To access the data/variable for the specific element that you clicked on, you have to pass the event object as a function paramater. This event object is given to you automatically by the click event (or any other event).
Elements have attributes, so you can put the information into an attribute. Custom attributes should usually be data attributes. On click, check if a parent element has one of the attributes you're interested in, and if so, toggle that parent.
document.addEventListener('click', (e) => {
const parent = e.target.closest('[data-spoiler]');
if (!parent) return;
const currentMarkup = parent.innerHTML;
parent.innerHTML = parent.dataset.spoiler;
parent.dataset.spoiler = currentMarkup;
});
<div data-spoiler="foo">text 1</div>
<div data-spoiler="bar">text 2</div>
That's the closest you'll get to "a variable that is local to a specific html element". To define the text completely in the JavaScript instead, one option is to use an array, then look up the clicked index of the spoiler element in the array.
const spoilerTexts = ['foo', 'bar'];
const spoilerTags = [...document.querySelectorAll('.spoiler')];
document.addEventListener('click', (e) => {
const parent = e.target.closest('.spoiler');
if (!parent) return;
const currentMarkup = parent.innerHTML;
const index = spoilerTags.indexOf(parent);
parent.innerHTML = spoilerTexts[index];
spoilerTexts[index] = currentMarkup;
});
<div class="spoiler">text 1</div>
<div class="spoiler">text 2</div>
There are also libraries that allow for that sort of thing, by associating each element with a component (a JavaScript function/object used by the library) and somehow sending a variable to that component.
// for example, with React
const SpoilerElement = ({ originalText, spoilerText }) => {
const [spoilerShown, setSpoilerShown] = React.useState(false);
return (
<div onClick={() => setSpoilerShown(!spoilerShown)}>
{ spoilerShown ? spoilerText : originalText }
</div>
);
};
const App = () => (
<div>
<SpoilerElement originalText="text 1" spoilerText="foo" />
<SpoilerElement originalText="text 2" spoilerText="bar" />
</div>
)
ReactDOM.createRoot(document.querySelector('.react')).render(<App />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div class='react'></div>
Thanks everybody for your answers, which helped immensely! However, as a minimalist, I took all that I learned from you and came up with what I believe is the simplest possible code achieving my goal:
<div spoiler = "foo" onclick="this.innerHTML=this.getAttribute('spoiler')">
Click for spoiler
</div>
<div spoiler = "bar" onclick="this.innerHTML=this.getAttribute('spoiler')">
Click for spoiler
</div>

Why is "onclick" only working once and fails to update?

I am trying to understand why this onclick button is only working once.
Basically I am testing to see if the "heart" or "wishlist" button is clicked on. When clicked, console.log the name of the product so I can confirm it. But it only picks up the first product. When I click the wishlist button on the second product.
It gives this error "Uncaught SyntaxError: Invalid or unexpected token (at products:1:10)"
When I go to that line it just show ''
I have also tried using a
const wishlistBtn = document.querySelector('.wishlistBtn');
wishlistBtn.addEventListener('click', (product_name) => { console.log(product_name) })
But it just returns that the property is null. I'm wondering if the reason is because of the innerHTML I am including all of this in.
Javascript:
const getProducts = () => {
return fetch('/get-products', {
method: 'POST',
headers: new Headers({'Content-Type':'application/json'}),
body: JSON.stringify({})
})
.then(res => res.json())
.then(data => {
createProductCards(data);
})
}
var wishlist = (product_name) => {
console.log(product_name);
}
const createProductCards = (data) => {
let parent = document.querySelector('.container');
let start = '<div class="product-container">';
let middle = '';
let end = '</div>';
for(let i = 0; i < data.length; i++){
if(data[i].id != decodeURI(location.pathname.split('/').pop()) && !data[i].draft){
middle += `
<div class="product-card">
<div class="product-image">
${data[i].discount === '0' ? ``:`
<span class="discount-tag">${data[i].discount}% off</span>
`}
<img src="${data[i].images[0]}" class="product-thumb" alt="">
<button class="card-btn wishlistBtn" onclick="wishlist('${data[i].name}')"><i class="bi-heart"></i></button>
</div>
<div class="product-info">
<h6 class="product-brand">${data[i].name}</h6>
${data[i].discount === '0' ? `<span class="price">$${data[i].totalPrice}</span>`:`
<span class="price">$${data[i].totalPrice}</span>
<span class="actual-price">$${data[i].actualPrice}</span>
`}
</div>
</div>
`;
}
}
parent.innerHTML = start + middle + end;
}
getProducts();
document.querySelector works only on the first matched element. You may need to use document.querySelectorAll & attach event after the for loop has completely finished it's execution
const wishlistBtn = document.querySelectorAll('.wishlistBtn').forEach((item) => {
item.addEventListener('click', getProductName)
})
function getProductName(product_name) {
console.log(product_name)
})
Here is an example
document.querySelectorAll('.test').forEach(item => {
item.addEventListener('click', getButtonValue)
})
function getButtonValue(elem) {
console.log(elem.target.innerHTML)
}
<button class="test">1</button>
<button class="test">2</button>
<button class="test">3</button>
<button class="test">4</button>
<button class="test">5</button>
<button class="test">6</button>
document.querySelector only returns the first instance of the selector. So the first wish list button on your page is the only one that gets a listener attached.
If you're coming from JQuery, this is a nuanced difference. To add the event listener to every .wishlistBtn you could do something like:
const wishlistBtns = document.querySelectorAll('.wishlistBtn');
[...wishlistBtns].forEach(wishListButton => wishListButton.addEventListener('click', (product_name) => { console.log(product_name) })
There are two differences:
The use of querySelectorAll returns a NodeList of all of the elements that match the .wishlistBtn selector.
Iterate over the NodeList and add an event listener to each individual node. Unfortunately NodeList isn't exactly an array so [...wishlistButtons] is a quick and dirty way to convert it to an array using the relatively new spread operator ...
I seem to have found my problem. The issue was with one of my products having quotations inside of it for some reason but once removed the onclick worked multiple times while sending the product name to a function to keep track.
The problem with the answers given was also that I didnt want to display the name at all inside the button itself <button class=“test”>Item</button> instead this is what I needed <button onclick=‘func(${passname})></button> so that would have not worked when attempted but it gave me a general idea for future references. Thanks!

Angular 12.1 add html element using typescript

I'm learning angular via youtube, but I'm trying to do something new, and I'm getting an error on that, my code is attached below, help me out.
I want to setAttribute like this div.setAttribute('(click)',"popUp($event)"); but I got error.
TypeScript
export class AppComponent {
createEl(){
console.time("timer");
for (let i = 0; i < 10; i++) {
let div = document.createElement("div");
div.textContent = `Hello, World! ${i}`;
div.setAttribute('(click)',"popUp($event)");
document.getElementById('divEl')?.appendChild(div);
};
console.timeEnd("timer");
}
HTML
<div id="divEl"></div>
<button (click)="createEl()">click me</button>
Error
This is not really the angular way of doing things. Try to avoid operations on document such as document.createElement.
A better way to achieve this would be to define what the repeating element would look like in the template and drive it from an array. That way we can keep the template doing display and the typescript doing processing, and Angular handling everything in between.
HTML
<div id="divEl">
<div *ngFor="let row of rows; index as i;" (click)="popUp($event)">
Hello, World! {{i}}
</div>
</div>
<button (click)="createEl()">click me</button>
Typescript
export class AppComponent {
rows: unknown[] = [];
createEl():void {
this.rows.push('something');
}
popUp(event:Event):void {}
}
More reading on loops: https://angular.io/api/common/NgForOf
That's right check below.
div.addEventListener('click', (e) => {
this.popUp(e);
});
Problem is you are trying to do angular stuff with pure javascript.
<div (click)="method()"> is angular.
In javascript you'd do someting like this <button onclick="myFunction()">Click me</button>
Other options are to use event handlers https://www.w3schools.com/js/js_htmldom_eventlistener.asp
Anyhow, angular doesn't recommend changes the DOM because then it won't recognize those changes. Here are multiple examples ho to properly change the dom
Correct way to do DOM Manipulation in Angular 2+
https://medium.com/#sardanalokesh/understanding-dom-manipulation-in-angular-2b0016a4ee5d
`
You can set the click event as shown below instead of using setAttribute
div.addEventListener('click', (e) => {
this.popUp(e);
});
(click) is not an html attribute, it is Angular event binding syntax
This syntax consists of a target event name within parentheses to the left of an equal sign, and a quoted template statement to the right.
You cannot use that with JavaScript. Use
div.onclick = popUp;
export class AppComponent {
createEl(){
console.time("timer");
for (let i = 0; i < 10; i++) {
let div = document.createElement("div");
div.textContent = `Hello, World! ${i}`;
div.addEventListener('click', (e) => {
this.popUp(e);
});
document.getElementById('divEl')?.appendChild(div);
};
console.timeEnd("timer");
}

Create an element similar to the element I have (including the content and the style)

I have 'DIV' element, and when I click on the button, I want get the same 'DIV' on the page (including the content, the style and the images it has).
that is the code I tryed:
HTML:
<div id="taskId">
<p id="taskWritten">
the task will written here
</p>
</div>
<button id="button">add item</button>
JavaScript:
let button = document.querySelector('#button');
button.addEventListener('click', ()=>{
let task = document.querySelector('#taskId');
let newTask = document.createElement(task);
document.appendChild(newTask);
})
this code doesn't work.
Can anyone explain to me where my error is?
(I am an absolute beginner with programming)
You are on the right track, but need to only make minor changes. The code should look like this.
let task = document.querySelector('#taskId');
const newTask = task.cloneNode(true);
document.appendChild(newTask);
here you are creating a new node which will model your original node entirely including the style and data
You must create a 'div' with 'createElement'. You can use the code below with some adjustments.
let button = document.querySelector('#button');
button.addEventListener('click', ()=>{
task=document.querySelector('#taskId');
let newTask = document.createElement('div');
newTask.innerHTML='<div><p>the task will written here</p></div>'
task.appendChild(newTask);
})
<div id="taskId">
<p id="taskWritten">
the task will written here
</p>
</div>
<button id="button">add item</button>

Setting HTML Button`onclick` in template literal

I have an html template that i'm using template literals for. The function looks like the below
// postCreator.js
export const blogPostMaker = ({ title, content, address, id }, deletePost) => {
const innerHtml = `
<blog-post>
<h1 class='title'>${title}</h1>
<p class='content'>${content}</p>
<p class='address'>${address}</p>
<button onclick='${() => deletePost(id)}'>Delete</button>
</blog-post>
`
return innerHtml
}
//Blog.js
postHelper.getSeriesOfPosts(10)
.then(resp => {
resp.forEach(post => (blogDiv.innerHTML += blogPostMaker(post, postHelper.deletePost)))
})
What I can't figure out is how to get the onclick to work. I've tried passing in an anon function in Blog.js to the postCreator as well with no luck.
Any ideas?
If you don't want to expose the event callback globally, you should attach it in the JS part of the code with addEventListener() :
// postCreator.js
export const blogPostMaker = ({ title, content, address, id }) =>
`
<blog-post id='${id}'>
<h1 class='title'>${title}</h1>
<p class='content'>${content}</p>
<p class='address'>${address}</p>
<button>Delete</button>
</blog-post>
`
//Blog.js
postHelper.getSeriesOfPosts(10).then(resp => {
resp.forEach(post => {
blogDiv.innerHTML += blogPostMaker(post)
blogDiv.querySelector(`blog-post[id="${post.id}"] > button`)
.addEventListener('click', () => postHelper.deletePost(post.id))
})
Note: it's not the most efficient way to do it but it keeps your file structure.
Instead I would create the <button> directly with createElement() and then add it to DOM with appendChild(), or I would use a DocumentFragment or a <template> in order to avoid querySelector() on all of the blogDiv.
If you absolutely want to use inline JS without exposing the helper you'll need to define your as a component (for example a Custom Element).
Miroslav Savovski's solution works but they did not explain why, so I thought I would add this answer with the reasoning behind that and a step-by-step of how it is actually working, and why the OP's solution was not working initially.
TLDR? Scroll to the last two code snippets.
With template literals when you put a function inside of them it executes that function, so let's say we have a simple function like this that just returns a string of 'blue':
const getBlueColor = () => 'blue'
And then we call this function inside of a template literal like this:
<div>${getBlueColor()}</div>
What happens is that the getBlueColor() is called right when that code is executed.
Now lets say we wanted to do this onclick instead like this:
<div onclick="${getBlueColor()}"></div>
What is happening here is that getBlueColor is not executed onclick, it's actually executed whenever this template literal is executed.
The way we fix this is to prevent the template literal from executing this function by simply removing the template literal:
<div onclick="getBlueColor()"></div>
But now let's say you want to pass in some parameters to a function like getOppositeColor(color) like this:
<div onclick="getOppositeColor(color)"><div>
This will not work because color won't be defined. So you need to wrap that with a template literal and in a string like this:
<div onclick="getOppositeColor('${color}')"><div>
Now with this you will be calling the onclick when the user clicks the button, and you will be passing it a string of the color like this:
getOppositeColor('blue')
const markUp = `
<button onclick="myFunction()">Click me</button>
`;
document.body.innerHTML = markUp;
window.myFunction = () => {
console.log('Button clicked');
};

Categories

Resources