1

I'm trying to change the value of another input field when a checkbox is ticked.

I added an eventlistener to the checkbox field, and tried to get the value of a corresponding input field when the checkbox changes.

At the moment, I'm getting an error with the input field not being defined. This is odd, because I can call the value of the input field when it's outside the event listener.

Can anyone please help with this? Thank you!

<form action="/question" method="post" id="myForm">
        <label for="sessionName">Session Name</label><input type="text" name="sessionName">
        <div class="question">
            <label for="question">Question: </label><input type="textarea" name="question">
            <ul>
                <li><label for="answer">Answer 1: </label><input type="textarea" name="answer"><input type="hidden" name="checked" value="false"><input type="checkbox" name="tick"></li>
                <li><label for="answer">Answer 2: </label><input type="textarea" name="answer"><input type="hidden" name="checked" value="false"><input type="checkbox" name="tick"></li>
                <li><label for="answer">Answer 3: </label><input type="textarea" name="answer"><input type="hidden" name="checked" value="false"><input type="checkbox" name="tick"></li>
                <li><label for="answer">Answer 4: </label><input type="textarea" name="answer"><input type="hidden" name="checked" value="false"><input type="checkbox" name="tick"></li>
            </ul>
        </div>
        <input type="submit" name="" value="Create Session">
    </form>
    <script>
        let checked = document.getElementsByName("checked");
        let tick = document.getElementsByName("tick");
        let form = document.getElementsByTagName("form")[0];
        for (i = 0; i < checked.length; i++) {
            tick[i].addEventListener("change", () => {
                console.log(document.getElementsByName("checked")[i].value)
            })
        }
    </script>
Scott Marcus
  • 64,069
  • 6
  • 49
  • 71
Chunky Low
  • 69
  • 7
  • when you click the checkbox, the value of `i` will be `checked.length` ... try `for (let i = 0;....` etc instead for an `i` that is scoped properly and can be used in the event handler - note: I didn't post this as an answer as there's duplicates of this on stack overflow :p – Bravo Nov 10 '19 at 02:25

2 Answers2

3

The problem is that you are creating a closure around your looping variable, i.

