Can input elements being positioned in Drupal 10 into a table

I have in Drupal 10 made a simple table that is being build with a PHP as part of a module.

When I use a buildForm-function, I can not position the input fields. I want these to be as if they were generated in the table column. That would be easy if all was pure in the PHP module directly. But how is that done with Drupal Forms when the fields are in a buildForm.

I tried with CSS positioning (absolute ) but this is not really good enough, because of the dynamic behavior of the table.

PHP CODE:

foreach ($questions as $question) {
  $sq_arr = explode(';',$question);
  $result_s .= '<tr>';
  $result_s .= '<td>' . $sq_arr[1] . ' : ' . $sq_arr[0] .'</td>';
  $result_s .= '    <td>'.$sq_arr[4].'</td>';
  $result_s .= '    <td> HERE THE INPUT ELEMENT </td>';
  $result_s .= '    <td>'.$sq_arr[5].'</td>';
  $result_s .= '</tr>';
}

buildForm Code:

public function buildForm(array $form, FormStateInterface $form_state) {

$form['userinput_0'] = [
    '#type' => 'textfield',
    '#title' => '',
    '#placeholder' => 'Enter Answer 1',
    '#attributes' => array(
       'id' => 'userinput_0',
       'autofocus' => 'autofocus',
       ) ] etc...

Visualisation:


I also found a another way of working, by building the table in the buildForm. But how do you pass an array (here $allData) to your buildForm from the module function?

public function buildForm(array $form, FormStateInterface $form_state) {
    
// This data should come from a function I have.
$allData = [
  ['tense+verb' => 'Indicatif Présent'          , 'person' => 'nous'    , 'your_answer' => '', 'correct_answer' => 'retournons'],
  ['tense+verb' => 'Indicatif Passé Composé', 'person' => 'nous'    ,' your_answer' => '', 'correct_answer' => 'avons isolé'],
  ['tense+verb' => 'Indicatif Passé Composé', 'person' => 'elles'   , 'your_answer' => '', 'correct_answer' => 'ont pesé'],
  ['tense+verb' => 'Indicatif Passé Composé', 'person' => 'elles'   , 'your_answer' => '', 'correct_answer' => 'ont pesé'],
  ['tense+verb' => 'Indicatif Passé Composé', 'person' => 'elles'   , 'your_answer' => '', 'correct_answer' => 'ont pesé'],
];

$form['my_table'] = [
  '#type' => 'table',
        '#header' => ['Tense+Verb','Person','Your Answer','Correct Answer'],
  '#prefix' => '<div class="table table-hover table-mc-light-blue">',
  '#suffix' => '</div>',
];
    

    foreach ($allData as $index => $row) {
        
        $form['my_table'][$index]['tense+verb'] = [
            '#markup' => $row['tense+verb'],
            ];

        $form['my_table'][$index]['person'] = [
            '#markup' => $row['person'],
        ];

        $form['my_table'][$index]['your answer'] = [
            '#type' => 'textfield',
            '#title' => '',
    '#size' => 30,
            '#attributes' => array(
                //'style' => 'height: 20px;',
                'placeholder' => 'Your answer',
                'id' => 'userinput_0',
                'autofocus' => 'autofocus',
            ),
        ];

        $form['my_table'][$index]['correct answer'] = [
            '#markup' => $row['correct_answer'],
        ];
    }

To position the input fields inside your table rows in Drupal correctly using buildForm(), you’re already on the right track with your second approach: using a #type => 'table' inside the buildForm() method. This way, Drupal keeps the form system and rendering under control without relying on manually constructed HTML.

Best Practice: Build Table in buildForm() with Form Elements

Here’s how you can cleanly and dynamically pass your data ($allData) into buildForm():

Step 1: Pass $allData to buildForm

You can pass external data into buildForm() by setting it in $form_state or using dependency injection or a service/controller. Here’s a simple way via $form_state:

// Controller or wherever you call the form:
$form_state = new \Drupal\Core\Form\FormState();
$form_state->set('my_data', $allData);
return \Drupal::formBuilder()->getForm('Drupal\your_module\Form\YourFormClass', $form_state);

Step 2: Access That Data in buildForm

In your buildForm() function:

public function buildForm(array $form, FormStateInterface $form_state) {
  $allData = $form_state->get('my_data') ?? [];

  $form['my_table'] = [
    '#type' => 'table',
    '#header' => ['Tense+Verb', 'Person', 'Your Answer', 'Correct Answer'],
    '#prefix' => '<div class="table table-hover table-mc-light-blue">',
    '#suffix' => '</div>',
  ];

  foreach ($allData as $index => $row) {
    $form['my_table'][$index]['tense_verb'] = [
      '#markup' => $row['tense+verb'],
    ];

    $form['my_table'][$index]['person'] = [
      '#markup' => $row['person'],
    ];

    $form['my_table'][$index]['your_answer'] = [
      '#type' => 'textfield',
      '#title' => '',
      '#size' => 25,
      '#attributes' => [
        'placeholder' => 'Your answer',
        'class' => ['user-answer-field'],
      ],
    ];

    $form['my_table'][$index]['correct_answer'] = [
      '#markup' => $row['correct_answer'],
    ];
  }

  $form['submit'] = [
    '#type' => 'submit',
    '#value' => $this->t('Check Answers'),
  ];

  return $form;
}

Key Tips:

  • Avoid absolute positioning in CSS unless you’re building something visually complex. Drupal will handle positioning when using form API correctly.
  • Use #type => 'table' with nested form elements to ensure alignment and form state compatibility.
  • Each field will retain its proper name and be accessible in submitForm().

Bonus: Getting Values in submitForm()

To access user answers later:

public function submitForm(array &$form, FormStateInterface $form_state) {
  $values = $form_state->getValue('my_table');
  foreach ($values as $index => $row) {
    $user_input = $row['your_answer'];
    // Process the user input...
  }
}