viernes, 30 de septiembre de 2011

Peticiones Ajax síncronas con jQuery

En la entrada pasada publiqué un código para asignar variables de sesión PHP desde jQuery con un POST enviado via Ajax. El código del script es el siguiente:

<script type="text/javascript">
  $(function() {
    $('.print_link').click(function() {
		var fixedurl = 'http://localhost/miapp/main/set_session_var';

		$.post( fixedurl,
		{ object_name: 'tmprow',
          id: $(this).attr('id'),
          monto: $(this).parent().parent().find('.monto').html()
		 },
			function(data){
			//donothing
		});

      return true;
    });
  });
</script>

Luego, en set_session_var (ya en PHP) se hacía el trabajo ordinario de asignar la variable de sesión. Hice la prueba con un ejemplo sencillo en que se tenían 2 vistas, test1 y test2, y cuando se hacía click a un enlace en test1 que llevaba a test2, se ejecutaba la función, justo antes de abandonar la página, de manera que ya en test2, "con suerte" se pudieran usar las variables recién asignadas.

He dicho "con suerte", porque, tratándose de Ajax y su asincronía, no tenemos la certeza de que cuando se cargue test2, ya las variables estén asignadas. El ejemplo mostrado era realmente sencillo, por lo que esto practicamente siempre se cumplía, pero luego comencé a hacer otras cosas en la función set_session_var, que hicieron que eventualmente, cuando cargara test2, aún no estaban mis variables de sesión.

¿La solución? Hacer el envío de forma síncrona, de manera que en el "return true;" garantice que ya se haya ejecutado el POST. Esto con $.post() de jQuery no se puede hacer, hay que utilizar $.ajax() directamente. Recordemos que $.post() es un atajo/simplificador de $.ajax para envío de POST asíncronos, el cual no permite especificar todas las opciones de $.ajax().

Bien, a $.ajax() podemos especificar la opción 'async' (que por omisión está en true, es decir, todo es enviado de forma asíncrona). Lo que tenemos que hacer es ponerla en false y ya. Es muy simple, en realidad. Dejo el código del script, sustituyendo el $.post() por $.ajax():

<script type="text/javascript">
  $(function() {
    $('.print_link').click(function() {
		var fixedurl = 'http://localhost/miapp/main/set_session_var';

		$.ajax({url: fixedurl,
		  type: 'POST',
		  async: false,
		  data: { object_name: 'tmprow',
           id: $(this).attr('id'),
           monto: $(this).parent().parent().find('.monto').html() }
		});
      return true;
    });
  });
</script>

Asignando variables de sesión PHP desde jQuery (con KumbiaPHP)

En esta entrada muestro un pequeño ejemplo en el que asignamos una variable de sesión PHP "dentro" de una función Javascript. ¿Para qué queremos esto? Pues para lo mismo que normalmente queremos asignar una variable de sesión, sólo que si estamos, por ejemplo, manejando un evento con jQuery, no estamos en PHP, estamos en Javascript, así que no vale un $_SESSION['clave'] = valor.

La solución que planteo aquí es la de enviar un POST via Ajax que se encargue de hacer la asignación. Simple. Voy a hacer uso de KumbiaPHP, con el cual ya he mostrado códigos anteriormente, pero en definitiva, el procedimiento es realmente simple e independiente del framework que utilicemos (si utilizamos alguno).

En este ejemplo, tengo una tabla con algunos datos sobre unas hipotéticas transacciones que presento al usuario, incluyendo un enlace a otra página. Será una tabla con 3 columnas: 1) tipo de transacción 2) monto de transacción y 3) enlace. Muestro el código HTML de la vista, llamada test1.phtml:

<table border="0" width="100%" class="grid-table" id="transacciones-table">
<thead>
  <tr>
    <th class="table-header-repeat line-left minwidth-1">Transacción</th>
    <th class="table-header-repeat line-left minwidth-1">Monto</th>
    <th class="table-header-repeat line-left minwidth-1">Detalle</th>
  </tr>
