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>
In my application, Iam having a table binded by calling Web Api service.
I have written code to display the selected row when a particular row is selected and it works fine.
Now, I want to show a selected row by default once the web api method returned success message.
How should i call 'SelectConfig' inside success method. When I tried it returns nothing. Please help me to get out of these problem.
My HTML:
<tbody data-bind="foreach: datas">
<tr>
<td style="vertical-align: middle;">
<asp:CheckBox ID="chkChild" runat="server" />
</td>
<td style="vertical-align: middle;">
<a id="aTag" data-bind="text: (Name().length > 20 ? Name().substring(0, 20) + '....' : Name()), value: ID, click: $parent.SelectConfig"></a>
</td>
<td style="vertical-align: middle;" data-bind="text: Type == '' || Type == null || Type == undefined ? '--' : Type"></td>
</tr>
</tbody>
<div data-bind="with: Selected, visible: isVisibleDetails">
<div class="col-md-8 value" data-bind="text: (Name() == '' || Name() == null || Name() == undefined) ? '--' : Name">
<select id="ddlType_E" data-bind="options: $root.ddlTypes, optionsText: 'Type', optionsValue: 'ID', optionsCaption: 'Select..', value: selectedTypeId" class="form-control"></select>
</div>
My View Model:
<script type="text/javascript">
function oppConfig(ID, Name,TypeID) {
this.ID = ko.observable(ID);
this.Name = ko.observable(Name);
this.selectedTypeId = ko.observable(TypeID);
}
function ViewModel() {
var self = this;
this.datas= ko.observableArray([]);
getdatas();
this.ddlTypes = ko.observableArray([]);
getOppType();
}
this.Selected = ko.observable(this.datas()[0]);
//selectItem
this.SelectConfig = function (oppConfig) {
alert(ko.toJSON(oppConfig));
self.Selected(oppConfig);
}
function datas() {
$.ajax({
type: "GET",
url: '---',
dataType: "json",
cache: false,
crossDomain: true,
processData: true,
success: function (data) {
self.datas.destroyAll();
if (data.length > 0) {
$.each(data, function (index) {
self.datas.push(new oppConfig(data[index].ID, data[index].Name, data[index].Type));
});
//Here, I want the to call the select config
}
else {
}
},
error: function (data) {
}
});
}
ko.applyBindings(new ViewModel());
</script>
You need to include the data-binding to the <tr> element in your HTML markup. Right now, you have the display div for what is selected, but not the selected value itself.
<tbody data-bind="foreach: model.datas">
<tr data-bind="click: $root.selectThing, css: { selected: isSelected} ">
Then in your view model, add a function to return the selected thing:
Also add an observable for the object itself.
function ViewModel() {
var self = this;
this.datas= ko.observableArray([0]);
getdatas();
this.ddlTypes = ko.observableArray([]);
getOppType();
self.CurrentDisplayThing = ko.observable();
self.selectThing = function(item){
self.model.CurrentSelected = ko.observable();
}
}
Finally, in your object representing your config class, add an isSelected variable, which is a knockout function.
function oppConfig(ID, Name,TypeID) {
this.ID = ko.observable(ID);
this.Name = ko.observable(Name);
this.selectedTypeId = ko.observable(TypeID);
self.isSelected = ko.computed(function(){
return selected() === self;
}
}
Some CSS to display your selection:
.selected { background-color: yellow; }
thead tr {
border:1px solid black;
background:lightgray;
}
tbody tr {
border:1px solid black;
cursor: pointer;
}
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