Search code examples
javascriptjqueryhtmldelete-rowtypeerror

JavaScript TypeError: Cannot read property 'id' of undefined


Preface:

I have an HTML table where user fills in Name and URL of social networks where they have account. Each row has a checkbox. With the help of jQuery and JavaScript, on pressing Add Row button, a row is dynamically added to the table and on pressing delete row button those rows are deleted for which checkbox is checked.

Objective:

When selected rows are deleted, I want to assign id to input elements and checkboxes of every remaining row in serial manner, so that the rows are always serially sound.

Problem:

I am not able to find what makes it throw error, as childNodes[] do not seem to lead to unavailable element. Please help!

Snippet:

    // variable to keep track of rows(records)
    var social_recs_num = 1;
    $(document).ready(function () {
    
      $("#social_add_rec").click(function () {
    
        social_recs_num++;
        var markup = "<tr> <th scope=\"row\"> <div class=\"form-group\"> <div class=\"form-check\"> <input type=\"checkbox\" class=\"form-check-input\" id=\"social_rec" + social_recs_num + "\" name=\"social_rec\"> <label class=\"form-check-label\" for=\"social_rec" + social_recs_num + "\"> " + social_recs_num + "</label> </div></div></th> <td> <div class=\"form-group shadow-sm\"> <label for=\"name_social" + social_recs_num + "\">Select or Type</label> <input type=\"text\" list=\"name_social" + social_recs_num + "\" class=\"form-control\" placeholder=\"Name\"> <datalist id=\"name_social" + social_recs_num + "\"> <option value=\"Wikipedia\">Wikipedia</option> <option value=\"Youtube\">Youtube</option> <option value=\"Facebook\">Facebook</option> <option value=\"Twitter\">Twitter</option> <option value=\"Pinterest\">Pinterest</option> </datalist> </div></td><td> <div class=\"form-group shadow-sm\"> <textarea class=\"form-control\" id=\"url_social" + social_recs_num + "\" cols=\"30\" rows=\"1\" placeholder=\"URL\"></textarea> </div></td></tr>"; //in case of id we need to do it like: "min_sysreq" +social_recs_num...as social_recs_num has been incremented priorly it will be the row number of row to be added
    
        $("table tbody").append(markup);
    
      });
    
      // Find and remove selected table rows...only condition is that there must be present attribute name='sysreq_rec' in input element for checkbox of every row
    
      $("#social_delete_rec").click(function () {
    
        $("table tbody").find('input[name="social_rec"]').each(function () {
    
          if ($(this).is(":checked")) {
            social_recs_num = social_recs_num - $(this).length;
            $(this).parents("tr").remove();
    
          }
    
        });
        // to be run when a row is deleted...this assigns ids to input elements in serial manner            
        var table = document.getElementById("mytab1");
        var rowCount = table.rows.length;
        
        // i has been initiated with 1, as header row is the 0th row
        for (var i = 1; i < rowCount; i++) {
          var row = table.rows[i];
    
          // on this line, getting error: `Uncaught TypeError: Cannot read property 'id' of undefined`
          row.childNodes[0].childNodes[0].childNodes[0].childNodes[0].id = "social_rec" + i;
          row.childNodes[0].childNodes[0].childNodes[0].childNodes[1].for = "social_rec" + i;
          row.childNodes[0].childNodes[0].childNodes[0].childNodes[1].innerHTML = " " + i;
    
          row.childNodes[1].childNodes[0].childNodes[0].for = "name_social" + i;
          row.childNodes[1].childNodes[0].childNodes[1].list = "name_social" + i;
          row.childNodes[1].childNodes[0].childNodes[2].id = "name_social" + i;
    
          row.childNodes[2].childNodes[0].childNodes[0].id = "url_social" + i;
    
          //iterate through rows
          //rows would be accessed using the "row" variable assigned in the for loop
    
        }
    
      });
    
    });
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<table class="table table-bordered" id="mytab1">
      <thead>
        <tr>
          <th scope="col">
            #
          </th>
          <th scope="col">
            Name
          </th>
          <th scope="col">
            URL
          </th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <th scope="row">
            <div class="form-group">
              <div class="form-check">
                <input type="checkbox" class="form-check-input" id="social_rec1" name="social_rec">
                <label class="form-check-label" for="social_rec1"> 1</label>
              </div>
            </div>
          </th>
          <td>
            <div class="form-group shadow-sm">
              <label for="name_social1">Select or Type</label>
              <input type="text" list="name_social1" class="form-control" placeholder="Name">
              <datalist id="name_social1">
                <option value="Wikipedia">Wikipedia</option>
                <option value="Youtube">Youtube</option>
                <option value="Facebook">Facebook</option>
                <option value="Twitter">Twitter</option>
                <option value="Pinterest">Pinterest</option>
              </datalist>
            </div>
          </td>
          <td>
            <div class="form-group shadow-sm">
              <textarea class="form-control" id="url_social1" cols="30" rows="1" placeholder="URL"></textarea>
            </div>
          </td>
        </tr>
      </tbody>
    </table>
    <button class="btn btn-primary" type="button" id="social_add_rec">Add Row</button>
    <button class="btn btn-primary" type="button" id="social_delete_rec">Delete Row</button>

