kurac
kurac

Reputation: 28

How to select multiple items in dropdown menu django?

I have django model:

class at(models.Model):
    ...
    apostols = models.ForeignKey(settings.AUTH_USER_MODEL,
 related_name='apostols',
 on_delete=models.CASCADE,
 null=True)
    ...

Above and below apostols field are more fields which aren't important now.

My form:

class AtCreateForm(forms.ModelForm):
...
class Meta:
        model = at
        fields = ['apostols']
        widgets = {
            'apostols': forms.widgets.TextInput(attrs={'class': 'form-control', 'list': 'user-list', 'autocomplete': 'off', 'multiple': True}),
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['apostols'].queryset = User.objects.all()

In this form, we have apostols field which we display in template:

<form method="post">
        {% csrf_token %}
        <input type="text" list="user-list" multiple>
        <datalist id="user-list">
          {% for user in form.apostols.field.queryset %}
            <option value="{{ user.username }}">{{ user.username }}</option>
          {% endfor %}
        </datalist>
        <input type="submit" value="Submit">
    </form>

this is how it works

This is how it works now.

This form actually works as intented, but I want to be able to select multiple users. At best, it would work like on stackoverflow.com the tag-select section when you select more tags.

Edit I have made a form with select2, but now I don't know how to save query that I get in view in models? Is there a way someone can help me with this?

Upvotes: 0

Views: 2588

Answers (7)

vinkomlacic
vinkomlacic

Reputation: 1877

You can do it easily using the django-easy-select2 package. You can get an idea of how it might look here.

Here are the installation instructions: https://django-easy-select2.readthedocs.io/en/latest/installation.html Don't forget to add it to INSTALLED_APPS.

What I also needed to do was to add the Django JQuery in the <head> because of some reference issues which would disable the select2 boxes.

<!-- Required for easy_select2 widgets. Note: these need to load before the body is rendered
because the easy_select2 JS is loaded in the body so if we load it in the footer we will
lose the complete functionality of the Select2 widgets due to reference issues. -->
<script type="text/javascript" src="{% static 'admin/js/vendor/jquery/jquery.min.js' %}"></script>
<script type="text/javascript" src="{% static 'admin/js/jquery.init.js' %}"></script>

Then, you can use the select2 form fields in your form code and render your forms as usual:

class AtCreateForm(forms.ModelForm):
    class Meta:
        widgets = {
            # Maybe you need to set choices on this widget cause you're adding
            # the queryset in the __init__ method.
            'apostols': Select2Multiple()
        }
        ...

    def __init__(self, *args, **kwargs):
        queryset = kwargs.pop('queryset')
        super().__init__(*args, **kwargs)
        self.fields['apostols'].queryset = queryset

EDIT: this package also works with ModelMultipleChoiceField (docs) field which can be used for model relations.

Upvotes: 1

Anee Mes
Anee Mes

Reputation: 54

"Dropdown" boxes don't support multiple selection in HTML. The SelectMultiple widget in Django can be used to enable multiple selections in a drop-down menu. Here's an illustration of how to apply it to a form: forms.py

from django import forms

class MyForm(forms.Form):
    CHOICES = (('choice1', 'Choice 1'), ('choice2', 'Choice 2'), ('choice3', 'Choice 3'))
    select_multiple = forms.MultipleChoiceField(widget=forms.SelectMultiple, choices=CHOICES)

The options that can be selected from the drop-down menu in this example's select multiple field are defined in the CHOICES tuple, which is a multiple choice field that uses the SelectMultiple widget. Also, another implementation can be using ModelMultipleChoiceField to select multiple items in a dropdown menu that is associated with a model. forms.py

from django import forms
from .models import MyModel

class MyModelForm(forms.ModelForm):
    class Meta:
        model = MyModel
        fields = ('field1', 'field2', 'field3')
        widgets = {
            'field1': forms.SelectMultiple,
        }

The select multiple field in the template can be used just like any other form field, but it will now display as a drop-down menu with multiple item selection capability. form.html

<form method="post">
    {% csrf_token %}
    {{ form.select_multiple }}
    <input type="submit" value="Submit">
</form>

Upvotes: 0

Niko
Niko

Reputation: 3783

I can see two approaches, the first one is using an extension of your code. And for that example I will just do half of what you want (create the "tag" and update the list). Later explaining why.

forms.py

class AtCreateForm(forms.ModelForm):
    class Meta:
        ...

    def __init__(self, *args, **kwargs):
        queryset = kwargs.pop('queryset')
        super().__init__(*args, **kwargs)
        self.fields['apostols'].queryset = queryset

A minor difference form your original form, with addition of one line queryset = kwargs.pop('queryset') where we pass a queryset as a keyword to dinamically update it.

views.py

class ApostolsView(View):
    queryset = User.objects.all()
    form_class = AtCreateForm
    template_name = 'apostols.html'
    apostols = []
    context = {}

    def get(self, request):
        self.apostols.clear()

        form = self.form_class(queryset=self.queryset)
        self.context = {
            'form': form,
            'apostols': self.apostols,
        }
        return render(request, self.template_name, self.context)

    def post(self, request):
        apostol_name = request.POST.get('apostols')
        csrftoken = request.POST['csrfmiddlewaretoken']
        obj = User.objects.get(username=apostol_name)
        
        data = {
            'csrfmiddlewaretoken': csrftoken,
            'apostols': obj.id
        }

        query_dict = QueryDict('', mutable=True)
        query_dict.update(data)
        form = self.form_class(query_dict, queryset=self.queryset)

        if form.is_valid():
            if form.cleaned_data['apostols'].username not in self.apostols:
                self.apostols.append(form.cleaned_data['apostols'].username)

            new_form = self.form_class(queryset=self.queryset.exclude(username__in=self.apostols))

        self.context = {
            'form': new_form,
            'apostols': self.apostols,
        }
        return render(request, self.template_name, self.context)

apostols.html

{% block content %}
<form method="post" action="/original/apostols/">
    {% csrf_token %}
    <input type="text" list="user-list" name="apostols">
    <datalist id="user-list">
      {% for user in form.apostols.field.queryset %}
        <option value="{{ user.username }}"></option>
      {% endfor %}
    </datalist>
    <input type="submit" value="Submit">
</form>

<span>
  {% for apostol in apostols %}
  <span class="badge bg-primary" id="{{apostol}}-badge" style="display: flex; align-items: center; justify-content: center; width: fit-content; margin: 10px 10px 0 0;">
    <button type="button" onclick="removeBadge()" value="{{apostol}}" class="btn-close" aria-label="Close" style="height: auto;"></button>
        {{apostol}}
    </span>
  {% endfor %}
</span>

<script>
  function removeBadge() {
    console.log('remove badge element and update queryset.')
  }
</script>
{% endblock %}

As you can see so far, it requires a little bit of work around at validating on POST request, due to the nature of <datalist> and Django QueryDict.

Anyway, the main pain points that I want to focus on is that we are going to need Javascript to remove the badge element. Also, included in that action we would have to send an AJAX request to the view to update the list via queryset.

In general you now can see how this would be very inefficient. Unnecessarily overloading the server(s) with requests and queries for every action, also worsening in performance as data on the table grows.

A full solution using Javascript

By using Javascript and AJAX we can lift this load off the server by querying for data only once and work it on client side until it is ready to be submitted.

To diminish the lines of code in one file some functions were moved to external files (also can be thought as a reuse resource). Namely getCookie() used to retrieve csrftoken into cookie.js and other two functions into fetch.js whose purpose is to get and post data.

fetch.js

async function getData(url) {
    const response = await fetch(url, {
            method: 'GET',
            headers: {
            'Content-Type': 'application/json',
            },
        });
    return response.json();
};

async function postData(url, csrftoken, data) {
    const response = await fetch(url, {
            method: 'POST',
            headers: {
            'Content-Type': 'application/json',
            'X-CSRFToken': csrftoken
            },
            body: JSON.stringify(data)
        });
    return response.json();
};

views.py

def apostols(request):
    if request.method == 'POST':
        data = json.load(request)
        apostols = data.get('apostols')
        # Do the deed with 'apostols'
        return JsonResponse({'msg': 'The deed is done', 'data': apostols})

    return render(request, 'apostols.html')

def user_list(request):
    users = list(User.objects.values())
    return JsonResponse({'users': users})

apostols.html

{% load static %}

{% block content %}
<div style="display: flex">
    <div style="width: auto; margin-right: 20px;">
        <input type="text" list="user-list" id="user-choice" name="user-choice">
        <datalist id="user-list">
        </datalist>
        
        <input type="button" onclick="appendChoice()" value="Add">
        <input type="button" onclick="sendApostols()" value="Send List">
    </div>
    <div style="width: 50%;">
        <div id="badge-list" style="display: flex; flex-flow: row wrap"></div>
    </div>
</div>

<br>



<script type="text/javascript" src={% static "js/cookie.js" %}></script>
<script type="text/javascript" src={% static "js/fetch.js" %}></script>
<script>
    let users = []
    let exclude = []

    document.addEventListener("DOMContentLoaded", function() {
        getData('/user/list/')
        .then((data) => {
            for (var i in data.users)
                users.push(data.users[i].username)

            updateUserList(users, exclude)
        })
    });

    function updateUserList(users, exclude) {
        datalist = document.getElementById('user-list');
        for (var i = 0; i<=users.length-1; i++){
            if (!exclude.includes(users[i])) {
                var opt = document.createElement('option');
                opt.value = users[i];
                opt.innerHTML = users[i];
                datalist.appendChild(opt);
            }
        }
    };

    function appendChoice() {
        datalist = document.getElementById('user-list');
        var choice = document.getElementById('user-choice');
        var badges = document.getElementById('badge-list');
        if (users.includes(choice.value)) {
            exclude.push(choice.value);
            datalist.innerHTML = '';

            updateUserList(users, exclude);
            badges.innerHTML += `
                <span class="badge bg-primary" id="${choice.value}-badge" style="display: flex; align-items: center; justify-content: center; width: fit-content; margin: 10px 10px 0 0;">
                <button type="button" onclick="removeBadge(this)" value="${choice.value}" class="btn-close" aria-label="Close" style="height: auto;"></button>
                    ${choice.value}
                </span>
            `
            
            choice.value = '';
        }
        
    };

    function removeBadge(elem) {
        badge = document.getElementById(elem.parentNode.id);
        badge.remove();
        const index = exclude.indexOf(elem.value);
        exclude.splice(index, 1);
        datalist.innerHTML = '';
        updateUserList(users, exclude);
    };

    function sendApostols(){
        const url = '/apostols/';
        const csrftoken = getCookie('csrftoken');
        data = {'apostols': exclude};
        postData(url, csrftoken, data)        
        .then((response) => {
            // You can:
            // Do an action
            console.log(response.msg);
            console.log(response.data);

            // Reload the page
            // location.reload();

            // Redirect to another page
            // window.location.href = 'http://localhost:8000/my/url/';
        });
    };
</script>
{% endblock %}

Other useful links:

  1. Use kwargs in Django form
  2. Remove value from JS arr
  3. get parent element id
  4. Add options to select element
  5. Fetch API

Upvotes: 0

Japheth
Japheth

Reputation: 23

MultipleChoiceField in Django Forms is a Choice field, for input of multiple pairs of values from a field

Upvotes: 0

Ibrahim
Ibrahim

Reputation: 34

"Dropdown" boxes don't support multiple selection in HTML; browsers will always render it as a flat box..

You probably want to use some kind of a JS widget - Select2 is a popular one. There are a couple of Django projects - django-select2 or django-easy-select

(And yes, that snippet - like many things on Djangosnippets - is massively out of date; "newforms" was renamed to "forms" even before version 1.0 of Django.)

Upvotes: 1

JPG
JPG

Reputation: 88689

The easy method would be setting the widget to SelectMultiple(...) as,

class YourModelForm(forms.ModelForm):
    class Meta:
        model = YourModelClass
        fields = ["your_field"]
        widgets = {
            "your_field": forms.SelectMultiple(),
        }

Example Setup

# models.py

class Product(models.Model):
    name = models.CharField(max_length=30)
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="products",
        related_query_name="product",
    )

    def __str__(self):
        return self.name
# forms.py

class ProductForm(forms.ModelForm):
    class Meta:
        model = Product
        fields = "__all__"
        widgets = {
            "user": forms.SelectMultiple(),
        }
# views.py

class ProductFormView(generic.FormView):
    form_class = ProductForm
    template_name = "index.html"
# index.html

{{ form.as_p }}

Result

Result

Upvotes: 0

hanspeters205
hanspeters205

Reputation: 371

The easiest way would be to implement a js library like https://select2.org/

Here's an example from https://select2.org/getting-started/basic-usage#multi-select-boxes-pillbox:

<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/select2.min.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/select2.min.js"></script>

<form method="post">
    {% csrf_token %}
    <select class="js-example-basic-multiple" name="users[]" multiple="multiple">
      {% for user in form.apostols.field.queryset %}
        <option value="{{ user.username }}">{{ user.username }}</option>
      {% endfor %}
    </select>
    <input type="submit" value="Submit">
</form>

<script>
$(document).ready(function() {
    $('.js-example-basic-multiple').select2();
});
</script>

Edit: JQuery ist required for select2 If you don't use JQuery there are multiple other libraries which'll do the job e.g. https://selectize.dev/

Upvotes: 1

Related Questions