Vanilla Javascript:如何多次使用克隆的 DOM 节点?

Vanilla Javascript: How to use cloned DOM node multiple times?

我正在创建一个非常基本的购物车。

它具有相关的下拉菜单和一个按钮 "Add more products",该按钮将再添加一行相同的下拉菜单。

有 2 个下拉菜单,在第一个菜单中选择一个选项之前,第二个菜单必须保持 disabled。在第二个菜单中选择一个选项之前,数量输入必须是 disabledAdd more products启用数量是add

我正在使用 cloneNode() 为新行添加代码。

因为每次单击 "Add more products" 按钮都会调用 new_products();

我正在使用最后添加的行来创建新的 Clone

添加了新行,但问题是第二个菜单和此行中输入的数量已经 enabled

请尝试在 Vanilla(纯)中给出解决方案 JavaScript。

编辑 1: 我已经走到一半了。

在附加克隆之前,我尝试访问这些元素并更改 disabled 属性值。

function new_products()中:

var order = document.getElementById('order_now');
var product = document.getElementsByClassName('product');
var clone = product[no_of_products-1].cloneNode(true);
clone.getElementsByClassName('second_select')[0].disabled=true;
clone.getElementsByClassName('add_btn')[0].disabled=true;

但这只适用于第二个下拉菜单。

数量input控制无效。

代码片段:

var productsByCategory = {
  A: ["Select sub-product", "CNC 1", "CNC 2", "CNC 3", "CNC 4"],
  B: ["Select sub-product", "LASER 1", "LASER 2", "LASER 3", "LASER 4"],
  C: ["Select sub-product", "RUBBER 1", "RUBBER 2", "RUBBER 3", "RUBBER 4", "RUBBER 5"],
  D: ["Select sub-product", "PRECISION 1", "PRECISION 2", "PRECISION 3"]
}
var valuesByCategory = {
  A: ["", "A1", "A2", "A3", "A4"],
  B: ["", "B1", "B2", "B3", "B4"],
  C: ["", "C1", "C2", "C3", "C4", "C5"],
  D: ["", "D1", "D2", "D3"]
}

var no_of_products = 1;

function dropdown() {
  var select = document.getElementsByClassName('first_select');
  var selected = select[no_of_products - 1].value;
  var target = document.getElementsByClassName('second_select');
  var targetLength = target[no_of_products - 1].length
  /*console.log("Length"+target.length);*/
  for (var i = targetLength; i >= 0; i--) {
    /*console.log(i);*/
    target[no_of_products - 1].remove(i);
  }
  if (selected == 0) {
    var option = document.createElement("option");
    option.text = "Select Product first";
    option.value = "";
    target[no_of_products - 1].add(option);
    target[no_of_products - 1].disabled = true;
  }
  if (selected == 1) {

    for (var i in productsByCategory['A']) {
      var option = document.createElement("option"); //If this is outside the lopp then only last option gets included.
      option.text = productsByCategory['A'][i];
      option.value = valuesByCategory['A'][i];
      target[no_of_products - 1].add(option);
      target[no_of_products - 1].disabled = false;
    }

  } else if (selected == 2) {
    for (var i in productsByCategory['B']) {
      var option = document.createElement("option");
      option.text = productsByCategory['B'][i];
      option.value = valuesByCategory['B'][i];
      target[no_of_products - 1].add(option);
      target[no_of_products - 1].disabled = false;
    }
  } else if (selected == 3) {
    for (var i in productsByCategory['C']) {
      var option = document.createElement("option");
      option.text = productsByCategory['C'][i];
      option.value = valuesByCategory['C'][i];
      target[no_of_products - 1].add(option);
      target[no_of_products - 1].disabled = false;
    }
  } else {
    for (var i in productsByCategory['D']) {
      var option = document.createElement("option");
      option.text = productsByCategory['D'][i];
      option.value = valuesByCategory['D'][i];
      target[no_of_products - 1].add(option);
      target[no_of_products - 1].disabled = false;
    }
  }
}

function dropdown2() {
  var select = document.getElementsByClassName('second_select');
  var selected = select[no_of_products - 1].value;
  /*console.log(selected);*/
  var submit = document.getElementsByClassName('s_btn');
  submit[no_of_products - 1].disabled = false;
  var add = document.getElementById('add_button');
  add.disabled = false;
}

