lunes, 3 de octubre de 2011

Cómo crear un tipo de campo de formulario personalizado.


Hola a todos. Esta es mi primera entrada en este blog, el cual utilizaré para compartir con la comunidad mis experiencias con Symfony2.

El presente trabajo está dedicado a la creación de un tipo de formulario en Symfony2. En Symfony1 esta caraterística era conocida como widget. Para evitar confusión le llamaremos también aquí widget.

Esta característica de Symfony2 todavía no está documentada, por lo que esta información es obtenida a partir del análisis que he hecho del propio framawork. Con el debate se podrán encontrar errores y hacer mejoras a este pequeño tutorial.

Lo que pretendo mostrar es cómo programar un widget “autocomplete” de jQuery. Después de implementado y ejecutado en una página, debe generar un código html y javascript como el mostrado en el siguiente listado.


<label for="contactotype_name_visible" class=" required">Name</label>
<input type="hidden" id="contactotype_name" name="contactotype[name]" required="required" value="" />
<input type="text" id="contactotype_name_visible" name="contactotype_visible[name]" required="required" value="" />
<script type="text/javascript">
//<![CDATA[
$(function() {
var data_contactotype_name = [
   {
     id: 'bajo',
     label: 'Rendimiento Bajo'
   },
   {
     id: 'medio',
     label: 'Rendimiento Medio'
   },
   {
     id: 'alto',
     label: 'Rendimiento Alto'
   }
];
$( "#contactotype_name_visible" ).autocomplete({
   source: data_contactotype_name,
   select: function( event, ui ) {
     $( "#contactotype_name_visible" ).val( ui.item.label );
     $( "#contactotype_name" ).val( ui.item.id );
     return false;
   }
 });
});
//]]></script>

El primer paso es crear la clase que representará al nuevo widget. Le llamaremos ”AutocompleteType”. Esta clase la podemos implementar en un directorio con el nombre “Type” dentro de nuestro bundle.
 

<?php
namespace Acme\DemoBundle\Type; //namesapce donde está ubicado nuestro nuevo
                                                   //widget.
//Ponemos todas las dependencias de nuestra clase.
use Symfony\Component\Form\AbstractType;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\Extension\Core\ChoiceList\ArrayChoiceList;

class AutocompleteType extends AbstractType
{
   /**
   * Constructor.
   *
   * @param ContainerInterface $container A container.
   */
   public function __construct(ContainerInterface $container)
   {
     //Al constructor le paso el container.
     $this->container = $container;
   }
   /**
   * {@inheritdoc}
   */
   public function getParent(array $options)
   {
     //Con este método le estoy indicando que el componente padre es un
     //field que es el utilizado para generar todos los widget de tipo input
     return 'field';
   }

   /**
   * {@inheritdoc}
   */
   public function getName()
   {
     //Nombre que va a tener el nuevo widget, debe ser un nombre distinto a
     //los ya utilizados.
     return 'jquery_autocomplete';
   }
   /**
   * {@inheritdoc}
   */
   public function buildView(FormView $view, FormInterface $form)
   {
     //En este método le pasamos todas las variables a la vista y preparamos
     //la vista para que pueda ser utilizada.

     //buscar la explicación de esta línea más abajo.
     $this->container->get('twig.extension.form.jquery')->setTheme($view,     array('AcmeDemoBundle:Type:fields.html.twig'));
     //Se le indican al objeto view todas las variables que se le deben pasar
     //a la vista. Como este widget está compuesto por dos input html, un
     //hidden y un text, paso un id y full_name para el text
     //choices: conjunto de opciones
     //id_visible: id del widget visible, (input text)
     //full_name_visible: nombre completo del widget visible, (input text)
     $view->set('choices', $form->getAttribute('choice_list')->getChoices())
       ->set('id_visible', $view->get('id').'_visible')
       ->set('full_name_visible', sprintf('%s_visible[%s]',   $view->getParent()->get('full_name'), $view->get('name')));
   }
   public function buildForm(FormBuilder $builder, array $options)
   {
     //Este método se preparan las opciones que ha puesto el usuario para el
     //formulario.
     if ($options['choice_list'] && !$options['choice_list'] instanceof ChoiceListInterface) {
throw new FormException('The "choice_list" must be an instance of "Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface".');
     }
     if (!$options['choice_list']) {
       $options['choice_list'] = new ArrayChoiceList($options['choices']);
     }
     $builder->setAttribute('choice_list', $options['choice_list']);
   }
   /**
   * {@inheritdoc}
   */
   public function getDefaultOptions(array $options)
   {
     //En este método se especifica las opciones por defecto.
     return array(
       'choice_list' => null,
       'choices' => array(),
     );
  }
}

El framework utiliza la plnatilla “form_div_layout.html.twig” ubicada en “vendor/symfony/src/Symfony/Bridge/Twig/Resources/views/Form” para renderear los distintos widget, cada uno tiene un bloque dedicado. Por ejemplo, para renderear un input hidden se utiliza un bloque con el nombre “hidden_widget”.
El framework funciona de la siguiente manera: primero trata de buscar un bloque con el nombre <nombre_type>_widget, si no aparece entonces busca si existe el bloque para el padre y así sucesivamente hasta que encuentre uno.
En nuestro caso necesitamos crear un nuevo bloque para el widget y debe tener el nombre “jquery_autocomplete_widget”. También necesitamos crear un bloque para renderear la label, cuya filosofía es la misma y tendría el nombre “jquery_autocomplete_label”.
Un paso importante es indicarle al framework la plantilla donde están definidos los nuevos bloques y este es el objetivo de la siguiente línea del método “buildView”
 
