Keep original array of elements when using .remove() - javascript

I have an array of delete button elements:
const DELETE_BUTTONS = document.getElementsByClassName('delete-button');
These delete buttons are in a table, and each of them are inside a table row in its own td. I add an event listener to it in three different ways.
To already rendered delete buttons:
for (let x = 0; x < DELETE_BUTTONS.length; x++) {
DELETE_BUTTONS[x].onclick = function () {
deleteProperty(DELETE_BUTTONS[x]);
};
}
To a new row in the table, which is an inline-form that I add with another onclick:
...
CLONED.classList.add('cloned', 'properties__row');
const DELETE_BUTTON = CLONED.lastElementChild.lastElementChild;
DELETE_BUTTON.onclick = function () {
deleteProperty(DELETE_BUTTON);
};
When the user has submitted the inline form, I get the data back with Ajax, and create a new tr and append it to the table body. (To create this element easily I use another JS function. Note that I did not include the bit where I create the tr itself, it is irrelevant.)
MAKE_ELEMENT('a', {onclick: 'deleteProperty(this)', class: ['btn-flat', 'waves-effect', 'btn-floating']},
MAKE_ELEMENT('i', ['material-icons', 'red-text'],
'delete_forever'
)
)
This is the function that is added with the onclick:
function deleteProperty(element) {
element.parentElement.parentElement.remove();
}
This works perfectly, right up until I delete a second element. Every time I delete an element, it also removes it from the array. Meaning that deleteProperty(DELETE_BUTTONS[19]) will delete/remove a different element than it did at first. How can I prevent the array from changing length?
I tried to make a copy of the array, but it behaved exactly like the original. I could also just insert an empty place holder in the array when I remove an element so it stays the same length, but this doesn't seem like good practice. Any pointers would be welcome, thanks.

Related

javascript iteration within if statement

