【问题标题】:Persuading scripts to play nicely together说服脚本一起玩得很好
【发布时间】:2016-02-21 12:30:38
【问题描述】:

我一直在玩 javascript/jquery,试图让各种脚本很好地结合在一起(例如,请参阅我最近在Populate an input with the contents of a custom option (data-) attribute 提出的问题)。

我现在有一大堆可以工作的笨重代码。它应该做 4 件事:

  1. 填充级联下拉列表,其中第二个下拉列表中的选项会根据第一个下拉列表中选择的内容而有所不同。
  2. 克隆第一行(也可以是最后一行),因此可以在表单中添加额外的行(在实际版本中,页面首次加载时可以有任意数量的行)
  3. 保持在第三个下拉列表中选择的值的运行总计
  4. 根据在第二个下拉列表中选择的值填充文本输入

第一行一切正常。但是,对于克隆的行,步骤 1 和 4 停止工作。我怀疑它是因为我没有唯一标识每个克隆实例。那正确吗?那我该怎么做呢?

    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>

抱歉,如果我错误地插入了 sn-p。

【问题讨论】:

  • 只需确保页面中的每个 id 都是唯一的 - 您需要处理的问题要少得多
  • 您不能有多个具有相同 ID 的元素。
  • 是的。这就是我正在努力解决的部分。
  • 它将帮助您更好地将您正在完成的工作概念化为操作数据 (myJson) 并将所述数据及其方法表示为 HTML 元素。然后,您用于操作 myJson 的代码可以专注于生成有效数据,并且可以泛化您的表示层以生成列表项和任务。
  • 您从伊恩那里得到了很好的回答,所以如果您认为这是要走的路,我认为添加另一个答案没有任何意义,...但是如果您希望找到解决方案加载尽可能少的库(实际上没有),这可以用纯 JavaScript 完成。如果你问我页面加载速度,好处将是它们中最好的功能,所以如果你的复杂性不会显着增加并且不会有数千行,请告诉我,我会给你一个答案。

标签: javascript jquery


【解决方案1】:

我会不顾一切地提出一些完全不同的建议。我知道您的问题被标记为jQuery,但我想提出一个不同的建议,我相信解决问题的更好方法。

我认为您在这里混合了 DOMJavaScript 太多,而使用绑定框架可能更简单。这些绑定框架将您的演示文稿与您的基础数据分开。有多个数字可供选择,以下只是其中的几个:

  • 角度
  • 淘汰赛
  • 反应

我个人对 Knockout 非常了解,因此我将如何创建类似于您使用 Knockout 制作的内容的方法。在这里,我已将您的代码减少了大约 50%,并且我相信通过从 JavaScript 中删除所有 DOM 的奖励显着提高了可读性

请注意,使用 Knockout.mapping 插件并调用 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>

这里的总体思路是observableobservableArray 都使用Knockout 绑定到DOM。它们会自动保持彼此同步。

computed 字段本质上是一个计算字段,所以我用它来生成您的总数,并提供一个下拉列表 - 您可能可以使用不同的方法,但这似乎很简单。

最后有一本手册subscribe 旨在更新您手头上job 的默认描述。这允许您更新该字段,而无需在用户设置一个时使用另一个可观察或计算字段来覆盖描述。

由于您在使用 ID 时也遇到了问题,因此我还将提及您如何解决这些问题。使用 Knockout,我们可以很容易地使用另一个计算字段为我们的 DOM 创建客户端唯一 ID,并在数组中返回它的顺序:

job.id = ko.computed(function() {
   return vm.jobs.indexOf(job); 
});

您甚至可以在 DOM 中反映这一点(注意 ID 不能以数字开头),如下所示:

<td data-bind="attr: { id: 'job_' + id() }"></td>

这会产生如下 DOM:

<td id="job_0"></td>

【讨论】:

  • 确实,我个人也会建议这个解决方案。 jQuery 不是一把金锤,也许它在某个地方有帮助(最近我发现它很少有用)但是如果你想解决给定的问题,你应该使用适当的工具,当涉及到数据绑定时,上面提到的那些是最好的,jquery dosn你自己发现的,根本帮不了你。
  • 太棒了——我不能给你奖励让你的东西更具可读性(标准设置得很低!),但我必须承认这比我自己的努力要优雅得多。显然,我必须对这类框架进行更多研究。再次感谢。
  • @Strawberry - 不客气。希望发展世界会稍微好一点。有大量的绑定框架可供选择,Knockout 已经有几年历史了,所以它不再是最新的锤子了,但我发现即使是一些非常复杂的东西,学习曲线非常温和,它也非常出色。随意阅读一些内容,然后选择最适合您的用例。
  • @Strawberry 顺便说一句,您还有什么想让我在我的回答中详细说明的吗?如果您希望我扩展或改进某些事情,请告诉我,我会在您的赏金到期之前尝试更新 :)
  • @Strawberry - 决定为您在示例中添加禁用未来下拉菜单 - 因为这是对 DOM 的 30 秒更改。
