Changing getElementById to getElementsByClassName [Undefined] [duplicate] - javascript

This question already has answers here:
What do querySelectorAll and getElementsBy* methods return?
(12 answers)
Closed 4 years ago.
I have a problem using getElementsByClassName. When clicking on the add button, it appears on the list with the name undefined and not the value "hello". What am I doing wrong?
CODE:
function addItem(){
var ul = document.getElementById("dynamic-list");
var candidate = document.getElementsByClassName("candidate");
var li = document.createElement("li");
li.setAttribute('class',candidate.value);
li.appendChild(document.createTextNode(candidate.value));
ul.appendChild(li);
}
function removeItem(){
var ul = document.getElementById("dynamic-list");
var candidate = document.getElementsByClassName("candidate");
var item = document.getElementsByClassName(candidate.value);
ul.removeChild(item);
}
<ul id="dynamic-list"></ul>
<button type="submit" class="item" value="hello" onclick="addItem()">ADD</button>
<button onclick="removeItem()">remove item</button>

The problem is that you try to get the value of an unexisting DOM-element candidate. The possible solution could be to add an id-attribute to the button ADD and get this element in your function:
function addItem() {
var ul = document.getElementById("dynamic-list");
var candidate = document.getElementById("item");
var li = document.createElement("li");
li.setAttribute('class', candidate.value);
li.appendChild(document.createTextNode(candidate.value));
ul.appendChild(li);
}
<ul id="dynamic-list"></ul>
<button type="submit" class="item" id="item" value="hello" onclick="addItem()">ADD</button>
<button onclick="removeItem()">remove item</button>

The primary issue with your code is that the class name candidate does not exist anywhere in your HTML. If your goal is to reference the "ADD" button with this class, then you either need to change its class to candidate in your HTML, or change your JS to reference its actual class, item.
Furthermore, getElementsByClassName returns, as its name suggests, a NodeList of multiple elements, rather than a single element. This means that you'll need to select a single element from that list upon which to act; your code, in contrast, treats this method as though it returned only a single element to be acted upon directly (i.e., a Node instead of a NodeList). For your addItem function, this change is trivial; there's only one element with the item class, so simply select the first element of the NodeList (i.e., element 0). For the removeItem function, assuming you want to remove the most recently-added nodes first (LIFO-style), you'll need to remove the last element in the NodeList (if one exists) from the DOM. See the updated snippet below for all of these changes and some explanatory comments:
function addItem(){
var ul = document.getElementById("dynamic-list");
// We know that the "candidate" is always the first—and only—element with the class name "item"
var candidate = document.getElementsByClassName("item")[0];
var li = document.createElement("li");
li.setAttribute('class',candidate.value);
li.appendChild(document.createTextNode(candidate.value));
ul.appendChild(li);
}
function removeItem(){
var ul = document.getElementById("dynamic-list");
// We know that the "candidate" is always the first—and only—element with the class name "item"
var candidate = document.getElementsByClassName("item")[0];
// The "items" variable is introduced to hold the list of all added ".hello" elements, the last of which we will remove
var items = document.getElementsByClassName(candidate.value);
// Safety check: it's possible that there are no more ".hello" elements remaining, in which case there's nothing to remove
if (items.length > 0) {
// Access the last element of the NodeList and remove it from the DOM
var item = items[items.length - 1];
ul.removeChild(item);
}
}
<ul id="dynamic-list"></ul>
<button type="submit" class="item" value="hello" onclick="addItem()">ADD</button>
<button onclick="removeItem()">remove item</button>

Related

How do I add a delete button next to my task list?

This is what i have right now, what code do i add to make buttons next to my tasks, so with every new task there must come a remove button as well
document.getElementById('submit').addEventListener('click', withClick);
function withClick(e) {
const li = document.createElement('li')
document.getElementById('listid').appendChild(li);
li.appendChild(document.createTextNode(document.getElementById("text-area").value))
li.id = 'list'
li.className = 'collection-item'
const button = document.createElement('button')
document.getElementById('list').appendChild(button);
button.appendChild(document.createTextNode('x'))
button.className = "button"
}
<ul id="listid">
</ul>
<input id="text-area">
<button id="submit">submit</button>
This is what it looks like in browser
id needs to be unique in HTML.
Currently every <li> element you create has id="list". So when you do this:
document.getElementById('list')
Which one do you expect it to find and why? The behavior is likely undefined, but what you're observing is that it finds the first one with that id and then stops searching. Because why wouldn't it? Once it finds the element with the target id, as far as the browser is concerned it has found the one target element you're looking for.
Instead of fetching the element by its id, you already have a reference to it in the li variable. Just use that:
document.getElementById('submit').addEventListener('click', withClick);
function withClick(e) {
const li = document.createElement('li')
document.getElementById('listid').appendChild(li);
li.appendChild(document.createTextNode(document.getElementById("text-area").value))
li.className = 'collection-item'
const button = document.createElement('button')
li.appendChild(button); // <---- here
button.appendChild(document.createTextNode('x'))
button.className = "button"
}
<ul id="listid">
</ul>
<input id="text-area">
<button id="submit">submit</button>

