I started self teaching myself JavaScript a couple of weeks ago and have run into a problem I am unable to solve. The program works by allowing a user to enter a list of names which is saved in an array and also shown on the screen as li elements. The program will then randomly select one of the people entered. My problem occurs when trying to remove a person from the list. I am able to remove them from the HTML but not from the array. I have attempted to use the .splice method as shown below but this only removes the last element in the array. I believe this to be due to indexOf(li.value) not being suitable for this use but do not know what else to try. Any help is much appreciated.
<!DOCTYPE html>
<html>
<head>
<title>Student Randomiser</title>
<link rel="stylesheet" href="custom.css">
</head>
<body>
<h1 id="myHeading">Student Randomiser</h1>
<div class="list">
<p class="description">Add Students:</p>
<input type="text" class="studentName" value="Write Students Here">
<button class="addStudent">Add student</button>
<ul id= "listStudentNames">
</ul>
<button class="randomStudent">Select a random student</button>
</div>
<script src="randomNamePicker.js"></script>
</body>
</html>
const addStudent = document.querySelector('button.addStudent');
const studentName = document.querySelector('input.studentName');
const randomStudent = document.querySelector('button.randomStudent');
const listStudentNames = document.querySelector("ul");
let students = [
]
let number
window.onload=function(){
addStudent.addEventListener('click', addStudentToList);
randomStudent.addEventListener('click', selectRandomStudent);
listStudentNames.addEventListener("click", removeStudent);
}
function addButtons(li){
let remove =document.createElement('button');
remove.className= "removeStudent";
remove.textContent = "Remove Student";
li.appendChild(remove)
}
function removeStudent (){
if (event.target.tagName === "BUTTON") {
let li = event.target.parentNode;
let ul = li.parentNode;
let i = students.indexOf(li.value);
students.splice(i,1);
ul.removeChild(li);
}}
function getRandomIntInclusive(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
number = Math.floor(Math.random() * (max - min)) + min;
}
function addStudentToList() {
students.push(studentName.value);
var ul = document.getElementById("listStudentNames");
var li = document.createElement("li");
li.appendChild(document.createTextNode(studentName.value));
addButtons(li);
ul.appendChild(li);
studentName.value = "";
}
function selectRandomStudent(){
getRandomIntInclusive(0, students.length);
alert(students[number]);
}
There are a variety of issues with your code, some are programmatic, some are stylistic and some are your logic.
Your main issue is that the only elements that have a value property are form elements. So, when you write:
let i = students.indexOf(li.value);
You have an issue because you set li up to be the parent node of event.target. event.target is the element that initiated the event, in this case a <button> and the parent of the button is <div>, which (again) doesn't have a value property and is not the correct element anyway.
This value is what you are basing your splice on. Instead, you need to get the index position of the li within the list of li elements or the array (they should be the same).
Next, you don't really have a need for an array in this scenario in the first place since all of the student names will be elements within a <ul> element, this means that they can be accessed via a "node list", which is an "array-like" object that supports a length property, is enumerable and indexable. Keeping the <li> element content in a synchronized array doesn't add any value here and makes the overall task more complex that it need be because you are forced to keep the HTML list in sync with the array.
Having said all of this, here's a working example of what you are attempting, with comments inline to explain why my code differs from yours.
// When the DOM is loaded and all elements are accessible...
window.addEventListener("DOMContentLoaded", function(){
// Get references to the DOM elements needed to solve problem
// Using "var" here is perfectly acceptable as their scope will
// be the entire parent function, which is what we want. Get all
// these references just once so we don't have to keep scanning
// the DOM for them each time we want to work with the list. Also,
// name your variables "noun"-like names when they refer to elements
var btnAdd = document.querySelector(".addStudent");
var btnRandom = document.querySelector(".randomStudent");
var list = document.getElementById("listStudentNames");
var student = document.querySelector("input[type='text']");
var output = document.querySelector(".report");
// Set up an empty array to keep the synchronized student list in
var students = [];
// Set up click event handling functions
btnAdd.addEventListener("click", addStudent);
btnRandom.addEventListener("click", getRandomStudent);
function addStudent(){
// Make sure there was valid input
if(student.value.trim() === ""){ return; }
// Create a new <li> element
var li = document.createElement("li");
// Set new element up with a click event handler that will
// cause the current element to be removed from the list
li.addEventListener("click", removeStudent);
// Populate the element with the text from the <input>
// The element gets raw text set with the .textContent property
// while content of form elements is gotten with the "value"
// property
li.textContent = student.value;
// Update the list id's to match the array indexes
sync();
// Add the element to the end of the <ul>'s list elements
list.appendChild(li);
// Add new student to the array:
students.push(student.value);
// Clear value from input
student.value = "";
logResults();
}
function getRandomStudent(){
console.clear();
if(students.length){
// Use the built-in JavaScript Math object to get a random number
// between 0 (inclusive) and 1 (exclusive) then multiply that
// number by the lenght of the <li> node list to get a random
// number between 0 and the amount of elements in the array
var random = Math.floor(Math.random() * list.children.length);
console.log("Random student is: " + list.children[random].textContent);
} else {
console.log("No students to choose from!");
}
}
function removeStudent (evt){
// Re-sync the indexes of the HTML elements to match the array
sync();
// Remove corresponding student from array first...
console.clear();
console.log("Student " + evt.target.id + " about to be removed");
students.splice(+evt.target.id, 1);
// Every event handling function automatically gets a reference
// to the event that triggered the function sent into it. We can
// access that event to get a reference to the actual DOM object
// the caused the event to be triggered with "event.target"
list.removeChild(evt.target);
logResults();
}
// We have to keep the HTML element indexes in sync with the array indexes
function sync(){
// Loop through the HTML elements and give them an id that corresponds
// to the index position of its counterpart in the array.
Array.prototype.forEach.call(list.children, function(el, index){
el.id = index;
});
}
// This is just a function for updating the display to show the contents
// of the array to confirm that it is in sync with the list
function logResults(){
output.innerHTML = "Array now contains: <br>" + students.join("<br>");
}
});
.list, .report { float:left; }
.report { background-color:aliceblue; }
<div class="list">
<input type="text" class="studentName" placeholder="Enter Student Name Here">
<button class="addStudent">Add student to list</button>
<ul id= "listStudentNames">
</ul>
<p class="description">(Click on a student to remove them from the list.)</p>
<button class="randomStudent">Select a random student</button>
</div>
<div class="report"></div>
Related
The Problem:
Hey everyone. I'm trying to create a simple function that identifies the next and previous elements of a current item within the ".length" of elements in a div, and then changes the ID of those two elements. Everything is working except for the part where it tries to identify the previous element at the beginning and the next element at the end.
What I've Tried:
It used to be that it would identify those items by using ".nextElementSibling" and ".previousElementSibling", but I realized that since it starts at the first element within the div then it would begin leaking out and trying to identify the previous element outside of the div. I decided to use a for loop that creates a list of the elements with the specific class name, which works as intended. It begins to run into issues again, though, when it reaches the beginning or the end of the list. I assumed that "[i - 1]" would automatically bring it to the last element if the current was the one at the beginning of the list, and that "[i + 1]" would automatically bring it to the first element if the current was the one at the end of the list. It seems that is not the case.
Is anyone able to help me figure this out? It would be much appreciated.
Note: For the sake of simplicity, I didn't include the JavaScript code that makes it switch between items within the div. That part is fully functional so I don't believe it should affect the underlying concept of this problem.
My Code:
HTML:
<div class="items">
<div id="current-item" class="current-div-item"></div>
<div id="item" class="div-item"></div>
<div id="item" class="div-item"></div>
<div id="item" class="div-item"></div>
<div id="item" class="div-item"></div>
</div>
Javascript:
var divItems = document.getElementsByClassName('div-item'); // Gets number of elements with the specific class.
for (i = 0; i < divItems.length; i++) { // Creates list of elements with the specific class.
if (divItems[i].classList.contains('current-div-item')) { // If it is the current item, then do this:
var next = divItems[i + 1] // Find the next element in the list
var previous = divItems[i - 1] // Find the previous element in the list
next.id = 'next-item' // Change the next element's ID to "next-item"
previous.id = 'previous-item' // Change the previous element's ID to "previous-item"
}
}
You are wanting the items to wrap around that isn't going to happen. For the first item the previous item will be index -1 and for the last item the next index will be 1 larger than the actual number of items in the array.
If you add in a ternary you can get the values to wrap.
var prevIndex = (i === 0) ? divItems.length - 1 : i - 1;
var nextIndex = (i === divItems.length - 1) ? 0 : i + 1;
var next = divItems[prevIndex] // Find the next element in the list
var previous = divItems[nextIndex] // Find the previous element in the list
Based on your HTML code, in logic JS to fetch the all the items based in class it would not fetch the current-div-item as you have written logic to fetch only div-item. So I assume that you also need to change the HTML code. As per my understanding about your requirement I have done some changes and uploading the modified code. Which is working as per you requirement.
HTML:
<div id="current-div-item" class="div-item">Current</div>
<div id="item" class="div-item">Div1</div>
<div id="item" class="div-item">Div2</div>
<div id="item" class="div-item">Div3</div>
<div id="item" class="div-item">Div4</div>
Java Script:
var divItems = document.getElementsByClassName('div-item'); // Gets number of elements with the specific class.
for (i = 0; i < divItems.length; i++) {
if (divItems[i].id=='current-div-item') {
var next;
if (i == divItems.length-1)
next = divItems[0];
else
next = divItems[i + 1];
var previous;
if (i == 0)
previous=divItems[divItems.length-1];
else
previous = divItems[i - 1] // Find the previous element in the list
next.id = 'next-item' // Change the next element's ID to "next-item"
previous.id = 'previous-item' // Change the previous element's ID to "previous-item"
}
}
Attached the screenshot of the modified elements id for your reference
I want to replace a specific div element with a different one, when it has reached 3 clicks on it. That is the only task, I am trying to accomplish with the code.
I have tried looking at some code that does this but all of them replace it with get go, they don't give you a number amount to specify when to replace it with.
Example: <div id="1"></div> has been clicked on 3 times by a user. Once it exceeds that amount replace it with <div id="3"></div>
Changing the id attribute is not a good idea, instead you can use data- attribute like the following way:
var count = 0; // Declare a variable as counter
$('#1').click(function(){
count++; // Increment the couter by 1 in each click
if(count == 3) // Check the counter
$(this).data('id', '3'); // Set the data attribute
console.log($(this).data('id'));
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="1" data-id="1">Click</div>
You could write a JavaScript function that keeps track how often you clicked on a specific DOM element (i. e. the div element with id="1"). As soon as the element was clicked three times, it will be replaced by another DOM element which can be created in JavaScript as well.
var clicks = 0;
function trackClick(el) {
clicks++;
if(clicks === 3) {
var newEl = document.createElement('div');
newEl.textContent = 'Div3';
newEl.id = '3';
el.parentNode.replaceChild(newEl, el);
}
}
<div id="1" onclick="trackClick(this)">Div1</div>
In case you should use a library like jQuery or have another HTML structure, please specify your question to improve this code snippet so that it fits for your purpose.
The main idea is to start listening click events on the first div and count them.
The below code shows this concept. Firstly we put first div into variable to be able to create event listeners on it and also create count variable with initial value: 0. Then pre-make the second div, which will replace the first one later.
And the last part is also obvious: put event listener on a div1 which will increment count and check if it is equal 3 each time click happens.
const div1 = document.querySelector('#id-1');
let count = 0;
// pre-made second div for future replacement
const divToReplace = document.createElement('div');
divToReplace.id = 'id-2';
divToReplace.innerText = 'div 2';
div1.addEventListener('click', () => {
count ++;
if (count === 3) {
div1.parentNode.replaceChild(divToReplace, div1);
}
});
<div id="id-1"> div 1 </div>
Note that this approach is easy to understand, but the code itself is not the best, especially if you will need to reuse that logic. The below example is a bit more complicated - we create a function which takes 2 arguments: one for element to track and another - the element to replace with. Such approach will allow us to reuse functionality if needed.
function replaceAfter3Clicks(elem, newElem) {
let count = 0;
div1.addEventListener('click', () => {
count ++;
if (count === 3) {
elem.parentNode.replaceChild(newElem, elem);
}
});
}
const div1 = document.querySelector('#id-1');
// pre-made second div for future replacement
const div2 = document.createElement('div');
div2.id = 'id-2';
div2.innerText = 'div 2';
replaceAfter3Clicks(div1, div2);
<div id="id-1"> div 1 </div>
If you know, how to use JQuery, just put a click event handler on your div 1. On that handler, increment a click counter to 3. If it reaches 3, replace the div with JQuery again.
If there are multiple divs to replace, use an array of counters instead of a single one, or modify a user-specific data attribute via JQuery.
Using native JavaScript, rather than relying upon library (for all the benefits that might offer), the following approach is possible:
// A named function to handle the 'click' event on the relevant elements;
// the EventObject is passed in, automatically, from EventTarget.addEventListener():
const replaceOn = (event) => {
// caching the element that was clicked (because I'm using an Arrow function
// syntax we can't use 'this' to get the clicked element):
let el = event.target,
// creating a new <div> element:
newNode = document.createElement('div'),
// retrieving the current number of clicks set on the element, after this
// number becomes zero we replace the element. Here we use parseInt() to
// convert the string representation of the number into a base-10 number:
current = parseInt(el.dataset.replaceOn, 10);
// here we update the current number with the decremented number (we use the
// '--' operator to reduce the number by one) and then we update the
// data-replace-on attribute value with the new number:
el.dataset.replaceOn = --current;
// here we discover if that number is now zero:
if (current === 0) {
// if so, we write some content to the created <div> element:
newNode.textContent = "Original element has been replaced.";
// and here we use Element.replaceWith() to replace the current
// 'el' element with the new newNode element:
el.replaceWith(newNode);
}
};
// here we use the [data-replace-on] attribute-selector to search
// through the document for all elements with that attribute, and
// use NodeList.forEach() to iterate over that NodeList:
document.querySelectorAll('[data-replace-on]').forEach(
// using an Arrow function we pass a reference to the current
// Node of the NodeList to the function, and here we use
// EventTarget.addEventListener() to bind the replaceOn function
// (note the deliberate lack of parentheses) to handle the
// 'click' event:
(element) => element.addEventListener('click', replaceOn)
);
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
div {
border: 1px solid #000;
display: inline-block;
padding: 0.5em;
border-radius: 1em;
}
div[data-replace-on] {
cursor: pointer;
}
div[data-replace-on]::before {
content: attr(data-replace-on);
}
<div data-replace-on="3"></div>
<div data-replace-on="13"></div>
<div data-replace-on="1"></div>
<div data-replace-on="21"></div>
<div data-replace-on="1"></div>
<div data-replace-on="6"></div>
<div data-replace-on="4"></div>
References:
CSS:
Attribute-selectors ([attribute=attribute-value]).
JavaScript:
Arrow function syntax.
ChildNode.replaceWith().
document.querySelectorAll().
EventTarget.addEventListener().
NodeList.prototype.forEach().
I have a "Maindiv" (div) with, say, 4 elements in it. Lets consider the elements belong to a class called "Subdiv". When I query the number of "Subdivs" with "Maindiv.getElementsByClassName('Subdiv').length;" , it returns 4, as expected. But if I create a new "Subdiv" and append it to my main "Maindiv" and instantly query for the length, it will return 4 (which is wrong), and until the NodeList is updated (usually 20-50 milliseconds after appending the new element) it returns 4. Finally after this interval it returns the right number (5). My question is, if there's a way to update/refresh the NodeList faster just after I append the new element?
<div>
<div id='Maindiv'>
<div class='Subdiv' id='Subdiv1'></div>
<div class='Subdiv' id='Subdiv2'></div>
<div class='Subdiv' id='Subdiv3'></div>
<div class='Subdiv' id='Subdiv4'></div>
</div>
<button type='button' onclick='CreateNewSubdivs()'>Create Subdiv</button>
</div>
<script>
function CreateNewSubdivs(){
var MainDiv = document.getElementById('Maindiv');
var SubdivsLength= MainDiv.getElementsByClassName('Subdiv').length;
var NewSubDiv = document.createElement('div');
var NewCopyNumber = SubdivsLength+1;
var NewSubDivID = 'Subdiv'+NewCopyNumber;
NewSubDiv.setAttribute('class', 'Subdiv');
NewSubDiv.setAttribute('id', NewSubDivID );
MainDiv.appendChild(NewSubDiv );
var SubdivsLength= MainDiv .getElementsByClassName('Subdiv').length;
console.log(SubdivsLength); /// This number is wrong until 20-50 millisec later
}
</script>
A NodeList can either be a static or "live" collection of nodes.
In some cases, the NodeList is live, which means that changes in the DOM automatically update the collection.
e.g. Node.childNodes
In other cases, the NodeList is static, where any changes in the DOM does not affect the content of the collection.
e.g. a list returned by querySelectorAll()
Source: https://developer.mozilla.org/en-US/docs/Web/API/NodeList
As you can see in this example, I set a reference to childNodes once. It is kept up to date as soon as the DOM changes.
const list = document.querySelector('#main');
const items = list.childNodes;
setInterval(() => {
const item = document.createElement('li');
item.innerHTML = '🌯';
list.appendChild(item);
console.log(items.length);
}, 500);
<ul id="main"></ul>
getElementsByClassName also returns a live collection of nodes:
const list = document.querySelector('#main');
const spans = list.getElementsByClassName('burrito');
setInterval(() => {
const item = document.createElement('li');
item.innerHTML = '<li><span class="burrito">🌯</span></li>';
list.appendChild(item);
console.log(spans.length);
}, 1);
<ul id="main">
</ul>
I've been struggling with the same piece of code for a few days by now...
So for the html part I have this :
<input type="text" id="search_immobilier_ville" name="search_immobilier[ville]">
<div class="collection" id="search-ville-collection"></div>
I have an input where I have to type any city name, then I want to filter the matching cities names into an existing array of cities like this :
let api_results = [['Le Moule',97152],['Lamentin',97189],...]
let ville_input = document.getElementById('search_immobilier_ville');
Then display the matching cities as a list of elements in the div #search-ville-collection.
On each keyup event, I need to perform this action, and update the visual list in real time.
My issue is that my filtering system is messed up, if I search "lam" for example, I can get a city called "lamentin" (pass the test) and another with just the "la" matching like "capesterre-de-marie-galante"
So far, I've done this :
// Previously filled by an API
let api_results = [[name,postalCode],[name,postalCode],...];
ville_input.addEventListener('keyup', () => {
let value = ville_input.value.toUpperCase().trim();
// User input
let input_val = ville_input.value.toUpperCase().trim();
// Filtering through var ville_input
api_results.forEach(el => {
if (el[0].toUpperCase().startsWith(input_val) || input_val.length >= 2 && el[0].toUpperCase().includes(input_val)) {
result_list.style.display = 'block';
// if city not present in the html list, add it
if (document.getElementById(el[1]) === null) {
$(result_list).append(`<a class="collection-item search-ville-results" id="${el[1]}"> ${el[0]} - ${el[1]} </a>`);
}
}
}); // End forEach
/* Looping through the collection child nodes to check
if there are cities that don't match the user input */
for (let child of result_list.children) {
console.log(child)
// if the user input doesn't match with an existing city in the node, delete the node
if (!child.text.toUpperCase().includes(input_val)) {
result_list.removeChild(child);
}
}
// Highlight first element of the list
result_list.firstElementChild.classList.add('active');
// empty results div if user input is empty
if (input_val == '') {
result_list.style.display = 'none';
result_list.innerHTML = '';
}
});
This code works PARTIALLY. For example, if I type "lam", I'm supposed to get only one result based on my result set, but check out this scenario :
Typing "l":
Typing "la":
Typing "lam":
(Here you begin to see the issue)
Typing "lame":
I'm sure there's something wrong in my code, but I can't figure out what.
Your problem is with the loop you are using to remove invalid items:
for (let child of result_list.children) {
console.log(child)
// if the user input doesn't match with an existing city in the node, delete the node
if (!child.text.toUpperCase().includes(input_val)) {
result_list.removeChild(child);
}
}
children returns a live HTMLCollection, meaning that if you modify it (eg, by removing items) it will update, which will cause issues with your loop. You need to go through the items in a way that will not be affected if the collection changes.
Wrong way:
This is an example of how the loop behaves currently. The button should remove all the items that contain "th", but note how it doesn't get them all and requires multiple clicks:
document.querySelector('#removeAll').addEventListener('click', () => {
let list = document.querySelector('#list')
for (let item of list.children) {
if (item.textContent.toLowerCase().includes('th')) {
list.removeChild(item)
}
}
})
<button type="button" id="removeAll">Remove All</button>
<ul id="list">
<li>Stuff</li>
<li>Things</li>
<li>Others</li>
<li>More</li>
</ul>
(A) correct way:
One way to loop through the collection in a way that is not affected by items being removed is to start at the last index and go backwards:
document.querySelector('#removeAll').addEventListener('click', () => {
let list = document.querySelector('#list')
let index = list.children.length
while (index--) {
let item = list.children[index]
if (item.textContent.toLowerCase().includes('th')) {
list.removeChild(item)
}
}
})
<button type="button" id="removeAll">Remove All</button>
<ul id="list">
<li>Stuff</li>
<li>Things</li>
<li>Others</li>
<li>More</li>
</ul>
Better way
As an additional note, you might be better off just clearing the list entirely and using filter to get the matching results and then update the list that way. As it is currently, you are doing a lot of checking to see if the list already contains the item, checking the current list for invalid items, etc. That will affect performance of your UI, especially on lower end devices.
Try to clear out your result_list as the first thing you do inside your keyup event.
result_list.innerHTML = '';
After that, make sure to filter your api_results.
const filteredResults = api_results.filter(result => result[0].toUpperCase().includes(input_val));
console.log(filteredResults); // Sanity check.
filteredResults.forEach(result => /* your old function. */);
I'm making a to-do list to help me understand Javascript. I've managed to create a series of <li> elements with text inside. I'd like to delete the <li> elements, when they are clicked. However, at the moment they are unresponsive.
JSFIDDLE: http://jsfiddle.net/tmyie/aYLFL/
HTML:
<input id="input" placeholder="Write here">
<button>Add</button>
<hr>
<ul></ul>
Javascript:
var doc = document, // creates a variable, changing 'document' to the variable 'doc'
list = doc.getElementsByTagName('ul')[0],
li = doc.getElementsByTagName('li')[0],
input = doc.getElementById('input'),
button = doc.getElementsByTagName('button')[0]; // creates a variable called 'button', that gets the first array of all the <button> elements
button.onclick = function () {
var mySubmission = doc.getElementById('input').value; // Get values of the input box and call it 'mySubmission'
var item = doc.createElement('li'); // Creates a <li> element in a variable called 'item'
item.innerHTML = mySubmission; // Inside the created 'item', the inner HTML becomes mySubmission + the class 'remove'
list.appendChild(item); // get <ul>, and add the variable 'item'.
doc.getElementById('input').value = ""; // resets input after submission
};
The remove function:
li.onclick = function () {
li.parentNode.removeChild(li);
};
Excuse the excessive comments, I'm try get a better understanding of Javascript.
You define li to be:
li = doc.getElementsByTagName('li')[0]
But there are no li elements to begin with, so doc.getElementsByTagName('li')[0] returns undefined.
You'll need to move that event handler into the other callback:
list.appendChild(item); // get <ul>, and add the variable 'item'.
item.onclick = function () {
list.removeChild(item);
};