Progressive enhancement with Django forms

Batteries included

Django, for better or worse, takes the approach of some web-frameworks such as Rails, and bundles as many utilities as necessary together with the core url-resolution framework, such as ORM, templating, administration, forms, and so on. This is in contrast to the philosophy of lighter-weight frameworks like Flask and platforms like node.js, which tend to take a more "mix-and-match" approach, encouraging the developer to pick out options for ORM, templating, forms, etc separately. A great deal can be said in favor of both approaches. While I prefer Django's approach in general, one downside is it occasionally results in programmers fighting against the provided components instead of using them carefully and creatively.

Django Forms is sometimes a source of trouble. The default forms are ghastly, useless for modern UX concerns. Well-styled, flexible forms, with real-time validation and interactive, complicated widgets, are essential parts of a modern web app's spec.

You might be tempted to completely disregard the forms feature, and just hardcode widgets in the templates, and pull in data from POST and validate data directly. While this is certainly a valid last resort or quick fix, it's best to take advantage of the Django philosophy and do things in a well-structured and idiomatic way.

Keep it simple

Graceful degradation to plain HTML forms for JavaScript-impaired browsers may seem like a bit of a blast from the past: there aren't too many web users left without JavaScript in today's web. However, this philosophy of implementation is particularly suited toward building easily readable and portable web applications in Django.

So, how do we do fancy custom widgets in Django forms, while still being able to do {{ form.as_p }}, or the equivalent? Depending on the level of customization needed, we can use the following process:

  1. Is a plain HTML form enough? Recall that using Django widget's attributes, we can use the new-ish CSS3/HTML5 validation and enhancements and do a lot that previously was only accessible from custom JS.
  2. Not enough? Do you have some pre-baked JavaScript, such as from bootstrap, to toss in there? Using widget attributes, we can invoke JS enhancements on a plain Django field.
  3. Need some custom JS enhancements? One thing to do, then, is attach a data-do-something sort of attribute to the widget, and then somewhere in your included JavaScript add the enhancement (e.g. $('[data-do-something]').each(function () { ... })), or perhaps just make the widget a hidden input, and put your JS and HTML in a template snippet.
  4. At this point, you'll probably need some custom validation or data processing. That's easy enough too: go ahead and implement some clean_* fields on your form, or even create a custom FormField which correctly cleans and parses your data type.
  5. Finally, if you truly hit a wall with the generated HTML, which you likely will with at least one form, it's time to write a custom widget! Writing custom widgets can include arbitrary Python-generated HTML, completely obsoleting hardcoded HTML forms. If you don't like the idea of including embedded HTML in Python, then include the render_to_string shortcut

Of course, if you go through this process, and still feel you are struggling against Django instead of taking advantage of the power and flexibility of Django forms, then you might really be hitting a limitation with the framework, and throwing in some custom HTML and validation code won't hurt in a pinch. However, I've found this a very rare circumstance. Using the above options, you can predictably produce re-usable components that are easily readable by other Django engineers.