function new_products() {
  var order = document.getElementById('order_now');
  var product = document.getElementsByClassName('product');
  var clone = product[no_of_products - 1].cloneNode(true);
  clone.getElementsByClassName('second_select')[0].disabled = true;
  clone.getElementsByClassName('add_btn')[0].disabled = true;
  var add = document.getElementById('add_button');

  product[no_of_products - 1].removeChild(add);

  /*console.log(clone);*/

  order.appendChild(clone);

  no_of_products += 1;
}
body {
  height: 100vh;
  margin: 0px;
  overflow-y: auto;
  font-family: 'Roboto';
}

#clear {
  clear: both;
}

.content {
  display: flex;
  background-color: white;
  height: auto;
  margin-top: 0px;
  font-family: 'Roboto';
  z-index: -1;
  min-height: 88%;
}

.link-contents {
  position: relative;
  display: block;
  float: left;
  left: 0px;
  width: 100%;
}

.option-links {
  display: block;
  font-size: 30px;
  cursor: pointer;
}

#op1 {
  background-color: #cccccc;
}

select,
button,
input {
  position: relative;
  top: 5em;
  display: block;
  width: 12em;
  height: 2em;
}

button {
  width: 8em;
}

.first_select {
  position: relative;
  float: left;
  left: 10%;
}

.second_select {
  position: relative;
  float: left;
  left: 20%;
}

.s_btn {
  position: relative;
  float: left;
  left: 30%;
}

.add_btn {
  float: left;
  top: 6em;
  width: 10em;
  left: 5em;
}

.footer {
  display: block;
  max-height: 4%;
}

.option-contents {
  display: none;
}

#order_now {
  display: block;
}
<!DOCTYPE html>
<html>

<head>
  <link href='https://fonts.googleapis.com/css?family=Roboto' rel='stylesheet'>
  <link rel="stylesheet" type="text/css" href="profile.css">
  <title></title>
</head>

<body>

  <div class="content">
    <div class="link-contents">
      <div class="option-contents" id="order_now">
        <div class="product">
          <select class="first_select" onchange="dropdown();">
            <option value="0">Select</option>
            <option value="1">CNS</option>
            <option value="2">Laser Cut</option>
            <option value="3">Rubber roller</option>
            <option value="4">Fixture</option>
          </select>

          <select class="second_select" onchange="dropdown2();" disabled>
            <option>Select Product first</option>
          </select>
          <input class="s_btn" type="number" min='1' value="1" disabled />
          <br/>
          <button class="add_btn" id="add_button" onclick="new_products();" disabled>Add more products</button>
          <div id="clear"></div>
        </div>
      </div>
    </div>

    <div id="clear"></div>

  </div>

  <div class="footer">
    A big thank you to all of you.
  </div>



</body>
<script type="text/javascript" src="profile.js"></script>

</html>

为了在每次单击 'Add more products' button.add_btn 时附加 div.product 的克隆,您需要创建一个全局变量,它保存 [=13] 的克隆=] 在页面加载时显示(我的意思是 div.product 具有 select.second_selectinput.s_btnbutton.add_btn 最初被禁用),然后当 button.add_btn 单击我们将创建一个新的 div 元素,给它一个 product 的 class,然后将 div 附加到 div#order_now,然后我们将使用克隆元素的 innerHTML 属性并将其分配给新创建的 div.

一些意见和建议

我会在我的答案末尾添加一个可运行的片段,但在此之前,我想告诉你一些要点:

  • 您正在使用 inline event listeners 这不是一个明智的选择,您应该改用 addEventListener 方法。使用 addEventListener 方法,你可以同时附加很多事件(当然当这些事件共享相同的处理逻辑时),你也可以为同一个事件附加任意数量的处理程序(唯一的问题是客户端内存和性能问题)。 addEventListener 方法的另一个重要特性是最终参数,它控制侦听器如何对冒泡事件做出反应,使用内联事件时没有等效项。

    Learn more about addEventListener method

    Learn more about events bubbling.

  • 为了让动态创建的元素不被事件捕获,例如select元素上的'change'事件,我们需要将事件附加到document 并检查应执行哪个处理函数。换句话说,我们将使用 Event Delegqtion.

    Learn more about Event Delegation, that Whosebug post may help you.

  • 在我的回答中,您代码中的一些变量将不再用作 no_of_products 变量和其他一些变量(我希望您会自己注意到它们)。

  • 我从 div.product 元素,因为 ID 在页面中必须是唯一的。此外,我删除了所有内联事件处理程序,因为我们将使用 addEventListener 方法。

  • 正如我所说,内联事件处理程序将被 addEventListener 方法取代,并且一些变量不再有用,要检查应该执行哪个处理函数,我们将依靠 Eventtarget 属性来查看哪个元素是当前事件的目标,因此我们将知道应该执行哪个处理函数。

    Learn more about Event.target.

  • 为了更好地控制我们的代码(特别是处理函数),将使用 this 关键字来引用当前事件的目标(像这样,不需要 no_of_products 变量,因为当每个元素是当前事件的目标时,将使用 this 关键字对其进行引用,甚至支持动态创建的元素。即:在 new_products 函数中, this 引用到单击的 button.add_btn 元素)。为此,我们将在处理函数上使用 call 方法,它允许我们指定哪个元素被处理函数中的 this 关键字引用。

    Learn more about call method.

