Compartir a través de


Vanguardia

Escritura que va por delante

Dino Esposito

Dino EspositoDesde los primeros días de la web, la mayoría de las páginas incluyen un cuadro de búsqueda para ayudar a encontrar rápidamente el contenido dentro de la propia página o en el sitio. Una función de búsqueda bien hecha es un requisito en los sitios más grandes. Esto ayuda a los usuarios a encontrar lo que desean de forma rápida y sencilla, omitiendo la arquitectura y el mapa del sitio.

En un sitio de compras, por ejemplo, podría ser interesante utilizar cadenas de consulta para buscar productos, ofertas, o noticias y alertas. En un sitio creado para algo como un equipo deportivo profesional, la función de búsqueda debe poder extraer noticias, resultados, nombres de deportistas, biografías, etc. La estructura de datos sobre la que tiene que trabajar una función de búsqueda nunca es evidente. Es claramente específica de la plataforma.

En lugar de reinventar la rueda una y otra vez, considere la posibilidad de usar un motor de búsqueda de texto completo ad hoc, como Lucene.Net, para realizar una copia de seguridad de su función de búsqueda. Un motor como Lucene.Net indexa una serie de documentos basados en cadenas y analiza cualquier cadena de consulta proporcionada con el índice. De esta manera, el motor permite que se especifiquen combinaciones complejas de cadenas de consulta. En muchos sitios y páginas, usar Lucene.Net podría ser excesivo, pero se sigue necesitando algún tipo de búsqueda que sea más eficaz que colocar una lista interminable de elementos en una lista desplegable.

En este artículo se presenta un pequeño marco de autocompletar creado con el archivo typeahead.js de Twitter como base. Este marco no es mágico, pero simplifica realmente el uso de autocompletar en las páginas web. El aspecto más llamativo de este marco es que permite combinar varios conjuntos de datos con la consulta dentro de la misma página y recuperar información distinta pero relacionada.

Configuración de Typeahead.js

En este artículo, se aclararán los conceptos básicos del uso de typeahead en un escenario realista con la versión de typeahead que encontrará en NuGet al escribir "typeahead", como se muestra en la Figura 1. Cuando se busca typeahead en Google, pueden encontrarse referencias anteriores, antiguos archivos de JavaScript o simplemente versiones bifurcadas del código del proyecto original. La documentación también puede resultar confusa.

El paquete NuGet de Typeahead.js de Twitter
Figura 1. El paquete NuGet de Typeahead.js de Twitter

El archivo de paquetes contiene todos los paquetes que componen la biblioteca, entre los que se incluye el motor Bloodhound para administrar las sugerencias en el explorador local. Para configurar una vista de página web o Razor para usar typeahead.js, solo necesita la conocida sintaxis similar a jQuery y activar el complemento en el campo de entrada seleccionado. Aquí se muestra un ejemplo:

<form action="@Url.Action("Query", "Home")" method="post">

  <input type="hidden" id="queryCode" name="queryCode" />

    <input type="text" name="queryString" id="queryString">

    <button id="queryButton" type="submit">Get</button>

</form>

<form action="@Url.Action("Query", "Home")" method="post">
  <input type="hidden" id="queryCode" name="queryCode" />
    <input type="text" name="queryString" id="queryString">
    <button id="queryButton" type="submit">Get</button>
</form>

Es importante tener en cuenta que para usar autocompletar en una página web y que sea útil, también necesitará un campo oculto amigo para recopilar algún tipo de identificador único de la sugerencia seleccionada. Hay muchos casos en los que puede utilizar un campo de entrada que se complete automáticamente. El escenario que más me interesa utiliza autocompletar para sustituir una lista desplegable que, si no, sería interminable. Permite una búsqueda ligera de estilo Bing en su sitio, sin necesidad de ayuda por parte de los motores de texto completo como Lucene.Net.

Código de script que se tendrá en la vista

Para utilizar typeahead.js, haga referencia a jQuery 1.9.1 o superior y al script de typeahead. La cantidad mínima de código que necesitará en la vista se muestra aquí:

$('#queryString').typeahead(
  null,
  {
    displayKey: 'value',
    source: hints.ttAdapter()
  }
});

De esta manera, se toman todos los valores predeterminados y se indica al motor que utilice la propiedad value en los datos devueltos para rellenar la lista desplegable. Se espera que exista una propiedad de nombre value en los datos que se filtren. En teoría, puede configurar autocompletar en cualquier matriz de datos de JavaScript. En la práctica, sin embargo, el uso de autocompletar está justificado principalmente cuando se descargan datos desde un origen remoto.

La descarga desde un origen remoto plantea numerosos problemas: la directiva de navegador de mismo origen, la captura previa y el almacenamiento en caché, por nombrar algunas. El archivo typeahead.js de Twitter incluye un motor de sugerencias llamado Bloodhound. Esto realiza la mayor parte de este trabajo de forma transparente. Si recibe un archivo JavaScript de paquetes de NuGet, puede simplemente empezar a llamar a BloodHound sin preocuparse por su descarga e instalación. La variable de sugerencias en el fragmento de código anterior es el resultado de la siguiente inicialización, que es bastante estándar, de Bloodhound:

