| 1 | = Ajax submiting and processing form errors with newforms and prototype = |
| 2 | |
| 3 | |
| 4 | == The task == |
| 5 | {{{ |
| 6 | #!html |
| 7 | <ul> |
| 8 | <li> Show a form;</li> |
| 9 | <li> Submit the form and show validation errors;</li> |
| 10 | <li> Handle both js enabled and js disabled (ajax and standard form submiting).</li> |
| 11 | </ul> |
| 12 | }}} |
| 13 | |
| 14 | I want to submit only the form and get validation errors from django newforms without submiting the whole page. Django Newforms library is really exciting and easy to use. Django is also great that doesn't restrict me to use any specific ajax/js library so I can use whatever I want in this case ''prototype''. |
| 15 | |
| 16 | So I'm using |
| 17 | {{{ |
| 18 | #!html |
| 19 | <ul> |
| 20 | <li> django.newforms </li> |
| 21 | <li> django.utils.simplejson which converts python data into JSON data so I can exchange it with the browser. </li> |
| 22 | <li> And I'm using the great <a class="ext-link" href="http://www.prototypejs.org"><span class="icon">prototype.js</span></a> library that makes javascript and ajax quite easy.</li> |
| 23 | </ul> |
| 24 | }}} |
| 25 | |
| 26 | |
| 27 | == The Form == |
| 28 | |
| 29 | The form is a simple contact form with all four fields required. Just to have something to validate. :) |
| 30 | |
| 31 | |
| 32 | {{{ |
| 33 | #!python |
| 34 | from django import newforms as forms |
| 35 | from django.newforms.widgets import * |
| 36 | |
| 37 | # A simple contact form with four fields. |
| 38 | class ContactForm(forms.Form): |
| 39 | name = forms.CharField() |
| 40 | email = forms.EmailField() |
| 41 | topic = forms.CharField() |
| 42 | message = forms.CharField(widget=Textarea()) |
| 43 | }}} |
| 44 | |
| 45 | |
| 46 | == The View == |
| 47 | |
| 48 | The view returns either JSON with the errors from the form or a whole page if javascript is disabled in the browser. |
| 49 | |
| 50 | I have a hidden input element in the form. I set it to None just before sending the form with javascript so the view knows what should it return, the whole page or just the errors with JSON. |
| 51 | |
| 52 | The rest is just a generic newforms code. I check if the form is valid and redirect the user if it is or return the errors if it's not. When the form is valid and using AJAX I use a special field {'OK': 'java script for the browser to execute'}. The client-side javascript "knows" that it should run the javascript when it encounters 'OK'. |
| 53 | |
| 54 | |
| 55 | {{{ |
| 56 | #!python |
| 57 | def contactview(request): |
| 58 | if request.method == 'POST': |
| 59 | theform = ContactForm(request.POST) |
| 60 | |
| 61 | # if "js" got here together with the form it means that the |
| 62 | # browser doesn't have javascript so handle the form without js |
| 63 | js = request.POST.get('js', None) |
| 64 | |
| 65 | if theform.is_valid(): |
| 66 | if not js: |
| 67 | # simplejson makes a json format so we can do an |
| 68 | # eval() and run the javascript the view returns |
| 69 | jsaction = 'window.location.pathname="/"' |
| 70 | data = simplejson.dumps({'OK':jsaction}) |
| 71 | else: |
| 72 | return HttpResponseRedirect('/') |
| 73 | else: |
| 74 | if not js: |
| 75 | data = simplejson.dumps(theform.errors) |
| 76 | else: |
| 77 | return render_to_response('contactform.html', {'form': theform}) |
| 78 | return HttpResponse(data, mimetype="text/javascript") |
| 79 | else: |
| 80 | # show a blank form |
| 81 | return render_to_response('contactform.html', {'form': ContactForm()}) |
| 82 | }}} |
| 83 | |
| 84 | |
| 85 | == The Javascript == |
| 86 | |
| 87 | |
| 88 | {{{ |
| 89 | #!text/html |
| 90 | <script type="text/javascript"> |
| 91 | // when the page get's loaded call the init function to hijack all the form |
| 92 | Event.observe(window, 'load', init, false); |
| 93 | |
| 94 | function init(){ |
| 95 | // hijack the form submit |
| 96 | Event.observe($('theform'), 'submit', processform, false); |
| 97 | } |
| 98 | |
| 99 | function processform(e) { |
| 100 | |
| 101 | // get the form that got submited ( the first parrent element of the element that trigger the event) |
| 102 | var aform = Event.findElement(e, 'form'); |
| 103 | // where to submit the form |
| 104 | var ajaxurl = aform.action; |
| 105 | |
| 106 | // get the submit button of the form and show an ajax-loader instead |
| 107 | var submitbutton = aform.getInputs('submit')[0]; |
| 108 | new Insertion.Before(submitbutton, '<img src="/site_media/ajax-loader.gif" id="ajaxloader" />'); |
| 109 | submitbutton.hide() |
| 110 | |
| 111 | // make the js field empty so that django knows how to handle the request |
| 112 | // if js is submited with the form django handles it as a normal form |
| 113 | // if js is None django returns AJAX variables |
| 114 | document.getElementById('js').value = '' |
| 115 | |
| 116 | var myAjax = new Ajax.Request( |
| 117 | ajaxurl, |
| 118 | { method: 'post', parameters: aform.serialize(true), onComplete: handlereq }); |
| 119 | |
| 120 | function handlereq(req) { |
| 121 | // evaulate the response (get the errors or run the javascript) |
| 122 | var errors = eval('(' + req.responseText + ')'); |
| 123 | |
| 124 | // clean the errors from the previous run (elements with class="errorlist" |
| 125 | var old = aform.getElementsByClassName('errorlist'); |
| 126 | for (i=0; i<old.length; i++) { old[i].remove(); } |
| 127 | |
| 128 | // if there is an OK attribute in the response, run the javascript |
| 129 | // if not show the errors in the for_fieldname element |
| 130 | if ( errors.OK ){ |
| 131 | eval(erros.OK) |
| 132 | } else { |
| 133 | $H(errors).each(function(pair) { |
| 134 | $('for_'+pair.key).innerHTML = '<ul class="errorlist"><li>'+pair.value+'</li></ul>'; |
| 135 | }); |
| 136 | |
| 137 | $('ajaxloader').remove() // remove the ajax-loader |
| 138 | submitbutton.show() // return the submit button |
| 139 | } |
| 140 | } |
| 141 | |
| 142 | // don't trigger the actual html submit form |
| 143 | Event.stop(e); |
| 144 | } |
| 145 | </script> |
| 146 | }}} |
| 147 | |
| 148 | |
| 149 | == The Template == |
| 150 | |
| 151 | The template for the form has a span element with id="for_fieldname" where the javascript puts the errors and {{ forms.fieldname.errors }} where django puts the errors if javascript is disabled. |
| 152 | |
| 153 | |
| 154 | {{{ |
| 155 | #!text/html |
| 156 | <html> |
| 157 | <head> |
| 158 | <title>Contact form</title> |
| 159 | <meta http-equiv="content-type" content="text/html; charset=utf-8"> |
| 160 | <script type="text/javascript" src="/site_media/prototype.js"></script> |
| 161 | </head> |
| 162 | <body> |
| 163 | <form method="post" action="." id="theform" name="theform"> |
| 164 | <fieldset> |
| 165 | <legend>Contact form</legend> |
| 166 | |
| 167 | <label for="id_name">Your name:</label> |
| 168 | {# in the for_fieldname element I put the errors with js #} |
| 169 | <span id="for_name"></span> |
| 170 | {# this are the errors when javascript is disabled #} |
| 171 | {{ form.errors.name }} |
| 172 | <br /> |
| 173 | {{ form.name }}<br /><br /> |
| 174 | <label for="id_email">Your email:</label> |
| 175 | <span id="for_email"></span> |
| 176 | {{ form.errors.email }} |
| 177 | <br /> |
| 178 | {{ form.email }}<br /><br /> |
| 179 | |
| 180 | <label for="id_topic">Topic:</label> |
| 181 | <span id="for_topic"></span> |
| 182 | {{ form.errors.topic }} |
| 183 | <br /> |
| 184 | {{ form.topic }}<br /><br /> |
| 185 | |
| 186 | <label for="id_message">Message:</label> |
| 187 | <span id="for_message"></span> |
| 188 | {{ form.errors.message }} |
| 189 | <br /> |
| 190 | {{ form.message }}<br /><br /> |
| 191 | |
| 192 | <br /> |
| 193 | <input type="hidden" name="js" id="js" value="disablewithjs"> |
| 194 | <input type="submit" id="submitbutton" value="Send"> |
| 195 | </fieldset> |
| 196 | </form> |
| 197 | </body> |
| 198 | </head> |
| 199 | |
| 200 | }}} |