In this JSFiddle (with the problem code commented out) the first click in an empty cell sets a value in a hidden input and sets the bgcolor of the cell to green. A click in a second empty table cell sets the value of another hidden input and changes the second cell bgcolor to red.
Now, based on feedback from another SO question I have tried to implement a check by looping through an array (All the commented out code) of all td's in the table to see if onclick, any cell already has a bgcolor set to green/red respectively and if true, set the bgcolor to empty/blank to allow for the NEW cell selection to get the bgcolor , so there should always be only 1 green block and 1 red block. Can someone explain to me how I am implementing the loop and check wrong and not getting the expected result.
the array looping works here -jsfiddle when not part of the existing code. But when I add it to code where it's needed, it does not work.
HTLM
<div id="starget"></div>
<div id="etarget"></div>
<table width="100%" id="test">
<thead>
<tr>
<th>Tech</th>
<th>0800</th>
<th>0900</th>
<th>1000</th>
<th>1100</th>
<th>1200</th>
</tr>
</thead>
<tr>
<td>Bill</td>
<td>XXX</td>
<td onclick="res(0900,this);"></td>
<td>XXX</td>
<td>XXX</td>
<td onclick="res(1200,this);"></td>
</tr>
</table>
SCRIPT
var x = 0;
var click = 0;
/* tdElements = document.getElementsByTagName("td"); */
/* I have tried the tdelements array inside and outside of the function */
function res(zz,el) {
if (click == 0) {
/* for(var key in tdElements) {
if (tdElements[key].style.backgroundColor=="green") {
tdElements[key].style.backgroundColor="";
}
} */
document.getElementById('starget').innerHTML=zz;
click = 1;
el.style.backgroundColor='green';
}
else {
/* for(var key in tdElements) {
if (tdElements[key].style.backgroundColor=="red") {
tdElements[key].style.backgroundColor="";
}
} */
document.getElementById('etarget').innerHTML=zz;
click = 0;
el.style.backgroundColor='red';
}
}
If you call .getElementsByTagName, you are not getting back an array! You are getting back a live HTMLCollection element, which contains some other items that you cannot ignore when using a for.. in loop. Heres where a for loop wil come in handy as your live HTMLCollection contains a length you can use!
/* HTMLCollections come with a length attribute we can use
* as every item is numbered, but in a object-style key-value pair */
for(var i = 0; i < tdElements.length; i++){
/* This was tripping up your code - the `item` function to fetch an
* element at the index defined in the HTMLCollection*/
tdElements.item(i).style.backgroundColor = "green"
}
This will turn all the backgrounds green. Now you will have to amend your code, but thats how it works. More info here: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection
What you really want to be doing is handling this with a class, rather than a style. The reason is that you can very easily pull up all of the elements that have a specific class using getElementsByClassName (note: in IE 9 and later).
Without deviating from your approach TOO much (I did make it a little more efficient :) ), your code would need to change to:
CSS:
.firstClick {background-color: green;}
.secondClick {background-color: red;}
Script:
var click = 0;
function res(zz,el) {
var currentlyClickedEls, currClass, divTarget;
if (click === 0) {
currClass = "firstClick";
divTarget = "starget";
click = 1;
}
else {
currClass = "secondClick";
divTarget = "etarget";
click = 0;
}
// get all the elements with the appropriate class
currentlyClickedEls = document.getElementsByClassName(currClass);
// remove that class from those elements
while (currentlyClickedEls.length > 0) {
currentlyClickedEls[0].className = "";
}
// add the class to the clicked element
el.className = currClass;
document.getElementById(divTarget).innerHTML = zz;
}
With this approach, on the click, the first thing that you would do is find all of the elements with the class firstClick or secondClick (based on the value of click) and remove the class. Since there should only ever be one, at the most, that makes for a VERY short loop to go through (but would also remove the class from more than one element, if that were to happen somehow).
EDIT:
So, you wouldn't have noticed it in your code, since you don't ever have more than one instance of each of the two classes, but, as somethingthere pointed out, we are dealing with a LIVE collection of DOM elements, so, as you remove the classes from the elements (the element that you removed them from, falls out of the collection.
For example, on the third click of your table, there will be one element in the collection that is returned by document.getElementsByClassName(currClass); = HTMLCollection[td.firstClick] (in the console). But, once you've removed that class from the element, it no longer matches the criteria, so the collection becomes empty (i.e., HTMLCollection[]).
So, while it works for your situation (because it stops on i = 1 because that is still "not less than" the new length of the collection (which is now 0).
However, in other situations, that might not be the case . . . for example, if there were 2 elements returned, after the first one had its class removed, i would change to 1, but the length would also drop to 1 and the loop would stop, without processing the second element.
So the approach really needs to be changed to handle that behavior correctly. I've done that by changing these lines:
for (i = 0; i < currentlyClickedEls.length; i++) {
currentlyClickedEls[i].className = "";
. . . to these lines:
while (currentlyClickedEls.length > 0) {
currentlyClickedEls[0].className = "";
This way, the element that has it's class removed is always the first one in the collection and the while loop checks to make sure that there is still an element in the collection, before it attempts to remove the class.
So, long story, short, when dealing with an HTML Collection, as somethinghere pointed out, you are dealing with a live collection of DOM elements, so, if you make a change that would affect an individual elements inclusion in that Collection, it WILL update the Collection accordingly and you need to be able to account for that in your code.

Can't understand Javascript eventhandler with for loop code

I'm trying to learn JavaScript and I saw a code to change the css style of a web page depending on the button you press.
I can't understand why or how a for loop indicate witch button was press. Here is the javascript part:
var buttons = document.getElementsByTagName("button");
var len = buttons.length;
for (i = 0; i < len; i++) {
buttons[i].onclick = function() {
var className = this.innerHTML.toLowerCase();
document.body.className = className;
};
}
http://jsfiddle.net/qp9jwwq6/
I looked on the net and w3 school but they don't explain that code with a for loop. Can someone explain it to me?
Thank you
Lets break it down.
First we need to have access to the DOM element on the page, so we do that by using a method on the document itself which will return the element we want to manipulate.
var buttons = document.getElementsByTagName("button");
The buttons var will be a list of ALL the buttons on the page. We want to do something with all of them, so first we cache the length of the list, i.e, count how many buttons we have.
var len = buttons.length;
Then we basically say: set i to 0, and step it up one until its equal to the number of buttons we have.
for (i = 0; i < len; i++) {
Now, to access one button from the list, we need to use the brackets notation. So buttons[0] is the first element, buttons[1] is the second, etc. Since i starts at 0, we put i in the brackets so that on each iteration it will access the next button in the list.
buttons[i].onclick = function() {
var className = this.innerHTML.toLowerCase();
document.body.className = className;
};
}
This is equivalent of doing:
buttons[0].onclick = function() {
var className = this.innerHTML.toLowerCase();
document.body.className = className;
};
buttons[1].onclick = function() {
var className = this.innerHTML.toLowerCase();
document.body.className = className;
};
buttons[2].onclick = function() {
var className = this.innerHTML.toLowerCase();
document.body.className = className;
};
// etc.
But of course that is super inefficient, and we may not know how many buttons the page has. So we get all the buttons there, find out how many there are, then go through each button and assign an event handler to it along with a new class.
Now, looking at the onclick handler itself we can see that it first finds the HTML within the button being clicked, turns it into lowercase letters, and assigns it to a variable:
var className = this.innerHTML.toLowerCase();
By using this we're ensuring that each button will know to get it's own innerHTML when clicked. We're not tracking which button is which, we're just telling each button to check it's own content.
Then what it does is change the class of the body HTML element to whatever it is that we just parsed
document.body.className = className;
So say you have something like
<button>success</button>
<button>failure</button>
<button>warning</button>
Clicking the first button would set the <body> element's class to success, clicking the second would set it to failure, and the third would set it to warning.
First line saves all buttons in a variable called buttons. This is actually an array since there can be several buttons on the page. Then you iterate through each button and define a function which should be executed onclick. Lets say you have 2 buttons then it will be buttons[0] and buttons[1] which get the function.
Firstly, speaking generally, the underlying basis for this code is a little wonky and unusual and non-robust, so don't anticipate that you're on the brink of learning any powerful insight into JavaScript or code design.
On to the answer:
The for-loop does not "indicate" which button was pressed. Rather, it loops through every button element on the page and assigns the exact same function definition to the onclick attribute of each element. The code that ends up running when a particular button element is clicked (here I'm talking about the function body) assigns a CSS class to the body element by assigning to document.body.className.
Your question is asking how the function knows which class name to assign to document.body.className. The function grabs the class name from the innerHTML of the button element, which is accessible as this.innerHTML (because in an event handler, this is a reference to the element on which the triggering event occurred). The HTML <button> element is a little bit special, in that, although it is generally a simple-looking button, it is also a non-leaf node, meaning it contains its own HTML. You can put a lot of things in there, but in this example, they just have a plain text node which consists of exactly (or nearly exactly) the class name (Normal for one and Changed for the other). That's how the function can get a CSS class name that is specific to that button; it grabs it from the text inside the clicked <button> element.
I said "nearly exactly" back there because there's actually a letter-case difference between the button text and the actual CSS classes they've defined in the CSS rules (which are normal and changed). That's why they have to lower the letter-case of the extracted button text (toLowerCase()) before assigning the class name. CSS classes are case-sensitive (see Are CSS selectors case-sensitive?).
As I said, this is unusual code. It is rather inadvisable to create a mapping (especially an inexact mapping!) between plain HTML text and code metadata (CSS classes in this case).

loop through and remove form elements in Javascript

Hi I have been writing a Javascript quiz whilst learning Javascript, but have encountered a problem.
I have one function that dynamically creates the question/answers with radio buttons to mark off the questions.
When I use this second function to attempt to remove the question/answers so I can show the new ones; it removes the text (in p tags) but doesn't remove the radio buttons, even though they also show as children to the form element.
function removeLastQuestions() {
var allQuestions = document.getElementById('questionForm');
for (var i = 0; i < allQuestions.children.length; i++) {
allQuestions.removeChild(allQuestions.children[i]);
}
}
The question/answers and buttons are contained within a form with the id of "questionForm"
I guess I could put the whole form within a div and remove the form, but I'm wondering why looping over them isn't working. I'm trying to do it without using Jquery.
Thanks for any help.
Try this way:
var node = document.getElementById('questionForm');
while (node.hasChildNodes()) {
node.removeChild(node.lastChild);
}
This will remove all form elements.
JSFiddle demo

Using an If statement on .map object in Jquery

Edited for clarity. (thanks)
On my page, there are dynamically created divs that contain input elements(they clone the original). Each div refers to a unique form, but the ID of the input elements are not unique in each div(this may be a problem).
The code below successfully searches my doc for all of the divs that have been created(Their ID is medical-report(random number), gathers their input elements and then outputs each string in the array separated by a comma.
var $inputs= $("div[id^='medical-report']").find("input").clone();
$("#multi").append(($inputs).map(function() {
return $(this).val();
})
.get()
.join(", "));
}
What I want to do instead, is traverse the array (not sure if I have truly created one), and then looks at the ID of each input element and append unique paragraph text for each to an area in my page. It would basically summarize the contents of each div.
(search for inputs, and clone them)
var $inputs= $("div[id^='medical-report']").find("input").clone();
Then
pseudo code:
for each div=
If the ID is "nameofdoc" add paragraph The name of the doctor is "nameofdoc.val"
If the ID is "datereceived" add paragraph the document was received on "datereceived.val"
and so on.
Just to restate, a problem here may be that "nameofdoc" and "datereceived" are not unique ID's. The container divs however are unique.
var $inputs= $("div[id^='medical-report']").find("input").clone();
$("#multi").append(($inputs).map(function(index) {
return $(this).val();
.get();
if ($(this).is("#MEDRIN")) {
$("p").text("The medical was received on" +index);
}
})
I have read up on .each and.contents in the jquery API but I am really unsure of what direction to go in. Fiddle is below.
http://jsfiddle.net/gdoax9q2/7/
There are a number of issues here. I will list the steps that I would take to solve this problem.
Rename the function from "sayHi" to a name that describes what this function does. "generateReportSummaries" seems appropriate.
I removed all of the innerHTML stuff because I had no idea what it was doing.
Use jQuery to grab a collection of all the forms we want to create summaries for (I gave the forms each a "report-form" class to make this easier.
Create a function that will take a a form element and return a formatted element that we can append to the document. I called my function "getReportElement".
For each form element, call getReportElement and append the result to #mutli.
Implement getReportFormat to map the values for all the form's inputs - include a conditional for when input's name attribute is 'MEDRIN'. Join the values array and append it to the new element that the function returns.
function generateReportSummaries () {
/* Get a jQuery wrapped collection of all .report-form elements. */
var $forms = $('.report-form');
/* Call getReportElement for each .report-from. */
$forms.each(function () {
$('#multi').append(getReportElement($(this)));
});
}
function getReportElement ($form) {
/* Create a new paragraph element to contain our text. */
var $paragraph = $('<p></p>');
/* Get an array of the form's input values. */
/* If input is [name="MEDRIN"], add prefix text to array value. */
var mapped_values = $form.find('input').map(function () {
if ($(this).is('[name="MEDRIN"]')) {
return 'The medical was received on ' + $(this).val();
} else {
return $(this).val();
}
}).get();
/* Join our values array and set it as our paragraph element's text. */
$paragraph.text(mapped_values.join(', '));
/* Return our (jQuery-wrapped) paragraph element. */
return $paragraph;
}
I removed the input ids and replaced them with name attributes because ids should not be duplicated. Also, the datepicker was causing issues for the cloned forms, so I changed things a little to make everything work. You can see my result at: http://jsfiddle.net/76484/qLeg5wm9/

InnerHTML content is different then actually seen on screen

I have dynamically created span elements that listen to an onkeyup action.
For example: When a user types into a textbox, it should replace the text inside the <span> too.
lets say I create N elements of span.
N-1 of then works perfectly, but the last one does not.
When I use Inspect Element (Chrome browser) and make the call
document.getElementById("span...").innerHTML
I see the new text, but on screen I still see the old one.
My code:
1. This is the part when i dynamically create the text inputs
titleInput.onkeyup = (function ()
{
var inputObject = titleInput;
var chartIndex = numberOfTds; return function () {
UpdateTitleInMagnifiedView(inputObject, chartIndex);
} })();
UpdateTitleInMagnifiedView - function that updates the span elemnts
document.getElementById("title_magnified_" + chartIndex).innerHTML = inputObject.value;

Categories

Resources