Wtforms, Add a Class to a Form Dynamically

Wtforms, add a class to a form dynamically

WTForms does not allow you to set display options (such as class name) in the field initialization. However, there are several ways to get around this:

  1. If all of your fields should include a class name as well as an ID then just pass in each field's short_name to it when you render it:

    <dl>
    {% for field in form %}
    <dt>{{field.label}}</dt>
    <dd>{{field(class_=field.short_name)}}</dd>
    {% endfor %}
    </dl>
  2. Create a custom widget mixin that provides the class name:

    from wtforms.fields import StringField
    from wtforms.widgets import TextInput

    class ClassedWidgetMixin(object):
    """Adds the field's name as a class
    when subclassed with any WTForms Field type.

    Has not been tested - may not work."""
    def __init__(self, *args, **kwargs):
    super(ClassedWidgetMixin, self).__init__(*args, **kwargs)

    def __call__(self, field, **kwargs):
    c = kwargs.pop('class', '') or kwargs.pop('class_', '')
    kwargs['class'] = u'%s %s' % (field.short_name, c)
    return super(ClassedWidgetMixin, self).__call__(field, **kwargs)

    # An example
    class ClassedTextInput(ClassedWidgetMixin, TextInput):
    pass

    class Company(Form):
    company_name = StringField('Company Name', widget=ClassedTextInput)

Add fields dynamically to WTForms form

Use setattr to add new fields as attributes of the form class. This will cause WTForms to set up the field correctly instead of keeping the unbound field.

# form class with static fields
class MyForm(FlaskForm):
name = StringField('static field')

record = {'field1': 'label1', 'field2': 'label2'}

# add dynamic fields
for key, value in record.items():
setattr(MyForm, key, StringField(value))

In the template you can iterate over the fields using the attr filter.

{% for key, value in record.items() %}:
{{ form|attr(key)() }}
{% endfor %}

Dynamically Add or Remove WTForms validators in Flask

Dynamically create an internal subclasses of the form within your view. Remove any validators from the fields of the internal subclass and then instance a form from the internal subclass. In code, something like:

Define a form, the first_name field has two validators.

class TestForm(FlaskForm):
first_name = StringField(validators=[InputRequired(), Length(8)])
submit = SubmitField()

In your view:

# Dynamic subclass
class F(TestForm):
pass

# Remove the length validator.
# Validators are in a dict called kwargs
validators = F.first_name.kwargs.get('validators')
for validator in validators:
if isinstance(validator, Length):
validators.remove(validator)

# instance a form
_form = F()
# use the form ....

Single file example app.py:

from flask import Flask, render_template_string
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import InputRequired, Length

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

class TestForm(FlaskForm):
first_name = StringField(validators=[InputRequired(), Length(8)])
submit = SubmitField()

html_template = '''
{% if form.first_name.errors %}
<ul class="errors">
{% for error in form.first_name.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
<form role="form" method="post" action="" name="Form1">
{{ form.hidden_tag() }}
{{ form.first_name }}
{{ form.submit() }}
</form>
'''

success_template = '''
<h1>Success</h1>
'''

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

# Dynamic subclass
class F(TestForm):
pass

# Remove the length validator.
# Validators are in a dict called kwargs
validators = F.first_name.kwargs.get('validators')
for validator in validators:
if isinstance(validator, Length):
validators.remove(validator)

# instance a form
_form = F()
# print the validators
for field in _form:
print(f"Field: {field.name} has {len(field.validators)} validator(s)")
for validator in field.validators:
print(validator)

if _form.validate_on_submit():
print('Validation passed')
return render_template_string(success_template)

return render_template_string(html_template, form=_form)

if __name__ == '__main__':
app.run()

Add a css class to a field in wtform

You actually don't need to go to the widget level to attach an HTML class attribute to the rendering of the field. You can simply specify it using the class_ parameter in the jinja template.

e.g.

    {{ form.email(class_="form-control") }}

will result in the following HTML::

    <input class="form-control" id="email" name="email" type="text" value="">

to do this dynamically, say, using the name of the form as the value of the HTML class attribute, you can do the following:

Jinja:

    {{ form.email(class_="form-style-"+form.email.name) }}

Output:

    <input class="form-style-email" id="email" name="email" type="text" value="">

For more information about injecting HTML attributes, check out the official documentation.

Submit WTform with dynamically generated fields

You can only have a single form result per form submission. To be able to submit an arbitrary and unknown number of inputs, you need to restructure your form with the help of WTForm's field enclosures.

forms.py

from flask_wtf import FlaskForm
from wtforms import (
FieldList, FormField, DateField FloatField, StringField, SelectField)
from wtforms import Form as NoCsrfForm

class ExpenseItem(NoCsrfForm):
expense_name = StringField('Expense_Item', validators=[DataRequired()])
cost = FloatField('Cost', validators=[DataRequired()])
due_date = DateField('Due Date', format='%Y-%m-%d',
validators=[DataRequired()],
default=datetime.datetime.today().date())
type = SelectField('Role', choices=[
('mutual', 'Mutual'),
('personal#1', 'Personal #1'),
('personal#2', 'Personal #2'),
])

class ExpensesForm(FlaskForm):
"""A collection of expense items."""
items = FieldList(FormField(ExpenseItem), min_entries=1)

I'd strongly recommend that you preface all your field names with expense, not just expense_name for sanity's sake.

index.html

<form class="form-horizontal" id="main-form" enctype=multipart/form-data role="form" method="post" action="/">
<input type="hidden" name="count" value="1"/>
{{ form.hidden_tag() }}
{% for expense_item in form.items %}
{{ form.expense_name(placeholder="Expense Name", value="") }}
{{ form.cost(placeholder="Cost", class="cost", value="") }}
{{ form.due_date() }}
{{ form.type(placeholder="Type") }}
{% endfor %}

<button id="b1" class="btn btn-info add-more" type="button">+</button>
<small>Press + to add another set of fields.</small>
<br>
<hr>
<button class="btn btn-sm btn-success" type="submit">Post Expense</button>
</form>

Note that the id attribute of the HTML input fields must follow a particular pattern. So for every new expense item field which you add by clicking on the + button, you need to re-number the id attribute of its input fields.

something.js

Everything else was comparatively easy. You now need to write a piece of .js which will re-index the id attributes of all the input fields every time a new expense item is added. I accomplished this using the Zepto library for Javascript. It wasn't fun, and my .js is terrible. The best I can do here is just paste the whole thing and hope it'll be of service to you. I know it's confusing, but I was added multiple classes to a course. For you, you'll want expense_item/expense_request or whatever you go with:

// append class-box when new class link clicked
$("#new-class").click(function(event) {
appendClassBox('#classes', {{ newclass|tojson|safe }});
reindexNames('.class-box');
return false;
})

// remove class box when its "remove" link is clicked
$(document).on('click', '#remove-class', function(){
var $toremove = $(this).closest('.class-box');
$toremove.remove();
reindexNames('.class-box');
return false;
})

// add a new class-box
function appendClassBox(container, content) {
$(container).append(content);
// raise last and hence newest class box
raiseClassBox($(container).children().last())
return false;
}

function isNormalInteger(str) {
var n = ~~Number(str);
return String(n) === str && n >= 0;
}

// re-index class-box names
function reindexNames(class_name) {
var $oboxen = $(class_name);
$oboxen.each(function(index) {
// Get all the input fields in the class-box.
var $labels = $oboxen.eq(index).find('label')
var $inputs = $oboxen.eq(index).find(
'input, select, textarea')
// Update the index contained in the name attribute.
$inputs.each(function(idx) {
var $name = $inputs.eq(idx).attr('name').split('-');
// If number in name, grab from number onwards.
var $has_num = false
for (var part in $name) {
if (isNormalInteger($name[part])) {
$has_num = true
$name = $name.slice(part)
$name[0] = index
break
}
}
// Re-index.
if ($has_num == false) {
$name.unshift(index)
}
var $prefix = 'questions'
if (class_name == '.class-box') {
$prefix = 'classes'
}
$name.unshift($prefix)
if (idx > 0) {
$labels.eq(idx - 1).attr('for', $name.join('-'));
}
$inputs.eq(idx).attr('id', $name.join('-'));
$inputs.eq(idx).attr('name', $name.join('-'));
})
})
}

