Cabeza de Ratón: versus el Amor Letal

AutoCompletado con Symfony

Denominamos ‘Autocompletado’  al comportamiento que se produce en campos de texto especiales donde a medida que tipeamos este nos devuelve, generalmente en forma de lista, los resultados de una búsqueda en función del texto introducido.

Es muy útil cuando consideramos que la cantidad de opciones de un campo tipo select es muy grande; por ejemplo, un listado de países.

Consideraciones Iniciales

En este ejemplo se muestra como implementar los scripts necesarios. Utilizaremos el widget sfWidgetPropelChoice con algunas funcionalidades adicionales incluidas en el plugin sfFormExtraPlugin [1].

Instalando el plugin

No vamos a perder mucho tiempo en esto; simplemente:

$ symfony plugin:install sfFormExtraPlugin
$ symfony clear:cache

Si surgen dudas puedes consultar el readme de la doc.

Agregando jQuery

Por si no lo comenté anteriormente, necesitamos el framework de javascript jQuery para nuestro input de autocompletado. El plugin sfFormExtraPlugin no lo incluye por lo que tendremos que hacerlo a manopla. Puedes descargar el framework desde este link [2]. En estos momentos yo he descargado jquery-1.3.2.min.js.

Luego tienes que incluirlo en tu aplicación, para eso edita el archivo view.yml que se encuentra en la carpeta /config de la misma. Algo así:

  javascripts:    [ jquery-1.3.2.min.js ]

Puedes verificar si sQuery es cargado por la aplicación ojeando el código html generado; hasta puedes copiar la ruta del script y cargarla en el del browser para la comprobación final.

Schema de nuestro ejemplo.

Usaremos como ejemplo una tabla denominada producto y otra categoria para la relación 1-n:

propel:
  categoria:
    _attributes: { phpName: Categoria, idMethod: native }
    id: { type: integer, required: true, primaryKey: true, autoIncrement: true }
    nombre: { type: varchar(50), required: true }
    descripcion: { type: longvarchar }

  producto:
    _attributes: { phpName: Producto, idMethod: native }
    id: { type: integer, required: true, primaryKey: true, autoIncrement: true }
    codigo: { type: varchar(20), required: true }
    nombre: { type: varchar(100), required: true }
    marca: { type: varchar(80) }
    descripcion: { type: longvarchar }
    categoria_id: { type: integer, foreignTable: categoria, foreignReference: id }

Creamos una aplicación y para simplificar la existencia generamos un módulo administrativo con el generador de propel. En mi caso yo generé la aplicación pruebas y el módulo a partir de la clase Producto.

$ symfony generate:app pruebas
$ propel:generate-admin pruebas Producto

Configurando el widget

Como todo hijo de vecino sabe Propel genera clases del modelo (ORM) y también las clases de formulario del modelo entre otras cositasa más. Tendremos que redefinir el widget para nuestro campo categoria_id.

Entonces, dentro de la clase ProductoForm.class.php definiremos:

  public function configure() {
   // ..
    $this->widgetSchema['categoria_id'] = new sfWidgetFormChoice(array(
      'choices'          => array(),
      'renderer_class'   => 'sfWidgetFormPropelJQueryAutocompleter',
      'renderer_options' => array(
        'model' => 'Categoria',
        'url'   => $this->getOption('url')
      )
    ));
  // ..
  }

Acá es donde invocamos a la clase sfWidgetFormPropelJQueryAutocompleter definida en el archivo sfWidgetFormPropelJQueryAutocompleter.class.php que es agregada en el plugin sfFormExtraPlugin.

La usamos como clase de renderizado en el parámetro renderer_class. También definimos el modelo ‘Categoria‘ que usamos para la relacion 1-n y finalmente definimos ‘url’ mediante el método getObject(‘url’);

El parámetro ‘url’ define la dirección url que utilizará javascript cuando realice las búsquedas, a través de AJAX, de las distintas categorías para nuestro ejemplo. Con ‘$this->getObject(‘url’)’ esperamos que este valor ‘venga’ cuando creamos el objeto de formulario; por lo que tendremos que definirlo.

