Skip to content

Commit

Permalink
refactor: use SelectChoice as SelectField choices
Browse files Browse the repository at this point in the history
- passing choices as `tuple` is deprecated
- makes the code much more simpler, provide stronger
  typing and more explicit arguments
- `<option>` can have custom attributes thanks to
  `SelectField.render_kw`
- `<optgroup>` rendering by passing a `dict` parameter
  is deprecated.
- `SelectField` does not render empty `<optgroup>`
  • Loading branch information
azmeuk committed Jul 22, 2023
1 parent f8fca13 commit 416842a
Show file tree
Hide file tree
Showing 10 changed files with 359 additions and 178 deletions.
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ Unreleased
- Added shorter format to :class:`~fields.DateTimeLocalField`
defaults :pr:`761`
- Stop support for python 3.7 :pr:`794`
- :class:`~fields.SelectField` refactor. Choices tuples and dicts are
deprecated in favor of :class:`~fields.Choice` :pr:`739`
- ``<option>`` HTML attributes can be passed using
:class:`~fields.Choice` :issue:`692` :pr:`739`

Version 3.0.1
-------------
Expand Down
85 changes: 46 additions & 39 deletions docs/fields.rst
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,28 @@ refer to a single input from the form.

.. autoclass:: MonthField(default field arguments, format='%Y:%m')

.. autoclass:: RadioField(default field arguments, choices=[], coerce=str)
.. autoclass:: SearchField(default field arguments)

.. autoclass:: SubmitField(default field arguments)

.. autoclass:: StringField(default field arguments)

.. code-block:: jinja
{{ form.username(size=30, maxlength=50) }}
.. autoclass:: TelField(default field arguments)

.. autoclass:: TimeField(default field arguments, format='%H:%M')

.. autoclass:: URLField(default field arguments)

Choice Fields
-------------

.. autoclass:: Choice

.. autoclass:: RadioField(default field arguments, choices=None, coerce=str)

.. code-block:: jinja
Expand All @@ -274,45 +295,43 @@ refer to a single input from the form.
Simply outputting the field without iterating its subfields will result in
a ``<ul>`` list of radio choices.

.. class:: SelectField(default field arguments, choices=[], coerce=str, option_widget=None, validate_choice=True)

.. class:: SelectField(default field arguments, choices=None, coerce=str, option_widget=None, validate_choice=True)

Select fields take a ``choices`` parameter which is either:

* a list of ``(value, label)`` pairs. It can also be a list of only values, in
which case the value is used as the label. The value can be of any
* a list of :class:`Choice`.
It can also be a list of only values, in which case the value is used
as the label. The value can be of any
type, but because form data is sent to the browser as strings, you
will need to provide a ``coerce`` function that converts a string
back to the expected type.
* a dictionary of ``{label: list}`` pairs defining groupings of options.
* a function taking no argument, and returning either a list or a dictionary.
* a function taking no argument, and returning a list of :class:`Choice`.


**Select fields with static choice values**::

class PastebinEntry(Form):
language = SelectField('Programming Language', choices=[('cpp', 'C++'), ('py', 'Python'), ('text', 'Plain Text')])

Note that the `choices` keyword is only evaluated once, so if you want to make
a dynamic drop-down list, you'll want to assign the choices list to the field
after instantiation. Any submitted choices which are not in the given choices
list will cause validation on the field to fail. If this option cannot be
applied to your problem you may wish to skip choice validation (see below).
language = SelectField('Programming Language', choices=[
Choice('cpp', 'C++'),
Choice('py', 'Python'),
Choice('text', 'Plain Text'),
])

**Select fields with dynamic choice values**::

def available_groups():
return [Choice(g.id, g.name) for g in Group.query.order_by('name')]

class UserDetails(Form):
group_id = SelectField('Group', coerce=int)
group_id = SelectField('Group', coerce=int, choices=available_groups)

def edit_user(request, id):
user = User.query.get(id)
form = UserDetails(request.POST, obj=user)
form.group_id.choices = [(g.id, g.name) for g in Group.query.order_by('name')]

Note we didn't pass a `choices` to the :class:`~wtforms.fields.SelectField`
constructor, but rather created the list in the view function. Also, the
`coerce` keyword arg to :class:`~wtforms.fields.SelectField` says that we
use :func:`int()` to coerce form data. The default coerce is
:func:`str()`.
Note that the `coerce` keyword arg to :class:`~wtforms.fields.SelectField` says
that we use :func:`int()` to coerce form data. The default coerce is :func:`str()`.

**Coerce function example**::

Expand All @@ -322,7 +341,11 @@ refer to a single input from the form.
return value

class NonePossible(Form):
my_select_field = SelectField('Select an option', choices=[('1', 'Option 1'), ('2', 'Option 2'), ('None', 'No option')], coerce=coerce_none)
my_select_field = SelectField('Select an option', choices=[
Choice('1', 'Option 1'),
Choice('2', 'Option 2'),
Choice('None', 'No option'),
], coerce=coerce_none)

Note when the option None is selected a 'None' str will be passed. By using a coerce
function the 'None' str will be converted to None.
Expand All @@ -347,29 +370,13 @@ refer to a single input from the form.
a list of fields each representing an option. The rendering of this can be
further controlled by specifying `option_widget=`.

.. autoclass:: SearchField(default field arguments)

.. autoclass:: SelectMultipleField(default field arguments, choices=[], coerce=str, option_widget=None)
.. autoclass:: SelectMultipleField(default field arguments, choices=None, coerce=str, option_widget=None)

The data on the SelectMultipleField is stored as a list of objects, each of
which is checked and coerced from the form input. Any submitted choices
which are not in the given choices list will cause validation on the field
to fail.

.. autoclass:: SubmitField(default field arguments)

.. autoclass:: StringField(default field arguments)

.. code-block:: jinja
{{ form.username(size=30, maxlength=50) }}
.. autoclass:: TelField(default field arguments)

.. autoclass:: TimeField(default field arguments, format='%H:%M')

.. autoclass:: URLField(default field arguments)


Convenience Fields
------------------
Expand Down Expand Up @@ -459,7 +466,7 @@ complex data structures such as lists and nested objects can be represented.
FormField::

class IMForm(Form):
protocol = SelectField(choices=[('aim', 'AIM'), ('msn', 'MSN')])
protocol = SelectField(choices=[Choice('aim', 'AIM'), Choice('msn', 'MSN')])
username = StringField()

class ContactForm(Form):
Expand Down
10 changes: 5 additions & 5 deletions docs/widgets.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,13 @@ class. For example, here is a widget that renders a
kwargs.setdefault('type', 'checkbox')
field_id = kwargs.pop('id', field.id)
html = ['<ul %s>' % html_params(id=field_id, class_=ul_class)]
for value, label, checked in field.iter_choices():
choice_id = '%s-%s' % (field_id, value)
options = dict(kwargs, name=field.name, value=value, id=choice_id)
if checked:
for choice in field.iter_choices():
choice_id = '%s-%s' % (field_id, choice.value)
options = dict(kwargs, name=field.name, value=choice.value, id=choice_id)
if choice._selected:
options['checked'] = 'checked'
html.append('<li><input %s /> ' % html_params(**options))
html.append('<label for="%s">%s</label></li>' % (choice_id, label))
html.append('<label for="%s">%s</label></li>' % (choice_id, choice.label))
html.append('</ul>')
return ''.join(html)

Expand Down

0 comments on commit 416842a

Please sign in to comment.