【解决方案2】:

这是一个非常肮脏的代码和平。我以同样肮脏的风格解决了您的问题,但我建议您以适当的风格编写全新的所有内容。 我通过将所有 id 更改为类属性解决了这个问题。并且每一行都有自己的唯一 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>

【讨论】:

  • 太棒了。谢谢。我几乎看不到你改变了什么,所以我得仔细研究一下……我可能需要一些时间……
  • 是的,这些变化更可怕 :( 我坚持 code is foremost for other people to read and secondly for a computer to execute 的声明
【解决方案3】:

另一个带有简单模板系统的版本,这次没有 jquery。

有趣的是,看看它是否难以转换,实际上并非如此,大多数将 jQuery 代码映射到现代浏览器的纯 JS 的东西都可以在这里找到 - YOU MIGHT NOT NEED JQUERY

代码如下:

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>

【讨论】:

  • 我投票认为这是一个非常好的纯 JavaScript 解决方案。我会做的唯一不同的主要事情是将克隆存储在一个变量中以供重用,而不是让它污染标记。现在,当提交表单时,需要添加不必要的编码,无论是客户端还是服务器端,以避免保存结果时数据混乱。
  • @Strawberry 我会看看如何做到最好,完成后(24 小时内)发布小提琴或答案。
  • @Strawberry @LGSon 其实这样的改变并不复杂,我更新了上面的例子,将模板代码缓存到变量中,并从标记中删除不可见节点(它将模板保存到 @987654324 @ 属性,为了避免将其放入 HTML 中,我们可以将其保存到全局 window 对象中)。但实际上像这样的 UI,from 可能会使用 ajax 提交,所以额外的标记不会有问题,只是一个将数据发送到服务器的 js 代码可以跳过不可见的节点。
  • @Strawberry 为您发布了答案,展示了它的外观。
【解决方案4】:

我赞成关于 Angular / Knokout / React 的答案 - 这实际上是我强烈建议在实际应用程序中使用的答案。

作为一个练习,如果您使用简单的模板系统,下面是代码的样子。

这个想法是您不应该“手动”构建 javascript,而是在 HTML 中声明模板。例如,项目选择可能如下所示:

<select class="project" name="">                                                                 
    <option value="">Select One</option>                                                         
    <option class='project-option-tpl' value="{id}" style='display:none'>{project_no}</o
</select>                                                                                        

这里的option 是项目选项的隐形模板。整个“行”和“任务”选择的工作方式类似,这里是实现逻辑的完整代码:

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
  });
});

“魔法”在template函数中实现:

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();
    }
  };
};

这是完整的可运行示例:

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>

【讨论】:

    【解决方案5】:

    查看为什么当前代码无法按预期工作的其他答案。 (简而言之:id 只能出现一次)。

    我花了一些时间重新组织了您的代码。至于我个人,它看起来并不那么可维护。我确实使用对象来整理东西。你也可以使用 Angular 等框架。但是其中的 JavaScript 本身也可以非常好。而且对于这么小的东西来说可能已经足够了。

    一些注意事项:

    • 在我的示例中的第一行是display:none 并称为模板(这样我们总是有一个“干净”的模板可供使用)。您可以仅在 Javascript 中使用此模板并按需插入,但这种方式将帮助您快速编辑设计。
    • 我的代码中有 4 个组
      • 数据(myjson)
      • 表对象声明(这样您也可以创建多个表)
      • 行对象声明
      • 初始化准备
    • 原型很有用,因为它不会重复每一行的代码 (目的)。所以每个select 都会执行相同的功能。但由于我们每次都使用不同的数据时使用对象。
    • 有些函数有类似hours.click(function(){tableElement.updateTime()}); 的东西,这个内部函数是必要的,因为它将函数的范围保持在对象而不是点击事件。通常当你捕获一个点击事件并设置一个函数时,那么在这个函数中this就是点击事件。我使用了对象,所以this 应该是对象而不是事件。

    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>

    【讨论】:

      【解决方案6】:

      还有 angular.js 版本,这里是完整的应用代码,其余在 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());
           };
         });
      

      完整代码:

      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>

      【讨论】:

        【解决方案7】:

        这是一个使用纯 javascript 的版本,它不需要/使用元素 id,将克隆存储为变量,除非选择项目,否则禁用任务选择。

        这个代码示例当然可以进行优化,封装到类等中,尽管我选择不这样做,以便尽可能轻松地跟踪代码流并查看发生了什么。

        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>

        【讨论】:

          猜你喜欢
          • 2013-07-19
          • 2020-04-20
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-11-27
          • 2015-04-30
          • 2011-03-16
          • 2016-11-13
          相关资源
          最近更新 更多