var hints = new Bloodhound({
  datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
  queryTokenizer: Bloodhound.tokenizers.whitespace,
  remote: "/hint/s?query=%QUERY"
});
hints.initialize();

Observe el atributo remoto. Tan solo es el extremo del servidor responsable de devolver sugerencias para que se muestren en la lista desplegable. Observe también la sintaxis %QUERY. Esto indica que la cadena en el campo de entrada que se envía al servidor para las sugerencias. En otras palabras, %QUERY es un marcador de posición para cualquier otro texto que se encuentre en el campo de entrada. De forma predeterminada, typeahead.js comienza a obtener sugerencias en cuanto se escribe un único carácter. Si desea esperar a que los caracteres se encuentren en el búfer antes de que autocompletar comience, agregue un objeto de configuración como el primer argumento del complemento:

$('#queryString').typeahead(
  {
    minLength: 2
  },
  {
    displayKey: 'value',
    source: hints.ttAdapter()
  }
});

Cuando el búfer está lo suficientemente lleno para iniciar las llamadas remotas, Bloodhound comienza a trabajar. Descarga datos JSON y los adapta para su visualización. En este punto, tiene un motor de autocompletar que apenas trabaja y que muestra sugerencias según la lógica que tiene en el servidor. Sin embargo, se debe hacer mucho más antes de poder usar autocompletar de manera eficaz en una página real.

Uso de Typeahead.js con Bootstrap

Cualquier complemento suficientemente complejo necesita un poco de CSS para tener una buena apariencia. Typeahead.js no es una excepción. El complemento incluye su propia interfaz de usuario predeterminada, pero es posible que desee aplicar algunos cambios, especialmente si lo usa con Twitter Bootstrap. También es posible que desee personalizar algunos atributos visuales, como los colores y el espaciado interno. En la Figura 2 se muestra una lista con algunas clases CSS que podrían resultarles interesantes para personalizar la apariencia del componente typeahead.js.

Figura 2. Las clases CSS que se deben modificar para personalizar el componente typeahead.js

Clase CSS Descripción
twitter-typeahead Determina el estilo del campo de entrada donde el usuario escribe las sugerencias.
tt-hint Determina el estilo del texto que representa la delta entre lo que ha escrito y la primera sugerencia. Esta clase solo se utiliza cuando la propiedad de sugerencia se establece en verdadera (es falsa de forma predeterminada).
tt-dropdown-menu Determina el estilo de la ventana emergente de la lista desplegable donde se muestran las sugerencias.
tt-cursor Determina el estilo de las sugerencias resaltadas en el cuadro de lista desplegable.
tt-highlight Determina el estilo de la parte del texto que coincide con la cadena de consulta.

La Figura 3 da una idea de lo que se puede obtener con las clases CSS personalizadas. También puede personalizar el comportamiento general del complemento desde un punto de vista funcional.

Las clases CSS personalizadas pueden conseguir distintos efectos en la aplicación
Figura 3. Las clases CSS personalizadas pueden conseguir distintos efectos en la aplicación

Agregar lógica de cliente

Un campo de autocompletar es más rápido que cualquier lista desplegable larga. Cuando el número de elementos que se pueden elegir se cuentan por cientos, cualquier lista desplegable clásica es lenta. Por lo tanto, si tiene previsto usar el campo de entrada de autocompletar para seleccionar un valor específico, como por ejemplo, el nombre de un producto o un cliente, un complemento de typeahead.js simple no es suficiente. Es necesario algún código de script adicional que se enlace al evento seleccionado del complemento:

$('#queryString').on('typeahead:selected', 
  function (e, datum) {
  $("#queryCode").val(datum.id);
});

La ventaja principal de la entrada de autocompletar es que los usuarios escriben algún nombre inteligible y el sistema recupera el identificador o código único asociado. Esta característica tiene que codificarse explícitamente. En el controlador del evento seleccionado, puede recuperar la información del identificador a partir del objeto de referencia y almacenarla de forma segura en un campo oculto. Cuando se publica el formulario al que pertenecen los campos de entrada de autocompletar, también se publica el identificador seleccionado. El formato del objeto de referencia, el elemento de datos seleccionado en la lista desplegable, depende del formato de los datos que recibe desde el servidor.

¿Qué sucede con el texto que se muestra en el campo de entrada? En este caso, probablemente no necesitará tener ningún texto significativo que se muestre en el campo de entrada. La entrada relevante para las operaciones siguientes es lo que almacena en el campo oculto. Lo que se muestra ahí depende de usted. Simplemente tenga en cuenta si especifica en la configuración del complemento una propiedad displayKey, ya que el valor de esa propiedad se muestra automáticamente en el campo de entrada. En cualquier caso, puede establecer cualquier valor mediante programación:

$("#queryString").val(datum.label);

En algunos casos, el cuadro de texto de autocompletar es el único elemento del formulario HTML. Esto significa que podría querer procesar los datos seleccionados en cuanto se seleccione. Si agrega la línea siguiente al controlador de eventos seleccionado de typeahead.js, se simula un clic en el botón de envío del formulario:

$("#queryButton").click();

Supongamos que un usuario empieza a escribir en el campo de entrada, hace que la lista desplegable se muestre y, a continuación, deja de escribir sin seleccionar nada. ¿Qué debe hacer cuando vuelva a escribir? Naturalmente, le permitiría escribir de nuevo hasta que realice una selección. Cuando el usuario seleccione algo, parte del código se ha almacenado para que cuando vuelva a editar, se deba cancelar esa selección. Necesitará una variable local para lograrlo:

var typeaheadItemSelected = false;
$('#queryString').on('typeahead:selected', function (e, datum) {
  $("#queryCode").val(datum.id);
  typeaheadItemSelected = true;
});

También necesita un controlador del evento de foco del campo de entrada para restablecer los datos almacenados:

$('#queryString').on('input', function () {
  if (typeaheadItemSelected) {
    typeaheadItemSelected = false;
    $('#queryString').val(''); 
    $("#queryCode").val('');
  }
});

El objetivo final de esta lógica de cliente adicional es garantizar que el campo de entrada de autocompletar funciona de la misma manera que una lista desplegable.

El lado del servidor de Autocompletar

Todo lo que puede hacer cualquier código del lado cliente depende completamente de los datos que se devuelven desde el servidor. Como mínimo, el extremo del servidor es solo una dirección URL que devuelve datos JSON. Cuando tiene un extremo que proporciona una colección de objetos Product, por ejemplo, puede utilizar la propiedad displayKey de typeahead.js para realizar algún tipo de enlace de datos en la lista desplegable de sugerencias. En su forma más simple, un método de controlador que devuelve JSON puede ser similar a lo siguiente:

public JsonResult P(string query)
{
  var productHints = _service.GetMatchingProducts(query);
  return Json(productHints, JsonRequestBehavior.AllowGet);
}

Si se espera que el campo de entrada de autocompletar muestre sugerencias de los elementos de datos homogéneos, este es un enfoque ideal. En el lado del cliente, de hecho, puede aprovechar fácilmente el mecanismo de plantillas integrado en typeahead.js y organizar vistas personalizadas de sugerencias:

$('#queryString').typeahead(
null,
{
  templates: {
    suggestion: Handlebars.compile('<b>({{Id}}</b>: {{notes}}')
  },
  source: hints.ttAdapter()
});

La propiedad templates reemplaza displayKey y establece un diseño personalizado para el contenido de la lista desplegable. La lista desplegable de la Figura 3 es el resultado del fragmento de código anterior. Al diseñar una plantilla, es recomendable usar un motor de plantillas ad hoc como Handlebars (handlebarsjs.com). Debe vincular Handlebars al proyecto por separado desde typeahead.js. El uso de Handlebars es opcional. Siempre puede dar formato a una plantilla HTML a través de código de JavaScript manual o incluso devolver un objeto del lado servidor con HTML con formato previo.

Las cosas se complican cuando su entrada de autocompletar devuelve sugerencias heterogéneas, como productos u ofertas. En este caso, debe devolver una matriz de algún tipo de datos intermedio que contenga información suficiente para que el usuario elija. El tipo de autocompletar que propone este artículo ofrece una clase AutoCompleteItem básica, como se muestra aquí:

public class AutoCompleteItem
{
  public String label { get; set; }
  public String id { get; set; }
  public String value { get; set; }
}

La propiedad de identificador contiene un identificador único. Cuando se publica, tiene importancia para el controlador de recepción. Normalmente contiene dos partes: el identificador real y un identificador que hace coincidir inequívocamente el identificador con uno de los conjuntos de datos (productos u ofertas) que se han podido devolver en la consulta. El valor de la propiedad es el contenido de la cadena que se mostrará en la lista desplegable. La otra propiedad es un tipo de propiedad de carga para cualquier otra cosa que necesite. La propiedad value también puede contener cadenas HTML organizadas del lado servidor para los diseños de sugerencias personalizadas. El código del lado servidor también es responsable de la ejecución de todas consultas necesarias y del envío de datos a una colección de objetos AutoCompleteItem.

Resumen

La facilidad de uso es cada día más importante en los sitios web modernos. Es bienvenida en la cara pública del sitio, pero es aún más importante en los back-ends del sitio web, donde se inserta la información real. En la parte de administración de uno de mis sitios web, una vez tenía una lista desplegable con más de 700 artículos. Funcionaba, pero era lenta. La sustituí con autocompletar y ahora es increíblemente rápida. Organicé un proyecto en GitHub para esta y otras utilidades en bit.ly/1zubJea.


Dino Esposito es el coautor de “Microsoft .NET: Architecting Applications for the Enterprise” (Microsoft Press, 2014) y “Programming ASP.NET MVC 5” (Microsoft Press, 2014). Esposito, evangelizador técnico para las plataformas Android y Microsoft .NET Framework en JetBrains y orador frecuente en eventos del sector en todo el mundo, comparte su visión de software en software2cents.wordpress.com y en Twitter en twitter.com/despos.

Gracias al siguiente experto técnico por su ayuda en la revisión de este artículo: Jon Arne Saeteras