I created an app that receives articles from news API. Each article is displayed in a card, which has a button "Open Modal".
This button opens a modal with the unique information that pertains to each respective article.
However, I am unable to close the modal once it's opened. I suspect it's because the modal is stuck in this state: modals.forEach((modal, index) => {modal.classList.toggle('open', index === openIndex);
Here is my current code:
{{!-- #each article --}}
<div class="row">
{{#each articles}}
<div class="col-12-sm col-6-md col-3-lg">
<div class="card m-2">
<div class="card-body">
<h5 class="card-title">{{title}}</h5>
<p class="card-text">{{description}}</p>
</div>
<img class="card-image" src="{{urlToImage}}" alt="Card image cap">
<button data-open-modal="{{#index}}">Open Modal</button>
</div>
</div>
{{/each}}
</div>
</div>
{{#each articles}}
<!-- The Modal -->
<div class="modal closed" id="Modal_{{#index}}">
<!-- Modal content -->
<div class="modal-content">
<span id="spm" class="close" >×</span>
<h2>{{title}}</h2>
<img src="{{urlToImage}}" alt="">
<p>{{content}}</p>
</div>
</div>
{{/each}}
<script>
//Store all modals and modal buttons in variables
const openModalButtons = document.querySelectorAll('[data-open-modal]');
const modals = document.querySelectorAll('.modal');
//Loop through all modal buttons and assign handler to each
openModalButtons.forEach(openModalButton => {
openModalButton.addEventListener('click', (event) => {
//Get index value from number clicked
const openIndex = Number(event.target.dataset.openModal); //Access dataset attribute to read and write
//Loop over each modal.
//Set modal class to open if index is equal to wanted index
modals.forEach((modal, index) => {
modal.classList.toggle('open', index === openIndex);
modal.classList.toggle('closed', index !== openIndex);
});
});
});
</script>
And here is what I tried adding to my script: (It gave no error but did nothing)
const span = document.querySelectorAll('.close');
let spanArr = Array.prototype.slice.call(span);
spanArr.forEach(spanArr => {
spanArr.addEventListener('click', (event) => {
const closeIndex = Number(event.target.dataset.closeModal);
spanArr[closeIndex].forEach(span => {
span.onclick = function() {
modal.style.display = "none";
}
});
});
});
I also tried adding event listeners to the spans, but I was unable to make it work. I am a beginner and this is my first time using handlebars, so thank you for any insight!
There are some issues here that perhaps are due to absent explanations on my part in https://stackoverflow.com/a/73738690/3397771.
First, the reason that const openIndex = Number(event.target.dataset.openModal); works is because there is a data-attribute called data-open-modal defined on each "open" button. It is that data-attribute that we are referencing with dataset.openModal and the value we will get back is the value on the right-hand-side of the equal sign in the attribute, the {{#index}} part.
However, the data-attribute approach is probably excessively complicated for our purposes here. We could, alternatively, use the index obtained in the forEach loop we use to iteratively add our click event listeners.
Next, there is no need for the spanArr[closeIndex].forEach(... loop inside our click handler. spanArr - despite its name - is not an arr(ay); it is a single span element.
The updated code becomes:
const span = document.querySelectorAll('.close');
span.forEach((spanArr, index) => {
spanArr.addEventListener('click', () => {
modals[index].style.display = "none";
});
});
Note: I have left the names of the variables as I found them, but they could and should be improved. For example, span does not communicate what purpose of these elements is or, for that matter, that it is a collection. closeButtons would be a better name. In fact, elements that behave like buttons should use the <button> element, not <span>, so as to be semantically correct and accessible.
I have created a new fiddle.
Related
I want click neighbor and change the main country. I click first card ok no problem. but other cards not working. This is my code:
function renderNeighbors(data) {
let html = "";
for (let country of data) {
html += `
<div class="col-2 mt-2">
<div class="card click-change">
<img src="${country.flags.png}" class="card-img-top">
<div class="card-body">
<h6 class="card-title neighborName">${country.name.common}</h6>
</div>
</div>
</div>
`;
}
document.querySelector("#neighbors").innerHTML = html;
document.querySelector(".click-change").addEventListener("click", () => {
let neighborName = document.querySelector(".neighborName").innerHTML;
getCountry(neighborName);
})
}
in photos I point red circle in my browser. that's only working card.
enter image description here
I want to click neighbors and change main country. but only work for first neighbor
here is my all html code https://pastebin.com/Sb9XWZhy
here, the problem you are setting your event only for the first chosen element that matches your locator .
querySelector return only the first element that matches.
you should add the event for all of your nodes that match the locator.
It's supposed that querySelectorAll returns an array that you can loop over it like that.
for (let element of document.querySelectorAll(".click-change")){
element.addEventListener("click", () => {
let neighborName =
element.children[1].children[0].innerHTML;//here I'm accessing the neighbourName from the element itself, I'm not sure about the indices but you can handle them yourself
getCountry(neighborName);
})
}
This question already has answers here:
Why does querySelector only select the first element and how can I fix this?
(4 answers)
Closed 1 year ago.
I have this function:
var togView = document.querySelector('.toggle-view');
var list = document.querySelector('.pt-list');
togView.onclick = function() {
const isAdded = togView.classList.toggle('list');
list.classList.toggle('list', isAdded);
};
Which I'm using to toggle a class called .list on and off on this little element called toggle-view and on a sibling element called pt-list
<div class="toggle-view">
<span class="thumb-view">Thumbnail View</span> | <span class="list-view">List View</span>
</div>
<div class="pt-list"></div>
This worked fine until I added another <div class="toggle-view"> element. Now only the first iteration of the toggle-view element works.
How could I get this function to work on both <div class="toggle-view"> elements independently without having to assign them an ID and duplicate the function?
I found a similar question on here from a few years ago and from what I understand, I need to use this?, but I can't figure out how to apply that to what's happening in my function.
Any guidance would be appreciated!
You can use "document.querySelectorAll('.toggle-view')" and querySelectorAll will return a list of html elements with class name of ".toggle-view". Then use foreach loop through this list and add click listener to trigger click event independently.
See the snippet below, the result might be vary, but logic is there:
var togView = document.querySelectorAll('.toggle-view');
var list = document.querySelector('.pt-list');
togView.forEach((button) => {
button.addEventListener('click', () => {
const isAdded = button.classList.toggle('list');
list.classList.toggle('list', isAdded);
//Test for showing the className onto html element, independently
button.innerHTML = button.classList.value
});
});
<div class="toggle-view">
<span class="thumb-view">Thumbnail View</span> | <span class="list-view">List View</span>
</div>
<div class="toggle-view">
<span class="thumb-view">Thumbnail View</span> | <span class="list-view">List View</span>
</div>
<div class="pt-list"></div>
I've been playing around with jQuery for ages but am finally trying to learn clean Vanilla JS.
I have a list of elements:
<div id="seriesList" class="seriesList rollable">
<div class="seriesLink" series="7">
<h3 class="name">Carrow Road</h3><p class="location">Norwich</p>
</div>
<div class="seriesLink" series="6">
<h3 class="name">White Heart Lane</h3><p class="location">London</p>
</div>
<div class="seriesLink" series="5">
<h3 class="name">Parc des Princes</h3><p class="location">Paris</p>
</div>
</div
I'm toggling a series of GSAP animation after clicking one of the .seriesLink. The first one i'm trying to achieve is making every elements exept the one clicked disapear.
i.e: I click on #carrow-road — #white-lane and #parc-des-princes would disapear.
I have this:
document.querySelectorAll(".seriesLink").forEach(item => {
item.addEventListener('click', event => {
// ForEach.Not ?
document.getElementById("seriesList").classList.toggle("rollable");
document.getElementById("home").classList.add("scrollable");
document.getElementById("rightPanel").classList.remove("scrollable");
tlOpenSeries.play();
})
})
The "class" system in Javascript is getting me lost, as I don't seem to be able to target my elements successfully.
I can't find a way to "reproduce" the each.not jquery provides. Any idea? Shall I add a class first to the clicked element and then target all elements without this "active" class? Is there a shortcut?
Many thanks
To accomplish that in vanilla JS you have to loop through the elements and check if the current element is not the clicked element.
Demo:
var divs = document.querySelectorAll(".seriesLink");
divs.forEach(item => {
item.addEventListener('click', event => {
for(var i = 0; i < divs.length; i++){
if(event.currentTarget != divs[i]){ // check here
divs[i].style.display = "none";
}
}
//.......
//.......
});
});
<div id="seriesList" class="seriesList rollable">
<div class="seriesLink" series="7">
<h3 class="name">Carrow Road</h3><p class="location">Norwich</p>
</div>
<div class="seriesLink" series="6">
<h3 class="name">White Heart Lane</h3><p class="location">London</p>
</div>
<div class="seriesLink" series="5">
<h3 class="name">Parc des Princes</h3><p class="location">Paris</p>
</div>
</div>
you can use filter:
const seriesLinks = document.querySelectorAll(".seriesLink");
seriesLinks.forEach(item => {
item.addEventListener('click', event => {
seriesLinks
.filter(i => i != item)
.forEach(i => // your logic... //);
//... rest of your code ... //
})
})
but anymay i think that a good practice is to split the code to simple little functions, for example hideAllExceptCurrent (allElemArray, currentElem), hideAllToggleCurrent (allElemArray, currentElem)
I have a parent div with some child elements. I want to re-order child elements based on two id values. for example 1,4. It means to grab the item with id 1 and insert it above the item with id 4.
<div class="parent">
<div id="1">First</div>
<div id="2">Second</div>
<div id="3">Third</div>
<div id="4">Fourth</div>
<div id="5">Fifth</div>
</div>
Making a drag and drop component for react. And this is what i have tried
const element = document.getElementById('1') //dragStart
const targetElement = document.getElementById('4') //dragEnter
const parent = document.querySelector('.parent') // drop
parent.insertBefore(element, targetElement)
But problem is when i grab the first element and want to put it on the bottom (last child). It fails to do so. How to put a child element after last child with insertBefore() method?
Don't know how you are using insertBefore() but there should not be any issues:
Update: The issue could be that your code is running before the DOM is fully loaded. You can wrap your code with DOMContentLoaded:
<script>
document.addEventListener('DOMContentLoaded', (event) => {
const element = document.getElementById('1') //dragStart
const targetElement = document.getElementById('4') //dragEnter
const parent = document.querySelector('.parent') // drop
parent.insertBefore(element, targetElement)
});
</script>
<div class="parent">
<div id="1">First</div>
<div id="2">Second</div>
<div id="3">Third</div>
<div id="4">Fourth</div>
<div id="5">Fifth</div>
</div>
Placing the first element as the last element using nextSibling:
<script>
document.addEventListener('DOMContentLoaded', (event) => {
const parentNode = document.querySelector('.parent');
const element = document.getElementById('1') //dragStart
const targetElement = document.querySelector('.parent').lastElementChild //get last child
parentNode.insertBefore(element, targetElement.nextSibling);
});
</script>
<div class="parent">
<div id="1">First</div>
<div id="2">Second</div>
<div id="3">Third</div>
<div id="4">Fourth</div>
<div id="5">Fifth</div>
</div>
Note: This answers the original question. The question has now been edited to reference React. You wouldn't use the following in a React project. You'd reorder the state that the DOM represents, and then let React handle updating the DOM.
You're right to use insertBefore:
function moveElement(move, before) {
// Get the element to move
const elToMove = document.getElementById(move);
// Get the element to put it in front of
const elBefore = document.getElementById(before);
// Move it
elBefore.parentNode.insertBefore(elToMove, elBefore);
}
function moveElement(move, before) {
const elToMove = document.getElementById(move);
const elBefore = document.getElementById(before);
elBefore.parentNode.insertBefore(elToMove, elBefore);
}
setTimeout(() => {
moveElement("1", "4");
}, 800);
<div class="parent">
<div id="1">First</div>
<div id="2">Second</div>
<div id="3">Third</div>
<div id="4">Fourth</div>
<div id="5">Fifth</div>
</div>
Side note: I suggest avoiding having id values that start with digits. Although they're perfectly valid HTML and they work just fine with getElementById, they're a pain if you need to target them with CSS, because a CSS ID selector (#example) can't start with an unescaped digit. For instance, document.querySelector("#1") fails. You have to escape the 1 with a hex sequence, which isn't terrifically clear: document.querySelector("#\\31") (the characters \, 3, and 1: 0x31 = 49 = the Unicode code point for 1).
Working with Bootstrap and JavaScript and I am using it as an accordion format - once clicked on the collapsed div it will open and show the items within the div based on the id.
Problem:
If the div doesn't contain any items i want it to open and show a message to the user:
"no items here"
How do I go about doing that? In the JavaScript ?
This is what I have:
View
<div class="accordion-body collapse state-loading" data-group-id="13" data-bind="attr: { 'id': 'GroupMember_' + Id(), 'data-type-id': ModelId() }" id="GroupMember_15" data-type-id="15">
<div class="accordion-inner no_border" data-bind="foreach: Children"></div><!--END: accordion-inner--></div>
</div>
If the Children are 0 i want it to open and have this text show: No items here
Javascript:
OnSuccess: function (data) {
var _groups = linq.From(options.groupData);
var _groupsToUpdate = _groups .Where(function (x) { return x.Id == options.groupId; });
if (_groupsToUpdate.Any()) {
_groupsToUpdate.First().Children = data.Items;
}
Not sure if i am missing anything else to share - let me know.
UPDATE
Div Layout:
<div class='accordion-group'>
<div class='accordion-heading'> Group 1 </div>
<div class='accordion-body'>
<div class='accordion-inner'>
<div class='element'>No items here</div>
</div>
</div>
</div>
I have to click on the 'accordion-heading' class in order to display the 'accordion-body' and get into the accordion-inner items
You'd need to bind to the show event on the accordion elements and perform your check there, from your classes I'm assuming your using Bootstrap v2.3.2:
$('.accordion .collapse').on('show', function () {
var $inner = $(this).find('.accordion-inner');
if($inner.is(':empty')){
$inner.html('No items here');
}
});
Demo fiddle
Note that the :empty selector is very picky, it will not work if there's any white space between the opening and closing tags of .accordion-inner.
You may also use if(!$.trim($inner.html())) to check if the element is empty or as #JL suggested check the length of the children elements just beware that text nodes are not treated like children, so a div with only text would be considered empty
Do you have jQuery installed? You can check if a <div> has children like this:
if ($('#divId').children().length == 0) {
$('#divId').append("no items here");
}
If you don't have jQuery:
if (!document.getElementById('divId').hasChildNodes()) {
document.getElementById('divId').innerHTML = "no items here";
}
Based on your edit, I think we're inspecting accordian-inner for children. If so, give it an ID and substitute that into our code. Note: You don't need a <div> to contain our "no items" message...the message will get printed with javascript (Plus, if you have a <div> there, then you've in effect added a child and the message no longer applies). Change your HTML to this:
<div class='accordion-group'>
<div class='accordion-heading'> Group 1 </div>
<div class='accordion-body'>
<div id='innerId' class='accordion-inner'>
<!-- Remove the 'element' div -->
</div>
</div>
</div>
Then:
if (!document.getElementById('innerId').hasChildNodes()) {
document.getElementById('innerId').innerHTML = "no items here";
}