How to implement an autocomplete-datalist form field inside a CreateView in Django?

1.1k Views Asked by At

I am very new to web development and specifically using Django Framework.

I am trying to find a clean, efficient and non external package dependant implementation for an autocomplete-datalist form field inside a Generic class based CreateView template in Django.

I have found numerous resources on various implementations, but most of them depend on external packages(autocomplete-light, jqueryCDN, etc.) and none of it is based on a class based generic CreateView.

I have been experimenting and I have managed to make the autocomplete-datalist work in a way but I am stuck in a very simple problem when I try to post my form with the data from the datalist element.

I get a validation error:

"city_name: This field is required"

I also noticed that the city object queried from the database inside the datalist has also the id of the city_name

models.py

from django.db import models


class City(models.Model):
    name = models.CharField(max_length=50)

    class Meta:
        verbose_name_plural = "cities"
        ordering = ['name']

    def __str__(self):
        return self.name


class Person(models.Model):
    first_name = models.CharField(max_length=40)
    last_name = models.CharField(max_length=40)
    address = models.CharField(max_length=150)
    city_name = models.ForeignKey(City, on_delete=models.CASCADE)

    def __str__(self):
        return f'{self.first_name} {self.last_name}'

views.py

from django.views.generic import ListView, CreateView
from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.auth.mixins import LoginRequiredMixin
from .models import Person, City
from .forms import PersonForm
# Create your views here.


class PersonList(LoginRequiredMixin, ListView):
    model = Person
    template_name = "home.html"
    paginate_by = 20
    login_url = "/login/"
    redirect_field_name = 'redirect_to'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        return context


class PersonCreate(LoginRequiredMixin, SuccessMessageMixin, CreateView):
    model = Person
    template_name = "testform.html"
    login_url = "/login/"
    form_class = PersonForm
    success_url = 'testapp/add/'
    success_message = 'Person registered successfully!'
    redirect_field_name = 'redirect_to'

forms.py

from django import forms
from .models import Person, City


class PersonForm(forms.ModelForm):

    class Meta:
        model = Person
        fields = ["first_name", "last_name", "address", "city_name"]

testform.html

{% extends 'home.html' %}
{% load static %}
{% block content %}
{% if messages %}
  {% for message in messages %}
  <div class="alert alert-success alert-dismissible fade show" role="alert">
    <span style="font-size: 18px;padding: 1mm"><i class="fa-solid fa-circle-check"></i></span>{{ message }}
    <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
  </div>
  {% endfor %}
{% endif %}
    <form method="POST">
        {% csrf_token %}
    
        <div class="mb-3">
            <label for="first_name charfield" class="form-label"> First Name</label>
            {{form.first_name}}
        </div>
        <div class="mb-3">
            <label for="last_name charfield" class="form-label">Last Name</label>
            {{form.last_name}}
        </div>
        <div class="mb-3">
            <label for="address charfield" class="form-label">Address</label>
            {{form.address}}
        </div>
        <div class="mb-3">
            <label for="city_name datalist" class="form-label">City Name</label>
            <input type="text" list="cities" class="form-control">
                <datalist id="cities">
                    {% for city in form.city_name %}
                    <option>{{ city }}</option>
                    {% endfor %}
                </datalist>
        </div>
        <button class="btn btn-outline-primary" type="submit">Submit</button>
    </form>
    {{form.errors}}
{% endblock %}

Result:

testform_html output

I believe it is a necessary feature for all modern web applications to have this kind of functionality within their database query-autocomplete form fields system. It is a pity that although Django provides this feature for the AdminModels through its autocomplete_fields attribute, it makes it so hard to implement on Generic Class Based Views on the actual application models.

How can I approach this issue, and is there a efficient and more optimized way to implement it?

3

There are 3 best solutions below

4
Pat On

If you don't want a field required you can set the attribute blank=True in the model class. A question I have is why would you want to have a Foreignkey to just a city name. Or are you trying to use the a list of cities to populate the drop down? In that case the Foreign Key is definitely not the answer.

0
johnperc On

This is an example I have to autocomplete a list of Titles from a database table - 'get_titles'. It uses a view to provide the title values Then in HTML I have produced a data list within a search field that enables autocomplete.

Views.py