$this->container->get('twig.extension.form.jquery')
  ->setTheme($view, array('AcmeDemoBundle:Type:fields.html.twig'));
 


El método “setTheme” de la clase “Symfony\Bridge\Twig\Extension\FormExtension” se utiliza para adicionarle nuevos temas a la hora de renderear los formularios. El servicio para “FormExtension” es creado como no público, por lo tanto se necesita crear un alias para poder acceder a él desde nuestro bundle

<service id="twig.extension.form.jquery" alias="twig.extension.form" />


El siguiente paso es crear los bloques para renderear la label y el widget en la plantilla “AcmeDemoBundle:Type:fields.html.twig”
Para renderear la label sería:
 
{% block jquery_autocomplete_label %}
{% spaceless %}
    {% set attr = attr|merge({'for': id_visible}) %}{#Le indicamos el id del#}
                                                    {#componente html al cual#}
                                                    {#debe apuntar la label#}
    {{ block('generic_label') }}
{% endspaceless %}
{% endblock jquery_autocomplete_label %}
 

Para renderear la widge sería:

{% block jquery_autocomplete_widget %}
    {% set type = type|default('hidden') %}
    {{ block('field_widget') }} {#Genero un campo oculto que será el que#}
                                {#almacene el dato que se envía al servidor#}
    {% set id_hidden  = id %} {#almaceno el valor del campo oculto#}
    {% set id         = id_visible %}{#le asigno al id el valor del id_visible#}
                                     {#para poder renderear el campo input#}
    {% set full_name  = full_name_visible %} {#hago lo mismo con el full_name#}
    {% set type = 'text' %} {#indico que el tipo que voy a renderear#}
                            {#es de tipo text#}
    {{ block('field_widget') }} {#rendereo el campo de tipo text#}
    {#código javascript para hacer el widget#}     
    <script type="text/javascript">
    //<![CDATA[
    $(function() {
        var data_{{ id_hidden }} = [
            {% for choice, label in choices %}
             {
                 id:    '{{ choice }}',
                 label: '{{ label }}'
             }{% if not loop.last %},{% endif %}
            {% endfor %}
        ];
        $( "#{{ id_visible }}" ).autocomplete({
            source: data_{{ id_hidden }},
            select: function( event, ui ) {
                $( "#{{ id_visible }}" ).val( ui.item.label );
                $( "#{{ id_hidden }}" ).val( ui.item.id );

                return false;
            }
        });
    });
    //]]>
    </script>    
{% endblock jquery_autocomplete_widget %}
 


Finalmente lo que queda es crear un servicio para el nuevo widget y que este sea cargado por el sistema de formulario de symfony.
 
<service id="form.type.jquery.autocomplete" class="Acme\DemoBundle\Type\AutocompleteType">
  <tag name="form.type" alias="jquery_autocomplete" />
    <argument type="service" id="service_container" />
</service>
 

Hasta aquí es el proceso de creación de un widget. Ahora lo que queda es utilizarlo dentro de un formulario.
 
namespace Acme\DemoBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;

class FormPruebaType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
            ->add('name', 'jquery_autocomplete', array(
                'choices' => array(
                    'bajo'  => 'Rendimiento Bajo',
                    'medio' => 'Rendimiento Medio',
                    'alto'  => 'Rendimiento Alto'
                 )
            ));
    }

    public function getName()
    {
        return 'form_prueba_type';
    }
}

Espero que les sea útil y gustaría comentarios. Disculpen el formato y los tipos de letras utilizados.
 

 

 



6 comentarios:

  1. Muchas gracias Alejandro, precisamente ando liado con este tema en este momento y me has aclarado algunos puntos. Buen trabajo :)

    ResponderEliminar
  2. Me alegra que le haya sido útil. Esto es solo un ejemplo, en la práctica quizás hayan que hacer varios ajustes. Inspirado en esto cree el siguiente bundle https://github.com/aprezcuba24/CULabsjQueryBundle donde implementé dos widget para completamiento. Uno que funciona como este y otro que completa desde el servidor. Mi objetivo es crear una gran cantidad de widget listos para ser usados.

    ResponderEliminar
  3. Una especie de toolkit, ¿verdad? Es una buena idea, felicidades, es posible que aporte alguno.

    ResponderEliminar
  4. Esa es la idea. Cualquier aporte sería útil.
    Gracias

    ResponderEliminar
  5. Hola, me gusto tu tutorial, pero tengo una duda, veo que los datos para el autocompletado estan en un array, pero quisiera saber cómo poder obtener los datos para el autocompletado consultando desde la base de datos

    ResponderEliminar
    Respuestas
    1. Hola, ante todo gracias por escribir. Ya este post lo hice hace mucho tiempo y de allá aca han pasado muchas cosas :-D para lo que quieres me parece que lo mejor es que uses este bundle "genemu/form-bundle": "2.2.*@dev" y aquí encontrarás lo que deseas y mucho más.

      Saludos

      Eliminar