Dynamic index setting in javascript - javascript

I have a table to which i need to add rows dynamically on click of a button. Each row has 3 textboxes and a clear button. On click of clear the data in the textboxes need to be cleared i.e. onclick of the button i send the index of the row to a method which deletes the contents of the textboxes at that index.
Problem - How do i specify the index number in the onClick property of the row button while adding the new row?

How do i specify the index number in the onClick property of the row button while adding the new row?
You don't. Instead, use the fact that the textboxes and the button are in the same row. I probably wouldn't use onclick on the button at all; instead, I'd have a single click handler on the table and handle the button clicks there (this is called event delegation). Something like this:
var table = document.getElementById("theTableID");
table.onclick = function(event) {
var elm, row, boxes, index, box;
// Handle IE difference
event = event || window.event;
// Get the element that was actually clicked (again handling
// IE difference)
elm = event.target || event.srcElement;
// Is it my button?
if (elm.name === "clear") {
// Yes, find the row
while (elm && elm !== table) {
if (elm.tagName.toUpperCase() === "TR") {
// Found it
row = elm;
break;
}
elm = elm.parentNode;
}
if (row) {
// Get all input boxes anywhere in the row
boxes = row.getElementsByTagName("input");
for (index = 0; index < boxes.length; ++index) {
box = boxes[index];
if (box.name === "whatever") {
box.value = "";
}
}
}
}
};
...but if you want to keep using the onclick attribute on the button instead, you can grab the middle of that:
The button:
<input type="button" onclick="clearBoxes(this);" ...>
The function:
function clearBoxes(elm) {
var row, boxes, index, box;
// Find the row
while (elm) {
if (elm.tagName.toUpperCase() === "TR") {
// Found it
row = elm;
break;
}
elm = elm.parentNode;
}
if (row) {
// Get all input boxes anywhere in the row
boxes = row.getElementsByTagName("input");
for (index = 0; index < boxes.length; ++index) {
box = boxes[index];
if (box.name === "whatever") {
box.value = "";
}
}
}
}
References:
DOM2 Core specification - well-supported by all major browsers
DOM2 HTML specification - bindings between the DOM and HTML
DOM3 Core specification - some updates, not all supported by all major browsers
HTML5 specification - which now has the DOM/HTML bindings in it, such as for HTMLInputElement so you know about the value and name properties.
Off-topic: As you can see, I've had to work around some browser differences and do some simple utility things (like finding the nearest parent element of an element) explicitly in that code. If you use a decent JavaScript library like jQuery, Prototype, YUI, Closure, or any of several others, they'll do those things for you, letting you concentrate on your actual problem.
To give you an idea, here's that first example (handling the click via event delegation) written with jQuery:
$("#theTableID").delegate("input:button[name='clear']", "click", function() {
$(this).closest("tr").find("input:text[name='whatever']").val("");
});
Yes, really. And other libraries will similarly make things simpler.

Best to use event delegation, or you can use this in JavaScript.
Event Delegation w/jQuery
<input class="clear-row-btn" type="button" >Clear Row</input>
.live event
$(".clear-row-btn").live("click", function(){
var $tr = $(this).closest("tr");
$tr.find("input[type='text']").val("");
});
HTML w/onclick method
<input type="button" onclick="clearRow(this)" >Clear Row</input>
jQuery
function clearRow(btn) {
var $tr = $(btn).closest("tr");
$tr.find("input[type='text']").val("");
}
JavaScript
function clearRow(element) {
while(element.nodeName!='TR'){
element = element.parentNode;
}
//find textboxes inside the element, which is now the parent <tr>
}

Related

Javascript Vanilla - Double Click event handler on inputs / GetElementsByTagName

Problem: Can't assign a double-click event handler to my generated inputs; is this feasible using the getElementsByTagName?
Thanks for any help
Here is the code:
Generated inputs
function list_tasks() {
let container = document.getElementById("todo");
container.innerHTML = ""
if (task_array.length > 0) {
for (let i = 0; i < task_array.length; i++) {
let input = document.createElement("input");
input.value = task_array[i];
input.classList.add("record");
input.disabled = true;
container.appendChild(input);
}
}
}
Attaching the event
document.getElementsByClassName("record").addEventListener("dblclick", editTask);
And the console.log is never called
function editTask(e){
console.log("double click")
}
Update
Trying to loop across the array, but still, no double click event is fired
let record = document.getElementsByClassName("record");
for(var i = 0; i <= record.length; i++){
document.getElementsByClassName("record")[i].addEventListener("dblclick", editTask);
}
getElementsByClassName returns a nodes list i.e. an array. To access the element you need to get the value form the array.
Try this:
document.getElementsByClassName("record")[0].addEventListener("dblclick", editTask);
This should work.
The reason why this doesn't work is because you are marking the inputs as disabled. Disabled inputs don't react to some events, and looks like double-click is one of them.
Also, as #Royson wrote, getElementsByClassName() returns a list of multiple elements. If you want to add an event listener to all of them you have 2 options:
The best one IMO, if possible, is to attach it while creating the elements in list_tasks() function:
let input = document.createElement("input");
input.value = task_array[i];
input.classList.add("record");
input.disabled = true;
input.addEventListener("dblclick", editTask); // <--- here
container.appendChild(input);
If this is not possible to due scopes being inaccessible, you just loop over the result of getElementsByClassName():
Array.from(document.getElementsByClassName("record")).forEach(el => el.addEventListener("dblclick", editTask));
Edit: The spec says that "click" events should be disabled on a disabled input. Event though double-click isn't specified directly, my guess is that it's implied by it being a click too, or it requires click to be enabled so it can catch two fast ones.
https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#enabling-and-disabling-form-controls%3A-the-disabled-attribute
The way to do it is to create items as list items and then make contentEditable on the list items on double click B-)

Deleting table rows using this in JavaScript

I'm trying to build an event that would delete a row in my table.In every row I have delete button and applicable row should be deleted once button in this row is clicked. Is there a way to do it using 'this' property? I tried with calculating indexes for row and button but it was too confusing, I am looking for simpler code. Here is what I got till now, part with 'this' doesn't work obviously. Can you advise if there is similar way to select applicable row?
document.addEventListener("DOMContentLoaded", function () {
var buttons = document.querySelectorAll(".deleteBtn");
function removeItem (e) {
var rows = document.querySelectorAll("tr");
rows[this].parentNode.removeChild(rows[this]);
}
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener("click", removeItem);
}
})
this will refer to your .deleteBtn element. Assuming that's inside the row you want to remove, you need to traverse up that element's parents to find the tr, and then remove it:
function removeItem(e) {
var tr = this;
while (tr.tagName != "TR") {
tr = tr.parentNode;
if (!tr) {
// Didn't find an ancestor TR
return;
}
}
tr.parentNode.removeChild(tr);
}
On modern browsers you could change the removeChild line to just:
tr.remove();
...but I have to admit I don't know how well-supported that is (I'm looking at you, Microsoft).

Event listener fails to attach or remove in some contexts

