Button Functionality in JS - javascript

I created A simple Website for basic add/remove friend.
But the button is only working in first box and not working in other two boxes.
Button is completly working in first box but second and button button is not responding
Edit - All the buttons only changing the first box text
var arr = [{
name: "Raju",
image: "./1.jpg",
status: "Stranger"
},
{
name: "Shyam",
image: "./2.jpg",
status: "Stranger"
},
{
name: "Babu Bhaiya",
image: "./3.jpg",
status: "Stranger"
},
];
var cluster = "";
arr.forEach(function(val) {
cluster = cluster + `
<div id="card">
<img src="${val.image}" alt="">
<h1>${val.name}</h1>
<p>${val.status}</p>
<button>Send Request</button>
</div>
`;
});
//Botton Functionality
document.getElementById("cluster").innerHTML = cluster;
const btn = document.querySelectorAll("button");
var fact = 0;
btn.forEach((btn) => {
btn.addEventListener("click", function() {
if (fact == 0) {
var timer = setTimeout(() => {
document.querySelector("p").textContent = "Friend";
document.querySelector("p").style.color = "rgb(66, 178, 113";
document.querySelector("button").textContent = "Cancel";
}, 2000);
fact = 1;
document.querySelector("p").textContent = "Request Pending";
document.querySelector("button").textContent = "Send Request";
} else {
clearTimeout(timer);
fact = 0;
document.querySelector("p").textContent = "Strenger";
document.querySelector("p").style.color = "rgb(221, 66, 66)";
document.querySelector("button").textContent = "Send Request";
fact = 0;
}
});
});
<div class="container">
<div id="cluster" class="box"></div>
</div>

