A new Django widget
Category : Python
I’m building websites with Bootstrap and Django. I’ve already created a form on one of them with a file input. The standard file input can’t really be styled with CSS apparently, so I’ve searched for a way to have a nice Bootstrap file input and found this. In short, it uses a button and a text input for display, and a transparent file input above them to use the browser access to local files. Some Javascript makes those two parts communicate.
First I’ve used this manually in a form directly in the template. Today I discovered that you can write your own Django widgets, so I tried to reorganize this file input.
Creating the widget
First I created a widgets
directory in my app, with the following files : __init__.py
(empty) and styledfileinput.py
.
styledfileinput.py
contains :
# StyledFileInput # coding: utf-8 from django.forms.widgets import FileInput from django.utils.html import format_html from django.forms.utils import flatatt class StyledFileInput(FileInput): class Media: css = { 'all': ('styledfileinput.css', ) } js = ('styledfileinput.js', ) def render(self, name, value, attrs=None): if value is None: value = '' final_attrs = self.build_attrs(attrs, type=self.input_type, name=name) if value != '': # Only add the 'value' attribute if a value is non-empty. final_attrs['value'] = force_text(self._format_value(value)) return format_html( '<div class="input-group">\n' + '<span class="input-group-btn">\n' + '<span class="btn btn-default btn-file">\n' + 'Browse... \n' + '<input multiple="" {} >\n' + '</span>\n' + '</span>\n' + '<input class="form-control" readonly="" type="text">\n' + '</div>\n', flatatt(final_attrs) )
I’ve created a StyledFileInput
class inheriting from FileInput
, and redefined the render
function with the whole HTML block. I’ve kept the attributes and value handling, so that Django can use it properly.
There is an inner Media
class to define the required static files. This two files are placed in the static
directory of my app.
Here is the content of styledfileinput.css
:
/* file buttons should be styled */ .btn-file { position: relative; overflow: hidden; } .btn-file input[type=file] { position: absolute; top: 0; right: 0; min-width: 100%; min-height: 100%; font-size: 999px; text-align: right; filter: alpha(opacity=0); opacity: 0; background: red; cursor: inherit; display: block; }
Then styledfileinput.js
:
(function($){ $(document) .on('change', '.btn-file :file', function() { var input = $(this), numFiles = input.get(0).files ? input.get(0).files.length : 1, label = input.val().replace(/\\/g, '/').replace(/.*\//, ''); input.trigger('fileselect', [numFiles, label]); }); $(document).ready( function() { $('.btn-file :file').on('fileselect', function(event, numFiles, label) { var input = $(this).parents('.input-group').find(':text'), log = numFiles > 1 ? numFiles + ' files selected' : label; if( input.length ) { input.val(log); } else { if( log ) alert(log); } }); }); })(django.jQuery);
You can see that this code is wrapped in a function called with django.jQuery
for $
. This is necessary as I also use the AdminDateWidget, which uses the following JavaScript :
/* Puts the included jQuery into our own namespace using noConflict and passing * it 'true'. This ensures that the included jQuery doesn't pollute the global * namespace (i.e. this preserves pre-existing values for both window.$ and * window.jQuery). */ var django = django || {}; django.jQuery = jQuery.noConflict(true);
Using the widget
Now I can use it in a Django form :
from app.widgets.styledfileinput import StyledFileInput class DocumentForm(forms.ModelForm): class Meta: model = Document fields = "__all__" widgets = { 'date': admin.widgets.AdminDateWidget(), 'filename' : StyledFileInput(), }
In the template, nothing changes in the form itself :
<form method="post" enctype="multipart/form-data"> {% csrf_token %} <div class="panel panel-default"> <div class="panel-body"> {{ doc_form|bootstrap_horizontal }} <button class="btn btn-default" type="submit">Envoyer</button> </div> </div> </form>
You just have to be sure to put somewhere in your template {{ doc_form.media }}
so that the required static files are included.
Conclusion
Now that the Bootstrap file input is redefined as a widget, it’s easy to attach to any classic Django forms, and it’s easy to reuse in any project.
If you have any suggestion to improve it, feel free to comment.