I've created a script that attaches an event listener to a collection of pictures by default. When the elements are clicked, the listener swaps out for another event that changes the image source and pushes the id of the element to an array, and that reverses if you click on the swapped image (the source changes back and the last element in the array is removed). There is a button to "clear" all of the images by setting the default source and resetting the event listener, but it doesn't fire reliably and sometimes fires with a delay, causing only the last element in a series to be collected.
TL;DR: An event fires very unreliably for no discernible reason, and I'd love to know why this is happening and how I should fix it. The JSFiddle and published version are available below.
I've uploaded the current version here, and you can trip the error by selecting multiple tables, pressing "Cancel", and selecting those buttons again. Normally the error starts on the second or third pass.
I've also got a fiddle.
The layout will be a bit wacky on desktops and laptops since it was designed for phone screens, but you'll be able to see the issue and inspect the code so that shouldn't be a problem.
Code blocks:
Unset all the selected tables:
function tableClear() {
//alert(document.getElementsByClassName('eatPlace')[tableResEnum].src);
//numResTables = document.getElementsByClassName('eatPlace').src.length;
tableArrayLength = tableArray.length - 1;
for (tableResEnum = 0; tableResEnum <= tableArrayLength; tableResEnum += 1) {
tableSrces = tableArray[tableResEnum].src;
//alert(tableSrcTapped);
if (tableSrces === tableSrcTapped) {
tableArray[tableResEnum].removeEventListener('click', tableUntap);
tableArray[tableResEnum].addEventListener('click', tableTap);
tableArray[tableResEnum].src = window.location + 'resources/tableBase.svg';
} /*else if () {
}*/
}
resTableArray.splice(0, resTableArray.length);
}
Set/Unset a particular table:
tableUntap = function () {
$(this).unbind('click', tableUntap);
$(this).bind('click', tableTap);
this.setAttribute('src', 'resources/tableBase.svg');
resTableArray.shift(this);
};
tableTap = function () {
$(this).unbind('click', tableTap);
$(this).bind('click', tableUntap);
this.setAttribute('src', 'resources/tableTapped.svg');
resTableArray.push($(this).attr('id'));
};
Convert the elements within the 'eatPlace' class to an array:
$('.eatPlace').bind('click', tableTap);
tableList = document.getElementsByClassName('eatPlace');
tableArray = Array.prototype.slice.call(tableList);
Table instantiation:
for (tableEnum = 1; tableEnum <= tableNum; tableEnum += 1) {
tableImg = document.createElement('IMG');
tableImg.setAttribute('src', 'resources/tableBase.svg');
tableImg.setAttribute('id', 'table' + tableEnum);
tableImg.setAttribute('class', 'eatPlace');
tableImg.setAttribute('width', '15%');
tableImg.setAttribute('height', '15%');
$('#tableBox').append(tableImg, tableEnum);
if (tableEnum % 4 === 0) {
$('#tableBox').append("\n");
}
if (tableEnum === tableNum) {
$('#tableBox').append("<div id='subbles' class='ajaxButton'>Next</div>");
$('#tableBox').append("<div id='cazzles' class='ajaxButton'>Cancel</div>");
}
}
First mistake is in tapping and untapping tables.
When you push a Table to your array, your pushing its ID.
resTableArray.push($(this).attr('id'));
It will add id's of elements, depending on the order of user clicking the tables.
While untapping its always removing the first table.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/shift
resTableArray.shift(this);
So, when user clicks tables 1, 2, 3. And unclicks 3, the shift will remove table 1.
Lets fix this by removing untapped table
tableUntap = function () {
$(this).unbind('click', tableUntap);
$(this).bind('click', tableTap);
this.setAttribute('src', 'http://imgur.com/a7J8OJ5.png');
var elementID = $(this).attr('id');
var elementIndex = resTableArray.indexOf(elementID);
resTableArray.splice(elementIndex, 1);
};
So you were missing some tables after untapping.
Well lets fix tableClear,
You have a array with tapped tables, but you are searching in main array.
function tableClear() {
tableLen = resTableArray.length;
for (var i = 0; i < tableLen; i++) {
var idString = "#" + resTableArray[i];
var $element = $(idString);
$element.unbind('click', tableUntap);
$element.bind('click', tableTap);
$element.attr("src", 'http://imgur.com/a7J8OJ5.png');
}
resTableArray = [];
}
Im searching only tapped tables, and then just untap them and remove handlers.
fiddle: http://jsfiddle.net/r9ewnxzs/
Your mistake was to wrongly remove at untapping elements.

How do we implement a cancel in plain Javascript?

