How to modify Bootstrap 4 form validation information presentation in Django?

Viewed 2107

I am using Django and Bootstrap 4. I want to customize the way form validation information is displayed in a template I am using. When an error occurs in the current template, the invalid input element has a red outline and the error message is displayed as a pop-up. Also, the input elements that have been inputted correctly don't have a green outline, they just remain without an outline.

I'd like to do the following:

  • display the error message underneath the field when an error occurs
  • give the green outline to the fields whose input is correct

I tried multiple solutions, including:

None worked from my case.

My current code:

submit_job_listing.html (the template):

{% extends "base.html" %}
{% block title %} Submit a job {% endblock %}
{% block nav_item_post_a_job %}active{% endblock nav_item_post_a_job %}
{% block head %}
    {{ job_listing_form.media.css }}
{% endblock %}
{% block content %}

    <h1>Submit a job listing</h1>
    <div class="container">
      <form method="post" enctype="multipart/form-data">
          {% csrf_token %}
          <div class="row text-center">
            <h2> <b>Job listing information</b> </h2>
          </div>
          <br/>
          {% for field in job_listing_form.visible_fields %}
            <div class="form-group">
              <div class="row">
                <p>{{ field.label_tag }}</p>
              </div>
              <div class="row">
                {% if field.help_text %}
                  <p class="form-text text-muted">{{ field.help_text }}</p>
                {% endif %}
              </div>
              <div class="row">
                {{ field }}
              </div>
            </div>
          {% endfor %}
          <br/>
          <div class="row text-center">
            <h2> <b>Employer information</b> </h2>
          </div>
          <br/>
          {% for field in employer_form.visible_fields %}
            <div class="form-group">
              <div class="row">
                <p>{{ field.label_tag }}</p>
              </div>
              <div class="row">
                {% if field.help_text %}
                  <p class="form-text text-muted">{{ field.help_text }}</p>
                {% endif %}
              </div>
              <div class="row">
                {{ field }}
              </div>
            </div>
          {% endfor %}
        <input type="submit" value="Submit" class="btn btn-primary" />
      </form>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
      {{ job_listing_form.media.js }}
    </div>
{% endblock %}

views.py (relevant parts):

def submitJobListing(request):
    if request.method == "POST":
        employer_form = EmployerForm(request.POST, request.FILES)
        job_listing_form = JobListingForm(request.POST, request.FILES)

        #check if employer with that name already exists
        employer_name = str(request.POST.get("name", ""))
        try:
            employer_with_the_same_name = Employer.objects.get(name=employer_name)
        except ObjectDoesNotExist:
            employer_with_the_same_name = None

        employer = None
        
        if (employer_with_the_same_name != None):
            employer = employer_with_the_same_name

        if employer == None and employer_form.is_valid() and job_listing_form.is_valid():
            employer = employer_form.save()

        job_listing = job_listing_form.save(commit=False)
        job_listing.employer = employer
        job_listing.save()
        job_listing_form.save_m2m()

        return SimpleTemplateResponse("employers/successful_job_listing_submission.html")

    else:
        employer_form = EmployerForm()
        job_listing_form = JobListingForm()

    context = {
        "employer_form": employer_form,
        "job_listing_form": job_listing_form,
    }

    return render(request, 'employers/submit_job_listing.html', context)

forms.py (relevant parts):

class EmployerForm(forms.ModelForm):
    name = forms.CharField(max_length=100)
    #location = forms.CharField(widget=LocationWidget)
    short_bio = forms.CharField(widget=forms.Textarea)
    website = forms.URLField()
    profile_picture = forms.ImageField()

    def __init__(self, *args, **kwargs):
        super(EmployerForm, self).__init__(*args, **kwargs)
        for visible in self.visible_fields():
            if isinstance(visible.field.widget, (forms.widgets.TextInput, forms.widgets.Textarea, forms.widgets.URLInput, LocationWidget)):
                visible.field.widget.attrs['class'] = 'form-control'
            if isinstance(visible.field.widget, forms.widgets.ClearableFileInput):
                visible.field.widget.attrs['class'] = 'form-control-file'

    class Meta:
        model = Employer
        fields = ["name", "short_bio", "website", "profile_picture"]

class JobListingForm(forms.ModelForm):
        job_title = forms.CharField(max_length=100, help_text="What's the job title?")
        job_description = forms.CharField(widget=forms.Textarea)
        job_requirements = forms.CharField(widget=forms.Textarea)
        what_we_offer = forms.CharField(widget=forms.Textarea)
        location = forms.CharField(widget=LocationWidget)
        remote = forms.BooleanField()
        job_application_url = forms.URLField()
        point_of_contact = forms.EmailField()
        categories = forms.ModelMultipleChoiceField(
                     queryset=Category.objects.all(),
                     widget=forms.CheckboxSelectMultiple,
                     required=True)

        def __init__(self, *args, **kwargs):
            super(JobListingForm, self).__init__(*args, **kwargs)
            for visible in self.visible_fields():
                if isinstance(visible.field.widget, (forms.widgets.TextInput, forms.widgets.Textarea, forms.widgets.URLInput, forms.widgets.EmailInput, LocationWidget)):
                    visible.field.widget.attrs['class'] = 'form-control'
                if isinstance(visible.field.widget, (forms.widgets.CheckboxInput, forms.widgets.CheckboxSelectMultiple)):
                    visible.field.widget.attrs['class'] = 'form-check'

        class Meta:
            model = JobListing
            fields = ["job_title", "job_description", "job_requirements", "what_we_offer", "location", "remote", "job_application_url", "categories", "point_of_contact"]

