Vanilla Javascript: как использовать клонированный узел DOM несколько раз? - PullRequest
0 голосов
/ 09 сентября 2018

Я создаю очень простую корзину для покупок.

Имеет зависимые выпадающие меню и кнопку "Add more products", которая добавит еще одну строку из тех же выпадающих меню.

Есть 2 выпадающих меню. 2-е меню должно оставаться disabled до тех пор, пока не будет выбрана опция в 1-м меню. Ввод количества должен быть disabled до тех пор, пока во втором меню не будет выбран параметр. Add more products включенное количество добавляется

Я использую cloneNode(), чтобы добавить код для новой строки.

Поскольку он работает только один раз, я создаю клон каждый раз, когда нажимается кнопка "Add more products", которая вызывает new_products();

Я использую последнюю добавленную строку для создания новой Clone

Добавляется новая строка, но проблема в том, что 2-е меню и количество ввода в этой строке уже enabled.

Пожалуйста, попробуйте дать решение в ванильном (чистом) 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;

Но это работало только для 2-го выпадающего меню.

Не работает для количества 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>

Ответы [ 2 ]

0 голосов
/ 09 сентября 2018

К сожалению, в вашем коде много ошибок. Из-за этого я опишу только важные ошибки:

  1. Атрибут id элемента должен быть уникальным для полной HTML-страницы. Если вы клонируете какой-то элемент и у него есть атрибут id, вам нужно создать новый идентификатор. И поскольку вы хотите удалить атрибут id с кнопки, я сделал это для вас.
  2. Не используйте element.disable для отключения элемента. Большинство браузеров поддерживают его, но это не стандарт. Если вы видите документацию для Element и для Node, то вы увидите, что у них нет этого свойства. Некоторые браузеры не поддерживают его. Используйте для этого случая функции Element.setAttribute() и Element.removeAttribute().
  3. Кнопка добавления следующей строки продукта не должна находиться в строке продукта.
  4. Не используйте свойство CSS float: left, если оно вам не нужно. В вашем случае вам это не нужно, потому что у вас есть inline-block элементов. Я также сильно изменил ваш CSS-код.
  5. Не повторять код снова и снова - это очень плохой стиль программирования. Читайте статью по последней ссылке. Из-за этого я замкнул твой код - я написал несколько функций с параметрами.
  6. Normaly лучше использовать 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>
0 голосов
/ 09 сентября 2018

Чтобы добавлять клон div.product каждый раз, когда нажимается «Добавить больше продуктов» button.add_btn, необходимо создать глобальную переменную, которая будет содержать клон div.product, который отображается, когда страница загружает (я имею в виду div.product, который имеет select.second_select, input.s_btn и button.add_btn, которые изначально отключены), затем при нажатии на button.add_btn мы создадим новый элемент div, давая это класс product, затем добавьте div к div#order_now, затем мы будем использовать атрибут innerHTML клонированного элемента и присвоим его вновь созданному div.

Некоторые замечания и рекомендации

Я добавлю исполняемый фрагмент в конце моего ответа, но перед этим я хочу сказать вам несколько моментов:

  • Вы используете inline event listeners, что не является разумным выбором, вместо этого следует использовать метод addEventListener. С помощью метода addEventListener вы можете присоединить много событий одновременно (конечно, когда эти события используют одну и ту же логику обработки), вы также можете прикрепить столько же хандеров, сколько хотите, к одному и тому же событию (единственное, что клиент проблемы с памятью и производительностью). Еще одна важная особенность метода addEventListener - последний параметр, который управляет реакцией слушателя на всплывающие события, при использовании встроенных событий аналога нет.

    Подробнее о addEventListener методе

    Подробнее о events bubbling.

  • Чтобы позволить динамически создаваемым элементам захватываться событиями, такими как событие 'change' в элементах select, нам необходимо прикрепить события к document и проверить, какая функция обработчика должна выполнить. Другими словами, мы будем использовать Event Delegqtion.

    Узнайте больше о Event Delegation, этот пост Stackoverflow может вам помочь.

  • В моем ответе некоторые переменные из вашего кода больше не будут использоваться как переменная no_of_products, а некоторые другие (надеюсь, вы заметите их сами).

  • Я удалил все избыточные атрибуты ID s, в частности, для button, input и select s, из элементов div.product, поскольку ID должен быть уникальным на странице. Также я удалил все встроенные обработчики событий, так как мы будем использовать метод addEventListener.

  • Как я уже сказал, встроенные обработчики событий будут заменены методом addEventListener, и некоторые переменные больше не будут полезны, чтобы проверить, какая функция обработчика должна быть выполнена, мы будем полагаться на Event Атрибут target, чтобы увидеть, какой элемент является целью текущего события, поэтому мы будем знать, какая функция обработчика должна выполняться.

    Подробнее о Event.target.

  • Чтобы обеспечить больший контроль над нашим кодом (в частности, функциями-обработчиками), мы будем использовать ключевое слово this для ссылки на цель текущего события (например, нет необходимости в переменной no_of_products, поскольку каждый элемент будет на который ссылается ключевое слово this, когда оно является целью текущего события, будут поддерживаться даже динамически созданные элементы, т.е. Для этого мы будем использовать метод call в функциях-обработчиках, который позволяет нам указать, на какой элемент ссылается ключевое слово this в функциях-обработчиках.

    Подробнее о методе call.

С учетом всего сказанного ниже приведен беглый фрагмент для иллюстрации:

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>

Этот пост Stackoverflow может помочь вам узнать больше о live nodes и static nodes.

Надеюсь, я подтолкнул тебя дальше.

...