how to get the index of the li clicked in javascript

Just using javascript, need to get the index of the li that's clicked with the following listener:
var ulList = document.getElementById('todo-list');
ulList.addEventListener('click', function(e){
if( e.target.nodeName == "BUTTON") {
//IS THERE A WAY INSIDE HERE TO GET THE INDEX OF THE li clicked
e.target.parentNode.remove();
}
});
each li looks like:
<li>
<input type="checkbox" value="1" name="todo[]">
<span class="centerSpan" style="text-decoration: none;">abc</span>
<button class="rightButton">X</button>
</li>
From the target (button, in this case) call closest() to get a reference to your li element
var li = e.target.closest('li');
Then get an array reference of your UL's children by using Array.from() and passing in the children HTMLCollection
var nodes = Array.from( ulList.children );
//or if you want to not depend on externally defined variables
var nodes = Array.from( li.closest('ul').children );
Finally call indexOf() to get the index
var index = nodes.indexOf( li );
If wanting to support old browsers will need to polyfill for methods like Array.from()

Trouble removing an element from an array

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>

Append multiple items in JavaScript

I have the following function and I am trying to figure out a better way to append multiple items using appendChild().
When the user clicks on Add, each item should look like this:
<li>
<input type="checkbox">
<label>Content typed by the user</label>
<input type="text">
<button class="edit">Edit</button>
<button class="delete">Delete</button>
</li>
and I have this function to add these elements:
function addNewItem(listElement, itemInput) {
var listItem = document.createElement("li");
var listItemCheckbox = document.createElement("input");
var listItemLabel = document.createElement("label");
var editableInput = document.createElement("input");
var editButton = document.createElement("button");
var deleteButton = document.createElement("button");
// define types
listItemCheckbox.type = "checkbox";
editableInput.type = "text";
// define content and class for buttons
editButton.innerText = "Edit";
editButton.className = "edit";
deleteButton.innerText = "Delete";
deleteButton.className = "delete";
listItemLabel.innerText = itemText.value;
// appendChild() - append these items to the li
listElement.appendChild(listItem);
listItem.appendChild(listItemCheckbox);
listItem.appendChild(listItemLabel);
listItem.appendChild(editButton);
listItem.appendChild(deleteButton);
if (itemText.value.length > 0) {
itemText.value = "";
inputFocus(itemText);
}
}
But you can notice that I am repeating three times the appendChild() for listItem. Is it possible to add multiple items to the appendChild() ?
You can do it with DocumentFragment.
var documentFragment = document.createDocumentFragment();
documentFragment.appendChild(listItem);
listItem.appendChild(listItemCheckbox);
listItem.appendChild(listItemLabel);
listItem.appendChild(editButton);
listItem.appendChild(deleteButton);
listElement.appendChild(documentFragment);
DocumentFragments allow developers to place child elements onto an
arbitrary node-like parent, allowing for node-like interactions
without a true root node. Doing so allows developers to produce
structure without doing so within the visible DOM
You can use the append method in JavaScript.
This is similar to jQuery's append method but it doesnot support IE and Edge.
You can change this code
listElement.appendChild(listItem);
listItem.appendChild(listItemCheckbox);
listItem.appendChild(listItemLabel);
listItem.appendChild(editButton);
listItem.appendChild(deleteButton);
to
listElement.append(listItem,listItemCheckbox,listItemLabel,editButton,deleteButton);
Personally, I don't see why you would do this.
But if you really need to replace all the appendChild() with one statement, you can assign the outerHTML of the created elements to the innerHTML of the li element.
You just need to replace the following:
listElement.appendChild(listItem);
listItem.appendChild(listItemCheckbox);
listItem.appendChild(listItemLabel);
listItem.appendChild(editButton);
listItem.appendChild(deleteButton);
With the following:
listItem.innerHTML+= listItemCheckbox.outerHTML + listItemLabel.outerHTML + editButton.outerHTML + deleteButton.outerHTML;
listElement.appendChild(listItem);
Explanation:
The outerHTML attribute of the element DOM interface gets the serialized HTML fragment describing the element including its descendants. So assigning the outerHTML of the created elements to the innerHTML of the li element is similar to appending them to it.
Merging the answers by #Atrahasis and #Slavik:
if (Node.prototype.appendChildren === undefined) {
Node.prototype.appendChildren = function() {
let children = [...arguments];
if (
children.length == 1 &&
Object.prototype.toString.call(children[0]) === "[object Array]"
) {
children = children[0];
}
const documentFragment = document.createDocumentFragment();
children.forEach(c => documentFragment.appendChild(c));
this.appendChild(documentFragment);
};
}
This accepts children as multiple arguments, or as a single array argument:
foo.appendChildren(bar1, bar2, bar3);
bar.appendChildren([bar1, bar2, bar3]);
Update – June 2020
Most all current browsers support append and the "spread operator" now.
The calls above can be re-written as:
foo.append(bar1, bar2, bar3);
bar.append(...[bar1, bar2, bar3]);
Let's try this:
let parentNode = document.createElement('div');
parentNode.append(...[
document.createElement('div'),
document.createElement('div'),
document.createElement('div'),
document.createElement('div'),
document.createElement('div')
]);
console.log(parentNode);
You need to append several children ? Just make it plural with appendChildren !
First things first :
HTMLLIElement.prototype.appendChildren = function () {
for ( var i = 0 ; i < arguments.length ; i++ )
this.appendChild( arguments[ i ] );
};
Then for any list element :
listElement.appendChildren( a, b, c, ... );
//check :
listElement.childNodes;//a, b, c, ...
Works with every element that has the appendChild method of course ! Like HTMLDivElement.
You can use createContextualFragment, it return a documentFragment created from a string.
It is perfect if you have to build and append more than one Nodes to an existing Element all together, because you can add it all without the cons of innerHTML
https://developer.mozilla.org/en-US/docs/Web/API/Range/createContextualFragment
// ...
var listItem = document.createElement("li");
var documentFragment = document.createRange().createContextualFragment(`
<input type="checkbox">
<label>Content typed by the user</label>
<input type="text">
<button class="edit">Edit</button>
<button class="delete">Delete</button>
`)
listItem.appendChild(documentFragment)
// ...
You could just group the elements into a single innerHTML group like this:
let node = document.createElement('li');
node.innerHTML = '<input type="checkbox"><label>Content typed by the user</label> <input type="text"><button class="edit">Edit</button><button class="delete">Delete</button>';
document.getElementById('orderedList').appendChild(node);
then appendChild() is only used once.
It's possible to write your own function if you use the built in arguments object
function appendMultipleNodes(){
var args = [].slice.call(arguments);
for (var x = 1; x < args.length; x++){
args[0].appendChild(args[x])
}
return args[0]
}
Then you would call the function as such:
appendMultipleNodes(parent, nodeOne, nodeTwo, nodeThree)
Why isn't anybody mentioning the element.append() function ?!
you can simply use it to append multiple items respectively as so:
listItem.append(listItemCheckbox, listItemLabel, editButton, deleteButton);
This is a quick fix
document.querySelector("#parentid .parenClass").insertAdjacentHTML('afterend', yourChildElement.outerHTML);
Guys I really recommend you to use this one.
[listItemCheckbox, listItemLabel, editButton, deleteButton]
.forEach((item) => listItem.appendChild(item));
Since you can't append multiple children at once. I think this one looks better.
Also here's a helper function that uses the fragment technique as introduced in the #Slavik's answer and merges it with DOMParser API:
function createHtmlFromString(stringHtml) {
const parser = new DOMParser();
const htmlFragment = document.createDocumentFragment();
const children = parser.parseFromString(stringHtml, "text/html").body
.children;
htmlFragment.replaceChildren(...children);
return htmlFragment;
}
Now to append multiple children with this, you can make the code much more readable and brief, e.g.:
const htmlFragment = createHtmlFromString(`<div class="info">
<span></span>
<h2></h2>
<p></p>
<button></button>
</div>
<div class="cover">
<img />
</div>
`);
Here's also a working example of these used in action: example link.
Note1: You could add text content in the above tags too and it works, but if it's data from user (or fetched from API), you'd better not trust it for better security. Instead, first make the fragment using the above function and then do something like this:
htmlFragment.querySelector(".info > span").textContent = game.name;
Note2: Don't use innerHTML to insert HTML, it is unsecure.
Great way to dynamically add elements to a webpage. This function takes 3 arguments, 1 is optional. The wrapper will wrap the parent element and it's elements inside another element. Useful when creating tables dynamically.
function append(parent, child, wrapper="") {
if (typeof child == 'object' && child.length > 1) {
child.forEach(c => {
parent.appendChild(c);
});
} else {
parent.appendChild(child);
}
if (typeof wrapper == 'object') {
wrapper.appendChild(parent);
}
}
I would like to add that if you want to add some variability to your html, you can also add variables like this:
let node = document.createElement('div');
node.classList.add("some-class");
node.innerHTML = `<div class="list">
<div class="title">${myObject.title}</div>
<div class="subtitle">${myObject.subtitle}
</div>`;