Instead of attaching listeners to all of the buttons you can use event delegation and attach one listener to a containing element that can listen out for events from its children as they "bubble up" the DOM.
In this example when a button is clicked we destructure out its previousElementSibling (the paragraph element), and its textContent. And then based on the textContent perform the operations.
(I've also used a switch statement instead of flags which makes the code a little cleaner, and map to produce the HTML rather than forEach.)
const arr=[{name:"Raju",image:"./1.jpg",status:"Stranger"},{name:"Shyam",image:"./2.jpg",status:"Friend"},{name:"Babu Bhaiya",image:"./3.jpg",status:"Request Pending"}];
// `map` over the array and produce an array of
// strings which is `joined` up at the end of the iteration
const html = arr.map(obj => {
return `
<div id="card">
<img src="${obj.image}" alt="">
<h3>${obj.name}</h3>
<p>${obj.status}</p>
<button type="button">Send Request</button>
</div>
`;
}).join('');
// Cache the container/cluster elements
const cluster = document.querySelector('#cluster');
const container = document.querySelector('.container');
// Add the event listener to the container
container.addEventListener('click', handleClick);
// Add the HTML to the cluster element
cluster.innerHTML = html;
// When the listener catches an event...
function handleClick(e) {
// ...first check that it's a button...
if (e.target.matches('button')) {
// ...destructure the previous element sibling
// element (relabeling it as `para`), and the textContent
// from the clicked element
const {
previousElementSibling: para,
textContent
} = e.target;
// Depending on the text content value
// choose the path to take
switch (textContent) {
case 'Send Request': {
para.textContent = 'Request Pending';
e.target.disabled = true;
setTimeout(() => {
para.textContent = 'Friend';
para.className = 'friend';
e.target.textContent = 'Cancel';
e.target.disabled = false;
}, 2000);
break;
}
case 'Cancel': {
para.textContent = 'Strenger';
para.className = 'strenger';
e.target.textContent = 'Send Request'
break;
}
}
}
}
.friend { color: rgb(66, 178, 113); }
.strenger { color: rgb(221, 66, 66); }
<div class="container">
<div id="cluster" class="box"></div>
</div>
Additional documentation
Destructuring assignment
map
matches

You are using document.querySelector() which will only select the first matched item.
According to MDN
The Document method querySelector() returns the first Element within the document that matches the specified selector, or group of selectors.
Instead what you need is querySelectorAll
const buttons = userList.querySelectorAll("button");
buttons.forEach((btn) => {
btn.addEventListener("click", function() {
//...
}
});
Edit:
All buttons changing the first text is the same issue as querySelector. It can be fixed by scoping it.
const buttons = userList.querySelectorAll("button");
buttons.forEach((btn) => {
btn.addEventListener("click", function() {
btn.querySelector(":scope > p").textContent = "Friend";
// ...
}
});

Related

I am unable to apply color styles for html elements generated via createElement in javascript

Issue is: after hitting the button on the HTML page, the html <h5> tag text changes on the page but the <h5> tag text color wont change to blue (expected behavior as CSS style doesn't reload after clicking the button).
What could be a possible workaround for solving this issue?
const btn = document.querySelector(".test");
btn.addEventListener("click", () => {
a1 = document.createElement('h5');
a1.className = "first";
a1.textContent = 'Blue updated.';
document.getElementById('position').innerHTML = a1.innerText;
//newtext = document.createTextNode('abc');
});
.test {
color: blue;
}
.first {
color: blue;
}
<h5 id="position">Text Color to be replaced to blue after hitting Blue button(but not happening)</h5>
<button class="test">Change to blue</button>
Above, after the button is clicked and the action listener is triggered, the HTML <h5> tag elements code are created with a1 = document.createElement('h5'); a1.className = "first"
The new text is displayed but the color didn't change (to blue).
You're inserting only the textContent instead of appending the entire new H5 element
const btn = document.querySelector(".test");
const pos = document.querySelector('#position');
btn.addEventListener("click", () => {
const h5 = document.createElement('h5');
h5.className = "first";
h5.textContent = 'Blue updated.';
pos.innerHTML = ""; // Empty
pos.append(h5); // Append!
});
.test, .first { color: blue; }
<h5 id="position">Text Color to be replaced to blue after hitting Blue button(but not happening)</h5>
<button class="test">Change to blue</button>
PS: You can also create some nifty reusable functions to handle the DOM querying and creation of elements, using a friendly syntax:
// DOM utility functions:
const el = (sel, par) => (par || document).querySelector(sel);
const elNew = (tag, prop) => Object.assign(document.createElement(tag), prop);
// Example:
const elPos = el('#position');
el(".test").addEventListener("click", () => {
const elH5 = elNew('H5', {
textContent: "Blue updated",
className: "first",
});
elPos.innerHTML = ""; // Empty
elPos.append(elH5); // Append!
});
.test, .first { color: blue; }
<h5 id="position">Text Color to be replaced to blue after hitting Blue button(but not happening)</h5>
<button class="test">Change to blue</button>

Switch paragraphs using inner HTML in JS

I am trying to switch two paragraphs after clicking the button but I am stuck. Is there any way how to do this using inner HTML without using IF or boolean? I tried this code but it doesn't work. Thanks
let elmsButton = document.querySelectorAll("button")[0];
let elmsParagraf1 = document.querySelectorAll(".prvni")[0];
let elmsParagraf2 = document.querySelectorAll(".druhy")[0];
elmsButton.addEventListener("click", () => {
elmsParagraf1.innerHTML = "<div class='druhy'></div>"
elmsParagraf2.innerHTML = "<div class='prvni'></div>"
});
Assign each DOM.innerHTML of paragraph to a variable then swap them like below example:
let elmsButton = document.querySelectorAll("button")[0];
let elmsParagraf1 = document.querySelectorAll(".prvni")[0];
let elmsParagraf2 = document.querySelectorAll(".druhy")[0];
elmsButton.addEventListener("click", () => {
const p1 = elmsParagraf1.innerHTML;
const p2 = elmsParagraf2.innerHTML
elmsParagraf1.innerHTML = p2;
elmsParagraf2.innerHTML = p1
});
<button>Click</button>
<div class='prvni'>Paragraph 1</div>
<div class='druhy'>Paragraph 2</div>
Why don't you use querySelector in place of using querySelectorAll and choose the first element?
By the way, I advise to delete and re-add the elements from the parent rather than using innerHTML. The use of innerHTML would not preserve listeners and have worse performances:
let elmsButton = document.querySelector("button");
let elmsParagraf1 = document.querySelector(".prvni");
let elmsParagraf2 = document.querySelector(".druhy");
elmsButton.addEventListener("click", () => {
swapElements(elmsParagraf1, elmsParagraf2);
});
function swapElements(elem1, elem2) {
// Check if siblings
if (elem1.parentElement !== elem2.parentElement) {
console.error('No sibling elements!');
return;
}
elem1.replaceWith(elem2.cloneNode(true));
elem2.replaceWith(elem1.cloneNode(true));
}
Example:
let elmsButton = document.querySelector("button");
elmsButton.addEventListener("click", () => {
let elmsParagraf1 = document.querySelector(".prvni");
let elmsParagraf2 = document.querySelector(".druhy");
swapElements(elmsParagraf1, elmsParagraf2);
});
function swapElements(elem1, elem2) {
// Check if siblings
if (elem1.parentElement !== elem2.parentElement) {
console.error('No sibling elements!');
return;
}
elem1.replaceWith(elem2.cloneNode(true));
elem2.replaceWith(elem1.cloneNode(true));
}
<button>Click me</button>
<div class="prvni">I am the first div</div>
<div class="druhy">I am the second div</div>
You can switch those two divs by creating a temporary variable that holds Paragraf1 and then change Paragraf1 to Paragraf2 and vice versa, but with the variable.
let temp = elmsParagraf1.innerHTML;
elmsParagraf1.innerHTML = elmsParagraf2.innerHTML
elmsParagraf2.innerHTML = temp

Javascript - Navigation as a loop

I would like to create a simple navigation:
<ul>
<li>Div1-name</li>
<li>Div2-name</li>
<li>Div3-name</li>
</ul>
When clicked, it goes to the div with that id. I don't want to do this permanently, it's supposed to change in the loop because the user can add new items and wants them to be looped.
The user adds new div - with a unique name and ID. How to construct a loop?
It's best to set a constant class for divs (e.g. gohere), you have to load it via javascript, then do li elements in a loop.
Can anyone help?
And these are the elements that the user adds:
<div class="divdiv" id="div_id">
<h3>DIV TITLE</h3>
<br>
Description
<br>
<p>Description</p>
<hr>
</div>
OK, it's really difficult to understand exactly what you ask for, but here (a bit hacky) an example. I hope that it can help you in the right direction.
var items = document.getElementById('items');
var main = document.getElementById('main');
items.addEventListener('click', e => {
e.preventDefault();
if (e.target.nodeName == 'A') {
console.log(`You clicked ${e.target.attributes['href'].value}`);
}
});
document.forms.new.addEventListener('submit', e => {
e.preventDefault();
let newid = items.children.length + 1;
console.log(newid);
let li = `<li>Div${newid}-name</li>`;
items.innerHTML += li;
let div = `<div class="divdiv" id="div${newid}">
<h3>DIV TITLE</h3><br>Description<br><p>Description</p><hr></div>`;
main.innerHTML += div;
});
<ul id="items">
<li>Div1-name</li>
<li>Div2-name</li>
<li>Div3-name</li>
</ul>
<form name="new">
<button>Add new</button>
</form>
<div id="main"></div>
Keep the navigation details in an array, and then iterate over it.
Use DOMParser to parse the HTML the user adds to extract the navigation information.
const arr = [
{ href: "div1", name: 'Div1-name' },
{ href: "div2", name: 'Div2-name' },
{ href: "div3", name: 'Div3-name' }
];
const nav = document.querySelector('#nav');
const main = document.querySelector('#main');
const text = document.querySelector('textarea');
const button = document.querySelector('button');
button.addEventListener('click', handleInput, false);
const parser = new DOMParser();
function handleInput() {
// Add the HTML to the page
main.insertAdjacentHTML('beforeend', text.value);
// Parse the HTML
const frag = parser.parseFromString(text.value, 'text/html');
// Extract the id (href) and name
const href = frag.querySelector('div').id;
const name = frag.querySelector('h3').textContent;
// Add this to the navigation array
// to the array
arr.push({ href: `#${href}`, name: name });
updateView();
}
function updateView() {
// `map` is useful here. It will produce a new array
// (of HTML), but make sure you `join` it up at the end
const links = arr.map(({ href, name }) => {
return `<li>${name}</li>`;
}).join('');
nav.innerHTML = `<ul>${links}</ul>`;
}
updateView();
<div id="nav"></div>
Add HTML:
<textarea></textarea>
<button>Add</button>
<div id="main"></div>

How to assign an event listener to every link I create in a forEach loop in Javascript

I am using a forEach loop and creating a link based on a condition. However, the problem I am facing is that the event listener is only added to the last item. I know this question has been asked before and I know the reason for the issue is Hoisting and I should use closure. I have tried to do that but I do not think I am doing it correctly. I would really appreciate any help as I am kind of stuck. Below are some snippets from my code (I have deleted some pieces from the code for the purpose of the question):
function edit(post, post_id){
alert(post_id);
}
function load_posts(event) {
fetch(`/posts?start=${start}&end=${end}`)
.then(response => response.json())
.then(data => {
document.querySelector('#posts').innerHTML = "<div></div>";
let c = 0;
data.forEach(i => {
if (c === data.length - 1){
return;
};
document.querySelector('#posts').innerHTML += `<div class = 'border' >`
let current_user = document.getElementById("logged_in").innerHTML;
document.querySelector('#posts').innerHTML += `<div class="row" ><div class="col-2"
id = border${i.id}></div>
</div></div>`;
if (i.user_id === parseInt(current_user)){
let element = document.createElement('a');
element.innerHTML = "Edit";
element.setAttribute('data-id', `${i.id}`);
element.setAttribute('data-post', `${i.id}`);
element.setAttribute('href', `#${i.id}`);
element.addEventListener('click', function()
{edit(this.dataset.id,this.dataset.post);});
console.log(element);
document.querySelector(`#border${i.id}`).append(element);
};
}
c++;
});});
Assigning to the innerHTML of an element will corrupt any existing listeners its children have:
document.querySelector('button').addEventListener('click', () => {
console.log('clicked');
});
container.innerHTML += '';
// click listener doesn't work anymore now
<div id="container">
<button>click</button>
</div>
Create the element with document.createElement instead of concatenating an HTML string:
const currentUserId = Number(document.getElementById("logged_in").textContent);
data.slice(0, data.length - 1).forEach((i, index) => {
const newPost = document.querySelector('#posts').appendChild(document.createElement('div'));
newPost.className = 'border';
newPost.innerHTML = `<div class="row" ><div class="col-2"
id = border${i.id}></div>
</div>`;
if (i.user_id === currentUserId) {
let element = document.createElement('a');
element.textContent = "Edit";
element.dataset.id = i.id;
element.dataset.post = i.id;
element.href = `#${i.id}`;
element.addEventListener('click', function() {
edit(this.dataset.id, this.dataset.post);
});
document.querySelector(`#border${i.id}`).append(element);
}
});
I'd also highly recommend changing your i variable name to something else - i is almost always used to indicate the index that you're iterating over. The element being iterated over should not be named i - call it something like postData instead, to avoid confusion.

Scope issues inside an Event Listener?

The following code basically shows/hides paragraph tags, I'm having to re-declare the paras variable. Is this because I'm dynamically injecting the button into the DOM, or is it to do with scope? How could I better construct this markup?
// vars
var revealContainer = document.querySelector('.reveal-more');
var paras = revealContainer.querySelectorAll('p');
var status = true;
// return
if (paras && paras.length <= 3) return;
// generate show more link
revealContainer.innerHTML += '<button class="button--text reveal-more__btn">Read more</button>';
var revealBtn = revealContainer.querySelector('.reveal-more__btn');
// click event
revealBtn.addEventListener('click', function () {
var paras = revealContainer.querySelectorAll('p');
// toggle show/hide class
for (var i = 0; i < paras.length; i++) {
var p = paras[i];
p.classList.toggle('is-shown');
}
// check status
if (status) {
this.textContent = 'Read less';
status = false;
} else {
this.textContent = 'Read more';
status = true;
}
});
You can use the live HTMLCollection returned by .getElementsByTagName() instead of the static NodeList returned by .querySelectorAll()
The getElementsByTagName method of Document interface returns an HTMLCollection of elements with the given tag name. The complete document is searched, including the root node. The returned HTMLCollection is live, meaning that it updates itself automatically to stay in sync with the DOM tree without having to call document.getElementsByTagName() again.
var paragraphs = document.getElementById("container").getElementsByTagName("p");
console.log(paragraphs.length);
setInterval(function() {
document.getElementById("container").insertAdjacentHTML("beforeend", "<p>p</p>");
}, 1000);
setInterval(function() {
console.log(paragraphs.length);
}, 2000);
<div id="container"></div>
Below is a really simple Snippet that demonstrates delegated events in pure Javascript, instead of using jQuery.
Here you can see I've attached the eventListener to the div with id elements, this will then listen for click events under this, a simple matches is used just in case you have other elements your not interested in..
document.querySelector("#elements").addEventListener("click", (e) => {
if (!e.target.matches('.element')) return
console.log(`Clicked ${e.target.innerText}`);
});
.element {
border: 1px solid black;
margin: 5px;
}
<div id="elements">
<div class="element">1</div>
<div class="element">2</div>
<div class="element">3</div>
<div>Clicking this does nothing.</div>
</div>

Categories