简体   繁体   中英

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

I am creating a very basic shopping cart.

It has dependent drop down menus and a button "Add more products" which will add one more row of the same drop down menus.

There are 2 drop down menus 2nd menu must remain disabled until an option is selected in 1st menu. The quantity input must be disabled until an option is selected in 2nd menu. The Add more products is enabled quantity is add

I am using cloneNode() to add code for new row.

Since it works only once I am creating clone each time "Add more products" button is clicked which calls new_products();

I am using last added row for creating new Clone

The new row gets added but the problem is 2nd menu and quantity input in this row are already enabled .

Please try giving a solution in Vanilla(pure) JavaScript.

EDIT 1: I have came half way.

Before appending the clone I tried to access those elements and change the disabled attribute value.

In 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;

But this only worked for 2nd drop down menu.

It's not working for quantity input control.

Code snippet:

 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> 

In order to append a clone of div.product each time the 'Add more products' button.add_btn is clicked, you need to create a global variable, that holds a clone of the div.product that shows up when the page loads(I mean the div.product that has the select.second_select , the input.s_btn and the button.add_btn that are initially disabled), then when the button.add_btn is clicked we'll create a new div element, give it a class of product , then append that div to div#order_now , then we'll use the innerHTML attribute of the cloned element and assign it to the newly created div .

Some remarks and recommendations

I'll add a runnable snippet in the end of my answer, but before that, I want to tell you some points:

  • You are using inline event listeners which is not a wise choice, you should use addEventListener method instead. With addEventListener method, you can attach many events at the same time(of course when these events share the same handling logic), you can also attach as many handers as you want to the same event(the only matter is the client-side memory and performance concerns). Another important feature of addEventListener method is the final parameter, which controls how the listener reacts to bubbling events, there is no equivalent when using inline events.

    Learn more about addEventListener method

    Learn more about events bubbling .

  • To allow the dynamically created elements from being capturable by the events, such as the 'change' event on the select elements, we need to attach the events to the document and check which handler function should execute. In other words, we'll be using Event Delegqtion .

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

  • In my answer, some variables from your code will no longer be used as no_of_products variable and some others(you'll notice them by yourself I hope).

  • I removed all the redundant ID s attribute, specically on the button , the input and the select s, from the div.product elements, because an ID must be unique in the page. Also, I removed all the inline event handlers as we'll be using the addEventListener method.

  • As I said, the inline event handlers will be replaced by the addEventListener method, and some variables are no longer useful, to check which handler function should be executed, we'll rely on the Event 's target attribute to see which element is the target of the current event, thus we'll know which handler function should execute.

    Learn more about Event.target .

  • To allow more control over our code(the handler functions particulary), will be using the this keyword to reference the current event's target(like so, no need for no_of_products variable because each element will be referenced using the this keyword when it's the current event's target, even the dynamically created elements will be supported. ie: In the new_products function, this refers to the clicked button.add_btn element). To do so, we'll be using the call method on the handler functions, which allows us to specify which element is referenced by the this keyword in the handler functions.

    Learn more about call method.

With all that being said here's a runnable snippet to illustrate:

 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 Stackoverflow post may help you to learn more about live nodes and static nodes .

Hope I pushed you further.

Unfortunately you have to much mistakes in your code. Because of this I will describe only important mistakes:

  1. The id attribute from an element must be unique in a full HTML page. If you clone some element and it has an id attribute then you have to create a new id. And because your wish is to remove id attribute from the button – so I did it for you.
  2. Do not use element.disable to disable an element. The most of browsers support it, but it is not standart. If you see the documentation for 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() functions.
  3. The button for adding of next product row must be not in product row.
  4. Do not use CSS property float: left if you do not need it. In your case you do not need it because you have inline-block elements. I have changed a lot of your CSS code too.
  5. Do not reapeat your code again and again – it is very bad programming style. Read the article under the last link. Because of this I have shorted your code – I wrote some functions with parameters.
  6. Normaly it is better to use addEventListener method instead of inline event listeners, but in your case it is disputable and because of this I do not use addEventListener – so you can better understand my code. But I add one parameter in your functions – note it.

I wrote the new code so that you do not have any type of errors. Enjoy it!

Complete solution

 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> 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM