Hero

Como crear un formulario multiple dentro de una tabla en Drupal 7

Marzo 08, 2014

enzo
Drupal
Desarrollo de Modulos

Para los programadores nuevos en Drupal que vienen de frameworks orientados a transacciones y que desean realizar labores en batch es frustrante que casi todo los proceso de ingreso de información es orientado a una sola entidad.

Obviamente contamos con los conceptos de entity_referece y field_collection pero al final de cuentas estos se relacionan a una única entidad.

Bueno hoy les traigo no una solución a esa frustración pero quizás un bien punto de partida, que puedes utilizar en tus módulos personalizados para tu propio modelo de datos.

  1. El problema.

Imaginemos que queremos hacer el típico ejemplo de modelo entidad relación maestro detalle y usamos el ejemplo aun más típico el de Orden y Detalle de la orden.

Lo idea seria tener un página en Drupal en la que podamos crear ordenes con un numero ilimitado y flexible de líneas de la orden sin andar recargando la página.

Lo anterior nos plantea el problema ¿como mostramos eso en pantalla y como hacemos para que sea flexible?

  1. La solución.

Esta implementación no se preocupa por el modelo de datos, sino por la recolección de datos. Para lo cual utilizaremos el elemento FAPI tableselect como lo vemos en la siguiente función que define un formulario.

/*
 * Function to create form to add an order.
 */
function MIMODULO_add_order($form, &$form_state) {

   $form_state['storage']['lines'] = isset($form_state['storage']['lines'])? $form_state['storage']['lines']:1;

   $form['order_lines_wrapper'] = array(
     '#type' => 'fieldset',
     '#title' => t('Order Lines'),
     '#collapsible' => FALSE,
     '#collapsed' => FALSE,
   );

   $form['order_lines_wrapper']['order_lines'] = array(
     '#type' => 'container',
     '#tree' => TRUE,
     '#prefix' => '<div id="order_lines">',
     '#suffix' => '</div>',
   );

   $header = array (
     'product' => t('Product'),
     'price' => t('Price'),
     'quantity' => t('Quantity'),
     'subtotal' => t('Subtotal'),
   );

   $options = array();
   for ($i = 1; $i <= $form_state['storage']['lines']; $i++) {
    $options[$i] =array
    (
      'product' => array(
        'data' => array(
          'product_' . $i => array(
            '#type' => 'textfield',
            '#value' => isset($form_state['input']['product_' . $i])?$form_state['input']['product_' . $i]:'',
            '#name' => 'product_' . $i,
            '#size' => 40,
           )
         ),
      ),
      'price' => array(
        'data' => array(
          'price_' . $i => array(
            '#type' => 'textfield',
            '#value' => isset($form_state['input']['price_' . $i])? $form_state['input']['price_' . $i]: '',
            '#name' => 'price_' . $i,
            '#size' => 20,
           )
         ),
      ),
      'quantity' => array(
        'data' => array(
          'quantity_' . $i => array(
            '#type' => 'textfield',
            '#value' => isset($form_state['input']['quantity_' . $i])? $form_state['input']['quantity_' . $i]: '',
            '#name' => 'quantity_' . $i,
            '#size' => 10,
           )
         ),
       ),
       'subtotal' => array(
        'data' => array(
          'subtotal_' . $i => array(
            '#type' => 'textfield',
            '#value' => isset($form_state['input']['subtotal_' . $i])? $form_state['input']['subtotal_' . $i]: '',
            '#name' => 'subtotal_' . $i,
            '#size' => 20,
           )
         ),
       ),
    );
  }


  $options = array_reverse($options);

  $form_state['storage']['lines'] = $i;

  $form['order_lines_wrapper']['order_lines']['table'] = array
  (
    '#type' => 'tableselect',
    '#header' => $header,
    '#options' => $options,
    '#empty' => t('No lines found'),
    );

  $form['submit'] = array
    (
      '#type' => 'submit',
      '#value' => t('Save Order'),
    );

  $form['add_more'] = array
    (
      '#type' => 'button',
      '#value' => t('Add new line'),
      '#ajax' => array(
       'callback' => 'MIMODULO_ajax_add_lines',
       'wrapper' => 'order_lines',
       ),
    );

  return $form;
}