I have a page and I display data in a table.
In each table I have a column with a checkbox which if is checked the user can modify the specific row via Javascript.
This is done as its td encapsulates either an input or a select and I make these editable for the user.
The user modifies the row and presses save and the changes are saved. So far ok.
My problem is how do I implement a cancel?
The user could choose many row i.e. check boxes and modify them but the user could also press cancel. On cancel the original values should be displayed (and the rows become non-editable again).
But how is a cancel operation implemented in Javascript? Do we store data in some global datastructures? Which would be this in Javascript?
Ok, after the addition of informations you provided I suggest you setup the following mecanism:
function getDatas() {
var oXhr;
//get datas from database:
oXhr = new XMLHttpRequest();
oXhr.onreadystatechange = function() {
if (oXhr.readyState == 4 && (oXhr.status == 200)) {
g_oData = (new DOMParser()).parseFromString(oXhr.responseText, "text/xml");
}
}
oXhr.open("POST", "yourphpscriptthatreturnsthexmldatas.php", true);
oXhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=utf-8");
oXhr.send();
}
function populateGrid() {
//use g_oData to populate your grid, but at first totally clean the body
var mygrid = document.getElementById("mygridid");
//mygrid.innerHtml = "<table><tr><td>...</td></tr></table>";
//use the xml library to parse g_oData and fill up the table:
var xmlRows = g_oData.getElementsByTagName("TAG");
var xmlRow;
iLen = xmlRows.length;
for (var i=0;i<iLen;i++) {
xmlRow = xmlRows[i];
//use xmlRow->textContent to build each cell of your table
}
}
function revertChange() {
//on cancel, revert the changes by populating the grid.
//it will use the global xml/json object loaded directly from database, to refill everything.
populateGrid();
}
I did it myself many times to refresh some datas in a page. That's basically what you're doing except that you're not requesting anything to the database, you just refill the fields.
You can just access the original value attribute of the input to get the defaultValue. Sample implementation:
$("table").on("dblclick", "td", function(e) {
var val = $(this).html();
$(this).empty().append($("<form/>").append(
$("<input/>", {type:"text"}).attr("value", val),
// ^^^^
// set the *attribute*, as if it was present in the parsed HTML
$("<button/>", {type:"reset"}).text("Reset"),
$("<button/>", {type:"button", class:"cancel"}).text("Cancel"),
$("<button/>", {type:"submit"}).text("Submit")
));
}).on("submit", "form", function(e) {
var val = $(this).find("input:text").val();
// ^^^^^
// which is equivalent to .prop("value")
/* then do something with val, e.g. send it to server via ajax */
$(this).parent().html(val);
e.preventDefault();
}).on("click", "button.cancel", function(e) {
var $form = $(this).parent(),
$input = $form.find("input:text"),
oldval = $input.attr("value");
// ^^^^^^^^^^^^^
// or .prop("defaultValue"), but not .val()!
if (oldval == $input.val() || confirm("Do you really want to discard your changes?"))
$(this).parent().html(oldval);
e.preventDefault();
});
(Demo at jsfiddle.net)
A maybe more simple solution might be to use the dblclick-handler that creates the form as a closure and just store the original html in a local variable there.
Here is a pretty simple way:
Don't replace the cell content with the form element. Keep the value (the text) in a span element and hide it when you show the form element. Then you don't have to do anything on cancel. Just show the span again and hide or remove the form element. Only update the span when the user wants to save the value.
Here is an example. The showing and hiding is all done with CSS.
<tr>
<td>
<span>value</span>
<input type='text' value='' />
</td>
<td>
<button class="save">Save</button>
<button class="revert">Revert</button>
</td>
</tr>
JS:
var rows = document.querySelectorAll('table tr');
for(var i = 0, l = rows.length; i < l; i++) {
rows[i].addEventListener('click', function(event) {
// all value display elements in the row
var spans = this.querySelectorAll('span');
// all form elements in the row
var inputs = this.querySelectorAll('input');
// handle click on save button
if (event.target.className === 'save') {
[].forEach.call(inputs, function(input, i) {
spans[i].innerHTML = input.value;
});
this.className = '';
}
// handle click on revert button
else if (event.target.className === 'revert') {
// not much to do
this.className = '';
}
else {
// update form element values
[].forEach.call(inputs, function(input, i) {
input.value = spans[i].innerHTML;
});
this.className = 'edit';
}
});
}
DEMO
You can use the HTML5 data- attributes to implement a revert function. This way, each <input> would hold it's original value in case a revert button would be used.
Here's how it'd look:
<table>
<tr>
<td><input type='text' value='change me' data-original='change me' /></td>
<td><input type='text' value='change me2' data-original='change me2' /></td>
<td><input type='button' value='revert' onclick='revert(this)'/></td>
</tr>
<table>
And the code that reverts:
function revert(btn) {
var parentTr = btn.parentNode.parentNode;
var inputs = parentTr.getElementsByTagName('input');
for(var i = 0; i < inputs.length; i++) {
if (inputs[i].type == 'text') {
inputs[i].value = inputs[i].getAttribute('data-original');
}
}
}
The data-original attribute could be generated:
By the server-side app who serves the page (see (1) demo fiddle here); or
by a JavaScript function that is executed as soon as the DOM is ready (see (2) demo fiddle for this here).
As a side solution, you could store the original values in a map object. Here's the (3) demo for this (notice I added the id for each input, so it can be used as key to the map).
Keep in mind, though, neither solutions (2) or (3) require changing in server side code (the 3 assuming your inputs have ids). And (2) feels clearer.
About the defaultValue attribute: The defaultValue attribute can be a solution only if the value to be reverted never changes and if the fields involved are text inputs.
Firstly, changing the "default value" is rather awkward and may break something else aling the page (one would expect the browsers make the defaultValue attribute read-only, but that does not seem to be the case). Secondly, you would be limited to inputs of the text type.
Still, if none of that is a problem, the code above can be quickly adapted to use them instead of data- attributes.

