'Display wtforms SelectMultipleField display as drop-down and not list

I am using wtforms SelectMultipleField to render a form with a list of choices.

Using a plain vanilla SelectField renders the choices as a drop-down (dropdown).

However, I need to be able to select multiple entries in the list, hence want to use SelectMultipleField. This, however, displays as a listbox (listbox).

This is my code for the form entries: test = SelectMultipleField('test', choices=[(1, 'a'), (2, 'b'), (3, 'c')])

Code to render: <a class="test" id="test">{{ form.test }}</a>

How can I render my SelectMultipleField to display as a dropdown?



Solution 1:[1]

UPDATE: found 'vanilla' solution! Here it is:

A similar question was asked here. I took the base and adjusted it to flask/jinja workflow:

main.py:

from flask import Flask, render_template, request
from flask_wtf import FlaskForm
from wtforms.fields import SelectMultipleField, SubmitField
from wtforms import widgets

app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret_key'


choices = [
    'apple',
    'banana',
    'cherry',
]


# This code from WTForms docs, this class changes the way SelectMultipleField
# is rendered by jinja
# https://wtforms.readthedocs.io/en/3.0.x/specific_problems/
class MultiCheckboxField(SelectMultipleField):
    """
    A multiple-select, except displays a list of checkboxes.

    Iterating the field will produce subfields, allowing custom rendering of
    the enclosed checkbox fields.
    """
    widget = widgets.ListWidget(prefix_label=False)
    option_widget = widgets.CheckboxInput()


class Form(FlaskForm):
    global choices
    select_multiple_field = MultiCheckboxField(choices=choices)
    submit = SubmitField()


@app.route('/', methods=['GET', 'POST'])
def index():
    form = Form()

    if request.method == 'POST':
        # Getting selected options
        form_data = form.select_multiple_field.data
        print(form_data)

    return render_template(
        'index.html',
        form=form,
    )


if __name__ == "__main__":
    app.run(debug=True)

index.html:

<!DOCTYPE html>
<html lang="en">

<head>
</head>

<body>

    <style>
        .dropdown-check-list {
            display: inline-block;
        }

        .dropdown-check-list .anchor {
            position: relative;
            cursor: pointer;
            display: inline-block;
            padding: 5px 50px 5px 10px;
            border: 1px solid #ccc;
        }

        .dropdown-check-list .anchor:after {
            position: absolute;
            content: "";
            border-left: 2px solid black;
            border-top: 2px solid black;
            padding: 5px;
            right: 10px;
            top: 20%;
            -moz-transform: rotate(-135deg);
            -ms-transform: rotate(-135deg);
            -o-transform: rotate(-135deg);
            -webkit-transform: rotate(-135deg);
            transform: rotate(-135deg);
        }

        .dropdown-check-list .anchor:active:after {
            right: 8px;
            top: 21%;
        }

        .dropdown-check-list ul.items {
            padding: 2px;
            display: none;
            margin: 0;
            border: 1px solid #ccc;
            border-top: none;
        }

        .dropdown-check-list ul.items li {
            list-style: none;
        }

        .dropdown-check-list.visible .anchor {
            color: #0094ff;
        }

        .dropdown-check-list.visible .items {
            display: block;
        }
    </style>

    <form action="{{ url_for('index') }}" method="POST">
        {{ form.hidden_tag() }}
        <div id="list1" class="dropdown-check-list" tabindex="100">
            <span class="anchor">Select Fruits</span>

            {{ form.select_multiple_field(class="items") }}

        </div>
        {{ form.submit }}
    </form>

    <script>
        var checkList = document.getElementById('list1');
        checkList.getElementsByClassName('anchor')[0].onclick = function (evt) {
            if (checkList.classList.contains('visible'))
                checkList.classList.remove('visible');
            else
                checkList.classList.add('visible');
        }
    </script>

</body>

</html>

enter image description here

==========================================

(this one depends on some JQuery library, but it feels closely to what you are looking for)

link to that library

main.py:

