skip to content

Formularios empotrados dinámicos en Symfony

Veamos los formularios Symfony 1.x en cierta profundidad

Los formularios embedded de Symfony 1.2 son una herramienta muy útil, pero ¿cómo agregarlos dinámicamente desde la vista con AJAX?

Supongamos una aplicación simple. Se trata de tarjetas con una pregunta y su respuesta.

formulario original

Podemos querer poner una imagen como pista, empotrando un formulario PictureForm:

embedded form

class CardForm extends BaseCardForm
{
  public function configure()
  {
    unset($this['cardset_id'],$this['usercard_list']);
    $this->embedForm('picture', new PictureForm());

  }
}

y quedaría así:

Luego esto con suerte lo maquetará otra persona ;)

¿Pero y si nos interesa que pueda haber más de una imagen, en número variable?

dynamic form

Pues en ese caso nos tenemos que arremangar y meter más código. Empezamos por el formulario (CardForm.class.php en mi caso):

class CardForm extends BaseCardForm
{
  public function configure()
  {
    unset($this['cardset_id'],$this['usercard_list']);

    //Empotramos al menos un formulario de pictures
    $pictures = $this->getObject()->getPictures();
    if (!$pictures){
      $picture = new Picture();
      $picture->setCard($this->getObject());
      $pictures = array($picture);
    }

    //Un formulario vacío hará de contenedor para todas las pictures
    $pictures_forms = new SfForm();
    $count = 0;
    foreach ($pictures as $picture) {
    $pic_form = new PictureForm($picture);
    //Empotramos cada formulario en el contenedor
    $pictures_forms->embedForm($count, $pic_form);
    $count ++;
  }
  //Empotramos el contenedor en el formulario principal
  $this->embedForm('pictures', $pictures_forms);

  }
}

Este código nos valdrá tanto para objetos nuevos (en ese caso empotramos un solo formulario de pictures para empezar), como para objetos con pictures ya empotradas.

Ahora vamos a hacer en el mismo fichero una función AddPicture() que nos será muy útil:

public function addPicture($num){
  $pic = new Picture();
  $pic->setCard($this->getObject());
  $pic_form = new PictureForm($pic);

  //Empotramos la nueva pícture en el contenedor
  $this->embeddedForms['pictures']->embedForm($num, $pic_form);
  //Volvemos a empotrar el contenedor
  $this->embedForm('pictures', $this->embeddedForms['pictures']);
}

Esta función añade un PictureForm a al formulario. Esto nos va a venir bien por dos razones (si esto no se entiende bien ahora, no importa, luego lo veremos con detalle):

  1. Al añadir con AJAX una picture, llamaremos a un action que ejecutará esta función para obtener un formulario con el número de pictures empotradas adecuado. Entonces el action renderizará solo la que nos interesa. Y esto será lo que insertaremos en la vista.
  2. Al asociar (bind) el formulario, necesitamos que el formulario que va asociarse a los datos que enviemos tenga el número correcto de pictures ya empotradas. Es decir, si vamos a asociar un formulario que ha introducido el usuario con un número determinado de pictures empotradas, el objeto formulario con el que vamos a hacer bind debe tener el mismo número de formularios empotrados. Para eso, sobreescribiremos la función bind para que añada tantas pictures empotradas como nos haga falta.

Vamos por pasos. El primero. En la vista, añadimos esta función de AJAX. Yo uso Jquery, si usas Prototype u otra cosa no te será difícil hacer algo parecido:

<script type=&quot;text/javascript&quot;>
var pics = <?php print_r($form['pictures']->count())?>;

function addPic(num) {
  var r = $.ajax({
    type: 'GET',
    url: '<?php echo url_for('card/addPicForm')?>'+'<?php echo ($form->getObject()->isNew()?'':'?id='.$form->getObject()->getId()).($form->getObject()->isNew()?'?num=':'&amp;num=')?>'+num,
    async: false
  }).responseText;
  return r;
}
$().ready(function() {
  $('button#add_picture').click(function() {
    $(&quot;#extrapictures&quot;).append(addPic(pics));
    pics = pics + 1;
  });
});
</script>

y tras el formulario un botón para añadir subformularios y un espacio para colocarlos cuando nos los la función AJAX.

<?php echo $form ?>
<div id=&quot;extrapictures&quot;/>

<tr><td><div><button id="add_picture" type="button"><?php echo "Añadir otra imagen"?></button></div></td></tr>

En el action, hacemos la función executeAddPicForm()

public function executeAddPicForm($request)
{
 $this->forward404unless($request->isXmlHttpRequest());
 $number = intval($request->getParameter("num"));

 if($card = CardPeer::retrieveByPk($request->getParameter('id'))){
 $form = new CardForm($card);
 }else{
 $form = new CardForm(null);
 }

 $form->addPicture($number);

 return $this->renderPartial('addPic',array('form' => $form, 'num' => $number));

}

Que básicamente lo que hace crear un formulario y llamar al addPicture() de ese formulario para tener un nuevo formulario con el número de elementos empotrados que nos interesa. Entonces le pasamos a una template addPic ese objeto formulario y el número del subformulario empotrado que queremos representar. La template tiene este código:

<?php echo $form['pictures'][$num]['id']->render()?>
<?php echo $form['pictures'][$num]['file']->renderRow();?>

Con esto ya tenemos un botón que añade nuevos subformularios. Pero ¿y a la hora de guardar? Como hemos dicho antes, necesitaremos que bind prepare el formulario. El nuevo código de bind será:

public function bind(array $taintedValues = null, array $taintedFiles = null)
{
  foreach($taintedValues['pictures'] as $key=>$newPic)
  {
    if (!isset($this['pictures'][$key]))
    {
      $this->addPicture($key);
    }
  }

  parent::bind($taintedValues, $taintedFiles);
}

Y con esto ya podemos usar este formulario como uno cualquiera. No es lo más cómodo del mundo, pero podemos seguir aprovechando sfForm totalmente en lugar de escribir toda la lógica de tratamiento de formulario nosotros mismos.

Solo nos quedará el trabajo de maquetación, descomponiendo campos de formularios para que todo quede bien :)