First you need some template tags
We're gonna need a few template tags, so create my_app/templatetags/forms.py looking like this:
from django import forms
from django.utils.safestring import mark_safe
from django import template
from django.utils.translation import gettext as _
register = template.Library()
Setting css classes to django form fields
Setting css classes to django form fields (or even ModelForm fields) makes people cry, so let's make a template tag that will make it easier for us:
@register.filter
def add_field_class(field, css_class):
"""adds css class to a field"""
if len(field.errors) > 0 and 'is-danger' not in css_class:
# automatically add .is-danger to fields with invalid values
css_class += ' is-danger'
if isinstance(field.field.widget, forms.SplitDateTimeWidget):
# I personally prefer using SplitDateTimeWidget for datetime
for subfield in field.field.widget.widgets:
if subfield.attrs.get('class'):
subfield.attrs['class'] += f' {css_class}'
else:
subfield.attrs['class'] = css_class
return field
if field.field.widget.attrs.get('class'):
field.field.widget.attrs['class'] += f' {css_class}'
else:
field.field.widget.attrs['class'] = css_class
return field
Checking field type
Now we need a template tag to check field type, so we can deal with the wrappers and such
@register.filter
def is_field_type(field, field_type):
"""checks field type"""
if field_type == 'file':
return isinstance(field.field.widget, forms.FileInput)
elif field_type == 'radio':
return isinstance(field.field.widget, forms.RadioSelect)
elif field_type == 'checkbox':
return isinstance(field.field.widget, forms.CheckboxInput)
elif field_type == 'split_dt':
return isinstance(field.field.widget, forms.SplitDateTimeWidget)
elif field_type == 'input':
return isinstance(field.field.widget, (
forms.TextInput,
forms.NumberInput,
forms.EmailInput,
forms.PasswordInput,
forms.URLInput,
forms.SplitDateTimeWidget,
))
elif field_type == 'textarea':
return isinstance(field.field.widget, forms.Textarea)
elif field_type == 'select':
return isinstance(field.field.widget, forms.Select)
elif field_type == 'any_datetime':
return isinstance(field.field.widget, (
forms.DateInput,
forms.TimeInput,
forms.DateTimeInput,
forms.SplitDateTimeWidget
)) # there is also forms.SplitHiddenDateTimeWidget, but we don't need to check that one imo
else:
raise ValueError(f"Unsupported field_type on |is_field_type:'{field_type}'")
Check for multiple fields
We also need a template tag to check for CheckboxSelectMultiple and SelectMultiple to set the proper bulma classes
@register.filter
def is_multiple(field):
"""checks multiple field"""
return (
isinstance(field.field.widget, forms.CheckboxSelectMultiple) or
isinstance(field.field.widget, forms.SelectMultiple)
)
Input type="date" and type="time"
Django by default sets input type="text" for DateInput, TimeInput and SplitDateTimeWidget, which is quite annoying imo. And since I'm lazy to deal with that in every Form/ModelForm, it's better to make a template tag for that too.
@register.filter
def set_input_type(field, field_type=None):
"""
changes the type by the widget, where django puts text-input by default instead of time/date/...
but you can also pass your own field type if you want
"""
if field_type:
pass
elif isinstance(field.field.widget, forms.DateInput):
field_type = 'date'
elif isinstance(field.field.widget, forms.TimeInput):
field_type = 'time'
elif isinstance(field.field.widget, forms.SplitDateTimeWidget):
for subfield in field.field.widget.widgets:
if isinstance(subfield, forms.DateInput):
subfield.input_type = 'date'
elif isinstance(subfield, forms.TimeInput):
subfield.input_type = 'time'
elif isinstance(field.field.widget, forms.DateTimeInput):
# field_type = 'datetime-local' # can't work with passing/returning ISO format
# field_type = 'datetime' # is deprecated, doesn't work in many browsers
# use widget=forms.SplitDateTimeWidget() instead
pass
if field_type:
field.field.widget.input_type = field_type
return field
Columns for SplitDateTimeWidget
Because SplitDateTimeWidget is actually made of two inputs, we need to wrap it with columns. Also I wanted a button to clear the values for non-required fields, so that needs a column too.
@register.filter
def wrap_columns(field):
"""
easiest way to wrap date and time with columns
it's kinda hack, but good enough for now :)
"""
r = field.as_widget()
if isinstance(field.field.widget, forms.SplitDateTimeWidget):
r = ''.join(['<div class="column"><{}></div>'.format(i) for i in r.strip(' ><').split('><')])
else:
r = '<div class="column">{}</div>'.format(r)
if not field.field.required:
r += '<div class="column is-1">'
r += '<button type="button" class="button is-danger is-outlined clear-field-value svg-no-margin" title="{}">'.format(_('Clear value'))
r += '<i class="fas fa-times"></i>'
r += '</button>'
r += '</div>'
r = '<div class="columns is-form-field {}">{}</div>'.format(field.field.widget.__class__.__name__, r)
return mark_safe(r)
Django messages framework and Bulma
Django messages framework uses type "error" while bulma uses "danger" in css classes, so let's make one more template tag for that.
@register.filter
def bulma_message_tag(tag):
"""
messages use type "error", while bulma use class "danger"
"""
return {
'error': 'danger'
}.get(tag, tag)
That should be enough of template tags. Time to make some templates.
Form template
I wanted my forms in modal windows, so there are some extra wrappers.
{% load i18n forms %}
<div class="modal">
<div class="modal-background"></div>
<form class="modal-card" action="{{ form_action }}" method="post">
{% csrf_token %}
{% for field in form.hidden_fields %}
{{ field }}
{% endfor %}
<header class="modal-card-head">
<p class="modal-card-title">{{ modal_title }}</p>
<button type="button" class="delete close-modal" aria-label="close"></button>
</header>
<section class="modal-card-body"><div class="content">
{% if form.non_field_errors %}
<div class="message is-danger">
<div class="message-header">
<button class="delete" aria-label="delete"></button>
</div>
<div class="message-body">
{% for non_field_error in form.non_field_errors %}
{{ non_field_error }}
{% endfor %}
</div>
</div>
{% endif %}
{% for field in form.visible_fields %}
<div class="field" data-field-name="{{ field.name }}">
{% if field|is_field_type:'checkbox' %}
<div class="control">
<div class="columns"><div class="column">
{% if field.auto_id %}
<label class="checkbox{% if field.field.required %} {{ form.required_css_class }}{% endif %}">
{{ field }} {{ field.label }}
</label>
{% endif %}
</div></div>
{% for error in field.errors %}
<span class="help is-danger {{ form.error_css_class }}">{{ error }}</span>
{% endfor %}
{% if field.help_text %}
<p class="help">
{{ field.help_text|safe }}
</p>
{% endif %}
</div>
{% elif field|is_field_type:'radio' %}
{% if field.auto_id %}
<label class="label{% if field.field.required %} {{ form.required_css_class }}{% endif %}">{{ field.label }}</label>
{% endif %}
<div class="control">
<div class="columns"><div class="column">
{% for choice in field %}
<label class="radio">
{{ choice.tag }}
{{ choice.choice_label }}
</label>
{% endfor %}
</div></div>
{% for error in field.errors %}
<span class="help is-danger {{ form.error_css_class }}">{{ error }}</span>
{% endfor %}
{% if field.help_text %}
<p class="help">
{{ field.help_text|safe }}
</p>
{% endif %}
</div>
{% elif field|is_field_type:'input' %}
<label class="label{% if field.field.required %} {{ form.required_css_class }}{% endif %}" for="{{ field.id_for_label }}">{{ field.label }}</label>
<div class="control">
{% if field|is_field_type:'split_dt' %}
{{ field|set_input_type|add_field_class:'input'|wrap_columns }}
{% elif field|is_field_type:'any_datetime' %}
{{ field|set_input_type|add_field_class:'input'|wrap_columns }}
{% else %}
{{ field|add_field_class:'input'|wrap_columns }}
{% endif %}
{% for error in field.errors %}
<span class="help is-danger {{ form.error_css_class }}">{{ error }}</span>
{% endfor %}
{% if field.help_text %}
<p class="help">
{{ field.help_text|safe }}
</p>
{% endif %}
</div>
{% elif field|is_field_type:'textarea' %}
<label class="label{% if field.field.required %} {{ form.required_css_class }}{% endif %}" for="{{ field.id_for_label }}">{{ field.label }}</label>
<div class="control">
{{ field|add_field_class:'textarea'|wrap_columns }}
{% for error in field.errors %}
<span class="help is-danger {{ form.error_css_class }}">{{ error }}</span>
{% endfor %}
{% if field.help_text %}
<p class="help">
{{ field.help_text|safe }}
</p>
{% endif %}
</div>
{% elif field|is_field_type:'select' %}
<label class="label{% if field.field.required %} {{ form.required_css_class }}{% endif %}" for="{{ field.id_for_label }}">{{ field.label }}</label>
<div class="control">
<div class="columns is-form-field Select">
<div class="column">
<span class="select{% if field|is_multiple %} is-multiple{% endif %}{% if field.errors|length > 0 %} is-danger{% endif %}">
{{ field }}
</span>
</div>
</div>
{% for error in field.errors %}
<span class="help is-danger {{ form.error_css_class }}">{{ error }}</span>
{% endfor %}
{% if field.help_text %}
<p class="help">
{{ field.help_text|safe }}
</p>
{% endif %}
</div>
{% elif field|is_field_type:'file' %}
<label class="label{% if field.field.required %} {{ form.required_css_class }}{% endif %}" for="{{ field.id_for_label }}">{{ field.label }}</label>
<div class="control">
<label class="file-label">
{{ field|add_field_class:'file-input'|wrap_columns }}
<span class="file-cta">
<span class="file-icon">
<i class="fas fa-upload"></i>
</span>
<span class="file-label">
Choose a file…
</span>
</span>
</label>
{% for error in field.errors %}
<span class="help is-danger {{ form.error_css_class }}">{{ error }}</span>
{% endfor %}
{% if field.help_text %}
<p class="help">
{{ field.help_text|safe }}
</p>
{% endif %}
</div>
{% else %}
{% if field.auto_id %}
<label class="label{% if field.field.required %} {{ form.required_css_class }}{% endif %}" for="{{ field.auto_id }}">{{ field.label }}</label>
{% endif %}
<div class="control{% if field|is_multiple %} multiple-checkbox{% endif %}">
{{ field|wrap_columns }}
{% for error in field.errors %}
<span class="help is-danger {{ form.error_css_class }}">{{ error }}</span>
{% endfor %}
{% if field.help_text %}
<p class="help">
{{ field.help_text|safe }}
</p>
{% endif %}
</div>
{% endif %}
</div>
{% endfor %}
</div></section>
<footer class="modal-card-foot">
<button type="button" class="button is-dark close-modal">{% translate 'Cancel' %}</button>
<button type="submit" class="button is-success" name="save" value="1">{% translate 'Save' %}</button>
</footer>
</form>
</div>
So now if you want to put a form in your template, you just do:
{% include "path/to/the/form/template.html" with form=your_form form_action="your_url" modal_title="Edit form" %}
Messages after submitting forms
If you use the django messages framework, you can use the bulma_message_tag we prepared before.
{% if messages %}
{% for msg in messages %}
<div class="notification is-{{ msg.level_tag|bulma_message_tag }}">
<button class="delete"></button>
{{ msg.message|safe }}
</div>
{% endfor %}
{% endif %}
JS to clear the values
As I said before, I added buttons to clear the values for non-required fields. So here is a jquery code to do the job.
$(form).on('click', '.clear-field-value', function(event) {
let columns = $(this).closest('.columns');
$('input, select, textarea', columns).each(function() {
let field = $(this);
if(!field.prop('disabled')) {
if(field.is('select, textarea')) {
field.val('');
} else {
if(field.attr('type') === 'text') {
field.val('');
} else { // input type=date and time are pain to clear
let type = field.attr('type');
field.attr('type', 'text').val('');
field.attr('type', type);
}
}
}
}).first().trigger('focus');
event.stopPropagation();
return false;
});
Summary
These are the basic hacks you'll need to work with django forms and bulma, you can modify it as you wish. Since we made the form template a snippet you can use it across your whole project.
It wasn't so hard, was it? Oh right, it really was... Django forms are really pain to work with. I would actually recommend to rather use iommi to avoid all the suffering and throw away all this code from your project :-) .