</thead>
<tbody>
  <tr class="row-1">
    <td class="descripcion">Apertura</td>
    <td class="monto">850.00</td>
    <td><a href="test2" class="print_link" id="671">imprimir</a></td>
  </tr>
  <tr class="row-2">
    <td class="descripcion">Intereses</td>
    <td class="monto">150.00</td>
    <td><a href="test2" class="print_link" id="686">imprimir</a></td>
  </tr>
  <tr class="row-1">
    <td class="descripcion">Intereses</td>
    <td class="monto">150.00</td>
    <td><a href="test2" class="print_link" id="778">imprimir</a></td>
  </tr>
  <tr class="row-2">
    <td class="descripcion">Cancelación</td>
    <td class="monto">850.00</td>
    <td><a href="test2" class="print_link" id="990">imprimir</a></td>
  </tr>
</tbody>
</table>

Como vemos una simple tabla con datos. Obsérvese que el anchor tiene un id y que el resto de las columnas tienen una clase representativa del dato que contienen. Queremos que cuando el usuario presione el enlace, el navegador se vaya a la vista test2.phtml para, por ejemplo, imprimir una planilla en pdf. En este caso, podríamos enviarle el valor del id de la fila que queremos imprimir y hacer una nueva consulta a la base de datos para pedir el resto de los datos, o podemos enviar un GET a test2 con los datos, para no tener que consultar, o alguna otra cosa. Pero como queremos es utilizar variables de sesión, pues eso haremos: con jQuery, crearemos una función para manejar el evento click de los enlaces, de manera tal de que justo antes de ir a test2, asignemos las variables de sesión y podamos luego utilizarlas sin problema. Es sólo cuestión de agregar la función a la vista test1.phtml, que quedaría de la siguiente forma:

<script type="text/javascript">
  $(function() {
    $('.print_link').click(function() {
		var fixedurl = 'http://localhost/miapp/main/set_session_var';
		$.post( fixedurl,
		{ object_name: 'tmprow',
          id: $(this).attr('id'),
          monto: $(this).parent().parent().find('.monto').html()
		 },
			function(data){
			//donothing
		});

      return true;
    });
  });
</script>

<table border="0" width="100%" class="grid-table" id="transacciones-table">
<thead>
  <tr>
    <th class="table-header-repeat line-left minwidth-1">Transacción</th>
    <th class="table-header-repeat line-left minwidth-1">Monto</th>
    <th class="table-header-repeat line-left minwidth-1">Detalle</th>
  </tr>
</thead>
<tbody>
  <tr class="row-1">
    <td class="descripcion">Apertura</td>
    <td class="monto">850.00</td>
    <td><a href="test2" class="print_link" id="671">imprimir</a></td>
  </tr>
  <tr class="row-2">
    <td class="descripcion">Intereses</td>
    <td class="monto">150.00</td>
    <td><a href="test2" class="print_link" id="686">imprimir</a></td>
  </tr>
  <tr class="row-1">
    <td class="descripcion">Intereses</td>
    <td class="monto">150.00</td>
    <td><a href="test2" class="print_link" id="778">imprimir</a></td>
  </tr>
  <tr class="row-2">
    <td class="descripcion">Cancelación</td>
    <td class="monto">850.00</td>
    <td><a href="test2" class="print_link" id="990">imprimir</a></td>
  </tr>
</tbody>
</table>

Muy simple y autoexplicativo (sobre todo para el que sabe jQuery). Simplemente declaramos un handler para el evento click de los objetos de clase "print_link", que envíe un POST via Ajax a la url especificada, con 3 datos: id (el id de la columna), monto (el monto de la transacción que sería el dato de la segunda columna) y object_name (con el nombre que le queremos asignar a la variable de sesión, 'tmprow' en este caso). Lo que queda es ver el controlador:

<?php

class MainController extends AppController
{

public function index()
{
	View::template('admin');
}

public function set_session_var()
	{
		View::template(null);
		$tmpname = '_object';
		foreach($_POST as $clave => $valor)
		{
			if($clave == 'object_name')
			{
				$tmpname = $valor;
				unset($_POST[$clave]);
			}
		}
		$_SESSION[$tmpname] = $_POST;
	}

	public function test1()
	{
		View::template('admin');
	}
	
	public function test2()
	{
		View::template('admin');
	}
}
?>

Y eso es todo, lo que acabamos de hacer es PHP básico, que no hace falta comentar. Ya podemos utilizar en test2 las variables de sesión que asignamos. Por ejemplo:

<p>En esta vista se puede hacer cualquier cosa con la información
que se guardó en sesión en test1.</p>

<p>El id de la transacción es: <?php echo $_SESSION['tmprow']['id'] ?></p>
<p>El monto de la transacción es: <?php echo $_SESSION['tmprow']['monto'] ?></p>

<p>Recuerda que siempre debes limpiar las variables de sesión cuando
ya no las utilices.</p>
<?php unset($_SESSION['tmprow']); ?>

Nótese que la función set_session_var() es reutilizada. Es decir, desde cualquier controlador, podemos enviarle un POST Ajax, con algunos datos y ella asignará nuestras variables de sesión. Se trata de un código sencillo pero que puede resultar muy útil.

Actualización
En esta entrada he mejorado el código, cambiando el $.post() por un $.ajax() para que la enviada sea síncrona, y así garantizar que en test2, ya tengamos nuestras variables con seguridad.

jueves, 8 de septiembre de 2011

Resaltado de código en blogger

Los que escribimos sobre programación, necesitamos una manera elegante y visualmente agradable de presentar nuestro código a los lectores. Yo, por ejemplo, alguna vez usé un pequeño script PHP que le pasaba mi código, y él me devolvía el HTML que resaltaba con colores palabras claves y otros elementos de PHP, C++, etc.

Últimamente he andado muy contento con JavaScript (con JQuery en realidad), y por casualidad llegó a mí SyntaxHighlighter, un resaltador de código fuente que utiliza exclusivamente javascript y css, y que se podría describir en una sola palabra: maravilloso. De verdad es una maravilla.

Es además muy muy sencillo de utilizar. Es cuestión de incluir algunos archivos javascript y css a nuestras páginas y luego encerrar nuestro código entre etiquetas PRE.

En este blog, dan indicaciones específicas para blogger. Básicamente lo que hay es que pegar justo antes de cerrar el header de nuestra plantilla, el siguiente código:

<link href='http://alexgorbatchev.com/pub/sh/current/styles/shCore.css' rel='stylesheet' type='text/css'/> 
<link href='http://alexgorbatchev.com/pub/sh/current/styles/shThemeDefault.css' rel='stylesheet' type='text/css'/> 
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shCore.js' type='text/javascript'></script> 
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushCpp.js' type='text/javascript'></script> 
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushCSharp.js' type='text/javascript'></script> 
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushCss.js' type='text/javascript'></script> 
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushJava.js' type='text/javascript'></script> 
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushJScript.js' type='text/javascript'></script> 
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushPhp.js' type='text/javascript'></script> 
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushPython.js' type='text/javascript'></script> 
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushRuby.js' type='text/javascript'></script> 
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushSql.js' type='text/javascript'></script> 
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushVb.js' type='text/javascript'></script> 
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushXml.js' type='text/javascript'></script> 
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushPerl.js' type='text/javascript'></script> 
<script language='javascript'> 
SyntaxHighlighter.config.bloggerMode = true;
SyntaxHighlighter.config.clipboardSwf = 'http://alexgorbatchev.com/pub/sh/current/scripts/clipboard.swf';
SyntaxHighlighter.all();
</script>
Sí, el código está resaltado, utilizando el resaltador. Luego, en nuestros posts, sólo tendremos que encerrar las porciones de código entre etiquetas PRE, especificando como clase "brush: alias", en donde alias es el nombre del lenguaje que se usará para resaltar. Por ejemplo, si queremos resaltar una función escrita en PHP, podemos utilizar:
<pre class="brush: php">
<?php 
public void Method($saludo) {
	$saludo .= " :)";
}
?>
</pre>
Sí, este código también fue resaltado utilizando el resaltador.Sencillamente maravilloso.
El único detalle que hay que tener en cuenta, es que debemos escapar todos los < (sustituirlos por el equivalente <). Yo hice un script que lo haga por mí, pero vamos que con un "replace all" en cualquier editor de texto, basta.

Sobre el reenvío de formularios via POST


