Search code examples
javascriptpythonflaskflask-wtforms

WTForms & getElementById() undefined


Using WTForms and JS I'm trying to create a form that allows the user to add fields to it dynamically on the front end. I followed this post to get a basic idea of what to do.

https://sagarkaurav.hashnode.dev/flask-wtf-forms-dynamic-fields-using-javascript

I'm now struggling to fetch the input field by id and creating unique ids for the newly generated input fields.

When i execute the script on the front end document.getElementById('items-item') returns undefined while identifyElement.getElementsByTagName('input') returns the correct amount of fields (1). Execution of the code is then stopped. The unedfined points me in the direction that html document isn't created yet upon code execution. I wrote a similar JS script with window.onload to tackle this, but it deliveres the exact same result and currently I don't understand why and what I'm missing here.

app.py

class purchaseOrderFormSubClass(FlaskForm):
    item = FieldList(StringField(''), min_entries=1, max_entries=99)
    complete = FieldList(BooleanField(''), min_entries=1, max_entries=99)
    ovp = FieldList(BooleanField(''), min_entries=1, max_entries=99)
    instructions = FieldList(BooleanField(''), min_entries=1, max_entries=99)

class purchaseOrderForm(FlaskForm):
    purchaseDate = DateField('Purchase Date', validators=[DataRequired()])
    items = FormField(purchaseOrderFormSubClass)

@app.route('/create_purchase_order')
def create_purchase_order():
    PurchaseOrderForm  = purchaseOrderForm()
    if PurchaseOrderForm.validate_on_submit():
        return render_template('create_purchase_order.html', form=PurchaseOrderForm)
    return render_template('create_purchase_order.html', form=PurchaseOrderForm)

Template for create_purchase_order.html

    <div class="container">
    <form method="POST" action="">
      {{ form.csrf_token }}
      {{form.purchaseDate.label}}
      {{ form.purchaseDate(class_="form-control") }} <br>
        <table class="table table-striped table-sm" id="table">
          <thead>
            <tr>
              <th scope="col">TH1</th>
              <th scope="col">TH2</th>
              <th scope="col">TH3</th>
              <th scope="col">TH4</th>
            </tr>
          </thead>
          <tbody id="tableBody">
          <tr>
            {% for field in form.items%}
            <td>
              {{ field.hidden_tag }}
              {{ field(class_='form-control') }}
            </td>
            {% endfor %}
          </tr>
          </tbody>
        </table>
        <button id="add-row" type="button" class="btn btn-primary">New Row onload</button>
        <button type="button" class="btn btn-primary" onclick="addRow()">New Row onclick</button>
    </form>
  </div>

EDIT

rendered HTML

 <div class="container">
    <form method="POST" action="">
      <input id="csrf_token" name="csrf_token" type="hidden" value="ImU2NzVjZTY5ZWJlMDRlMzQzZmEwZWZlMjg4MTk5Y2RmMWNmOTFiMDIi.ZfDXXQ.NJc4fzGNyipMe5kyPuFhKk3zoss">
      <label for="purchaseDate">Purchase Date</label>
      <input class="form-control" id="purchaseDate" name="purchaseDate" required type="date" value=""> <br>
        <table class="table table-striped table-sm" id="table">
          <thead>
            <tr>
              <th scope="col">TH1</th>
              <th scope="col">TH2</th>
              <th scope="col">TH3</th>
              <th scope="col">TH4</th>
            </tr>
          </thead>
          <tbody id="tableBody">
          <tr scope="row" class="align-middle">
            
            <td>
              <ul class="form-control" id="items-item"><li><label for="items-item-0"></label> <input id="items-item-0" name="items-item-0" type="text" value=""></li></ul>
            </td>
            
            <td>
              <ul class="form-control" id="items-complete"><li><label for="items-complete-0"></label> <input id="items-complete-0" name="items-complete-0" type="checkbox" value="y"></li></ul>
            </td>
            
            <td>
              <ul class="form-control" id="items-ovp"><li><label for="items-ovp-0"></label> <input id="items-ovp-0" name="items-ovp-0" type="checkbox" value="y"></li></ul>
            </td>
            
            <td>
              <ul class="form-control" id="items-instructions"><li><label for="items-instructions-0"></label> <input id="items-instructions-0" name="items-instructions-0" type="checkbox" value="y"></li></ul>
            </td>
            
            <td>
              <input class="form-control" id="items-csrf_token" name="items-csrf_token" type="hidden" value="ImU2NzVjZTY5ZWJlMDRlMzQzZmEwZWZlMjg4MTk5Y2RmMWNmOTFiMDIi.ZfDXXQ.NJc4fzGNyipMe5kyPuFhKk3zoss">
            </td>
            
          </tr>
          </tbody>
        </table>

        <button id="add-row" type="button" class="btn btn-primary">New Row onload</button>
        <button type="button" class="btn btn-primary" onclick="addRow()">New Row onclick</button>
    </form>
  </div>

JS

      function addRow() {
                  alert('clicked');
                  let tableBody = document.getElementById('tableBody');
                  let identifyElement = document.getElementById('items-item');
                  let countAllElements = identifyElement.getElementsByTagName('input');
                  let newRow = document.createElement('tr');
                  let identifyElementAlert = identifyElement.value;
                  let countAllElementsAlert = countAllElements.length;
                  alert(identifyElementAlert);
                  alert(countAllElementsAlert);
                  let newElementID = []
                            for(let i = 0; i = countAllElements.length; i++) {
                              newElementID.push(parseInt(countAllElements[i].name.split('-')[1]));
                            }
                            let newFieldName = `items-item${Math.max(...newElementID) + 1}`;
                  alert(newElementID);
                  alert(newFieldName);
                  newRow.innerHTML= `
                  <td>
                    <label for="${newFieldName}"></label> <input id="${newFieldName}" name="${newFieldName}" type="text" value="" class="form-control">
                  </td>
                  <td>
                    <label for="items-complete-0"></label> <input id="items-complete-0" name="items-complete-0" type="checkbox" value="y">
                  </td>
                  <td>
                    <label for="items-ovp-0"></label> <input id="items-ovp-0" name="items-ovp-0" type="checkbox" value="y">
                  </td>
                  <td>
                    <label for="items-instructions-0"></label> <input id="items-instructions-0" name="items-instructions-0" type="checkbox" value="y">
                  </td>
                  `;
                  tableBody.appendChild(newRow);
      }


Solution

  • There is a typo in your for loop. Instead of comparing, you assign a new value to i. Because of this, the current element can no longer be found and undefined is returned.

    Here is an alternative version to your code.

    function addRow() {
        const elems = document.querySelectorAll('input[id^="items-item-"]');
        if (elems.length) {
            const elem = elems[elems.length-1], 
                    tr0 = elem.closest('tr'), 
                    tr1 = tr0.cloneNode(true)
            Array.from(tr1.querySelectorAll('label[for^="items-"]')).forEach(label => {
                const attr = label.getAttribute('for'), 
                        input = tr1.querySelector(`input[name="${attr}"]`), 
                        s = attr.replace(/items-(\w+)-(\d+)/, (match, p1, p2) => `items-${p1}-${parseInt(p2)+1}`); 
                label.setAttribute('for', s);
                input.id = input.name = s;
            })
            tr0.parentNode.insertBefore(tr1, tr0.nextSibling);
        }
    }