En el controlador …

En el controlador, en nuestra clase de acciones, vamos a redefinir el método executeNew.

  public function executeNew(sfWebRequest $request)
  {
    $this->producto = new Producto ();
    $this->form = new ProductoForm ($this->producto, array('url' => $this->getController()->genUrl('producto/request2Autoomplete')));
  }

Hemos creado un nuevo objeto de formulario donde se ha pasado dentro del array de opciones el parámetro ‘url‘ con el valor del link (renderizado) de la acción ‘producto/autocomplete‘.

Esto significa que el browser, gracias a los scripts de javascript, hará que cada vez que escribamos en el input se produzca un request a esta acción y es esta misma quien deberá ‘contestar’ ese pedido con los datos (en formato JSON) de la búsqueda en la tabla ‘categoria’ dentro de la base de datos.

Entonces nosotros hemos definido el nombre de la acción ‘autoComplete‘ de nuestro módulo ‘producto’ por lo que tendremos que programarla:

  public function executeAutoComplete($request)
  {
    $this->getResponse()->setContentType('application/json');
    $categorias = CategoriaPeer::retrieveForAutoSelect($request->getParameter('q'), $request->getParameter('limit'));
    return $this->renderText(json_encode($categorias));
  }

En esta acción hemos definido el content-type de la página de respuesta como application/json, creado una variable $categorias que son todas las categorías que nos devuelve el método de la clase CategoriaPeer denominado ‘retrieveForAutoSelect’ y finalmente renderizamos la respuesta de la acción transformando el array $categorias a notación JSon [3].

Por si no te has dado cuenta el método peer ‘retrieveForAutoSelect’ no existe. Amigo .. a arremangarse.

El método Peer

Finalmente debemos programar el método Peer que nos devuelve un array con todas las categorías encontradas en función de los datos que vamos introduciendo en el campo de texto:

  static public function retrieveForAutoSelect($q, $limit)
  {
    $criteria = new Criteria();
    $criteria->add(CategoriaPeer::NOMBRE, '%'.$q.'%', Criteria::LIKE);
    $criteria->addAscendingOrderByColumn(CategoriaPeer::NOMBRE);
    $criteria->setLimit($limit);

    $categorias = array();
    foreach (CategoriaPeer::doSelect($criteria) as $categoria)
    {
      $categorias[$categoria->getId()] = (string) $categoria;
    }

    return $categorias;
  }

Final.

Con todo lo realizado deberíamos tener funcionando nuestro autocompletado en el campo de las categorías. He subido un ejemplo onLine con el comportamiento correcto para que se pueda apreciar el funcionamiento.

Al agregar un producto nuevo [6] se aprecia el funcionamiento en el campo ‘categoria_id’. Puedes agregar más categorías [7] para que el ejemplo funcione mejor.

Gran parte de mi comprensión sobre este tema se debe a la traducción de formularios de librosWeb.es [4], hoy por hoy material imprescindible para el buen aprendizaje de symfony en idioma castellano; y al artículo Haz tu Elección de Roberto Puentes Díaz [5].

Saludos …

Enlaces

[1] http://www.symfony-project.org/plugins/sfFormExtraPlugin
[2] http://docs.jquery.com/Downloading_jQuery
[3] http://es.wikipedia.org/wiki/JSON
[4] http://www.librosweb.es/symfony_formularios/capitulo12/widgets_para_elecciones.html
[5] http://www.puentesdiaz.com.ar/symfony/haz-tu-eleccion.php
[6] http://cabezaderaton.com.ar/symfony/web/index.php/producto/new
[7] http://cabezaderaton.com.ar/symfony/web/index.php/producto/new

  • Excelente post!

    Pero muy similar a varios que he visto, algo interesante sería cómo utilizar el autocompletado más como una sugerencia, puesto que por defecto requieres que el objeto que vas escribiendo exista ya en la base de datos, pero que tal si no existe, y quieres que tu ingreso sea creado en la base de datos?

You can follow any responses to this entry through the RSS 2.0 feed.