Can someone tell me how can I achieve the effects that I want?

Update 1:

I tried adding the code that checks for field.errors, but it doesn't work.

This is the submit_job_listing.html template file:

{% extends "base.html" %}
{% load widget_tweaks %}
{% block title %} Submit a job {% endblock %}
{% comment %} https://stackoverflow.com/questions/50028673/changing-active-class-in-bootstrap-navbar-not-staying-in-inherited-django-templa {% endcomment %}
{% block nav_item_post_a_job %}active{% endblock nav_item_post_a_job %}
{% block head %}
    {{ job_listing_form.media.css }}
    <!---
    <style>
        input, select {width: 100%}
    </style>
    --->
{% endblock %}
{% block content %}

{% comment %}
https://simpleisbetterthancomplex.com/article/2017/08/19/how-to-render-django-form-manually.html
{% endcomment %}

    <h1>Submit a job listing</h1>
    <div class="container">
      <form method="post" enctype="multipart/form-data">
          {% csrf_token %}
          <div class="row text-center">
            <h2> <b>Job listing information</b> </h2>
          </div>
          <br />
          <div class="row text-center">
            <p> <b>Note: Fields marked with an asterisk (*) are required</b> </p>
          </div>
          <br/>
          {% for field in job_listing_form.visible_fields %}
            <div class="form-group">
              <div class="row">
                <p>{{ field.label_tag }}{{ field.field.required|yesno:"*," }}</p>
              </div>
              <div class="row">
                {% if field.help_text %}
                  <p class="form-text text-muted">{{ field.help_text }}</p>
                {% endif %}
              </div>
              <div class="row">
                {{ field }}
                <p> {{ field.errors }} </p>
                {% if field.errors %}
                  <div class="border-bottom-danger">
                      {{ field.errors }}
                  </div>
                {% endif %}
              </div>
            </div>
          {% endfor %}
          <br/>
          <div class="row text-center">
            <h2> <b>Employer information</b> </h2>
          </div>
          <br />
          <div class="row text-center">
            <p> <b>Note: Fields marked with an asterisk (*) are required</b> </p>
          </div>
          <br/>
          {% for field in employer_form.visible_fields %}
            <div class="form-group">
              <div class="row">
                <p>{{ field.label_tag }}{{ field.field.required|yesno:"*," }}</p>
              </div>
              <div class="row">
                {% if field.help_text %}
                  <p class="form-text text-muted">{{ field.help_text }}</p>
                {% endif %}
              </div>
              <div class="row">
                {{ field }}
              </div>
            </div>
          {% endfor %}
        <input type="submit" value="Submit" class="btn btn-primary" />
      </form>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
      {{ job_listing_form.media.js }}
    </div>
{% endblock %}

When I just click the submit button without inputting anything, all the inputs have a red outline, but nothing gets printed out as field.errors. I don't know why is that. Why is that?

2 Answers

Did you tried debugging in the template ?

I did something like you want to achieve :

<form method="post"> {% csrf_token %}
    {% for field in form %}
        <div class="form-group">
            {{ field }}
        <label class="control-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
            {% if field.errors %}
                <div class="border-bottom-danger">
                    {{ field.errors }}
                </div>
            {% endif %}
        </div>
    {% endfor %}
    <button class="btn btn-block" type="submit">Submit</button>
</form>

Where :

  1. {{ field }} is the input, where you are correctly adding visible.field.widget.attrs['class'] = 'form-control'
  2. Then, errors you want (I guess) are stored in the field, so you could get its with {{ field.errors }} if it has some.

The difference between your code and mine is that I'm using field.errors instead if field.help_text, I hope it can help.

I suggest you to put breakpoint in your template, then analyze your form object. Then you can do whatever you want in your frontend.

  1. If no error : Add class outline-green to your input wrapper (the <div class="row")
  2. If error : Display the error within a div underneath your input

PS : If you want to outline the input, and not the row wrapper, you can do it through css (or better with Sass if you are using it):

.row.outline-green > input{
   border: 1px solid green;
}
<div class="row outline-green">
{{ field }}
</div>

{% load widget_tweaks %}

<div class="col-12 col-md-6">
                        <div class="form-group">
                            <label for="{{ form.field1.id_for_label }}" class="text-capitalize">{{ form.field1.label|title }}{% if form.field1.field.required %}<span class="text-danger ml-1">*</span>{% endif %}</label>

                            {% if form.is_bound %}
                                {% if form.field1.errors %}
                                    {% render_field form.name class="form-control is-invalid" %}
                                    {% for error in form.field1.errors %}
                                        <div class="invalid-feedback">
                                        {{ error }}
                                        </div>
                                    {% endfor %}
                                {% else %}
                                    {% render_field form.field1 class="form-control is-valid" %}
                                {% endif %}
                            {% else %}
                                {% render_field form.field1 class="form-control" %}
                            {% endif %}

                            {% if form.field1.help_text %}
                                <small class="form-text text-muted">{{ form.field1.help_text }}</small>
                            {% endif %}
                        </div>
                    </div>
</div>
Related