-2

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>
Abhay
  • 474
  • 8
  • 24
  • 1
    As I understood, the newly created clone elements should have: the second `select` element disabled, the `input` element disabled and the `button` disabled ? Am I right ? – ths Sep 09 '18 at 10:24
  • @ths yes sir you got it right. – Abhay Sep 09 '18 at 10:25
  • But, you will have same `ID`s on the page which is non recommended. Relying on the class-names is more accurate, so, for example the add `button` `ID` should be removed, only the class-name should stay. – ths Sep 09 '18 at 10:27
  • @ths In JavaScript I have used `class-names` to access 1st and 2nd select. I used `ID` to access "Add more products" button only because before cloning I did remove the previous button using `removeChild()`. Only after removing it I am adding Cloned code. – Abhay Sep 09 '18 at 10:32
  • But, the cloned items will have the same `ID`s even though you didn't use their `ID`s in your code. The `ID` attribute must be unique in a webpage. – ths Sep 09 '18 at 10:37
  • @ths okay sir I got it. I will remove it. Also would that solve my problem? – Abhay Sep 09 '18 at 10:39
  • 1
    That doesn't have any impact on your actual problem. But as soon as you want to attach event listeners, for example, based on the `ID` attribute, only the first element with the specified `ID` will receive that even listener. – ths Sep 09 '18 at 10:41
  • Your welcome ! about your issue, try to make a global variable that holds a clone of the first `.product` div and then append it whenever you want. – ths Sep 09 '18 at 10:44
  • @ths sir I did the same at first. But it only works once. I searched online and everywhere it's stated that such clone only work once. So for multiple appending multiple clones must be created. – Abhay Sep 09 '18 at 10:46
  • 1
    I know that it will work only for once, that's due to your logic in your code. Try to figure the issue out by yourself, I'll be working on it either, I may post an answer as soon as I can. – ths Sep 09 '18 at 10:49
  • @ths sir I have came halfway . Now 2nd drop down does gets `disabled` . Please check EDIT above Code Snippet. – Abhay Sep 09 '18 at 11:13
  • I'm working on it, a moment then I'll post an answer. – ths Sep 09 '18 at 11:49
  • 1
    I added an answer, check it out ! – ths Sep 09 '18 at 15:44

2 Answers2

2

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 IDs attribute, specically on the button, the input and the selects, 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. i.e: 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.

ths
  • 2,839
  • 1
  • 13
  • 22
  • Thank you Sir for detailed explainantion. But there is a small error. Try this: after 2nd row gets added try changing the 1st row's 2nd drop down menu option. It is throwing this error -->> "Uncaught TypeError: Cannot set property 'disabled' of undefined" – Abhay Sep 09 '18 at 15:56
  • 2
    @Bharata We're good bro ! I was using my phone when I wrote the answer, of course no console to check. I think your answer is much more understandable for the OP as mine uses some advanced programming, although I tried to comment each line and to give docs links for each of the advanced method/operators I use. – ths Sep 10 '18 at 13:12
  • If you wrote it using your phone, then you are like a superman! ;-) It is very difficalt! Cool! Good work! But do not waste your time with phone answers because from PC it is much faster! ;-) – Bharata Sep 10 '18 at 13:19
  • 2
    @Bharata sadly I don't have a PC, sometimes I borrow one from my brothers for an amount of time. – ths Sep 10 '18 at 13:21
  • @ths I really appreciate your answer. But in case some other person follows the question the checked answer might help him get the most error-free answer so have to change checked answer. – Abhay Sep 10 '18 at 17:25
  • @user8810517 we're good bro. You should accept the answer that you see more accurate for you. Am here for any help. – ths Sep 10 '18 at 21:56
2

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>
Bharata
  • 11,779
  • 6
  • 27
  • 42