title: Problem with setting up dynamic list of subforms with flask, wtforms and jinja2
body: <p>I'm fairly new to the Python world (planing to make a switch and leave the CurlyBracesCamelCaseWorld) and I'm working on the simple app go get exp in the whole initial stack (database, server, handling html pages &amp; assets, etc).</p>
  
  <p>So far so good, the pace of the development never ceases to amaze me and the amount of resources is hearth worming. </p>
  
  <p>But I've encounter the problem that I just can't get past by.</p>
  
  <p>What I try to do now:</p>
  
  <ul>
  <li>Have a main wtform with a static set of fields</li>
  <li>Add dynamically generated list of sub-forms - prepopulated with values and with custom labels</li>
  </ul>
  
  <p>In my apps scenario user will be able to specify the list of questions to answer - so there will be a form with common details + the dynamic list, that I can use to save the data to the database on submit.</p>
  
  <p>I have a whole list of issues here</p>
  
  <p>1) The main problem is that I can't read the subforms data in the .validate() when I submit the form</p>
  
  <p>2) The other thing is that I can't force the labels to show the custom values That I want to set dynamically</p>
  
  <p>3) I need to do more reading on handling csfr in subforms and how to work around that as well</p>
  
  <p>4) And the last one - how do I validate the subforms - for required fields, length, etc</p>
  
  <p>1 &amp; 2 is my main concern now and I have a feeling that the issues have the same root-cause
  My gut feeling tells me that the broken element id is meaningful ('content' for each subform string field, instead of indexed 'entries-0-content' - that I see on submit)</p>
  
  <p>I wasn't able to find the full example how to do that and I'm struggling to connect the pieces I collected. I prepared the simple code, both python &amp; jinja2 template, ready to run, to demonstrate the problems. I'll gladly post the full working code after I figure it out as I'd kill to find just that..</p>
  
  <p>So, the server -> </p>
  
  <pre class="lang-py prettyprint-override"><code>from flask import Flask, redirect, url_for, render_template
  from flask_wtf import FlaskForm
  from wtforms import StringField, FieldList, FormField, SubmitField, HiddenField, Label
  from wtforms.validators import DataRequired
  
  app = Flask(__name__, template_folder='flaskblog/templates')
  app.config['SECRET_KEY'] = 'SECRET_KEY-SECRET_KEY-SECRET_KEY'
  
  
  # subforms
  class SubForm(FlaskForm):
      # how to handle hidden id that I can use to properly commit that on submit?
      # entry_type_id = HiddenField()
  
      # validators for subforms don't work, but that's something I'll try to address later
      content = StringField(validators=[DataRequired()])
  
      # I use custom __init__ set custom label for the field - or rather I try, as it doesn't work..
      def __init__(self, custom_label=None, *args, **kwargs):
          # not sure if safe - even just for the subform! #
          # Without that, I get 'TypeError: argument of type 'CSRFTokenField' is not iterable' on main_form.validate_on_submit()
          kwargs['csrf_enabled'] = False
          FlaskForm.__init__(self, *args, **kwargs)
  
          if custom_label is not None:
              self.content.label = Label(self.content.id, custom_label)
              print(f'INIT // id: [{self.content.id}] // content.data: [{self.content.data}] // label: [{self.content.label.text}]')
  
  
  # main forms
  class MainForm(FlaskForm):
      title = StringField('title')
      entries = FieldList(FormField(SubForm))
      submit = SubmitField('Post')
  
  
  @app.route("/test", methods=['GET', 'POST'])
  def test_route():
      # the main form
      main_form = MainForm(title='title')
  
      # sub forms, created before validate_on_submit()
      sub_form_1 = SubForm(content='Default answer 1', custom_label='Question 1')
      sub_form_2 = SubForm(content='Default answer 2', custom_label='Question 2')
  
      main_form.entries.append_entry(sub_form_1)
      main_form.entries.append_entry(sub_form_2)
  
      if main_form.validate_on_submit():
          for entry in main_form.entries.entries:
              print(f'LOOP // id: [{entry.content.id}] // content.data: [{entry.content.data}] // label: [{entry.content.label.text}]')
  
          return redirect(url_for('test_route'))
  
      print(f'INSTANCE_1 // id: [{sub_form_1.content.id}] // content.data: [{sub_form_1.content.data}] // label: [{sub_form_1.content.label.text}]')
      print(f'INSTANCE_2 // id: [{sub_form_2.content.id}] // content.data: [{sub_form_2.content.data}] // label: [{sub_form_2.content.label.text}]')
  
      return render_template('test_form.html', title='Test Form', main_form=main_form, legend='Test Form')
  
  
  if __name__ == '__main__':
      app.run(debug=True)
  
  </code></pre>
  
  <p>And the html template -> </p>
  
  <pre class="lang-html prettyprint-override"><code>&lt;!DOCTYPE html&gt;
  &lt;html lang="en"&gt;
  &lt;head&gt;
      &lt;meta charset="utf-8"&gt;
      &lt;meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"&gt;
      &lt;title&gt; Confused -.-' &lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
  &lt;div class="content-section"&gt;
  
      &lt;form action="" method="post"&gt;
          {{ main_form.hidden_tag() }}
          {{ main_form.title.label(class="form-control-label") }}: {{ main_form.title(class="form-control form-control-lg") }}
  
          {% for entry_line in main_form.entries %}
              &lt;div class="form-group"&gt;
                  {{ entry_line.content.label(class="form-control-label") }}
                  {{ entry_line.content.data(class="form-control form-control-lg") }}
              &lt;/div&gt;
          {% endfor %}
  
          {# For the main form I use main_form.title(), main_form.submit(), etc - without .data(). #}
          {# If I try to do main_form.title.data() I get the ex that I can't call on 'str' #}
  
          {# But, for entry_lines, if I don't add .data() and just use entry_line.content()  #}
          {# I can see the input field, but it's prepopulated with HTML for that input instead of the value (that I see in that html) #}
  
          &lt;div class="form-group"&gt;
              {{ main_form.submit(class="btn btn-outline-info") }}
          &lt;/div&gt;
      &lt;/form&gt;
  
  &lt;/div&gt;
  &lt;/body&gt;
  &lt;/html&gt;
  
  </code></pre>
  
  <hr>
  
  <p>Debug on GET:</p>
  
  <pre><code>INIT // id: [content] // content.data: [Default answer 1] // label: [Question 1]
  INIT // id: [content] // content.data: [Default answer 2] // label: [Question 2]
  INSTANCE_1 // id: [content] // content.data: [Default answer 1] // label: [Question 1]
  INSTANCE_2 // id: [content] // content.data: [Default answer 2] // label: [Question 2]
  </code></pre>
  
  <p>Debug on POST:</p>
  
  <pre><code>INIT // id: [content] // content.data: [my ans 1] // label: [Question 1]
  INIT // id: [content] // content.data: [my ans 1] // label: [Question 2]
  LOOP // id: [entries-0-content] // content.data: [&lt;input id="content" name="content" type="text" value="my ans 1"&gt;] // label: [Content]
  LOOP // id: [entries-1-content] // content.data: [&lt;input id="content" name="content" type="text" value="my ans 1"&gt;] // label: [Content]
  INIT // id: [content] // content.data: [Default answer 1] // label: [Question 1]
  INIT // id: [content] // content.data: [Default answer 2] // label: [Question 2]
  INSTANCE_1 // id: [content] // content.data: [Default answer 1] // label: [Question 1]
  INSTANCE_2 // id: [content] // content.data: [Default answer 2] // label: [Question 2]
  </code></pre>
  
  <p>There is clearly some issue with ids (2x content vs entries-0-content) &amp; I get the first result twice.. (value="my ans 1")</p>
  
  <p>I'd love to be able to generate the full form list based on the list of questions (I just use a static 2 here), set the custom labels per subform and then get the data in the server, so I can do the rest of the job inn there.</p>
  
  <p>I can fight with validation and csfr myself after that, but having the working scaffolding seems like a valid first step. I've spend a lot of time poking around but I feel like I'm running in circle now. </p>
  
  <p>And ofc - if you think that my assumptions on how to achieve what I want to achieve are wrong and I should use  to do that - let me know. I want to write the right thing, not just something that works. </p>
  
  <p>Pastebin links, if you prefer </p>
  
  <ul>
  <li><a href="https://pastebin.com/Ye7VYede" rel="nofollow noreferrer">app.py</a></li>
  <li><a href="https://pastebin.com/RBsb2Z2D" rel="nofollow noreferrer">test_form.html</a></li>
  <li><a href="https://pastebin.com/Brm5mQgS" rel="nofollow noreferrer">result HTML page</a></li>
  </ul>
  
  <h1>EDIT - working code</h1>
  
  <p>Thanks to @Nick K9!</p>
  
  <pre><code>from flask import Flask, redirect, url_for, render_template, request
  from flask_wtf import FlaskForm
  from wtforms import StringField, FieldList, FormField, SubmitField, HiddenField, Label
  from wtforms.validators import DataRequired
  
  app = Flask(__name__, template_folder='flaskblog/templates')
  app.config['SECRET_KEY'] = 'SECRET_KEY-SECRET_KEY-SECRET_KEY'
  
  subform_datasource = {
      0: {'question': 'Question 1', 'answare': 'Answare 1'},
      1: {'question': 'Question 2', 'answare': 'Answare 2'}
  }
  
  
  # subforms
  class SubForm(FlaskForm):
      entry_type_id = HiddenField()
      content = StringField(validators=[DataRequired()])
  
  
  # main forms
  class MainForm(FlaskForm):
      title = StringField('title')
      entries = FieldList(FormField(SubForm))
      submit = SubmitField('Post')
  
  
  @app.route("/test", methods=['GET', 'POST'])
  def test_route():
      main_form = MainForm()
      if main_form.validate_on_submit():
          for entry in main_form.entries.entries:
              entry_message = (
                  f'POST // wtform id: [{entry.content.id}] '
                  f' //  entry_type_id id: [{entry.entry_type_id.data}]'
                  f' //  content.data: [{entry.content.data}]'
                  f' //  label: [{entry.content.label.text}]'
              )
              print(str(entry_message))
  
          return redirect(url_for('test_route'))
      elif request.method == 'GET':
          # You can indeed set the default values, but you need to pass the dict, not the SubForm instance!
          for key, subform in subform_datasource.items():
              main_form.entries.append_entry({'content': subform['answare'], 'entry_type_id': key})
  
      # Moved out from the constructor - on subform failed validation labels reset to the default value 'Content'
      # I guess that matching what was send to the form does not cast back the labels but creates the fresh instances with just the value
      # What, of course, makes sense - it's an edge case, no point in affecting performance for everyone
      for entry in main_form.entries.entries:
          entry.content.label.text = subform_datasource[entry.entry_type_id.data]['question']
  
      return render_template('test_form.html', title='Test Form', main_form=main_form, legend='Test Form')
  
  
  if __name__ == '__main__':
      app.run(debug=True)
  
  </code></pre>
  
  <p>2 links that also helped me </p>
  
  <ul>
  <li><a href="https://gist.github.com/doobeh/5d0f965502b86fee80fe" rel="nofollow noreferrer">an actual example I was looking for</a></li>
  <li><a href="https://www.reddit.com/r/flask/comments/3nn460/af_populate_fieldlistformfield/" rel="nofollow noreferrer">a question from the same realm</a> </li>
  </ul>