########################## Autocomplete Search Field ##############

@require_GET
def get_titles(request):
    term = request.GET.get('term')
    if term:
        #titles = ComicInput.objects.filter(Title__icontains=term).annotate(Title=F('Title')).values('Title').distinct()
        titles = ComicInput.objects.filter(Category = 'Sell').filter(Title__icontains=term).values('Title')
    else:
        titles = []
    title_list = [title['Title'] for title in titles] # Convert QuerySet to list
    return JsonResponse(title_list, safe=False)

Html

<script>
        var $j = jQuery.noConflict();
        $j(document).ready(function() {
            $j("#search").autocomplete({
                source: function(request, response) {
                    $j.getJSON("{% url 'get_titles' %}", {
                        term: request.term
                    }, function(data) {
                        var uniqueValues = [];
                        $j.each(data, function(index, value) {
                            if ($j.inArray(value, uniqueValues) === -1) {
                                uniqueValues.push(value);
                            }
                        });
                        response(uniqueValues);
                    });
                },
                minLength: 2,
                select: function(event, ui) {
                    $j("#search").val(ui.item.label);
                    $j("#search-form").submit();
                }
            }).autocomplete("widget").addClass("custom-autocomplete");
    });
</script>

<style>
    .custom-autocomplete {
        position: absolute;
        padding: 20px 0;
        width: 200px;
        background-color: #DBF9FD;
        border: 1px solid #ddd;
        max-height: 200px;
        overflow-y: auto;
    }
    .custom-autocomplete li {
        padding: 5px;
        cursor: pointer;
    }
    .custom-autocomplete li:hover {
        background-color: #eee;
    }
</style>

</head>

<body>

<form action="{% url 'search_page' %}" method="get" class="search-bar"  style="width: 200px; height: 30px;font-size: small; align: top;" >
                            <input list="Titles" type="search"  id ="search" name="search" pattern=".*\S.*" placeholder="Search Collections by Title" onFocus="this.value=''" >
                                <datalist id="title-options">
                                    {% for title in titles %}
                                        <option value="{{ title }}">
                                    {% endfor %}
                                </datalist>

        </div>
0
nigel222 On

I was looking for help with solving this question, but ended up rolling my own widget class. It's not actually as hard as it might seem. Subclass TextInput, and attach the desired datalist to what it generates. The relevant Django source is here.

Code and usage: The Widget class

from django.forms.widgets import TextInput
from django.utils.safestring import mark_safe

class DatalistTextInput(TextInput):
    def __init__(self, attrs=None):
        super().__init__( attrs)
        if 'list' not in self.attrs or 'datalist' not in self.attrs:
            raise ValueError(
              'DatalistTextInput widget is missing required attrs "list" or "datalist"')
        self.datalist_name = self.attrs['list']

        # pop datalist for use by our render method. 
        # its a pseudo-attr rather than an actual one
        # a string of option values separated by dunders ('__')
        self.datalist = self.attrs.pop('datalist') 

    def render(self, **kwargs):
        DEBUG( self, kwargs)
        part1 = super().render( **kwargs)
        opts = ' '.join(
            [ f'<option>{x}</option>' for x in self.datalist.split('__') ]
        )
        part2 = f'<datalist id="{self.datalist_name}">{opts}</datalist>'
        return part1 + mark_safe( part2)

And a form and a view to test it

class TestDatalist( forms.Form):
    foo = forms.CharField(
        max_length=10,
        widget = DatalistTextInput( attrs={
            'list':'foolist',
            'datalist': "foo__bar__baz__quux"
            }
    ))

class TestView( FormView):
    form_class = TestDatalist
    template_name = 'jobs/simple_form.html'
    success_url='/wafers/OK'
    initial={ 'foo':'b'}
    def form_valid( self, form):
        print( form.cleaned_data)  # in real life do something useful!
        return super().form_valid( form)

A snip of the generated HTML ({{form.as_table()}}:

<tr><th><label for="id_foo">Foo:</label></th>
<td><input type="text" name="foo" value="b" list="foolist" maxlength="10" required id="id_foo">
<datalist id="foolist">
  <option>foo</option> 
  <option>bar</option> 
  <option>baz</option> 
  <option>quux</option>
</datalist>
</td></tr>