综上所述,这里有一个可运行的片段来说明:

var productsByCategory = {
    A: ["Select sub-product", "CNC 1", "CNC 2", "CNC 3", "CNC 4"],
    B: ["Select sub-product", "LASER 1", "LASER 2", "LASER 3", "LASER 4"],
    C: ["Select sub-product", "RUBBER 1", "RUBBER 2", "RUBBER 3", "RUBBER 4", "RUBBER 5"],
    D: ["Select sub-product", "PRECISION 1", "PRECISION 2", "PRECISION 3"]
  },
  valuesByCategory = {
    A: ["", "A1", "A2", "A3", "A4"],
    B: ["", "B1", "B2", "B3", "B4"],
    C: ["", "C1", "C2", "C3", "C4", "C5"],
    D: ["", "D1", "D2", "D3"]
  },
  /**
   * create a clone element using 'querySelector' method 
   * which DOESN'T return a 'live node' (it returns a 'static node'), thus gain performance.
   **/
  clone = document.querySelector('.product').cloneNode(true);
/**
 * add event listeners to the body rather than the specific elements
 * thus the dynamically created elements are also supported and catchable by the events.
 * using some checking to get the desired handler function to be called.
 * using the 'call' method, we specify to which element in the handler functions the 'this' keyword refers to. 
 **/
document.addEventListener('change', function(e) {
  (e.target instanceof HTMLSelectElement && ((e.target.classList.contains('first_select') && dropdown.call(e.target)) || (e.target.classList.contains('second_select') && dropdown2.call(e.target))));
  /**
   * the above code is the same as the next but it's faster.

    if (e.target instanceof HTMLSelectElement && e.target.classList.contains('first_select')) {
      dropdown.call(e.target)
    } else if (e.target instanceof HTMLSelectElement && e.target.classList.contains('second_select')) {
      dropdown2.call(e.target)
    }
    
  **/
});
document.addEventListener('click', function(e) {

  (e.target instanceof HTMLButtonElement && e.target.classList.contains('add_btn') && new_products.call(e.target));
  /**
  * the above code is the same as the next but it's faster.
  
    if(e.target instanceof HTMLButtonElement && e.target.classList.contains('add_btn')) {
      new_products.call(e.target);
    }
    
  **/
  
})

/**
* from now on, the handler function use the 'this' keyword to reference the desired element, even the dynamically created ones are supporyted.
* So, 'this' === the argument that passed to the 'call' method.
**/

function dropdown() {

  var selected = this.value;
  var target = this.parentNode.getElementsByClassName('second_select')[0];
  var targetLength = target.length;
  for (var i = targetLength; i >= 0; i--) {
    target.remove(i);
  }
  if (selected == 0) {
    var option = document.createElement("option");
    option.text = "Select Product first";
    option.value = "";
    target.add(option);
    /**
    * you missed to disable the 'button' and the 'input' if the selected value is '0'. 
    **/
    this.parentNode.querySelector('.s_btn').disabled = true;
    this.parentNode.querySelector('.add_btn').disabled = true;
    target.disabled = true;
  } else if (selected == 1) {

    for (var i in productsByCategory['A']) {
      var option = document.createElement("option");
      option.text = productsByCategory['A'][i];
      option.value = valuesByCategory['A'][i];
      target.add(option);
      target.disabled = false;
    }

  } else if (selected == 2) {
    for (var i in productsByCategory['B']) {
      var option = document.createElement("option");
      option.text = productsByCategory['B'][i];
      option.value = valuesByCategory['B'][i];
      target.add(option);
      target.disabled = false;
    }
  } else if (selected == 3) {
    for (var i in productsByCategory['C']) {
      var option = document.createElement("option");
      option.text = productsByCategory['C'][i];
      option.value = valuesByCategory['C'][i];
      target.add(option);
      target.disabled = false;
    }
  } else {
    for (var i in productsByCategory['D']) {
      var option = document.createElement("option");
      option.text = productsByCategory['D'][i];
      option.value = valuesByCategory['D'][i];
      target.add(option);
      target.disabled = false;
    }
  }
}