from flask import Flask, render_template, request
from flask_wtf import FlaskForm
from wtforms.fields import SelectMultipleField, SubmitField

app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret_key'


choices = [
    'apple',
    'banana',
    'cherry',
]


class Form(FlaskForm):
    global choices
    select_multiple_field = SelectMultipleField(choices=choices)
    submit = SubmitField()


@app.route('/', methods=['GET', 'POST'])
def index():
    form = Form()

    if request.method == 'POST':
        # Getting selected options
        form_data = form.select_multiple_field.data
        print(form_data)

    return render_template(
        'index.html',
        form=form,
    )


if __name__ == "__main__":
    app.run(debug=True)

index.html:

<!DOCTYPE html>
<html lang="en">

<head>
    <link href="https://unpkg.com/[email protected]/dist/multiple-select.min.css" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
    <script src="https://unpkg.com/[email protected]/dist/multiple-select.min.js"></script>
</head>

<body>

    <style>
        select {
            width: 20%;
        }
    </style>

    <form action="{{ url_for('index') }}" method="POST">
        {{ form.hidden_tag() }}
        {{ form.select_multiple_field(**{"multiple": "multiple"}) }}
        {{ form.submit }}
    </form>

    <script>
        $(function () {
            $('select').multipleSelect({
                multiple: true,
                multipleWidth: 60
            })
        })
    </script>

</body>

</html>

enter image description here

==========================================

(feel free to contact me if this answer was not exactly what you are looking for and tell why so, I'll try to adjust it :D)

Solution 2:[2]

There is a solution but it is completely custom that doesn't depend on how wtf forms display the field, that's why it probably doesn't behave like normal wtf forms input. It is useful to customize each part in the field and how to display it

1- Create a custom macro to display the field [create a file named macros.html and place it inside the templates folder]

macros.html

{% macro multi_bootstrap_select_field(field, class="", options_classes="") %}
   {% if field %}
     {{ field.label }}: <select {% if field.id  %}id="{{field.id}}"{% endif %} {% if field.name  %}name="{{field.name}}"{% endif %} {% if class  %} class="{{class}}"{% endif %} {% if field.placeholder  %} placeholder="{{field.placeholder}}"{% endif %} {% if field.flags.required %}"required=required"{%endif%} {% if field.type|lower == 'selectmultiplefield' or field.type|lower == 'multicheckboxfield' %}multiple="multiple"{% endif %}>
       {% if field.choices %}
         {% for choice_index in range(field.choices|length)  %}
           {% if field.choices[choice_index]|length == 2 %}
             <option {% if field.name %}name="{{field.name}}"{% endif %} value="{{field.choices[choice_index][0]}}">{{field.choices[choice_index][1]}}</option>
           {% endif %}
         {% endfor %}
       {% endif %}
     </select>
     {% for error in field.errors %}
       <div class="alert alert-danger" role="alert">{{ error }}</div>
     {% endfor %}
   {% endif %}
{% endmacro %}

2- Import "macros.html" into the page containing the field

index.html

{% from 'macros.html' multi_bootstrap_select_field %}
 
{{ multi_bootstrap_select_field(form.nums, class="form-control") }}

forms.py

from flask_wtf import FlaskForm
from wtforms import SubmitField, SelectMultipleField, widgets
from wtforms.validators import InputRequired

class MultiCheckboxField(SelectMultipleField):
    widget = widgets.ListWidget(prefix_label=False)
    option_widget = widgets.CheckboxInput()



class addLyrics(FlaskForm):
    nums = MultiCheckboxField('Tags',
                           coerce=str,
                           choices=[(1, 'one'), (2, 'two'), (3, 'three')],
                           validators=[InputRequired()])
    submit = SubmitField("Create")

If you are going to pass additional arguments, you need to define them in your custom macro function

for example

{% macro multi_bootstrap_select_field(field, class="", options_classes="", placeholder="") %}

{{ multi_bootstrap_select_field(form.nums, class="form-control", placeholder="something") }}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2