What this means is that you use i in the event handler function that you are assigning out to each of the checkboxes and by the time a user comes along and clicks on one of them, the loop is done and its final value is checked.length or 4 in this case. Since there is no element within the checkbox node list with an index of 4 (the highest index is 3, so you are referring to an element that doesn't exist, hence your error message).

There are several solutions here. The simplest is to change the scope of your looping variable so that instead of all the event callbacks sharing the same i value, which is 4 by the time any of those callbacks run, you give your i variable its own scope for each loop iteration, meaning that each event handler will get its own value instead of sharing it. This can be done simply by declaring the loop variable with let:

<form action="/question" method="post" id="myForm">
        <label for="sessionName">Session Name</label><input type="text" name="sessionName">
        <div class="question">
            <label for="question">Question: </label><input type="textarea" name="question">
            <ul>
                <li><label for="answer">Answer 1: </label><input type="textarea" name="answer"><input type="hidden" name="checked" value="false"><input type="checkbox" name="tick"></li>
                <li><label for="answer">Answer 2: </label><input type="textarea" name="answer"><input type="hidden" name="checked" value="false"><input type="checkbox" name="tick"></li>
                <li><label for="answer">Answer 3: </label><input type="textarea" name="answer"><input type="hidden" name="checked" value="false"><input type="checkbox" name="tick"></li>
                <li><label for="answer">Answer 4: </label><input type="textarea" name="answer"><input type="hidden" name="checked" value="false"><input type="checkbox" name="tick"></li>
            </ul>
        </div>
        <input type="submit" name="" value="Create Session">
    </form>
    <script>
        let checked = document.getElementsByName("checked");
        let tick = document.getElementsByName("tick");

        for (let i = 0; i < checked.length; i++) {
            tick[i].addEventListener("change", () => {
                console.log(document.getElementsByName("checked")[i].value)
            })
        }
    </script>

Now another way to solve this issue is to do away with the loop counter entirely and use much more modern code to solve the problem. All modern browsers support the Array.forEach() method on the node lists that are returned from DOM queries and forEach() does the iterations on its own without you needing to declare and manage a counting variable. If you don't have the variable, then you don't set up the closure, which is the cause of the problem in the first place.

<form action="/question" method="post" id="myForm">
        <label for="sessionName">Session Name</label><input type="text" name="sessionName">
        <div class="question">
            <label for="question">Question: </label><input type="textarea" name="question">
            <ul>
                <li><label for="answer">Answer 1: </label><input type="textarea" name="answer"><input type="hidden" name="checked" value="false"><input type="checkbox" name="tick"></li>
                <li><label for="answer">Answer 2: </label><input type="textarea" name="answer"><input type="hidden" name="checked" value="false"><input type="checkbox" name="tick"></li>
                <li><label for="answer">Answer 3: </label><input type="textarea" name="answer"><input type="hidden" name="checked" value="false"><input type="checkbox" name="tick"></li>
                <li><label for="answer">Answer 4: </label><input type="textarea" name="answer"><input type="hidden" name="checked" value="false"><input type="checkbox" name="tick"></li>
            </ul>
        </div>
        <input type="submit" name="" value="Create Session">
    </form>
    <script>
        let checked = document.getElementsByName("checked");
        let tick = document.getElementsByName("tick");

        tick.forEach(function(chk) {
            chk.addEventListener("change", () => {
                console.log(chk.previousSibling.value)
            });
        });
    </script>

Now finally, you've got some very old code that you are using that you really shouldn't be. getElementsByName() returns a "live" node list that can cause serious performance issues. What you should be using is .querySelectorAll(), which allows for a CSS selector to be passed in.

Additionally, you've written input type="textarea", but there is no input with a type of textarea, so you are getting the default input type, which is text. A textarea is written like this: <textarea></textarea>.

Also, I'm not sure what value the hidden form fields play in your scenario. What is most common is for checkboxes to have a value, which you don't have currently.

See comments inline below:

<form action="/question" method="post" id="myForm">
        <label for="sessionName">Session Name</label><input type="text" name="sessionName">
        <div class="question">
            <label for="question">Question: </label><input type="textarea" name="question">
            <ul>
                <li><label for="answer">Answer 1: </label><input type="text" name="answer1"><input type="checkbox" name="tick"></li>
                <li><label for="answer">Answer 2: </label><input type="text" name="answer2"><input type="checkbox" name="tick"></li>
                <li><label for="answer">Answer 3: </label><input type="text" name="answer3"><input type="checkbox" name="tick"></li>
                <li><label for="answer">Answer 4: </label><input type="text" name="answer4"><input type="checkbox" name="tick"></li>
            </ul>
        </div>
        <input type="submit" value="Create Session">
    </form>
    <script>
        // Get all the checkboxes into a collection
        let checkboxes = document.querySelectorAll("input[type='checkbox']");      

        // Loop over the checkboxes the modern way
        checkboxes.forEach(function(cb, i) {
            // Use the click event since change only fires when the field gets a new value
            cb.addEventListener("click", () => {
                // Set the value of the checkbox to the value of the textbox that comes
                // right before it.
                cb.value = cb.previousSibling.value;
                
                console.log(cb.value);  // Log the checkbox value
            });
        });
    </script>
Scott Marcus
  • 64,069
  • 6
  • 49
  • 71
0

Objective

By the structure of the HTML and OP's explanation of the problem it looks like the form's purpose is to facilitate the creation of a multiple choice quiz. It appears that the user needs to:

  1. enter a title for the quiz..............................................<input type="text">
  2. enter a question........................................................ <input type="textarea">
  3. enter 2 to 4 answers...................................................<input type="textarea">
  4. check the correct answer*...........................................<input type="checkbox">
  5. the correct answer is then stored in another element <input type="hidden">
  6. submit the data to /question.....................................<input type="submit">

*Assuming that only one answer is correct per QA (Question/Answer)


If the above is correct then there are a few things to consider:

Changes

  1. When a form is submitted (see step #6), form data is collected as <key>/<value> pairs from form controls that have a [name] (ie <key>) and a [value] (ie <value> of course) and then sent to a server.

  2. Assuming that there is only one correct answer per QA, the checkboxes (see step #4) are not ideal because more than one can be checked at a time. In the demo below each <input type='checkbox'> is now <input type='radio>. In order to enable a group of radio buttons to have mutually exclusive checked status (ie only one radio at a time can be checked), each radio must share a [name]. The [value] is also mutually exclusive as well which means only the [value] of the checked radio is considered.

    Example

    <input id="a" name='x' type='radio' value='A'>
    <input id="b" name='x' type='radio' value='B'>
    <input id="c" name='x' type='radio' value='C' checked>
    <input id="d" name='x' type='radio' value='D'>
    

    Data collected from the example above would be: x: C. Each value corresponds to its position in the <ol class='answers'> list.

  3. Data cannot be totally hidden on a webpage only on a server can data be secured. Moreover, using 4 <input type='hidden'> (see step #5) elements for one piece of data is completely unnecessary because the <input id="c" name='x' type='radio' value='C' checked> element already has the data. So all <input type='hidden'> are removed from the demo.

  4. type="textarea" does not exist. All <input type='textarea'> (see step #2 and #3) are replaced by:

    Example

    <textarea id='A' name='A' rows='1' rows='1' cols='60'></textarea>    
    
  5. One minor discrepancy is the <label for="ID"> elements are not completely effective. The [for] attribute only works for form controls with #ID not [name]. All applicable form controls have now have #ID (see Demo 1 and 2)

  6. Although it has probably not been considered yet, any additional QA would have duplicated [name]s when submitted. In Demo 2 <input id='order' type='number'> was added. The user enters any number 1 to 100 and all descendant elements of <fieldset name="QA"> that have a [name] get a number (ie order.value) suffixed to its [name]. #order is bound to the input event and QANumber() is the callback function that adds the number to each [name] (see Demo 2 section to see it in action).

All of the changes are corrected to standards and oriented in a way that doesn't need to monitor constant change to individual checkboxes. Only the input event has been utilized on one element so the submitted data is organized by a numbered [name]. The submit event is covered by the <form> and <input type='submit'>


DOM APIs

Be aware that <form> and form controls have a few APIs dedicated to them. The syntax is terse, the additional features can be leveraged to a significant advantage when setup correctly and they are very stable. The following is a list of references:

Event Binding

Registering identical event listeners to multiple elements individually is inefficient. Event delegation is a pattern in which only an ancestor element of the elements you want to bind events to is registered instead (an ancestor element is an element that contains the elements ex. <form> contains <textarea>, <input>, etc). The following is an example of event delegation as it would apply to a <form> and form controls.


Demo 1

Event Delegation, DOM APIs, Form Control Sync

const quiz = document.forms.quiz;

quiz.oninput = test;

function test(e) {
  const fc = quiz.elements;
  const rads = [...fc.radio.elements];
  // console.log(rads);
  const view = fc.display;
  // e.target is the element that interacts with the user directly
  const input = e.target;
  if (input !== this) {
    for (let rad of rads) {
      if (rad === input) {
        view.value = input.value;
      }
    }
  }
}
form,
fieldset {
  text-align: center;
  margin: -10px auto
}

input,
label,
output {
  display: inline-block;
  margin: 0px 10px;
  cursor: pointer
}

output {
  margin: 10px;
  font-size: 2rem
}
<form id='quiz'>
  <fieldset name='radio'>
    <legend>Mutually Exclusive Checked State</legend>
    <input id='r1' name='rad' type='radio' value='A'>
    <input id='r2' name='rad' type='radio' value='B'>
    <input id='r3' name='rad' type='radio' value='C'>
    <input id='r4' name='rad' type='radio' value='D'>
  </fieldset>
  <output name='display'></output>
  <fieldset name='label'>
    <legend>[for]/#ID Attribute Association</legend>
    <label for='r1'>A</label>
    <label for='r2'>B</label>
    <label for='r3'>C</label>
    <label for='r4'>D</label>
  </fieldset>
</form>

Demo 2

HTML Form Standards, Tag Attributes, Form Submittal

When submitted, the data will be sent to a live test server. At the bottom of the page is an <iframe> which will generate a link to that data dump for review. Do not click the link, copy it and paste it to browser address bar.

<!DOCTYPE html>
<html>

<head>
  <meta charset='utf-8'>
  <style>
    #quiz,
    iframe {
      width: 100vw
    }
    
    .answers {
      list-style: lower-alpha;
      margin-left: -20px
    }
    
    [name=Q] {
      margin-left: 20px
    }
    
    [type=submit] {
      float: right;
    }
  </style>
</head>
  <body>
    <form id="quiz" action="http://ptsv2.com/t/quiz/post" method="post" target="response">
      <label for="quizTitle">Quiz Title: </label>*<br>
      <input id="quizTitle" name="quizTitle" type="text" size="66" required><br>
      <fieldset name="QA">
        <label for="Q">Question </label><input id="order" type="number" min="1" max="100" maxlength="3" size="3" value="1" required>*<br>
        <textarea id='Q' name="Q" rows="2" cols="60" required></textarea>
        <ol class="answers">
          <li><label for="a">Answer: </label><input id="a" name="x" type="radio" value="A" required>*<br>
            <textarea id="A" name="A" rows="1" cols="60" required></textarea></li>
          <li><label for="b">Answer: </label><input id="b" name="x" type="radio" value="B">*<br>
            <textarea id="B" name="B" rows="1" cols="60" required></textarea></li>
          <li><label for="c">Answer: </label><input id="c" name="x" type="radio" value="C"><br>
            <textarea id="C" name="C" rows="1" cols="60"></textarea></li>
          <li><label for="d">Answer: </label><input id="d" name="x" type="radio" value="D"><br>
            <textarea id="D" name="D" rows="1" cols="60"></textarea></li>
        </ol>
        *<small>Required</small>
        <input type="submit" value="Create QA">
      </fieldset>
      <p>For server response copy the link below and paste it into the browser address bar.</p>
    </form>
    <iframe name="response"></iframe>

    <script>
      const quiz = document.forms[0];
      const order = quiz.elements.order;
      order.oninput = QANumber;

      function QANumber(e) {
        const fields = this.parentElement;
        const QA = fields.querySelectorAll('textarea, input');
        let order = this.value;
        [...QA].forEach(field => {
          if (field.hasAttribute('name')) {
            const name = field.name.split('');
            field.setAttribute('name', `${name[0]}${order}`);
          }
        });
      }
    </script>
  </body>

</html>
zer00ne
  • 41,936
  • 6
  • 41
  • 68