querySelectorAll not working

I have a requirement where I have to pickup the last .div within a container and apply some business logic to it. The selection of the last .div has to be dynamic because the user has the option to add/remove .div elements.
Initially I tried with querySelectorAll but it did not seem to work. So I decided to change it to getElementsByClassName and surprisingly it worked with the same logic. Can somebody please help me with the reason for why the remove_div doesn't work while the second one (remove_div_2) does?
Note: I am not looking for a fix/solution to the issue because I have already proceeded with the second option. I just want to know the reason why the option with querySelectorAll doesn't work.
Below is my code:
HTML:
<div id='container'>
<div id='div1' class='div'>This is Div 1</div>
<div id='div2' class='div'>This is Div 2</div>
<div id='div3' class='div'>This is Div 3</div>
</div>
<button type='button' id='append_div'>Append Div</button>
<button type='button' id='remove_div'>Remove Div</button>
<button type='button' id='remove_div_2'>Remove Div 2</button>
JavaScript:
window.onload = function () {
var elementToStyle = document.querySelectorAll("#container .div");
elementToStyle[elementToStyle.length - 1].classList.add('red');
document.getElementById('append_div').onclick = function () {
var divToInsert = document.createElement('div');
divToInsert.id = 'new_div';
divToInsert.className = 'div';
divToInsert.innerHTML = 'This is an appended div';
document.getElementById('container').appendChild(divToInsert);
var elToStyle = document.querySelectorAll("#container .div");
for (i = 0; i < elToStyle.length; i++)
elToStyle[i].classList.remove('red');
elToStyle[elToStyle.length - 1].classList.add('red');
};
document.getElementById('remove_div').onclick = function () {
var elToStyle = document.querySelectorAll("#container .div");
document.getElementById('container').removeChild(elToStyle[elToStyle.length - 1]);
elToStyle[elToStyle.length - 1].classList.add('red');
};
document.getElementById('remove_div_2').onclick = function () {
var elToStyle = document.getElementsByClassName('div');
document.getElementById('container').removeChild(elToStyle[elToStyle.length - 1]);
elToStyle[elToStyle.length - 1].classList.add('red');
};
}
The reason is because querySelectorAll method returns a static list. Any changes made to the document after the querySelectorAll is used (like removeChild in this case) will not be reflected in the list of nodes returned. Hence elToStyle[elToStyle.length - 1] would still point to the node that was removed.
Whereas, getElementsByClassName on the other hand returns a live list of nodes. This implies that elToStyle[elToStyle.length - 1] would always point to the last .div irrespective of any changes were done to the document after the node list was prepared or not.
Below is the extract from the official documentation available here
The NodeList object returned by the querySelectorAll() method must be
static, not live ([DOM-LEVEL-3-CORE], section 1.1.1). Subsequent
changes to the structure of the underlying document must not be
reflected in the NodeList object. This means that the object will
instead contain a list of matching Element nodes that were in the
document at the time the list was created.
Note: You can see this by doing a console.log(elToStyle); both before and after the removeChild.
If you want to reference the last division element just do the following...
var id = 'container';
var d = document.getElementById(id).getElementsByTagName('div')
var lastDiv = d[d.length - 1];
..and then apply your querySelector.

Categories