function dropdown2() {
  this.parentNode.getElementsByClassName('s_btn')[0].disabled = false;
  this.parentNode.getElementsByClassName('add_btn')[0].disabled = false;
}

function new_products() {
  var order = document.getElementById('order_now'),
  /**
  * create a 'div' element which will hold the cloned element's 'innerHTML'.
  **/
      product = document.createElement('div');
  /**
  * give that 'div' element the 'product' class.
  **/
  product.className = 'product';
  /**
  * append that 'div' to the 'div#order_now' element.
  * it's now the last child of the 'div#order_now' element.
  **/
  order.appendChild(product);
  /**
  * assign the cloned element's 'innerHTML' to the newly created 'div' using the 'lastChild' attribute.
  * with that we eliminate the possibility of directly appending the cloned element to 'div#order_now' to run only once.
  **/
  order.lastChild.innerHTML = clone.innerHTML;
  this.parentNode.removeChild(this)
}
body {
  height: 100vh;
  margin: 0px;
  overflow-y: auto;
  font-family: 'Roboto';
}

#clear {
  clear: both;
}

.content {
  display: flex;
  background-color: white;
  height: auto;
  margin-top: 0px;
  font-family: 'Roboto';
  z-index: -1;
  min-height: 88%;
}

.link-contents {
  position: relative;
  display: block;
  float: left;
  left: 0px;
  width: 100%;
}

.option-links {
  display: block;
  font-size: 30px;
  cursor: pointer;
}

#op1 {
  background-color: #cccccc;
}

select,
button,
input {
  position: relative;
  top: 5em;
  display: block;
  width: 12em;
  height: 2em;
}

button {
  width: 8em;
}

.first_select {
  position: relative;
  float: left;
  left: 10%;
}

.second_select {
  position: relative;
  float: left;
  left: 20%;
}

.s_btn {
  position: relative;
  float: left;
  left: 30%;
}

.add_btn {
  float: left;
  top: 6em;
  width: 10em;
  left: 5em;
}

.footer {
  display: block;
  max-height: 4%;
}

.option-contents {
  display: none;
}

#order_now {
  display: block;
}

select,
input {
  display: block !important;
}
<div class="content">
  <div class="link-contents">
    <div class="option-contents" id="order_now">
      <div class="product">
        <select class="first_select">
          <option value="0">Select</option>
          <option value="1">CNS</option>
          <option value="2">Laser Cut</option>
          <option value="3">Rubber roller</option>
          <option value="4">Fixture</option>
        </select>

        <select class="second_select" disabled>
          <option>Select Product first</option>
        </select>
        <input class="s_btn" type="number" min='1' value="1" disabled />
        <br/>
        <button type="button" class="add_btn" disabled>Add more products</button>
        <div id="clear"></div>
      </div>
    </div>
  </div>
  <div id="clear"></div>

This Whosebug post may help you to about live nodes and static nodes.

希望我能把你推得更远。

不幸的是,您的代码中有很多错误。因此,我将只描述重要的错误:

  1. 元素的 id 属性在整个 HTML 页面中必须是唯一的。如果你克隆了一些元素并且它有一个 id 属性,那么你必须创建一个新的 id。因为您希望从按钮中删除 id 属性 – 所以我为您做了。
  2. 不要使用 element.disable 禁用元素。大多数浏览器都支持它,但它不是标准的。如果您查看 Element and for Node then you will see that they do not have this property. Some browser would not support it. Use for this case Element.setAttribute() and Element.removeAttribute() 函数的文档。
  3. 添加下一个产品行的按钮不能在产品行中。
  4. 如果不需要,请不要使用CSS属性float: left。在您的情况下,您不需要它,因为您有 inline-block 个元素。我也更改了您的很多 CSS 代码。
  5. Do not reapeat your code again and again – 这是非常糟糕的编程风格。阅读上一篇link下的文章。因此我缩短了你的代码——我写了一些带参数的函数。
  6. 通常最好使用 addEventListener 方法而不是内联事件侦听器,但在你的情况下这是有争议的,因此我不使用 addEventListener - 所以你可以更好地理解我的代码。但是我在你的函数中添加了一个参数——注意它。

我写了新代码,所以你没有任何类型的错误。好好享受吧!

完整解决方案

