Persuading scripts to play nicely together - javascript

I've been playing around with javascript/jquery, trying to get various bits of script to play nicely together (see for instance my recent question at Populate an input with the contents of a custom option (data-) attribute).
I've now got a clunky mass of code that kind of works. It's supposed to do 4 things:
Populate a cascading dropdown, whereby the options in the second dropdown vary according to whatever was selected in the first dropdown.
Clone the first row (or it could be the last row), so additional rows can be added to the form (in the real version there could be any number of rows when the page first loads)
Keep a running total of values selected in a 3rd drop down
Populate a text input based upon values selected in the second dropdown
For the first row everything works fine. However, for cloned rows steps 1 and 4 stop working. I suspect its because I haven't uniquely identified each cloned instance. Is that right? So how might I do that?
function cloneRow() {
var row = document.getElementById("myrow"); // find row to copy
var table = document.getElementById("mytable"); // find table to append to
var clone = row.cloneNode(true); // copy children too
clone.id = "newID"; // change id or other attributes/contents
table.appendChild(clone); // add new row to end of table
}
function createRow() {
var row = document.createElement('tr'); // create row node
var col = document.createElement('td'); // create column node
var col2 = document.createElement('td'); // create second column node
row.appendChild(col); // append first column to row
row.appendChild(col2); // append second column to row
col.innerHTML = "qwe"; // put data in first column
col2.innerHTML = "rty"; // put data in second column
var table = document.getElementById("tableToModify"); // find table to append to
table.appendChild(row); // append row to table
}
window.sumInputs = function() {
var inputs = document.getElementsByName('hours'),
result = document.getElementById('total'),
sum = 0;
for(var i=0; i<inputs.length; i++) {
var ip = inputs[i];
if (ip.name && ip.name.indexOf("total") < 0) {
sum += parseFloat(ip.value) || 0;
}
}
result.value = sum;
}
var myJson =
{
"listItems":[
{
"id":"1",
"project_no":"1001",
"task":[
{
"task_description":"Folding stuff",
"id":"111",
"task_summary":"Folding",
},
{
"task_description":"Drawing stuff",
"id":"222",
"task_summary":"Drawing"
}
]
},
{
"id":"2",
"project_no":"1002",
"task":[
{
"task_description":"Meeting description",
"id":"333",
"task_summary":"Meeting"
},
{
"task_description":"Administration",
"id":"444",
"task_summary":"Admin"
}
]
}
]
}
$(function(){
$.each(myJson.listItems, function (index, value) {
$("#project").append('<option value="'+value.id+'">'+value.project_no+'</option>');
});
$('#project').on('change', function(){
$('#task').html('<option value="000">-Select Task-</option>');
for(var i = 0; i < myJson.listItems.length; i++)
{
if(myJson.listItems[i].id == $(this).val())
{
$.each(myJson.listItems[i].task, function (index, value) {
$("#task").append('<option value="'+value.id+'" data-description="'+value.task_description+'">'+value.task_summary+'</option>');
});
}
}
});
});
$('#task').change(function() {
$('#taskText').val( $(this).find('option:selected').data('description') )
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<html>
<form>
<table id='mytable'>
<tr>
<td>Project</td>
<td>Workstage</td>
<td>Hours</td>
<td>Description</td>
</tr>
<tr>
<td></td>
<td></td>
<td><input size=3 id='total' disabled='disabled'/></td>
<td></td>
</tr>
<tr id='myrow'>
<td>
<select id="project" name="">
<option value="">Select One</option>
</select>
</td><td>
<select id="task" name="" onchange="updateText('task')">>
<option value="">Select One</option>
</select>
</td>
<td>
<select name = 'hours' onmouseup="sumInputs()">
<option>1.0</option>
<option>1.5</option>
<option>2.0</option>
</select>
<td><input type="text" value="" id="taskText" /></td>
</tr>
</table>
<input type="button" onclick="cloneRow()" value="Add Row" />
</form>
Apologies if I've inserted the snippet incorrectly.

I'm going to go out on a limb and suggest something completely different. I understand that your question was tagged with jQuery but I want to suggest a different and I believe better way of solving the problem.
I think here you're mixing your DOM and your JavaScript too much, and instead using a binding framework may be simpler. These binding frameworks split out your presentation from your underlying data. There are a number to chose from, here are but are just a few:
Angular
Knockout
React
I personally know Knockout fairly well, so here is how I'd create something similar to that which you've produced using Knockout. Here I've reduced your code by about 50% and I believe significantly improved the readability with the bonus that I've removed all DOM from the JavaScript
Note that it's really easy to get your JSON view (my structure might not exactly match yours but should give you the idea) by using the Knockout.mapping plugin and calling ko.toJS(vm.jobs)
var vm = {};
vm.projects = ko.observableArray([]);
vm.projects.push({
id: 1001,
name: "Project A",
stages: [{ name: "folding", description: "folding stuff" },
{ name: "drawing", description: "drawing shapes" }]
});
vm.projects.push({
id: 1002,
name: "Project B",
stages: [{ name: "meeting", description: "Talking" },
{ name: "admin", description: "everyday things" }]
});
vm.jobs = ko.observableArray([]);
vm.totalHours = ko.computed(function() {
var sum = 0;
for (var i = 0; i < vm.jobs().length; i++) {
sum += vm.jobs()[i].time();
}
return sum;
})
createJob = function() {
var job = {};
// Set fields on the job
job.project = ko.observable();
job.stage = ko.observable();
job.stages = ko.computed(function() {
if (job.project()) return job.project().stages;
return [];
});
job.stage.subscribe(function() {
job.description(job.stage().description);
});
job.description = ko.observable();
job.time = ko.observable(1);
vm.jobs.push(job);
};
createJob();
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<form>
<table id='mytable'>
<tr>
<td>Project</td>
<td>Workstage</td>
<td>Hours</td>
<td>Description</td>
</tr>
<tr>
<td></td>
<td></td>
<td>
<input size=3 id='total' disabled='disabled' data-bind="value: totalHours" />
</td>
<td></td>
</tr>
<!-- ko foreach: jobs -->
<tr id='myrow'>
<td>
<select class="project" data-bind="options: $root.projects, optionsText: 'name', value: project, optionsCaption: 'Select a Project'"></select>
</td>
<td>
<select class="stage" data-bind="options: stages, value: stage, optionsText: 'name', optionsCaption: 'Select a Stage', enable: project()"></select>
</td>
<td>
<select class="hours" data-bind="options: [1.0, 1.5, 2.0], value: time, enable: stage()"></select>
</td>
<td>
<input type="text" data-bind="value: description, enable: stage()" />
</td>
</tr>
<!-- /ko -->
</table>
<input type="button" onclick="createJob()" value="Add Row" />
</form>
The general idea here is that both the observable and the observableArray are bound to the DOM using Knockout. They'll automatically keep each other in sync.
The computed field is essentially a calculated field, so I've used that to generate your total, and also to feed one of the dropdowns - you could probably use a different approach but that seemed quite simple.
Finally there is a manual subscribe which is designed to update your default description for the job at hand. This allows you to update the field, without having to mess around with another observable or computed fields to override a description if the user set one.
As you've also had a problem with ID's for things, I'm also going to mention how you could address those. Using Knockout we could quite easily create client side unique ID's for our DOM using another computed field and returning it's order in the array:
job.id = ko.computed(function() {
return vm.jobs.indexOf(job);
});
You can then even reflect this in the DOM (note an ID can't start with a number) like so:
<td data-bind="attr: { id: 'job_' + id() }"></td>
This would produce DOM like:
<td id="job_0"></td>

That's a really dirty peace of code. I solved your problem in the same dirty style but I recommend you to write everything completely new in a proper style.
I solved the problem by changing all id's to class attributes. And each row has it's own unique id.
var rowNum =1;
function cloneRow() {
var row = document.getElementById("row0"); // find row to copy
var table = document.getElementById("mytable"); // find table to append to
var clone = row.cloneNode(true); // copy children too
clone.id = "row"+rowNum; // change id or other attributes/contents
table.appendChild(clone); // add new row to end of table
initProject(clone.id);
rowNum++;
}
function createRow() {
var row = document.createElement('tr'); // create row node
var col = document.createElement('td'); // create column node
var col2 = document.createElement('td'); // create second column node
row.appendChild(col); // append first column to row
row.appendChild(col2); // append second column to row
col.innerHTML = "qwe"; // put data in first column
col2.innerHTML = "rty"; // put data in second column
var table = document.getElementById("tableToModify"); // find table to append to
table.appendChild(row); // append row to table
}
window.sumInputs = function() {
var inputs = document.getElementsByName('hours'),
result = document.getElementById('total'),
sum = 0;
for(var i=0; i<inputs.length; i++) {
var ip = inputs[i];
if (ip.name && ip.name.indexOf("total") < 0) {
sum += parseFloat(ip.value) || 0;
}
}
result.value = sum;
}
var myJson =
{
"listItems":[
{
"id":"1",
"project_no":"1001",
"task":[
{
"task_description":"Folding stuff",
"id":"111",
"task_summary":"Folding",
},
{
"task_description":"Drawing stuff",
"id":"222",
"task_summary":"Drawing"
}
]
},
{
"id":"2",
"project_no":"1002",
"task":[
{
"task_description":"Meeting description",
"id":"333",
"task_summary":"Meeting"
},
{
"task_description":"Administration",
"id":"444",
"task_summary":"Admin"
}
]
}
]
}
function initProject(rowId){
console.log(rowId);
if(rowId == 'row0'){
$.each(myJson.listItems, function (index, value) {
$("#"+rowId+" .project").append('<option value="'+value.id+'">'+value.project_no+'</option>');
});
}
$('#'+rowId+' .project').on('change', function(e){
rowElem = e.target.closest(".row");
$('#'+rowId+' .task').html('<option value="000">-Select Task-</option>');
for(var i = 0; i < myJson.listItems.length; i++)
{
if(myJson.listItems[i].id == $(this).val())
{
$.each(myJson.listItems[i].task, function (index, value) {
$('#'+rowId+' .task').append('<option value="'+value.id+'" data-description="'+value.task_description+'">'+value.task_summary+'</option>');
});
}
}
});
$('#'+rowId+' .task').change(function() {
$('#'+rowId+' .taskText').val( $(this).find('option:selected').data('description') )
})
}
initProject('row0');
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<html>
<head>
</head>
<body>
<form>
<table id='mytable'>
<tr>
<td>Project</td>
<td>Workstage</td>
<td>Hours</td>
<td>Description</td>
</tr>
<tr>
<td></td>
<td></td>
<td><input size=3 id='total' disabled='disabled'/></td>
<td></td>
</tr>
<tr id='row0' class="row">
<td>
<select class="project" name="">
<option value="">Select One</option>
</select>
</td><td>
<select class="task" name="" onchange="updateText('task')">
<option value="">Select One</option>
</select>
</td>
<td>
<select name = 'hours' onmouseup="sumInputs()">
<option>1.0</option>
<option>1.5</option>
<option>2.0</option>
</select>
<td><input type="text" value="" class="taskText" /></td>
</tr>
</table>
<input type="button" onclick="cloneRow()" value="Add Row" />
</form>
</body>

Another version with simple templating system, this time without the jquery.
It was interesting to see if it will be difficult to convert, and it wasn't, most of things to map jQuery code to plain JS for modern browsers can be found here - YOU MIGHT NOT NEED JQUERY
Here is the code:
var myJson =
{
"listItems":[
{
"id":"1",
"project_no":"1001",
"task":[
{
"task_description":"Folding stuff",
"id":"111",
"task_summary":"Folding",
},
{
"task_description":"Drawing stuff",
"id":"222",
"task_summary":"Drawing"
}
]
},
{
"id":"2",
"project_no":"1002",
"task":[
{
"task_description":"Meeting description",
"id":"333",
"task_summary":"Meeting"
},
{
"task_description":"Administration",
"id":"444",
"task_summary":"Admin"
}
]
}
]
}
var template = function(target) {
var _parent = target.parentNode;
var _template = _parent.getAttribute('dataTemplate');
if (!_template) {
// no template yet - save it and remove the node from HTML
target.style.display = '';
target.classList.add('clone');
_template = target.outerHTML;
_parent.setAttribute('dataTemplate', JSON.stringify(_template));
_parent.removeChild(target);
} else {
// use saved template
_template = JSON.parse(_template);
}
return {
populate: function(data) {
var self = this;
this.clear();
data.forEach(function(value) {
self.clone(value);
});
},
clone: function(value) {
var clone = target.cloneNode(true);
_parent.appendChild(clone);
var html = _template;
if (value) {
for (var key in value) {
html = html.replace('{'+key+'}', value[key]);
}
}
clone.outerHTML = html;
clone = _parent.lastChild;
if (value) {
clone.setAttribute('dataTemplateData', JSON.stringify(value));
}
return clone;
},
clear: function() {
var clones = _parent.querySelectorAll('.clone')
Array.prototype.forEach.call(clones, function(el) {
_parent.removeChild(el);
});
}
};
};
function createRow() {
var clone = template(document.querySelector('.myrow-template')).clone();
template(clone.querySelector('.project-option-tpl')).populate(myJson.listItems);
updateHours();
bindEvents();
}
function bindEvents() {
var elements = document.querySelectorAll('#mytable .project');
Array.prototype.forEach.call(elements, function(elem) {
elem.addEventListener('change', function() {
var data = JSON.parse(this.options[this.selectedIndex].getAttribute('dataTemplateData'));
template(this.parentNode.parentNode.querySelector('.task-option-tpl')).populate(data.task);
});
});
elements = document.querySelectorAll('#mytable .task');
Array.prototype.forEach.call(elements, function(elem) {
elem.addEventListener('change', function() {
var data = JSON.parse(this.options[this.selectedIndex].getAttribute('dataTemplateData'));
this.parentNode.parentNode.querySelector('.task-text').value = data.task_description;
});
});
elements = document.querySelectorAll('#mytable .hours');
Array.prototype.forEach.call(elements, function(elem) {
elem.addEventListener('mouseup', function() {
updateHours();
});
});
}
function updateHours() {
var total = 0;
var hours = document.querySelectorAll('.hours');
Array.prototype.forEach.call(hours, function(item) {
if (item.parentNode.parentNode.style.display.length == 0) {
total += parseFloat(item.value) || 0;
}
});
document.getElementById('total').value = total;
}
function ready(fn) {
if (document.readyState != 'loading'){
fn();
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
ready(function(){
createRow();
document.querySelector('.add-row').addEventListener('click', function() {
createRow();
});
});
<form>
<table id='mytable'>
<tr>
<td>Project</td>
<td>Workstage</td>
<td>Hours</td>
<td>Description</td>
</tr>
<tr>
<td></td>
<td></td>
<td><input size=3 id='total' disabled='disabled'/></td>
<td></td>
</tr>
<tr class='myrow-template' style='display:none'>
<td>
<select class="project" name="">
<option value="">Select One</option>
<option class='project-option-tpl' value="{id}" style='display:none'>{project_no}</option>
</select>
</td>
<td>
<select class="task" name="">
<option value="000">-Select Task-</option>
<option class='task-option-tpl' value="{id}" data-description="{task_description}" style='display:none'>{task_summary}</option>
</select>
</td>
<td>
<select class='hours'>
<option>1.0</option>
<option>1.5</option>
<option>2.0</option>
</select>
</td>
<td><input type="text" value="" class="task-text" /></td>
</tr>
</table>
<input type="button" class="add-row" value="Add Row" />
</form>

I upvoted the answer about Angular / Knokout / React - this is actually what I highly recommend to use in the real application.
Just as an exercise, here is how the code can look like if you use a simple templating system.
The idea is that you should not build the javascript "manually" and instead declare the templates in the HTML. For example the projects select can look like this:
<select class="project" name="">
<option value="">Select One</option>
<option class='project-option-tpl' value="{id}" style='display:none'>{project_no}</o
</select>
Here the option is an invisible template for project options. It works similar for the whole "row" and "tasks" select, here is the full code to implement the logic:
function createRow() {
var $clone = template($('.myrow-template')).clone();
template($clone.find('.project-option-tpl')).populate(myJson.listItems);
updateHours();
}
function updateHours() {
var total = 0;
$('.hours:visible').each(function (index, item) {
total += parseFloat($(item).val()) || 0;
});
$('#total').val(total);
}
$(function() {
createRow(); // create the first row
$('#mytable').on('change', '.project', function() {
// handle project change - get tasks for the selected project
// and populate the tasks template
var data = $(this).find(':selected').data('template-data');
template($(this).parent().parent().find('.task-option-tpl')).populate(data.task);
});
$('#mytable').on('change', '.task', function() {
// task change - update the task description
var data = $(this).find(':selected').data('template-data');
$(this).parent().parent().find('.task-text').val(data.task_description);
});
$('#mytable').on('mouseup', '.hours',function() {
updateHours(); // re-calculate total hours
});
$('.add-row').on('click', function() {
createRow(); // add one more row
});
});
The "magic" is implemented in the template function:
var template = function($target) {
$target = $($target.get(0));
return {
populate: function(data) {
// for each item in the data array - clone and populate the
// item template
var self = this;
this.clear();
$.each(data, function (index, value) {
self.clone(value);
});
},
clone: function(value) {
// clone a template for a single item and populate it with data
var $clone = $target.clone();
$clone.addClass('clone').appendTo($target.parent()).fadeIn('slow');
if (value) {
var html = $clone.get(0).outerHTML;
for (var key in value) {
html = html.replace('{'+key+'}', value[key]);
}
$clone.get(0).outerHTML = html;
$clone = $target.parent().find(':last')
$clone.data('template-data', value);
}
return $clone;
},
clear: function() {
// remove cloned templates
$target.parent().find('.clone').remove();
}
};
};
Here is the full runnable example:
var myJson =
{
"listItems":[
{
"id":"1",
"project_no":"1001",
"task":[
{
"task_description":"Folding stuff",
"id":"111",
"task_summary":"Folding",
},
{
"task_description":"Drawing stuff",
"id":"222",
"task_summary":"Drawing"
}
]
},
{
"id":"2",
"project_no":"1002",
"task":[
{
"task_description":"Meeting description",
"id":"333",
"task_summary":"Meeting"
},
{
"task_description":"Administration",
"id":"444",
"task_summary":"Admin"
}
]
}
]
}
var template = function($target) {
$target = $($target.get(0));
return {
populate: function(data) {
var self = this;
this.clear();
$.each(data, function (index, value) {
self.clone(value);
});
},
clone: function(value) {
var $clone = $target.clone();
$clone.addClass('clone').appendTo($target.parent()).fadeIn('slow');
if (value) {
var html = $clone.get(0).outerHTML;
for (var key in value) {
html = html.replace('{'+key+'}', value[key]);
}
$clone.get(0).outerHTML = html;
$clone = $target.parent().find(':last')
$clone.data('template-data', value);
}
return $clone;
},
clear: function() {
$target.parent().find('.clone').remove();
}
};
};
function createRow() {
var $clone = template($('.myrow-template')).clone();
template($clone.find('.project-option-tpl')).populate(myJson.listItems);
updateHours();
}
function updateHours() {
var total = 0;
$('.hours:visible').each(function (index, item) {
total += parseFloat($(item).val()) || 0;
});
$('#total').val(total);
}
$(function(){
createRow();
$('#mytable').on('change', '.project', function() {
var data = $(this).find(':selected').data('template-data');
template($(this).parent().parent().find('.task-option-tpl')).populate(data.task);
});
$('#mytable').on('change', '.task', function() {
var data = $(this).find(':selected').data('template-data');
$(this).parent().parent().find('.task-text').val(data.task_description);
});
$('#mytable').on('mouseup', '.hours',function() {
updateHours();
});
$('.add-row').on('click', function() {
createRow();
});
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<form>
<table id='mytable'>
<tr>
<td>Project</td>
<td>Workstage</td>
<td>Hours</td>
<td>Description</td>
</tr>
<tr>
<td></td>
<td></td>
<td><input size=3 id='total' disabled='disabled'/></td>
<td></td>
</tr>
<tr class='myrow-template' style='display:none'>
<td>
<select class="project" name="">
<option value="">Select One</option>
<option class='project-option-tpl' value="{id}" style='display:none'>{project_no}</option>
</select>
</td>
<td>
<select class="task" name="">
<option value="000">-Select Task-</option>
<option class='task-option-tpl' value="{id}" data-description="{task_description}" style='display:none'>{task_summary}</option>
</select>
</td>
<td>
<select class='hours'>
<option>1.0</option>
<option>1.5</option>
<option>2.0</option>
</select>
</td>
<td><input type="text" value="" class="task-text" /></td>
</tr>
</table>
<input type="button" class="add-row" value="Add Row" />
</form>

See the other answers why the current code is not working as expected. (in short: id may only appear once).
I took some time and reorganized your code. As for me personally it doesn't look that maintainable. I did use objects to neaten things up a bit. You could also use a framework like angular etc. But JavaScript in it self can also be really nice. And probably is good enough for something that small.
Some notes:
The first row in my example is display:none and called template (this way we always have a "clean" template ready to use). You could have this template only in Javascript and insert it on demand, but this way will help you to quickly edit the design.
There are 4 groups in my code
The data (myjson)
The table object declaration (this way you could also create multiple tables)
The row object declaration
The init preparation
The prototype is useful because it doesn't duplicate the code for each row
(object). So every select will execute the same function. But since we use objects each time we have different data.
Some functions have something like hours.click(function(){tableElement.updateTime()}); this inner function is necessary as it keeps the scope of the function to the object and not the click event. Normally when you catch a click event and set a function, then in this function this is the click event. I used objects, so this should be the object and not the event.
var myJson = {
"listItems": [{
"id": "1",
"project_no": "1001",
"task": [{
"task_description": "Folding stuff",
"id": "111",
"task_summary": "Folding",
}, {
"task_description": "Drawing stuff",
"id": "222",
"task_summary": "Drawing"
}]
}, {
"id": "2",
"project_no": "1002",
"task": [{
"task_description": "Meeting description",
"id": "333",
"task_summary": "Meeting"
}, {
"task_description": "Administration",
"id": "444",
"task_summary": "Admin"
}]
}]
};
/*
Table
*/
function projectPlan(tableElement) { // Constructor
this.tableElement = tableElement;
this.totalTimeElement = tableElement.find('tr td input.total');
this.rows = [];
};
projectPlan.prototype.appendRow = function(template) { // you could provide different templates
var newRow = template.clone().toggle(); // make a copy and make it visible
this.tableElement.append(newRow);
this.rows.push( new workRow(newRow, this) );
// update the time right away
this.updateTime();
};
projectPlan.prototype.updateTime = function() {
var totalWork = 0;
for(var i = 0; i < this.rows.length; i++) totalWork += this.rows[i].hours.val()*1; // *1 makes it a number, default is string
this.totalTimeElement.val(totalWork);
};
/*
Row
*/
function workRow(rowElement, tableElement) { // Constructor
// set the object attributes with "this"
this.rowElement = rowElement;
this.tableElement = tableElement;
this.projects = rowElement.find( "td select.projects" );
this.tasks = rowElement.find( "td select.tasks" );
this.hours = rowElement.find( "td select.hours" );
this.taskText = rowElement.find( "td input.taskText" );
// set all the event listeners, don't use this since the "function(){" will have a new scope
var self = this;
this.projects.change(function(){self.updateTasks()});
this.tasks.change(function(){self.updateTaskText()});
this.hours.change(function(){tableElement.updateTime()});
}
workRow.prototype.updateTasks = function() {
// delete the old ones // not the first because it's the title
this.tasks.find('option:not(:first-child)').remove();
if(this.projects.val() != "-1") {
var tmpData;
for (var i = 0; i < myJson.listItems[this.projects.val()].task.length; i++) {
tmpData = myJson.listItems[this.projects.val()].task[i];
this.tasks.append('<option value="' + i + '">' + tmpData.task_summary + '</option>');
}
}
this.taskText.val('');
}
workRow.prototype.updateTaskText = function() {
if(this.tasks.val() == "-1") this.taskText.val('');
else this.taskText.val( myJson.listItems[ this.projects.val() ].task[ this.tasks.val() ].task_description );
}
/*
Setup
*/
// Prepare the template (insert as much as possible now)
rowTemplate = $('#rowTemplate');
var projectList = rowTemplate.find( "td select.projects" );
var tmpData;
for (var i = 0; i < myJson.listItems.length; i++) {
tmpData = myJson.listItems[i];
projectList.append('<option value="' + i + '">' + tmpData.project_no + '</option>');
}
// setup table
var projectPlan = new projectPlan( $('#projectPlan') );
// Print the first row
projectPlan.appendRow(rowTemplate);
$('#buttonAddRow').click(function(){ projectPlan.appendRow(rowTemplate) });
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<form>
<table id="projectPlan">
<tr>
<td>Project</td>
<td>Workstage</td>
<td>Hours</td>
<td>Description</td>
</tr>
<tr>
<td></td>
<td></td>
<td><input size=3 class='total' disabled='disabled' /></td>
<td></td>
</tr>
<tr id='rowTemplate' style="display:none">
<td>
<select class="projects">
<option value="-1">Select One</option>
</select>
</td>
<td>
<select class="tasks">>
<option value="-1">Select One</option>
</select>
</td>
<td>
<select class='hours'>
<option>1.0</option>
<option>1.5</option>
<option>2.0</option>
</select>
</td>
<td>
<input class="taskText" type="text"/>
</td>
</tr>
</table>
<input type="button" id="buttonAddRow" value="Add Row" />
</form>

And angular.js version, here is the complete application code, the rest is declared in the HTML:
angular.module('MyApp', [])
.controller('TasksController', function() {
var self = this;
this.newRow = function() {
return { project: null, task: null, hours: 1.0 };
};
this.total = 0;
this.hours = [1.0, 1.5, 2.0];
this.projects = projects;
this.rows = [ this.newRow() ];
this.total = function() {
return this.rows.reduce(function(prev, curr) {
return prev + parseFloat(curr.hours);
}, 0);
};
this.addRow = function() {
this.rows.push(this.newRow());
};
});
The full code:
var projects = [{
"id":"1",
"project_no":"1001",
"task":[ {
"task_description":"Folding stuff",
"id":"111",
"task_summary":"Folding",
}, {
"task_description":"Drawing stuff",
"id":"222",
"task_summary":"Drawing"
} ]
}, {
"id":"2",
"project_no":"1002",
"task":[ {
"task_description":"Meeting description",
"id":"333",
"task_summary":"Meeting"
}, {
"task_description":"Administration",
"id":"444",
"task_summary":"Admin"
} ]
}];
angular.module('MyApp', [])
.controller('TasksController', function() {
var self = this;
this.newRow = function() {
return { project: null, task: null, hours: 1.0 };
};
this.total = 0;
this.hours = [1.0, 1.5, 2.0];
this.projects = projects;
this.rows = [ this.newRow() ];
this.total = function() {
return this.rows.reduce(function(prev, curr) {
return prev + parseFloat(curr.hours);
}, 0);
};
this.addRow = function() {
this.rows.push(this.newRow());
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<body ng-app="MyApp">
<form ng-controller="TasksController as ctrl">
<table id='mytable'>
<tr>
<td>Project</td>
<td>Workstage</td>
<td>Hours</td>
<td>Description</td>
</tr>
<tr>
<td></td>
<td></td>
<td><input size=3 id='total' value="{{ctrl.total()}}" disabled='disabled'/></td>
<td></td>
</tr>
<tr ng-repeat="row in ctrl.rows">
<td>
<select ng-model="row.project" ng-options="project as project.project_no for project in ctrl.projects">
<option value="">Select One</option>
</select>
</td>
<td>
<select ng-model="row.task" ng-options="task as task.task_summary for task in row.project.task">
<option value="">Select Task</option>
</select>
</td>
<td>
<select ng-model='row.hours' ng-options="hour as (hour | number : 1) for hour in ctrl.hours">
</select>
</td>
<td><input type="text" value="{{row.task.task_description}}" class="task-text" /></td>
</tr>
</table>
<input type="button" ng-click="ctrl.addRow()" value="Add Row" />
</form>
</body>

Here is a version using plain javascript, which doesn't need/use element id's, store the clone as a variable, disabled task select unless a project is chosen.
This code sample can of course be optimized, wrapped in to classes etc. though I chose not to, to make it as easy as possible to follow the code flow and see what's going on.
var myJson =
{
"listItems":[
{
"id":"1",
"project_no":"1001",
"task":[
{
"task_description":"Folding stuff",
"id":"111",
"task_summary":"Folding",
},
{
"task_description":"Drawing stuff",
"id":"222",
"task_summary":"Drawing"
}
]
},
{
"id":"2",
"project_no":"1002",
"task":[
{
"task_description":"Meeting description",
"id":"333",
"task_summary":"Meeting"
},
{
"task_description":"Administration",
"id":"444",
"task_summary":"Admin"
}
]
}
]
}
var template = function(target) {
return {
clone: function(value) {
if (!template.clone_item) {
// create clone variable
template.clone_item = target.cloneNode(true);
return target;
} else {
// return/append clone variable
return target.parentNode.appendChild(template.clone_item.cloneNode(true));
}
},
init: function(value) {
// first select (projects)
var sel = target.querySelector('select');
sel.addEventListener('change', function() {
// second select (tasks)
var sel = target.querySelectorAll('select')[1];
sel.addEventListener('change', function() {
var selvalues = this.options[this.selectedIndex].value.split('|');
var data = value[selvalues[0]].task[selvalues[1]].task_description;
// last inout (tasks descript.)
var inp = target.querySelector('input');
inp.value = data;
});
// clear last used task select options
for (i=sel.length-1;sel.length >1;i--) {
sel.remove(i);
}
// clear last used task descript.
var inp = target.querySelector('input');
inp.value = '';
// disable task select
sel.disabled = true;
// add task select options
var selvalue = this.options[this.selectedIndex].value;
if (selvalue != '') {
sel.disabled = false;
var data = value[selvalue].task;
var index = 0;
for (var key in data) {
createOption(sel,selvalue + '|' + index++,data[key].task_summary);
}
}
});
var index = 0;
// add project select options
for (var key in value) {
createOption(sel,index++,value[key].project_no);
}
// hours
var inp = target.querySelector('.hours');
inp.addEventListener('change', function() {
updateHours();
});
updateHours();
}
};
};
function createRow() {
var clone = template(document.querySelector('.myrow')).clone();
template(clone).init(myJson.listItems);
}
function createOption(sel,val,txt) {
var opt = document.createElement('option');
opt.value = val;
opt.text = txt;
sel.add(opt);
}
function updateHours() {
var total = 0;
var hours = document.querySelectorAll('#mytable .hours');
for (i = 0; i < hours.length;i++) {
total += parseFloat(hours[i].value) || 0;
}
document.getElementById('total').value = total;
}
function ready(fn) {
if (document.readyState != 'loading'){
fn();
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
ready(function(){
createRow();
document.querySelector('.add-row').addEventListener('click', function() {
createRow();
});
});
<form>
<table id='mytable'>
<tr>
<td>Project</td>
<td>Workstage</td>
<td>Hours</td>
<td>Description</td>
</tr>
<tr>
<td></td>
<td></td>
<td><input size=3 id='total' disabled='disabled'/></td>
<td></td>
</tr>
<tr class='myrow'>
<td>
<select class="project" name="">
<option value="">Select One</option>
</select>
</td>
<td>
<select class="task" name="" disabled=disabled>
<option value="000">-Select Task-</option>
</select>
</td>
<td>
<select class='hours'>
<option>1.0</option>
<option>1.5</option>
<option>2.0</option>
</select>
</td>
<td><input type="text" value="" class="task-text" /></td>
</tr>
</table>
<input type="button" class="add-row" value="Add Row" />
</form>

Related

Trying to set an ID for an element using knockout.js and getting undefined

I am new to knock out and fairly new to JavaScript so apologies if I have done something silly here...
I am using knockout.js (3.4.2) to maintain a list of dependents, I have the add and delete working OK but I want to set an ID on an element to allow me to make it read only.
I have based my code to do this on the knockout example, their working example can be seen here: Demo
When I run my code the ID is set to undefined:
<input data-bind="uniqueId: dependantName, value: dependantName" id="undefined" type="text">
My code on JS fiddle
https://jsfiddle.net/zacmarshall/khb13ha4/
HTML:
<!DOCTYPE html>
<head>
<script src="javascripts/vendor/knockout-3.4.2.js" type="text/javascript"></script>
<script src="javascripts/vendor/jquery-1.11.1.min.js"></script>
<script src="javascripts/vendor/jquery.validate.js" type="text/javascript"></script>
<head>
<body>
<input data-bind="value: newDependantName" />
<input data-bind="value: newDobDD" />
<button data-bind="click: addDependant">Add</button>
<pre data-bind="text: ko.toJSON($data, null, 2)"></pre>
<table>
<thead>
<tr>
<th>Name</th>
<th>Date of Birth</th>
<th></th>
</tr>
</thead>
<tbody id="qualifications-summary" data-bind="foreach: dependants">
<!--div data-bind="uniqueId: dependantName"-->
<div>
<tr>
<td><label data-bind="uniqueFor: dependantName">Before</label>
<input type="text" data-bind="uniqueId: dependantName, value: dependantName"/>
</td>
<td><input data-bind="value: dobDD" /></td>
<td><input data-bind="value: itemIndex"/></td>
<td>Remove</td>
</tr>
</div>
</tbody>
</table>
</body>
javascript:
function Dependant(data) {
this.itemIndex = ko.observable(0);
this.dependantName = ko.observable(data.dependantName);
this.dobDD = ko.observable(data.dobDD);
this.readOnly = ko.observable(data.readOnly);
this.showEditButton = ko.observable(data.showEditButton);
}
function DependantListViewModel() {
// Data
var self = this;
self.dependants = ko.observableArray([]);
self.newDependantName = ko.observable();
self.newDobDD = ko.observable();
self.readOnly = ko.observable();
self.showEditButton = ko.observable();
self.itemIndex = ko.observable();
var i = 0;
// Operations
self.addDependant = function() {
var data1 = { itemIndex : (i),
dependantName : this.newDependantName(),
dobDD : this.newDobDD(),
readOnly : "readonly",
showEditButton : (!0)
};
self.dependants.push(data1);
self.newDependantName("");
self.newDobDD("");
i++
};
self.removeDependant = function(dependant) { self.dependants.remove(dependant) };
self.editDependant = function(i) {
console.log("editedDependant");
self.readOnly(undefined);
self.showEditButton(!1)
};
self.saveDependantItem = function(self) {
self.readOnly("readonly"), self.showEditButton(!0)
};
}
ko.bindingHandlers.uniqueId = {
init: function(element, valueAccessor) {
var value = valueAccessor();
value.id = value.id || ko.bindingHandlers.uniqueId.prefix + (++ko.bindingHandlers.uniqueId.counter);
console.log("in handler");
element.id = value.id;
},
counter: 0,
prefix: "unique"
};
ko.bindingHandlers.uniqueFor = {
init: function(element, valueAccessor) {
var value = valueAccessor();
value.id = value.id || ko.bindingHandlers.uniqueId.prefix + (++ko.bindingHandlers.uniqueId.counter);
element.setAttribute("for", value.id);
}
};
ko.applyBindings(new DependantListViewModel());
The bug refers to this in your init:
var value = valueAccessor(); // this is undefined
value.id = value.id // therefore this falls over
This is the issue:
self.dependants.push(data1);
It should be:
self.dependants.push(new Dependant(data1));
And then update your Dependant model to pass the index:
this.itemIndex = ko.observable(data.itemIndex);
Here is a working Fiddle

Javascript: loop through array of objects to find key value pair and render them in the table td

Here is the following table code created dynamically and I want to get the last 2 values stored in TD. the last td value is in the form of "key-value-pair".
I want to display something like below:
<tbody>
{{#items}}
<tr>
<td>{{name}}</td>
<td>{{description}}</td>
<td id="newval">{{new_value}}</td>
<td id="oldval">{{old_value}}</td>
</tr>
{{/items}}
</tbody>
here is the json obtained from the server:
{
"result":"OK",
"items":[
{
"name":"abc",
"description":"desc",
"new_value":{
"description":"newvalue"
},
"old_value":{
"description":"oldvalue "
}
},
{
"name":"abc3",
"description":"desc2",
"new_value":{
"interval":"newvalue2"
},
"old_value":{
"interval":"oldvalue2 "
}
}
]
}
here is the js:
$.each(lists,function(i,items){
for (var key in items) {
if(key == 'new_value'){
for(value in items[key]){
$("td#newval").html(value + " : " +items[key][value]);
}
}
}
});
Similarly I'm doing it for the old value, just replacing the "td" id and value.
but what is happening is only the first td gets updated with all the values of "new_value" in the array and the remaining td's are rendered blank.
I want to display in such a way that it will loop through the array and find the key value pair associated with the "new_value" object and render both values for the respective td's.
<td>description: newvalue</td>
<td>interval: newvalue2</td>
how can i achieve this?
Thanks!
Since Id of an element must be unique, you should use some other attribute of to group elements.
Here one possible approach is to use a class and a data attribute to specify the property to map to a field like
var data = {
"result": "OK",
"items": [{
"name": "abc",
"description": "desc",
"new_value": {
"description": "newvalue"
},
"old_value": {
"description": "oldvalue "
}
}, {
"name": "abc3",
"description": "desc2",
"new_value": {
"interval": "newvalue2"
},
"old_value": {
"interval": "oldvalue2 "
}
}]
};
var trmap = {};
$('table tr').each(function() {
trmap[$(this).children(':first-child').text().trim()] = this;
});
$.each(data.items, function(i, item) {
if (trmap[item.name]) {
var $tr = $(trmap[item.name]);
$tr.find('.val').text(function() {
var key = $(this).data('key');
var arr = [];
$.each(item[key], function(key, value) {
arr.push(key + ': ' + value)
});
return arr.join('');
});
}
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<table>
<tbody>
<tr>
<td>abc</td>
<td>desc</td>
<td class="val" data-key="new_value">key: newvalue</td>
<td class="val" data-key="old_value">key: oldvalue</td>
</tr>
<tr>
<td>abc3</td>
<td>desc2</td>
<td class="val" data-key="new_value">key: newvalue2</td>
<td class="val" data-key="old_value">key: oldvalue2</td>
</tr>
</tbody>
</table>

show/hide elements dynamically using knockout

I have a table which has four columns namely Code, Name, Quantity and Price. Out of these, I want to change the content/element of Quantity column dynamically. Normally, it should show the element with quantity displayed in it and when user click on element, I want to show the element so user can edit the quantity. I'm trying to implement as per "Example 2" on this knockout documentation link.
Following is my code :
Page Viewmodel
function OrderVM (vm) {
var self = this;
self.OrderNo= ko.observable(vm.OrderNo());
.....
.....
self.OrderedProducts = ko.observableArray([]);
for (i = 0; i < vm.OrderedProducts().length; i++) {
var p = new ProductVM(vm.OrderedProducts()[i]);
self.OrderedProducts.push(p);
}
.....
}
function ProductVM(vm) {
var self = this;
self.Code = ko.observable(vm.Code());
self.Name = ko.observable(vm.Name());
self.Quantity = ko.observable(vm.Quantity());
self.Price = ko.observable(vm.Price());
self.IsEditing = ko.observable(false);
this.edit = function () {
self.IsEditing(true);
}
}
In my Razor view I have following code :
<tbody data-bind="foreach:OrderedProducts">
<tr>
<td class="lalign"><span data-bind="text:Code"/></td>
<td class="lalign"><span data-bind="text:Name" /></td>
<td class="ralign" style="padding:1px!important;">
<span data-bind="visible: !IsEditing(), text: Quantity, click: edit"
style="width:100%;float:left;text-align:right;padding-right:10px;" />
<input data-bind="value: Quantity,visible:IsEditing,hasfocus:IsEditing"
style="width:100%;text-align:right;padding-right:10px;" />
</td>
<td class="ralign rightbordernone" style="padding-right:20px!important;"><span data-bind="text:Price"/></td>
</tr>
With above code, when I click on span element in my Quantity column of table, "edit" function is called and "IsEditing" value is set to true but I don't see input element visible in my cell. After clicking on span element, If I look at the html using "Inspect Element", I can still see the element only and not but on screen in my view, I see neither span nor input element.
This is very simple logic and executes as expected however final result on view is not as expected. Can anyone help me to detect what's wrong with above code?
The problem is tricky. It lies in the fact that span is not a self-closing element. This will work:
<td>
<span data-bind="visible: !IsEditing(), text: Quantity, click: edit"></span>
<input data-bind="value: Quantity, visible: IsEditing, hasfocus: IsEditing" />
</td>
Here's a full demo:
function ProductVM(vm) {
var self = this;
self.Code = ko.observable(vm.Code());
self.Name = ko.observable(vm.Name());
self.Quantity = ko.observable(vm.Quantity());
self.Price = ko.observable(vm.Price());
self.IsEditing = ko.observable(false);
this.edit = function () {
self.IsEditing(true);
}
}
ko.applyBindings({ OrderedProducts: [new ProductVM({
Code: function() { return 1; },
Name: function() { return "Apples"; },
Quantity: function() { return 10; },
Price: function() { return 12.50; }
})]})
span { padding: 5px; background: pink; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
(Click "10" e.g. the Quantity for demo.)
<table>
<tbody data-bind="foreach: OrderedProducts">
<tr>
<td><span data-bind="text:Code"></span></td>
<td><span data-bind="text:Name"></span></td>
<td>
<span data-bind="visible: !IsEditing(), text: Quantity, click: edit"></span>
<input data-bind="value: Quantity, visible: IsEditing, hasfocus: IsEditing" />
</td>
<td><span data-bind="text:Price"></span></td>
</tr>
</tbody>
</table>
Remember, the same holds for div elements, don't use them in a self-closing manner or Knockout will fool you when you least expect it.

Knockout options binding: how to remove items dynamically from popup if they are selected

Here is a sample for the problem, in this case someone selects toppings for a pizza (my real world problem is analogue to that):
http://jsfiddle.net/csabatoth/aUH2C/4/
HTML:
<h2>Pizza toppings (<span data-bind="text: toppings().length"></span>)</h2>
<table class="table table-bordered">
<thead><tr>
<th>Topping</th><th>Number of units</th>
</tr></thead>
<tbody data-bind="foreach: toppings">
<tr>
<td><select data-bind="options: $root.availableToppings(), value: name, optionsText: 'name'"></select></td>
<td><input data-bind="value: num" /></td>
<td>Remove</td>
</tr>
</tbody>
</table>
<button type="button" class="btn btn-primary" data-bind="click: addTopping, enable: toppings().length < 5">Add another topping</button>
JS:
// Class to represent a possible topping
function Topping(name) {
var self = this;
self.name = name;
// This will have other properties
}
// Class to represent a row in the grid
function ToppingRow(topping, num) {
var self = this;
self.topping = ko.observable(topping);
self.num = ko.observable(num);
self.toppingName = ko.computed(function() {
return self.topping().name;
});
}
function ToppingsViewModel() {
var self = this;
// Non-editable catalog data - would come from the server
self.availableToppings = ko.observableArray([
new Topping("Mushroom"),
new Topping("Pepperoni"),
new Topping("Cheese"),
new Topping("Olives"),
new Topping("Chicken")
]);
// Editable data
self.toppings = ko.observableArray([
new ToppingRow(self.availableToppings()[0], 1)
]);
// Operations
self.addTopping = function() {
self.toppings.push(new ToppingRow(self.availableToppings()[0], 1));
}
self.removeTopping = function(topp) { self.toppings.remove(topp) }
}
ko.applyBindings(new ToppingsViewModel());
What I would like is: when the user selects a topping, that option should disappear from the popup list. Once the user removes the topping, it should reappear in the popup. In other words: I don't want the user to add the same topping more than once. How to do that?
(Or now I think if I should approach this in a totally different way and would have a list with the toppings on the left, and the user could drag&drop to the right destination list from there...). In the real world example the number of "toppings" would be maybe some dozen I think.
You could simplify this slightly by having a topping selector with 1 drop down. Then when you click add it inserts the currently selected item to a selected toppings section and then removes the option from the available list.
If you are feeling clever you could also bind the drop-down to a computed collection of items that does not include already selected items. (underscore.js will also help with this immensely).
(Fiddle)
JS
// Class to represent a possible topping
function Topping(name) {
var self = this;
self.name = name;
// This will have other properties
}
// Class to represent a row in the grid
function ToppingRow(topping, num) {
var self = this;
self.topping = topping;
self.num = ko.observable(num);
self.toppingName = ko.computed(function() {
return self.topping.name;
});
}
function ToppingsViewModel() {
var self = this;
// Non-editable catalog data - would come from the server
self.allToppings = ko.observableArray([
new Topping("Mushroom"),
new Topping("Pepperoni"),
new Topping("Cheese"),
new Topping("Olives"),
new Topping("Chicken")
]);
self.selectedToppings = ko.observableArray([]);
self.availableToppings = ko.computed(function(){
return _.reject(self.allToppings(), function(topping) {
return _.contains(_.pluck(self.selectedToppings(), 'topping'), topping);
})
});
self.currentlySelectedTopping = ko.observable();
self.currentlySelectedToppingNumber = ko.observable(1);
// Operations
self.addTopping = function() {
self.selectedToppings.push(new ToppingRow(self.currentlySelectedTopping(), self.currentlySelectedToppingNumber()));
}
//self.removeTopping = function(topp) { self.toppings.remove(topp) }
}
ko.applyBindings(new ToppingsViewModel());
HTML
<h2>Pizza toppings</h2>
<table class="table table-bordered">
<thead><tr>
<th>Topping</th><th>Number of units</th>
</tr></thead>
<tbody>
<tr>
<td><select data-bind="options: availableToppings, value: currentlySelectedTopping, optionsText: 'name'"></select></td>
<td><input data-bind="value: currentlySelectedToppingNumber" /></td>
<td>Remove</td>
</tr>
<!-- ko foreach: selectedToppings -->
<tr><td data-bind="text: toppingName"></td><td data-bind="text: num"></td></tr>
<!-- /ko -->
</tbody>
</table>
<button type="button" class="btn btn-primary" data-bind="click: addTopping, enable: availableToppings().length">Add another topping</button>
I dont why you are making simple things difficult. Here is some modified version.
function Topping(name) {
var self = this;
self.name = name;
self.active = ko.observable(false)
self.toggle = function () {
self.active(!self.active())
}
// This will have other properties
}
// Class to represent a row in the grid
function ToppingRow(name, num) {
var self = this;
self.name = name;
self.num = num;
}
function ToppingsViewModel() {
var self = this;
// Non-editable catalog data - would come from the server
self.availableToppings = ko.observableArray([
new Topping("Mushroom"),
new Topping("Pepperoni"),
new Topping("Cheese"),
new Topping("Olives"),
new Topping("Chicken")
]);
self.list = ko.observableArray()
self.num = ko.observable(1)
self.selected = ko.observable()
// Operations
self.addTopping = function() {
self.list.push(new ToppingRow(self.selected(),self.num()));
self.setAvailableToppings(self.selected())
}
self.removeTopping = function(item) {
self.list.remove(item)
self.setAvailableToppings(item.name)
}
self.setAvailableToppings = function (name) {
var items = []
ko.utils.arrayForEach(self.availableToppings(),function (item) {
if(item.name == name){
item.toggle()
}
items.push(item)
})
self.availableToppings([])
self.availableToppings(items)
var selected = ko.utils.arrayFirst(self.availableToppings(),function (item) {
return item.active() == false
})
if(selected){
self.selected(selected.name)
}
}
self.setOptionDisable = function(option, item) {
ko.applyBindingsToNode(option, {disable: item.active()}, item);
}
}
$(document).ready(function(){
ko.applyBindings(new ToppingsViewModel());
})
And view
<h2>Pizza toppings (<span data-bind="text: list().length"></span>)</h2>
<table class="table table-bordered">
<thead><tr>
<th>Topping</th><th>Number of units</th>
</tr></thead>
<tbody data-bind="foreach: list">
<tr>
<td data-bind="text:name"></td>
<td data-bind="text: num"></td>
<td>Remove</td>
</tr>
</tbody>
</table>
<br clear="all"/>
<select data-bind="
options: $root.availableToppings(),
value: $root.selected,
optionsText: 'name',
optionsValue : 'name',
optionsAfterRender: $root.setOptionDisable,
enable: list().length < 5
">
</select>
<input data-bind="value: $root.num,enable: list().length < 5" />
<br clear="all"/>
<button type="button" class="btn btn-primary"
data-bind="
click: addTopping,
enable: list().length < 5
">Add another topping</button>
Fiddle Demo

Remove table row by finding content with Javascript

i have this code:
<table>
<tbody>
<tr>
<td align="left">X</td>
<td>X1</td>
</tr>
<tr>
<td width="150" align="left">Y</td>
<td>Y1</td>
</tr>
<tr>
<td align="left">Status: </td>
<td colspan="4">
<select name="status" size="1">
<option selected="selected" value="2">one</option>
<option value="1">two</option>
</select>
</td>
</tr>
<tr>
<td width="150" align="left">Z</td>
<td>Z1</td>
</tr>
</tbody>
</table>
and want to remove with Javascript this line:
<tr>
<td align="left">Status: </td>
<td colspan="4">
<select name="status" size="1">
<option selected="selected" value="2">one</option>
<option value="1">two</option>
</select>
</td>
</tr>
the problem is that i don't have any id's or classes for identification.
is there a way to remove the row by searching "Status: " or name="status" with javascript for Firefox only (using it for Greasemonkey)?
i can't use jQuery and i can't edit the code to set any id's
best regards
bernte
If you can afford not being compatible with IE7, You can do that :
​var elements = document.querySelectorAll('td[align="left"]');
for (var i=0; i<elements.length; i++) {
var text = elements[i].textContent || elements[i].innerText;
if (text.trim()=='Status:') {
elements[i].parentNode.parentNode.removeChild(elements[i].parentNode);
}
}
Demonstration
To be compatible with IE7, you'd probably have to iterate on all rows and cells, which wouldn't really be slower but would be less clear.
Note that I used trim which doesn't exist on IE8. To make it work on this browser if needed, you might add this usual fix (from MDN) :
if(!String.prototype.trim) {
String.prototype.trim = function () {
return this.replace(/^\s+|\s+$/g,'');
};
}
function removeRowByCellValue(table,cellValue) {
var cells = table.getElementsByTagName("TD");
for(var x = 0; x < cells.length; x++) {
// check if cell has a childNode, prevent errors
if(!cells[x].firstChild) {
continue;
}
if(cells[x].firstChild.nodeValue == cellValue) {
var row = cells[x].parentNode;
row.parentNode.removeChild(row);
break;
}
}
}
First get a reference to your table, since you do not have an ID you can use getElementsByTagName.. (here i'm assuming that it is the first table in your document)
var myTable = document.getElementsByTagName("table")[0];
Then you can invoke the function with the following parameters
removeRowByCellValue(myTable,"Status: ");
var xpathResult = document.evaluate("//td[starts-with(.,'Status:')]", document.body, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
var element = xpathResult.singleNodeValue.parentNode;
while (element.firstChild)
element.removeChild(element.firstChild);
Jsbin http://jsbin.com/urehuw/1/edit
And if you'd like to be compatible with older IE:
function closest(el, tag) {
if (!el || !tag) {
return false;
}
else {
return el.parentNode.tagName.toLowerCase() == tag ? el.parentNode : closest(el.parentNode, tag);
}
}
// gets all 'select' elements
var sel = document.getElementsByTagName('select');
// iterates through the found 'select' elements
for (var i=0, len = sel.length; i<len; i++) {
// if the 'select' has the name of 'status'
if (sel[i].name == 'status') {
// uses the closest() function to find the ancestor 'tr' element
var row = closest(sel[i], 'tr');
// access the 'tr' element's parent to remove the 'tr' child
row.parentNode.removeChild(row);
}
}
JS Fiddle demo.
simply use this
var gettag =document.getElementsByTagName("tr")[3] ; // the third tag of tr
document.removeChild(gettag);

Categories

Resources