P.S. :

  1. I have used jQuery and Bootstrap.
  2. this question seemed close, but I think I don't have the same mistake.

Edit:

I need to display(or store) inputs by user, for which I wrote following JavaScript code.

var social = function () {

    var val = "social:\n";
    for (x = 1; x <= social_recs_num; x++) {
      //where social_recs_num is number of rows
      val = val + "\s\s-\n";
      val = val + "\s\s\s\s" + "name: " + getElementById("name_social" + x) + "\n";
      val = val + "\s\s\s\s" + "url: " + getElementById("url_social" + x) + "\n";
    }

    return val;

}

function social returns the value input by user in YAML format. For this code, when selected rows are deleted, I want to assign id to input elements and checkboxes of every remaining row in serial manner.


Solution

  • The Problem:

    The issues you are seeing is because you are trying to get the 'childNodes' property of an html element. In order to access that property, you would have to cast the element as a jQuery object by adding '$( )' around it. In this case, that would be '$(row)'.

    However, if you do that with your code, you still get errors because of the mess of nested .childNodes[0]. You could get it working using that existing code, however...

    Suggested Refactoring:

    ...As other stated, there are more elegant ways to approach this problem. There are a number of ways you could tackle it, but given your current html I would refactor to something like this:

    //...
    
    
    // to be run when a row is deleted...this assigns ids to input elements in serial manner                
    $('#mytab1 tbody tr').each(function() {
        var rowNum = $(this)[0].rowIndex;
    
        var formcheck = $(this).find('.form-check');  
        formcheck.find('.form-check-input')[0].id = 'social_rec' + rowNum;
        formcheck.find('.form-check-label')[0].for = 'social_rec' + rowNum;
        formcheck.find('.form-check-label')[0].innerHTML = ' ' + rowNum;
    
        var formName = $(this).find('td')[0];
        $(formName).find('label')[0].for = 'social_rec' + rowNum;
        $(formName).find('input')[0].list = 'social_rec' + rowNum;
        $(formName).find('datalist')[0].id = 'social_rec' + rowNum;
    
        var formUrl = $(this).find('td')[1];
        $(formUrl).find('textarea')[0].id = 'social_rec' + rowNum;          
    });
    
    
    //...
    

    This replaces your for loop with a jquery .each loop and then uses the .find() function to loop up each control you want to manipulate. You'll also note that it is using the rowIndex() function to determine what number to assign the row.

    I think the advantage to using an approach like this is that it's much easier to read the code and determine what controls are being affected. You could also additionally refactor your html to use classes that are unique to the controls you want to target, then you could just use the class selector to change those controls.

    Hope that helps!