16

I have this form field:

email = forms.EmailField(
  required=True,
  max_length=100,
)

It has the required attribute, but in the html it is not adding the html attribute required. In fact it's not even using email as the field type, it's using text... though it appears to get max_length just fine.

Actual:

<input id="id_email" type="text" name="email" maxlength="100">

Expected:

<input id="id_email" type="email" name="email" maxlength="100" required="true">

How can I get Django to use the correct attributes in html forms?

Daniel X Moore
  • 14,637
  • 17
  • 80
  • 92

6 Answers6

20

Django form elements are written against <input /> as it exists in HTML 4, where type="text" was the correct option for e-mail addresses. There was also no required="true".

If you want custom HTML attributes, you need the attrs keyword argument to the widget. It would look something like this:

email = forms.EmailField(
    max_length=100,
    required=True,
    widget=forms.TextInput(attrs={ 'required': 'true' }),
)

You can check out more documentation about widgets here. Discussion of attrs is near the bottom of that page.

Regarding type="email", you might be able to send that to your attrs dictionary and Django will intelligently override its default. If that isn't the result you get, then your route is to subclass forms.TextInput and then pass it to the widget keyword argument.

Luke Sneeringer
  • 9,270
  • 2
  • 35
  • 32
10

Combining Daniel and Daniel answers, I usually use this mixin for my forms:

from django.contrib.admin.widgets import AdminFileWidget
from django.forms.widgets import HiddenInput, FileInput


class HTML5RequiredMixin(object):

    def __init__(self, *args, **kwargs):
        super(HTML5RequiredMixin, self).__init__(*args, **kwargs)
        for field in self.fields:
            if (self.fields[field].required and
               type(self.fields[field].widget) not in
                    (AdminFileWidget, HiddenInput, FileInput) and 
               '__prefix__' not in self.fields[field].widget.attrs):

                    self.fields[field].widget.attrs['required'] = 'required'
                    if self.fields[field].label:
                        self.fields[field].label += ' *'

So when i have to create a new form or modelform i just use:

class NewForm(HTML5RequiredMixin, forms.Form):
    ...
Jiloc
  • 3,338
  • 3
  • 24
  • 38
  • In my case, I got ' Form object has no attribute 'fields' ' because python evaluates '__init__' from left to right and form object wasn't available at the time of HTML5RequiredMixin '__init__' call. After I change the order of class declaration like this and part of __init__ of NewForm it worked. ' class NewForm(forms.Form, HTML5RequiredMixin) ' , then __init__ of NewForm, call ' HTML5RequiredMixin.__init__ ' – Imju Jun 16 '16 at 23:24
7

Since Django 1.10, this is built-in.

From the release notes:

Required form fields now have the required HTML attribute. Set the new Form.use_required_attribute attribute to False to disable it.

karyon
  • 1,957
  • 1
  • 11
  • 16
6

There's also the template-only solution using a filter. I recommend django-widget-tweaks:

{% load widget_tweaks %}

{{ form.email|attr:'required:true' }}

That was easy.

John Lehmann
  • 7,975
  • 4
  • 58
  • 71
6

Monkeypatching Widget is your best bet:

from django.forms.widgets import Widget
from django.contrib.admin.widgets import AdminFileWidget
from django.forms import HiddenInput, FileInput

old_build_attrs = Widget.build_attrs

def build_attrs(self, extra_attrs=None, **kwargs):
    attrs = old_build_attrs(self, extra_attrs, **kwargs)

    # if required, and it's not a file widget since those can have files
    # attached without seeming filled-in to the browser, and skip hidden "mock"
    # fileds created for StackedInline and TabbedInline admin stuff
    if (self.is_required
            and type(self) not in (AdminFileWidget, HiddenInput, FileInput)
            and "__prefix__" not in attrs.get("name", "")):
        attrs['required'] = 'required'

    return attrs

Widget.build_attrs = build_attrs
Flimm
  • 136,138
  • 45
  • 251
  • 267
Daniel X Moore
  • 14,637
  • 17
  • 80
  • 92
  • 4
    Might I suggest that you change the if statement to read: `if self.is_required and type(self) not in (AdminFileWidget,HiddenInput, FileInput) and "__prefix__" not in attrs["name"]:` That'll prevent the browser from adding `required="true"` when an object already has a file attached to it, as well as skip the hidden "mock" fields created for StackedInline and TabbedInline admin stuff. – Daniel Quinn Mar 29 '12 at 13:17
4

As you've realized, setting your Field required attribute to True is only for backend validation, as explained in the Django documentation.

What you really want is to add a required attribute to the Widget of the field:

email.widget.attrs["required"] = "required"

But if you really want to write elegant, DRY code, you should make a base form class that dynamically looks for all your required fields and modifies their widget required attribute for you (you can name it whatever you wish, but "BaseForm" seems apt):

from django.forms import ModelForm

class BaseForm(ModelForm):
    def __init__(self, *args, **kwargs):
        super(BaseForm, self).__init__(*args, **kwargs)
        for bound_field in self:
            if hasattr(bound_field, "field") and bound_field.field.required:
                bound_field.field.widget.attrs["required"] = "required"

And then have all your Form objects descend from it:

class UserForm(BaseForm):
    class Meta:
        model = User
        fields = []

    first_name = forms.CharField(required=True)
    last_name = forms.CharField(required=True)
    email = forms.EmailField(required=True, max_length=100)
kfan
  • 1,641
  • 1
  • 13
  • 16