La parte de la flexibilidad para crear ordenes con distinto numero de líneas se realiza por medio de llamados Ajax para lo cual creamos un botón de ajax y un wrapper para generar el contenido dinámicamente como se muestra a continuación.

function MIMODULO_ajax_add_lines($form, &$form_state) {
 return $form['order_lines_wrapper']['order_lines'];
}

Si quiere profundizar en esto lo invito a leer la entrada de blog Como crear botón “Agregar otro campo” con FAPI en Drupal 7.

Teniendo resuelto el tema de que la cantidad de lineas se controla por medio de Ajax queda la labor como hacer para presentar el formulario por lineas, los que han podido observar cualquier sistema de facturación, reconocería que la interfaz mas común es un grid o una tabla.

En Drupal no tenemos este tipo de control, pero con algo de magia y el control tableselect se puede hacer, analicemos un poco a fondo el siguiente código.

$options = array();
   for ($i = 1; $i <= $form_state['storage']['lines']; $i++) {
    $options[$i] =array
    (
      'product' => array(
        'data' => array(
          'product_' . $i => array(
            '#type' => 'textfield',
            '#value' => isset($form_state['input']['product_' . $i])?$form_state['input']['product_' . $i]:'',
            '#name' => 'product_' . $i,
            '#size' => 40,
           )
         ),
      ),
      'price' => array(
        'data' => array(
          'price_' . $i => array(
            '#type' => 'textfield',
            '#value' => isset($form_state['input']['price_' . $i])? $form_state['input']['price_' . $i]: '',
            '#name' => 'price_' . $i,
            '#size' => 20,
           )
         ),
      ),
      'quantity' => array(
        'data' => array(
          'quantity_' . $i => array(
            '#type' => 'textfield',
            '#value' => isset($form_state['input']['quantity_' . $i])? $form_state['input']['quantity_' . $i]: '',
            '#name' => 'quantity_' . $i,
            '#size' => 10,
           )
         ),
       ),
       'subtotal' => array(
        'data' => array(
          'subtotal_' . $i => array(
            '#type' => 'textfield',
            '#value' => isset($form_state['input']['subtotal_' . $i])? $form_state['input']['subtotal_' . $i]: '',
            '#name' => 'subtotal_' . $i,
            '#size' => 20,
           )
         ),
       ),
    );
  }


  $options = array_reverse($options);

El ciclo anterior lo que hacer es crear controles de tipo textfield dentro de la propiedad data utilizada dentro de las opciones de un tableselect, data comúnmente es usada para colocar enlaces o texto, pero nada impide que se pueda utilizar cualquier elemento del FAPI de Drupal.

El truco para poder recuperar la información es utilizar la propiedad #name para colocarle un identificador único, para poder recuperar posteriormente del $form_state[‘input’], que como se ve es lo que se usa para al refrescar el ajax volver a renderizar los valores antes ingresados y que no se pierda nada. Esta misma técnica se debe hacer en el submit del forma a la hora de procesar ya el formulario completo.

Una vez con las opciones generadas, solo se debe hacer el render del tableseelect como se muestra a continuación.

  $form['order_lines_wrapper']['order_lines']['table'] = array
  (
    '#type' => 'tableselect',
    '#header' => $header,
    '#options' => $options,
    '#empty' => t('No lines found'),
    );

Este elemento es el que se remplaza en las llamadas a Ajax y aunque se dibuja toda la tabla, el efecto es suficientemente rápido para dar una agradable sensación al usuario.

El resultado seria algo similar a la siguiente imagen.

tableselect con multi form

El proceso de guardar las lineas en la base de datos se deja a gusto del lector.

Espero que haya sido de su agrado.

Recibe consejos y oportunidades de trabajo 100% remotas y en dólares de weKnow Inc.