Attaching change() even listeners to select/dropdown dynamically

I'm creating 3 dropdowns/select boxes on the fly and insert them in the DOM through .innerHTML.
I don't know the ID's of the dropdowns until I created them in Javascript.
To know which dropdowns have been created, I create an array where I store the ID's of the dropdowns I have created.
for(var i=0; i<course.books.length; i++)
{
output+="<label for='book_"+course.books[i].id+"'>"+ course.books[i].name +"</label>";
output+="<select id='variant"+course.books[i].id+"' name='book_"+course.books[i].id+"'>";
output+="<option value='-'>-- Select one --</option>";
for(var j=0; j<course.books[i].options.length; j++)
{
output+="<option value='"+course.books[i].options[j].id+"'>"+course.books[i].options[j].name+"</option>";
}
output+="</select>";
}
Now I have an array with 3 id's like:
dropdown1
dropdown2
dropdown3
What I want to accomplish with Javascript (without using jQuery or another framework) is to loop over these 3 dropdowns and attach a change event listener to them.
When a user changes the selection in one of these dropdown, I want to call a function called updatePrice for example.
I'm a bit stuck on the dynamic adding of event listeners here.
Now you have added your code its straight forward and you can ignore my verbose answer !!!
output+="<select id='variant"+course.books[i].id+"' name='book_"+course.books[i].id+"'>";
could become :
output+="<select onchange="updatePrice(this)" id='variant"+course.books[i].id+"' name='book_"+course.books[i].id+"'>";
This will call the updatePrice function, passing the select list that changed
However
IMO its far better (from a performance point of view for a start) to create elements in the DOM using the DOM.
var newSelect = document.createElement("select");
newSelect.id = "selectlistid"; //add some attributes
newSelect.onchange = somethingChanged; // call the somethingChanged function when a change is made
newSelect[newSelect.length] = new Option("One", "1", false, false); // add new option
document.getElementById('myDiv').appendChild(newSelect); // myDiv is the container to hold the select list
Working example here -> http://jsfiddle.net/MStgq/2/
You got the array already? Then you can do this:
function updatePrice()
{
alert(this.id + " - " + this.selectedIndex);
}
var list = ["dropdown1", "dropdown2"];
for(var i=0;i<list.length;i++)
{
document.getElementById(list[i]).onchange = updatePrice;
}
Fiddle: http://jsfiddle.net/QkLMT/3/
That won't work across browsers.
You'll want something like
for(var i = 0; i < list.length; i++)
{
$("#"+list[i]).change(updatePrice);
}
in jquery.

Categories