Friday, January 19, 2018

Emulating a ManyToManyField with multiple radio buttons

Leave a Comment

A Django ManyToManyField renders something like this in HTML:

<form action="" method="post">   <select name="answers" multiple="multiple">     <option value="1" >Question 1, Answer 1</option>     <option value="2">Question 1, Answer 2</option>     <option value="3">Question 1, Answer 3</option>     <option value="4">Question 2, Answer 1</option>     <option value="5">Question 2, Answer 2</option>   </select>   <input type="submit"> </form> 

I can select one item manually from the question 1 group, and one item from the question 2 group. When this is sent in a POST request, I get a POST array containing answers = [1, 3] or similar.

I want to get the same behavior from groups of radio buttons, as this is a problem better suited to radio buttons. For example, if I do the following:

<form action="" method="post">     <fieldset>         <legend>Question 1</legend>         <input name="answers" id="id_1" value="1" type="radio">         <label for="id_1">Answer 1</label>         <br>          <input name="answers" id="id_2" value="2" type="radio">         <label for="id_2">Answer 2</label>         <br>          <input name="answers" id="id_3" value="3" type="radio">         <label for="id_3">Answer 3</label>         <br>     </fieldset>     <fieldset>         <legend>Question 2</legend>         <input name="answers" id="id_4" value="4" type="radio">         <label for="id_4">Answer 1</label>         <br>         <input name="answers" id="id_5" value="5" type="radio">         <label for="id_5">Answer 2</label>         <br>     </fieldset> </form> 

It doesn't actually let the user select more than one radio button at once. On the other hand, if I name the radio buttons answers[0] and answers[1], in POST, it sends two separate entities with those names instead of sending a combined answers.

I ask because on the back-end, I have a Django ManyToManyField and a ModelForm with a custom widget, and I'm trying to save the data into a ManyToManyField from this custom widget without resorting to too much trickery on the back end, but I keep getting the error "Enter a list of values."

Edit: JavaScript is acceptable so long as it doesn't send the original data as well, as is Django inheritance/custom parsing.

Edit 2: Here is my widget for the ManyToManyField as it stands now.

{% if questions %}     {% for question in questions %}         <fieldset>             <legend>{{ question.question }}</legend>             {% if question.options %}                 {% for option in question.options %}                     <input type="checkbox"  class="form-check-input" name="answers" id="id_{{ option.id }}" value="{{ option.id }}">                     <label for="id_{{ option.id }}">{{ option.text }}</label>                     <br>                 {% endfor %}             {% else %}                 <p>No options for this question.</p>             {% endif %}         </fieldset>     {% endfor %} {% else %}     <p>No questions in test.</p> {% endif %} 

2 Answers

Answers 1

It doesn't actually let the user select more than one radio button at once. On the other hand, if I name the radio buttons answers[0] and answers[1], in POST, it sends two separate entities with those names instead of sending a combined answers.

This doesn't fullfil your requirements, but atleast resolve one mystery. When you use answers[] as name, backend will recognize it as array and join all values into array. This is possible with any input type except single (radio/select).

And this is js solution you can use, but your situation shows that model you are using is not optimal for this case. Ofc course it can be solved hacky way like bellow, but you will always have to solve workarounds when you meet this code..

So my advice: change approach in backend

document.getElementById('answers').onsubmit = function(e) {    e.preventDefault(); //prevent submit;    let clone = this.cloneNode(true);    let inputs = clone.querySelectorAll('input[type=radio]');    let formData = new FormData(clone); // HTML5    // dumb check for validity    if (Array.from(formData.values()).length != clone.querySelectorAll('fieldset').length) {    	alert('Invalid form!')    	return false;    }      for (let i=0;i<inputs.length;i++) {      inputs[i].type = "checkbox";      inputs[i].name = "answers[]";    }    // only for snippet    formData = new FormData(clone);    console.log(Array.from(formData.entries()));    // commented in snippet    //clone.submit();   }    /* This is cleaner submit by JS without reload  document.getElementById('answers').onsubmit = function(e) {  	let formData = new FormData(this); // HTML5    let answers = Array.from(formData.values());    // dumb check for validity    if (answers.length != this.querySelectorAll('fieldset').length) {    	alert('Invalid form!')    	return false;    }    let newFormData = new FormData();    newFormData.set('answers', answers);    let request = new XMLHttpRequest();  	request.open(this.method || "POST", this.action || '/default/post/link');  	request.send(newFormData);    return false;  }  */
<form action="" method="post" id="answers">      <fieldset>          <legend>Question 1</legend>          <input name="question_1" id="id_1" value="1" type="radio">          <label for="id_1">Answer 1</label>          <br>            <input name="question_1" id="id_2" value="2" type="radio">          <label for="id_2">Answer 2</label>          <br>            <input name="question_1" id="id_3" value="3" type="radio">          <label for="id_3">Answer 3</label>          <br>      </fieldset>      <fieldset>          <legend>Question 2</legend>          <input name="question_2" id="id_4" value="4" type="radio">          <label for="id_4">Answer 1</label>          <br>          <input name="question_2" id="id_5" value="5" type="radio">          <label for="id_5">Answer 2</label>          <br>      </fieldset>      <button type="submit">Submit</button>  </form>

Answers 2

Changing the radio buttons to checkboxes should allow you to select multiple values.

<form action="" method="post">     <fieldset>         <legend>Question 1</legend>         <input name="answers" id="id_1" value="1" type="checkbox">         <label for="id_1">Answer 1</label>         <br>          <input name="answers" id="id_2" value="2" type="checkbox">         <label for="id_2">Answer 2</label>         <br>          <input name="answers" id="id_3" value="3" type="checkbox">         <label for="id_3">Answer 3</label>         <br>     </fieldset>     <fieldset>         <legend>Question 2</legend>         <input name="answers" id="id_4" value="4" type="checkbox">         <label for="id_4">Answer 1</label>         <br>         <input name="answers" id="id_5" value="5" type="checkbox">         <label for="id_5">Answer 2</label>         <br>     </fieldset> </form> 

The HTTP POST generated by this form should be exactly the same as the example you gave above with the select tag.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment