WTForms FieldList subform entries disappear after form submission
I’m implementing a web page in Python using WTForms with FieldList to populate subform data. When the page first renders, the form correctly displays two subform entries. However, after submitting a SubmitField, one of the subform entries disappears, even though I’m supplying the exact same dictionary to populate the form each time.
Here are my form and subform classes:
class RunControl(FlaskForm):
start = SubmitField('Start Run')
delete = SubmitField('Delete Run')
plot = SubmitField('Plot')
runID = StringField('Run ID', render_kw={'readonly': True})
imageURL = StringField('Image URL', render_kw={'readonly': True})
class RunsForm(FlaskForm):
runs = FieldList(FormField(RunControl))
stop = SubmitField('Stop Run')
activeRunID = StringField('Active Run ID', render_kw={'readonly': True})
activeDescr = StringField('Active Run Description', render_kw={'readonly': True})
And here’s the beginning of my view function:
@app.route('/', methods=['GET', 'POST'])
def index():
runList = getRunList()
runsForm = RunsForm(runs=runList, min_entries=len(runList))
Each time the view function executes, runList
contains the same dictionary with two items. However, when I add a debug statement to print the length of runsForm.runs
right after creating the form, it shows:
- 2 entries when the page first loads
- 1 entry after submitting a SubmitField and the view function is called again
What could be causing this behavior? Why would the form initialization ignore one of the dictionary items on the second pass? Am I misunderstanding how min_entries
works in WTForms?
WTForms FieldList Subform Entries Disappearing After Submission
Brief Answer
The issue is likely caused by how WTForms processes FieldList entries containing submit buttons. When your form is submitted, WTForms may not properly identify all subform entries if their submit buttons don’t follow the correct naming convention. To fix this, ensure you’re using the prefix
parameter correctly when processing the form and that your form submission handling accounts for all FieldList entries.
Contents
- Understanding the Problem
- Potential Causes for Missing Entries
- Solutions and Best Practices
- Debugging FieldList Issues
- Complete Example Implementation
Understanding the Problem
When working with WTForms FieldList containing subforms with submit buttons, you’ve observed that the form initially displays correctly with all entries, but after submission, some entries disappear. This behavior is particularly common when:
- Subforms contain their own submit fields
- The form processing doesn’t properly account for all FieldList entries
- The FieldList isn’t being repopulated correctly after form submission
The core issue lies in how WTForms processes submitted data from FieldLists with multiple submit buttons. Each submit button in your subforms has a specific naming convention (like runs-0-start
, runs-1-delete
, etc.), and WTForms uses these names to determine which subform entry should be processed.
Potential Causes for Missing Entries
1. Submit Field Processing Issues
When a submit button inside a FieldList is clicked, WTForms uses the field’s name to identify which subform entry should be processed. If there’s a mismatch between how the fields are named and how WTForms expects them to be named, it might not recognize all entries.
2. Form Data Processing Without Proper Prefixing
In your view function, you’re creating the form with:
runsForm = RunsForm(runs=runList, min_entries=len(runList))
However, when processing POST requests, you need to ensure the form is properly populated with submitted data. Without proper prefixing or data handling, WTForms might not reconstruct all FieldList entries correctly.
3. min_entries Misunderstanding
The min_entries
parameter ensures at least that many empty form entries are created initially. However, it doesn’t guarantee that entries will be preserved after form submission if the submitted data doesn’t include all entries.
4. FieldList Validation and Processing
FieldList has specific validation and processing requirements that differ from regular forms. When processing submissions, WTForms might be dropping entries that don’t have corresponding submitted data.
Solutions and Best Practices
Solution 1: Proper Form Processing in View
Modify your view function to properly handle both GET and POST requests:
@app.route('/', methods=['GET', 'POST'])
def index():
runList = getRunList()
runsForm = RunsForm()
if request.method == 'POST':
# Process the form with submitted data
runsForm = RunsForm(request.form)
if runsForm.validate():
# Process form data
pass
else:
# For GET requests, populate with existing data
runsForm = RunsForm(runs=runList, min_entries=len(runList))
return render_template('your_template.html', form=runsForm)
Solution 2: Use FormField Correctly with Submit Fields
When using FormField with submit fields, ensure your subform doesn’t interfere with the parent form’s processing. Consider moving submit buttons to the parent form or handling them differently:
class RunControl(FlaskForm):
# Remove submit fields from subform
start = HiddenField() # Instead of SubmitField
delete = HiddenField()
plot = HiddenField()
runID = StringField('Run ID', render_kw={'readonly': True})
imageURL = StringField('Image URL', render_kw={'readonly': True})
class RunsForm(FlaskForm):
runs = FieldList(FormField(RunControl))
# Add submit buttons here instead
start = SubmitField('Start Selected')
delete = SubmitField('Delete Selected')
plot = SubmitField('Plot Selected')
stop = SubmitField('Stop Run')
activeRunID = StringField('Active Run ID', render_kw={'readonly': True})
activeDescr = StringField('Active Run Description', render_kw={'readonly': True})
Solution 3: Dynamic Entry Management
Implement a proper mechanism for managing entries:
@app.route('/', methods=['GET', 'POST'])
def index():
runList = getRunList()
runsForm = RunsForm()
if request.method == 'POST':
# Process the form with submitted data
runsForm = RunsForm(request.form)
# Ensure we have the correct number of entries
current_entries = len(runsForm.runs)
desired_entries = len(runList)
if current_entries < desired_entries:
# Add missing entries
for i in range(desired_entries - current_entries):
runsForm.runs.append_entry()
elif current_entries > desired_entries:
# Remove extra entries
del runsForm.runs[desired_entries:]
if runsForm.validate():
# Process form data
pass
else:
# For GET requests, populate with existing data
runsForm = RunsForm(runs=runList)
return render_template('your_template.html', form=runsForm)
Debugging FieldList Issues
Debugging Steps
-
Add Debug Logging:
pythonprint(f"Form entries: {len(runsForm.runs)}") for i, entry in enumerate(runsForm.runs): print(f"Entry {i}: ID={entry.runID.data}")
-
Check Submitted Data:
pythonif request.method == 'POST': print("Submitted form data:") for key in request.form.keys(): print(f"{key}: {request.form[key]}")
-
Inspect Form Population:
python# After form creation print(f"Initial form entries: {len(runsForm.runs)}") for i, entry in enumerate(runsForm.runs): print(f"Entry {i}: runID={entry.runID.data}")
Common Pitfalls
- Missing CSRF Token: Ensure your template includes the CSRF token for each FieldList entry.
- Incorrect Field Names: Verify that field names match the expected pattern (
fieldlistname-entrynumber-fieldname
). - Template Rendering Issues: Check how you’re rendering the FieldList in your template.
Complete Example Implementation
Here’s a complete working example that addresses the issue:
Form Classes
from flask import Flask, render_template, request
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, FormField, FieldList, HiddenField
from wtforms.validators import DataRequired
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
class RunControl(FlaskForm):
runID = StringField('Run ID', validators=[DataRequired()], render_kw={'readonly': True})
imageURL = StringField('Image URL', render_kw={'readonly': True})
# Use HiddenField instead of SubmitField to avoid processing issues
start = HiddenField()
delete = HiddenField()
plot = HiddenField()
class RunsForm(FlaskForm):
runs = FieldList(FormField(RunControl), min_entries=1)
# Submit buttons in the parent form
start_selected = SubmitField('Start Selected')
delete_selected = SubmitField('Delete Selected')
plot_selected = SubmitField('Plot Selected')
stop = SubmitField('Stop Run')
activeRunID = StringField('Active Run ID', render_kw={'readonly': True})
activeDescr = StringField('Active Run Description', render_kw={'readonly': True})
def getRunList():
# Mock function - replace with your actual data source
return [
{'runID': 'run1', 'imageURL': 'url1'},
{'runID': 'run2', 'imageURL': 'url2'}
]
@app.route('/', methods=['GET', 'POST'])
def index():
runList = getRunList()
runsForm = RunsForm()
if request.method == 'POST':
# Process the form with submitted data
runsForm = RunsForm(request.form)
# Ensure we have the correct number of entries
current_entries = len(runsForm.runs)
desired_entries = len(runList)
if current_entries < desired_entries:
# Add missing entries
for i in range(desired_entries - current_entries):
runsForm.runs.append_entry()
elif current_entries > desired_entries:
# Remove extra entries
del runsForm.runs[desired_entries:]
if runsForm.validate():
# Process form data based on which button was clicked
if 'start_selected' in request.form:
# Handle start action
pass
elif 'delete_selected' in request.form:
# Handle delete action
pass
elif 'plot_selected' in request.form:
# Handle plot action
pass
else:
# For GET requests, populate with existing data
runsForm = RunsForm(runs=runList, min_entries=len(runList))
return render_template('index.html', form=runsForm)
if __name__ == '__main__':
app.run(debug=True)
Template Example (index.html)
<!DOCTYPE html>
<html>
<head>
<title>WTForms FieldList Example</title>
</head>
<body>
<form method="POST" action="">
{{ form.hidden_tag() }}
<h2>Runs List</h2>
{% for run in form.runs %}
<div style="border: 1px solid #ccc; padding: 10px; margin-bottom: 10px;">
<p>Run ID: {{ run.runID.data }}</p>
<p>Image URL: {{ run.imageURL.data }}</p>
{# Hidden fields for action tracking #}
{{ run.start }}
{{ run.delete }}
{{ run.plot }}
</div>
{% endfor %}
<div>
{{ form.start_selected() }}
{{ form.delete_selected() }}
{{ form.plot_selected() }}
</div>
<hr>
<div>
<p>Active Run ID: {{ form.activeRunID.data }}</p>
<p>Active Run Description: {{ form.activeDescr.data }}</p>
{{ form.stop() }}
</div>
</form>
</body>
</html>
Conclusion
The disappearing entries issue with WTForms FieldList typically stems from how submit fields within subforms are processed during form submission. By moving submit buttons to the parent form or using HiddenField instead of SubmitField in subforms, you can avoid this problem. Additionally, ensure proper form processing in your view function, including handling both GET and POST requests correctly and maintaining the correct number of entries throughout the form lifecycle.
Key recommendations:
- Consider moving submit fields to the parent form when working with FieldList
- Implement proper entry management in your view function
- Add debugging code to track form state during processing
- Ensure your template correctly renders all form entries and hidden fields
With these changes, your FieldList should maintain all entries consistently across form submissions.