var productsByCategory =
{
    A: ["Select sub-product", "CNC 1", "CNC 2", "CNC 3", "CNC 4"],
    B: ["Select sub-product", "LASER 1", "LASER 2", "LASER 3", "LASER 4"],
    C: ["Select sub-product", "RUBBER 1", "RUBBER 2", "RUBBER 3", "RUBBER 4", "RUBBER 5"],
    D: ["Select sub-product", "PRECISION 1", "PRECISION 2", "PRECISION 3"]
};

var valuesByCategory =
{
    A: ["", "A1", "A2", "A3", "A4"],
    B: ["", "B1", "B2", "B3", "B4"],
    C: ["", "C1", "C2", "C3", "C4", "C5"],
    D: ["", "D1", "D2", "D3"]
};

//var no_of_products = 1; //WE DO NOT NEED IT

function selectHelper(category, targetObj)
{
    for(var i in productsByCategory[category])
    {
        var option = document.createElement("option");

        option.text = productsByCategory[category][i];
        option.value = valuesByCategory[category][i];

        targetObj.add(option);
    }
    
    setEnabled(targetObj);
}

function dropdown(obj)
{
    var selected = obj.value,
        //second select:
        target = obj.nextElementSibling;

    for(var i = target.length; i--; )
        target.remove(i);

    if(selected == 0)
    {
        var option = document.createElement("option");
        option.text = "Select Product first";
        option.value = "";

        target.add(option);
        setDisabled(target);
        //set disabled input field:
        setDisabled(target.nextElementSibling)
    }
    else
    {
        if(selected == 1)
            selectHelper('A', target);
        else if(selected == 2)
            selectHelper('B', target);
        else if(selected == 3)
            selectHelper('C', target);
        else
            selectHelper('D', target)
    }
}

function dropdown2(obj)
{
    setEnabled(obj.nextElementSibling);

    setEnabled(document.getElementsByClassName('add_btn')[0]);
}

function new_products()
{
    var allProducts = document.getElementsByClassName('product');
        lastProduct = allProducts[allProducts.length - 1],
        clone = lastProduct.cloneNode(true);

    setDisabled(clone.getElementsByClassName('second_select')[0]);
    //set disabled input field:
    setDisabled(clone.getElementsByClassName('s_btn')[0]);
    setDisabled(document.getElementsByClassName('add_btn')[0]);

    //may be "insertBefore" is weird, but we do here insert new product after the last product:
    document.getElementById('order_now').insertBefore(clone, lastProduct.nextSibling);
}

function setDisabled(obj)
{
    obj.setAttribute("disabled", "disabled");
}

function setEnabled(obj)
{
    obj.removeAttribute("disabled");
}
body
{
    height: 100vh;
    margin: 0px;
    overflow-y: auto;
    font-family: 'Roboto';
}

.content
{
    background-color: white;
    height: auto;
    margin-top: 0px;
    z-index: -1;
    min-height: 88%;
}

.link-contents
{
    position: relative;
    left: 0px;
    width: 100%;
}

.option-links
{
    display: block;
    font-size: 30px;
    cursor: pointer;
}

#op1 {background-color: #cccccc}

select, button, input
{
    position: relative;
    top: 5em;
    width: 12em;
    height: 2em;
}

button {width: 8em}

.first_select
{
    position: relative;
    left: 10%;
}

.second_select
{
    position: relative;
    left: 20%;
}

.s_btn
{
    position: relative;
    left: 30%;
}

.add_btn
{
    top: 6em;
    width: 10em;
}

.footer
{
    display: block;
    max-height: 4%;
}

.option-contents {display: none}
#order_now {display: block}
<link href='https://fonts.googleapis.com/css?family=Roboto' rel='stylesheet'>
<link rel="stylesheet" type="text/css" href="profile.css">

<div class="content">
    <div class="link-contents">
        <div class="option-contents" id="order_now">
            <div class="product">
                <select class="first_select" onchange="dropdown(this)">
                    <option value="0">Select</option>
                    <option value="1">CNS</option>
                    <option value="2">Laser Cut</option>
                    <option value="3">Rubber roller</option>
                    <option value="4">Fixture</option>
                </select>

                <select class="second_select" onchange="dropdown2(this)" disabled>
                    <option>Select Product first</option>
                </select>

                <input class="s_btn" type="number" min='1' value="1" disabled />
            </div>
            <center><button class="add_btn" onclick="new_products()" disabled>Add more products</button></center>
        </div>
    </div>
</div>
<div class="footer">A big thank you to all of you.</div>