A todo el mundo le ha pasado que en una aplicación web hay un formulario, la data se envía por POST, se realiza alguna acción con esa data, se muestra algo al usuario, el usuario pincha un link para ir a otra página del sitio, luego le da al botón Atrás y... ¡PUM! El navegador muestra al usuario ese horrible mensaje de que debe reenviar la información del formulario. Pasa en eztv.it, por ejemplo.

No es un bug de la aplicación, ni del navegador, pero es simplemente es desastroso para la imagen de nuestra aplicación frente al cliente.

Solución 1

Existen diversas formas de tratar el problema. La solución obvia, es no usar POST, sino GET. Particularmente no me gusta el método GET, pero si enviamos la data del formulario por esta via, no tendremos el problema, pues va en la propia URL.

Solución 2

Usar POST, pero con Ajax. Simple y elegante. Enviamos via AJAX nuestra mensaje POST a un método que nos devuelva el código HTML de la data que queremos y lo incrustamos en el DIV que tengamos destinados para mostrar la data. Con JQuery, por ejemplo hacer esto es realmente sencillo.

Ejemplo

Voy a mostrar un pequeño ejemplo, utilizando la librería KumbiaPHP con JQuery. Queremos una página para buscar (y listar) clientes de una tabla de clientes en BD. En la vista tenemos un campo input para que el usuario introduzca la palabra clave y un botón de submit

El controlador cliente_controller.php:

<?php
Load::models('cliente');

class ClienteController extends AppController
{
	public function index()
	{
		$cliente = new cliente();

		if(Input::hasPost('search_key'))
		{
			$search_key = Input::post('search_key');
			$this->clientes = $cliente->selectClienteByCedulaLike($search_key);
			View::select('p_clientes');
			View::template(null);
		}
		else
		{
			$this->clientes = null;
		}
	}
}
?>

La vista index.phtml:

<script type="text/javascript">
$(function() {
	$('#validateform').submit(function() {
		var url = $(location).attr('href');
		var key = $('#cedula').val();
		$.post( url,
		{ search_key: key },
			function(data){
			var capa = $('#tablecontainer');
			capa.html(data);
		});
		return false;
	});
});
</script>

<div id="content">

	<div id="page-heading"><h1>Gestión de Clientes</h1></div>

	<!--  start searchForm  -->
	<?php echo Form::open('','post','id="validateform"'); ?>
	<table>
	<tr>
	<td><?php echo Form::text('cedula','class="inp-form"') ?></td>
	<td><?php echo Form::submit('Buscar', 'class="form-search"') ?></td>
	</tr>
	</table>
	<?php echo Form::close() ?>
	<!--  end searchForm -->

	<div> </div>
	<div class="clear"></div>

	<!--  start client-table -->
	<div id="tablecontainer">
	<!--  Aqui es donde ira el contenido que luego incluiremos via Ajax -->
	</div>
	<!--  end client-table -->
</div>

La vista en la que crearemos la tabla p_clientes.phtml:

<table border="0" width="100%" cellpadding="0" cellspacing="0" id="client-table">
<tr>
	<th class="table-header-repeat line-left minwidth-1"><a href="">Cedula</a></th>
	<th class="table-header-repeat line-left minwidth-1"><a href="">Nombre</a></th>
	<th class="table-header-repeat line-left"><a href="">Telefono</a></th>
</tr>


<?php if($clientes != null) foreach ($clientes as $cliente) : ?>
<tr>
	<?php echo "<tr>"; ?>
	<td><?php echo $cliente->Cedula ?></td>
	<td><?php echo $cliente->nombre ?></td>
	<td><?php echo $cliente->telefono ?></td>
</tr>
<?php endforeach; ?>
</table>

Y eso es todo. Cada vez que el usuario de click al botón buscar, se ejecutará una petición POST con la palabra clave que introdujo el usuario, con el resultado de la búsqueda se construirá una tabla en la vista p_clientes.phtml, que será incrustada dentro del div "tablecontainer" de nuestra vista index.phtml.

No incluí el modelo, porque no lo considero relevante para lo que se quiere mostrar aquí.

Préstese especial atención al script que se encarga de sustituir la data. Es una simple función que se ejecuta cada vez que el usuario presiona el boton de enviar. Obsérvese además que esa función retorna false, pues no queremos que el post sea enviado tradicionalmente. Por supuesto, para mayor detalle, acudir a la documentación oficial de jQuery.