views.py

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

# Iterate over a collection of new expense items.
if form.validate_on_submit():
for item in form.items.data:
print(item['expense_name'])
print(item['cost'])
print(item['due_date'])
print(item['type'])

create dynamic fields in WTform in Flask

Adding fields dynamically

I think best would be to create an empty form and then add fields in a for-loop, however if a form is posted back how can I validate the form if I don't have it defined in a class?

Add the fields to the form class using setattr before the form is instantiated:

def update_form(request):
table = query()

class MyForm(Form):
pass

for row in table:
setattr(MyForm, row.key, SomeField())

form = MyForm(request.form)

However, I think your question is part of a bigger problem, which I have tried to address below.

Mapping tables to forms

Your table seem to map very nicely to the form itself. If you want to create forms dynamically from your tables, you could write the logic yourself. But when the range of fields and options to support grows, it can be a lot of work to maintain. If you are using SQLAlchemy, you might want to take a look at WTForms-Alchemy. From its introduction:

Many times when building modern web apps with SQLAlchemy you’ll have
forms that map closely to models. For example, you might have a
Article model, and you want to create a form that lets people post new
article. In this case, it would be time-consuming to define the field
types and basic validators in your form, because you’ve already
defined the fields in your model.

WTForms-Alchemy provides a helper class that let you create a Form
class from a SQLAlchemy model.

The helper class is ModelForm, and in the style of your table, below is a Python 2/3 sample with WTForms-Alchemy. Install the package wtforms-alchemy first, which will pull in SQLAlchemy and WTForms as well.

from __future__ import print_function
from __future__ import unicode_literals

import sqlalchemy as sa
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from wtforms_alchemy import ModelForm

engine = create_engine('sqlite:///:memory:')
Base = declarative_base(engine)
Session = sessionmaker(bind=engine)
session = Session()

class MyClass(Base):
__tablename__ = 'mytable'

id = sa.Column(sa.BigInteger, autoincrement=True, primary_key=True)
title = sa.Column(sa.Unicode(5), nullable=False)
gender = sa.Column(sa.Enum('male', 'female', name='gender'))
text = sa.Column(sa.Text)

class MyForm(ModelForm):
class Meta:
model = MyClass

form = MyForm()

print('HTML\n====')
for field in form:
print(field)

Running the above code prints:

HTML
====
<input id="title" name="title" required type="text" value="">
<select id="gender" name="gender"><option value="male">male</option><option value="female">female</option></select>
<textarea id="text" name="text"></textarea>

As you can see, WTForms-Alchemy did a whole lot with MyForm. The class is essentially this:

class MyForm(Form):
title = StringField(validators=[InputRequired(), Length(max=5)])
gender = SelectField(choices=[('male', 'male'), ('female', 'female')])
text = TextField()

The documentation for WTForms-Alchemy seems to be very comprehensive. I have not used it myself, but if I had a similar problem to solve I would definitely try it out.

Dynamic Flask-Form construction intended for use under Jinja

The fields in a wtforms.Form class are set up by the wtforms.forms.FormMeta metaclass. By the time a form instance's __init__ method is called it's too late to add a new field to the form.

You could modify your form class dynamically, as described in the documentation. Note that this will affect all future instances of the class.

setattr(LoginForm, 'submit', wtforms.SubmitField('Hit here'))
form = LoginForm()

If you only want to customise the label for a field in a particular instance, you can do this in the instance's __init__ method.

class LoginForm(wtforms.Form): 

submit = wtforms.SubmitField()

def __init__(self, label, **kwargs):
super().__init__()
self.submit.label = wtforms.Label(self.submit.id, label)


Related Topics



Leave a reply



Submit