Hacer algo cuando inicia o termina el evento AJAX con Prototype

Introducción.

De manera análoga a como hace poco había mostrado como manejar el evento de inicio y terminación de AJAX con jQuery para realizar algún tipo de acción específica como el mostrar un indicador de carga, ahora experimentaremos como hacerlo con el framework de Prototype el cual nuevamente estaré utilizando en el proyecto de los próximos meses.

Procedimiento.

Ajax.Responders.register({
    onCreate: function()
    {
        // An AJAX request has been initialized!
    },

    onComplete: function()
    {
        // An AJAX request has been completed!
    }
});

Adicionalmente hay otros eventos que pueden manejarse de igual manera onUninitialized, onLoading, onLoaded, onInteractive y onException, además de los ya mencionados onCreate y onComplete.

Enlaces.

Ejemplo rápido y simple de AJAX con PHP y PrototypeJS

Introducción.

Intentando recuperar mis neuronas que saben de Prototype para continuar por fin con uno de los proyectos que se encontraba en pausa permanente, el día de hoy me día a la breve tarea de recordar un poco la invocación asíncrona y la manipulación del DOM utilizando esta librería.  Para hacer una pequeña práctica decidí modificar el ejemplo de la calculadora que se basaba en jQuery y PHP para migrar su código de acuerdo con la especificación de Prototype.

Las modificaciones necesarias se centraron en la inclusión de la librería javascript en la página web (frontend) y en reescribir la invocación asíncrona de la aplicación web en PHP (backend).

Procedimiento.

Reemplazar la inclusión de la librería de jQuery por la de Prototype.  Para este caso se continúo utilizando el API de Google AJAX.

<script src="http://www.google.com/jsapi"></script>
<script>google.load("prototype", "1.6");</script>

Posteriormente se asoció el manejo del evento de presión del botón igual a la invocación de la función procesar, esto se realiza tan pronto como la estructura de la página (código HTML) se encuentra cargada completamente por el navegador.

/* Código a ejecutarse tan pronto como la
   página ha sido cargada por el navegador */

document.observe('dom:loaded', function()
{
    /* Asociar el evento de clic del botón 'igual'
       con la lógica del negocio de la aplicación */

    Event.observe('igual', 'click', procesar);
});

Finalmente se implementa la función procesar que realizará la invocación asíncrona del cálculo matemático.  Esta función consta de las siguientes partes.

  1. Información básica del requerimiento.
  2. Que hacer en caso de éxito.
  3. Que hacer en caso de fracaso.

El requerimiento incluye la siguiente información básica de conexión.

  • El URL de la aplicación remota a invocar (backend).
  • El método HTTP a utilizar.
  • Los parámetros de la página web a enviarse.
function procesar()
{
    new Ajax.Request('calcular.php',                                         /* URL a invocar asíncronamente */
    {
        method:       'post',                                                /* Método utilizado para el requerimiento */
        parameters:   $('formulario').serialize(true),                       /* Información local a enviarse con el requerimiento */

En caso de que la invocación asíncrona tenga un resultado exitoso se deberán realizar los siguientes pasos.

  • Mostrar un mensaje de éxito en color verde.
  • Desplegar el valor del resultado obtenido en el campo definido para tal fin.
        /* Que hacer en caso de ser exitoso el requerimiento */

        onSuccess: function(transport)
        {
            /* Cambiar el color del texto a verde */

            $('mensaje').setStyle('color: #0ab53a');

            /* Mostrar un mensaje informando el éxito sucedido */

            $('mensaje').update("Operación realizada exitosamente");

            /* Mostrar el resultado obtenido del cálculo solicitado */

            $('resultado').update(transport.responseText);
        },

En caso de que fracase el proceso de invocación asíncrona se deberán realizar los siguientes pasos análogos.

  • Mostrar el mensaje de error proveniente del servidor, en color rojo.
  • Limpiar cualquier resultado previo para evitar confusiones con la operación.
        /* Que hacer en caso de que sea fallido el requerimiento */

        onFailure: function(transport)
        {
            /* Cambiar el color del texto a rojo */

            $('mensaje').setStyle('color: #ff0e0e');

            /* Mostrar el mensaje de error */

            $('mensaje').update('Error: ' + transport.responseText);

            /* Limpiar cualquier resultado anterior */

            $('resultado').update('Error');
        }
    });
}

Enlaces.

Resumen de AJAX con Prototype

Introducción.

Para finalizar la serie de artículos que había escrito acerca del uso de AJAX con el framework Prototype voy a realizar un muy breve resúmen de la sintaxis de las tres formas de realizar la invocación que me serán útiles para futuras referencias rápidas.

Utilizando el Updater.

El Ajax.Updater es una facilidad que toma el contenido del resultado del llamado asíncrono y inserta en el interior del componente especificado.

new Ajax.Updater(ID_COMPONENTE,
                 URL,
{
    parameters: $('FORMULARIO').serialize(true),
    method: 'post'
});

Utilizando el Request.

Esta opción es mas flexible que la anterior y permite manipular el contenido de la respuesta del llamado asíncrono.

new Ajax.Request(URL,
{
    parameters: $('FORMULARIO').serialize(true),
    method: 'post',
    onSuccess: function(transport)
    {
        // Hacer algo en éxito.
    },
    onFailure: function(transport)
    {
        // Hacer algo en fracaso.
    },
    onComplete: function(transport)
    {
        // Hacer algo al terminar.
    }
});

Utilizando JSON para el transporte.

El procedimiento es similar al anterior.  La diferencia es que la respuesta del llamado asíncrono viene empaquetada en un objeto JSON, diferente a la invocación anterior en la que se recibe como una cadena de texto.

new Ajax.Request(URL,
{
    parameters: $('FORMULARIO').serialize(true),
    method: 'post',
    requestHeaders:
    {
        Accept: 'application/json'
    },
    onSuccess: function(transport)
    {
        // Hacer algo en éxito.
        var json = transport.responseText.evalJSON(true);
        // var valorX = json.x;
    },
    onFailure: function(transport)
    {
        // Hacer algo en fracaso.
    },
    onComplete: function(transport)
    {
        // Hacer algo al terminar.
    }
});

Del lado del servidor, en PHP, el resultado final se debe encapsular en un objeto JSON antes de enviarlo al cliente.

echo "/*-secure-n";
echo json_encode($contacto -> toArray());
echo "n*/";
header('Content-type: application/json');
header("Status: 200 OK", false, 200);

En caso de error se debe retornar, al igual que en los demás casos, un mensaje cuyo tipo sea != 200.

header("Status: 400 Bad request", false, 400);

Obtener el valor de la selección de un grupo de radiobuttons con Prototype

Suponendo que se tiene el siguiente formulario se requiere determinar cual es el valor seleccionado en el grupo de radiobuttons utilizando las facilidades que ofrece Prototype.

<form id='formulario'>
    <b>Código</b> <input type='radio' id='opc1' name='opciones' value='vCodigo' />
    <b>Nombre</b> <input type='radio' id='opc2' name='opciones' value='vNombre' />
    <b>Documento</b> <input type='radio' id='opc3' name='opciones' value='vDocumento' />
</form>

Desafortunadamente no es posible realizar lo siguiente ya que el radiobuttongroup no es una entidad como tal sino un agrupamiento de las mismas, mas aún, no se referencia por su id sino por su name, motivo por el cual no funcionará con el getElementById.

resultado = $('opciones').value;

Por suerte Prototype es muy flexible y encontré dos soluciones diferentes para este problema que me parecieron muy interesantes, ambas en una sola instrucción compuesta.

La primera utiliza la búsqueda por selector para obtener los radiobuttons del documento que se encuentren seleccionados y que pertenezcan al radiobuttongroup elegido para posteriormente utilizar el método pluck para extraerle su atributo value.

resultado = $$('input:checked[type="radio"][name="opciones"]').pluck('value');

La segunda optiene los radiobuttons de un radiobuttongroup específico accediendo directamente al formulario que los contiene para posteriormente utilizar el método find que retornará al primer elemento encontrado y que se encuentre seleccionado.

resultado = $('formulario').getInputs('radio','opciones').find(function(radio) { return radio.checked; }).value;

Enlaces.

Ejemplo 2 de Ajax con Prototype y PHP

Mi interés en esta miniserie de artículos es la enseñar de manera fácil y didáctica las bases prácticas del desarrollo de aplicaciones web basadas en Javascript Asíncrono (o AJAX).  Para esto se utilizará la librería Prototype que facilitará la implementación de Javascript en el lado del cliente y PHP como lenguaje dinámico en el  lado del servidor, accediendo a una base de datos SQLite que se utilizará como capa de persistencia.

La serie de artículos se divide según cada una de las etapas del desarrollo de la aplicación de ejemplo, la cual consiste en un administrador de contactos muy simple.  Los capitulos mencionados a continuación comprenden la implementación del código XHTML, Javascript general, la creación de la base de datos, el código PHP para acceder a la base de datos y el acceso a la información mediante AJAX.

  1. La interfaz del usuario.
  2. Preparando el escenario con JavaScript.
  3. Estableciendo la persistencia.
  4. Creando el modelo de datos.
  5. Implementando las acciones con AJAX (I).
  6. Implementando las acciones con AJAX (II).

Cada uno de los artículos divide el código de la implementación del subtema que le compete para ser comentado paso a paso.  En el caso en que se prefiera dar un vistazo al código consolidado del capítulo se puede consultar al final de los artículos el producto final de la implementación allí realizada, así como descargar el código fuente de la aplicación implementada hasta el momento.

Espero que esta miniserie les sea de interés y utilidad, y que les permita conocer el desarrollo de aplicaciones web asíncronas mediante el uso de las herramientas elegidas.

Enlaces.

Ejemplo 2 de Ajax con Prototype: implementando las acciones con AJAX (II)

Introducción.

En este capítulo final se va a realizar la implementación de las acciones que permanecen pendientes.

  • Editar a un contacto.
  • Remover a un contacto.

Estas acciones requieren que se haya realizado con anterioridad la implementación de la lista y la carga de contactos para que el usuario tenga la posibilidad de ver los contactos existentes en la base de datos y de seleccionar uno específico sobre el cual se va a ejecutar la acción elegida.

Estas acciones estan basadas en Ajax.Request motivo por el cual su implementación es muy similar a la acción de agregar contactos y carecen de aspectos particulares para destacar.

Editando contactos.

En el lado del cliente.

Se actualiza la implementación de la función JavaScript enEditarRegistro en la cual se realizan las siguientes actividades.

  • Verificar que el contenido del campo oculto que almacena el ID del contacto que se va a editar no se encuentre vacío.
    if($('tId').value.strip().length == 0)
    {
        alert('Para editar un registro primero debe seleccionarlo.');

        return false;
    }
  • Realizar la invocación del llamado asíncrono con las siguientes características.
  • Como parámetros se envían los campos del formulario.
  • Si la invocación asíncrona termina exitosamente se muestra un mensaje y se actualiza el listado de contactos registrados para que se reflejen los cambios.
  • Si la invocación termina de manera fallida se muestra un mensaje indicando el error.
    new Ajax.Request('editar.php',
    {
        method:     'post',

        parameters: $('formulario').serialize(true),

        onSuccess: function(transport)
        {
            ponMensajeBien('<b>' + transport.responseText + '</b>');

            enActualizarBusqueda(null);
        },

        onFailure: function(transport)
        {
            ponMensajeMal('<b>' + transport.responseText + '</b>');
        }
    });

    return true;

En el lado del servidor.

La implementación se realiza en el archivo editar.php que se ubica en el directorio raíz de la aplicación web en el cual se desarrollan las siguientes actividades.

  • Establecer la conexión con la base de datos.
include('config.php');
include_once('basededatos.php');
include_once('contacto.php');

$bd = new BaseDeDatos(BaseDeDatos::crearDSN($bdInfo));
  • Obtener la nueva información del contacto enviada por el cliente.
$tId          = filter_var($_POST['tId'],          FILTER_SANITIZE_STRING);
$tNombres     = filter_var($_POST['tNombres'],     FILTER_SANITIZE_STRING);
$tApellidos   = filter_var($_POST['tApellidos'],   FILTER_SANITIZE_STRING);
$tCorreo      = filter_var($_POST['tCorreo'],      FILTER_SANITIZE_EMAIL);
$tFNacimiento = filter_var($_POST['tFNacimiento'], FILTER_SANITIZE_STRING);
  • Crear una instancia de la clase Contacto con la información recién recibida y realizar la actualización del contacto a través de ella.
$contacto = new Contacto();

$contacto -> ponId($tId);
$contacto -> ponNombres($tNombres);
$contacto -> ponApellidos($tApellidos);
$contacto -> ponCorreo($tCorreo);
$contacto -> ponFechaDeNacimiento($tFNacimiento);

$control = $contacto -> editar($bd);

$bd -> desconectar();
  • Enviar al cliente el mensaje informativo y el código HTTP apropiado según el tipo de terminación del proceso.
if($control > 0)
{
    echo utf8_encode("Registro editado");

    header("Status: 200 OK", false, 200);
}
else
{
    echo utf8_encode("El registro no pudo ser editado, inténtelo nuevamente");

    header("Status: 400 Bad request", false, 400);
}

Removiendo contactos.

Nuevamente el procedimiento es similar al anterior, se utiliza Ajax.Request.

En el lado del cliente.

  • Verificar que el contenido del campo oculto que almacena el ID del contacto que se va a remover no se encuentre vacío.
    if($('tId').value.strip().length == 0)
    {
        alert('Para remover un registro primero debe seleccionarlo.');

        return false;
    }
  • Realizar la invocación asíncrona de la acción consultando al archivo remover.php.
    new Ajax.Request('remover.php',
    {
        method:     'post',

        parameters: $('formulario').serialize(true),

        onSuccess:  function(transport)
        {
            ponMensajeBien('<b>' + transport.responseText + '</b>');

            enActualizarBusqueda(null);
        },

        onFailure:  function(transport)
        {
            ponMensajeMal('<b>' + transport.responseText + '</b>');
        }
    });

    return true;

En el lado del servidor.

  • Establecer la conexión con la base de datos.
include('config.php');
include_once('basededatos.php');
include_once('contacto.php');

$bd = new BaseDeDatos(BaseDeDatos::crearDSN($bdInfo));
  • Obtener la información del contacto enviada por el cliente, es de particular interés la información del campo tId.
$tId = filter_var($_POST['tId'], FILTER_SANITIZE_STRING);
  • Crear una instancia de la clase Contacto con la información recién recibida y realizar la remoción del contacto a través de ella.
$contacto = new Contacto();

$contacto -> ponId($tId);

$control = $contacto -> remover($bd);

$bd -> desconectar();
  • Enviar al cliente el mensaje informativo y el código HTTP apropiado según el tipo de terminación del proceso.
if($control > 0)
{
    echo utf8_encode("Registro removido");

    header("Status: 200 OK", false, 200);
}
else
{
    echo utf8_encode("El registro no pudo ser removido, inténtelo nuevamente");

    header("Status: 400 Bad request", false, 400);
}

Conclusiones.

En los últimos dos capítulos se realiza la implementación de los llamados asíncronos desde la interfaz del usuario (navegador web) utilizando JavaScript que son respondidos por las acciones implementadas en PHP.

Las implementaciones de Ajax hace uso de sus tres presentaciones:

  1. Ajax.Request: útil para realizar el llamado asíncrono, recibir la información de respuesta y realizar cierto grado de procesamiento sobre ella.  Utilizado para agregar, editar y remover contactos.
  2. Ajax.Updater: es una facilidad basada en el tipo anterior.  Su utilidad radica en que el mismo framework se encarga de desplegar la información recibida como respuesta del servidor en el componente especificado de manera automática.  Utilizado para mostrar el listado de los contactos registrados en el sistema.
  3. Ajax.Request con JSON: el involucrar JSON como formato para la respuesta recibida del servidor le permite al cliente realizar fácilmente cualquier procesamiento o transformación de esta información ya que es inmediatamente convertida a un objeto JavaScript con todas las ventajas que esto proporciona.  Utilizado para mostrar la información (cargar) del contacto seleccionado por el usuario.

Enlaces.

Ejemplo 2 de Ajax con Prototype: implementando las acciones con AJAX (I)

Introducción.

Para terminar este mini-tutorial se deben implementar las acciones que corresponden con los requerimientos pendientes de los capítulos anteriores.

La implementación de estas acciones se va a realizar del lado del servidor utilizando AJAX mediante Prototype y PHP y la infraestraestructura recién creada del lado del servidor.  Respecto a AJAX se van a implementar las tres modalidades que permite Prototype.

  1. Ajax.Request.  Realiza la solicitud asíncrona y recibe la respuesta del servidor permitendo manipularla y procesarla según se requiera.  Es posible determinar el éxito o fracaso de la comunicación con el servidor.
  2. Ajax.Updater.  Es un subcaso del método anterior.  Realiza el llamado asíncrono y actualiza automáticamente el contenido de un componente XHTML con la respuesta obtenida del servidor.
  3. Ajax.Request con JSON.  Realiza la solicitud asíncrona y recibe la respuesta del servidor en formato JSON facilitando su manipulación.

Si se reucerda las acciones que quedaron pendientes de implementación fueron las siguientes.

  • Agregar un registro.
  • Editar un registro.
  • Eliminar un registro.
  • Actualizar el listado de contactos.
  • Cargar un contacto específico.

La primera de las acciones que se va a implementar es la de agregar registros ya que le permitirá al usuario empezar a poblar la base de datos.  La implementación de cada acción incluirá modificaciones a los archivos del lado del cliente (JavaScript) y del lado del servidor (PHP).

Agregando registros.

En el lado del cliente.

Se debe actualizar el manejo del evento que sucede ante la solicitud del usuario de agregar un nuevo registro de un Contacto, para esto editamos el archivo jsfunciones.js y modificamos el contenido de la función enAgregarRegistro.

La función debe realizar las siguientes actividades.

  • Realizar las validaciones necesarias para garantizar que los datos se encuentran dispuestos para ejecutarse la acción solicitada.
    • Verificar que todos los campos tengan contenido, es decir, el contenido de ninguno de los campos puede ser vacío.
    var camposVacios = new Array();

    $('formulario').select('.CampoEstilo').each(function (elemento)
    {
        if(elemento.value.strip().length == 0)
        {
            camposVacios.push(elemento.title);
        }
    });

    if(camposVacios.length > 0)
    {
        alert('Los siguientes campos se encuentran vacíos,n' +
              'por favor complemente su información: nn' +
              camposVacios.toString());

        return false;
    }
  • Realizar el llamado asíncrono a la acción agregar.php para adicionar a la base de datos la información contenida en el formulario.
    • En caso de ser exitosa la ejecución de la acción se deberá mostrar en color verde (.MensajeBien) la respuesta obtenida del servidor y actualizar el listado de los clientes registrados donde se deberá reflejar la adición del nuevo registro.
    • En caso de ser fallida la ejecución, se deberá mostrar en color rojo (.MensajeMal).
    new Ajax.Request('agregar.php',
    {
        method:     'post',

        parameters: $('formulario').serialize(true),

        onSuccess: function(transport)
        {
            ponMensajeBien('<b>' + transport.responseText + '</b>');

            enActualizarBusqueda(null);
        },

        onFailure: function(transport)
        {
            ponMensajeMal('<b>' + transport.responseText + '</b>');
        }
    });

    return true;

En el lado del servidor.

Se define la implementación en el archivo agregar.php en el directorio raíz de la aplicación web.

Las actividades realizadas por esta acción se relacionan a continuación.

  • Establecer la conexión con la base de datos.
include('config.php');
include_once('basededatos.php');
include_once('contacto.php');

$bd = new BaseDeDatos(BaseDeDatos::crearDSN($bdInfo));
  • Obtener la información enviada por el cliente.  Esta información viene a través del método POST y los nombres de las variables suministradas corresponden con los nombres de los campos del formulario.
$tNombres     = filter_var($_POST['tNombres'],     FILTER_SANITIZE_STRING);
$tApellidos   = filter_var($_POST['tApellidos'],   FILTER_SANITIZE_STRING);
$tCorreo      = filter_var($_POST['tCorreo'],      FILTER_SANITIZE_EMAIL);
$tFNacimiento = filter_var($_POST['tFNacimiento'], FILTER_SANITIZE_STRING);
  • A partir de la información recién obtenida se crea una instancia del objecto Contacto y almacena su información utilizando su método agregar.
$contacto = new Contacto();

$contacto -> ponNombres($tNombres);
$contacto -> ponApellidos($tApellidos);
$contacto -> ponCorreo($tCorreo);
$contacto -> ponFechaDeNacimiento($tFNacimiento);

$control = $contacto -> agregar($bd);

$bd -> desconectar();
  • Según el resultado de la operación (valor de $control) se define si la ejecución fue exitosa o fallida y se envía al cliente el mensaje y código HTTP apropiado.
if($control > 0)
{
    echo utf8_encode("Registro agregado");

    header("Status: 200 OK", false, 200);
}
else
{
    echo utf8_encode("El registro no pudo ser agregado");

    header("Status: 400 Bad request", false, 400);
}

Listando contactos registrados.

Esta acción permitirá que se verifique si efectivamente la adición de registros anterior se encuentra funcionando correctamente o no al desplegar en la interfaz (6) el listado de contactos existente en la base de datos de acuerdo con el filtro de búsqueda seleccionado.

En el lado del cliente.

La misión de esta acción es la de actualizar al div Resultados con la información de la base de datos de acuerdo con los criterios contenidos en los campos sLetra y sCampo.  Se utiliza el Ajax.Updater para este fin.

    new Ajax.Updater('Resultados',
             'listar.php',
             {
                parameters: $('sLetra').serialize(true) + "&" + $('sCampo').serialize(true),

                method: 'post'
             });

En el lado del servidor.

Se realiza la implementación del archivo listar.php en el cual desarrollan las siguientes actividades.

  • Establecer la conexión con la base de datos.
include('config.php');
include_once('basededatos.php');
include_once('contacto.php');

$bd = new BaseDeDatos(BaseDeDatos::crearDSN($bdInfo));
  • Obtener el valor del campo sCampo y verificar que sea válido, es decir, sea nom o ape.
$campos = array('nom' => 'nombres',
                'ape' => 'apellidos');

$sCampo = filter_var($_POST['sCampo'], FILTER_SANITIZE_STRING);

if(!isset($campos[$sCampo]))
{
    echo utf8_encode("<span class='MensajeMal'>Los parámetros de búsqueda especificados son inválidos.</span>");

    header("Status: 400 Bad request", false, 400);

    exit;
}
  • Obtener el valor del campo sLetra.
$campo = $campos[$sCampo];

$sLetra = filter_var($_POST['sLetra'], FILTER_SANITIZE_STRING);
  • Crear la condición de filtro de búsqueda y obtener el listado de los Contactos que cumplen con dicho criterio.
$letra1 = (($sLetra == 'todos' ) ? "" : strtoupper($sLetra[0])) . "%";
$letra2 = (($sLetra == 'todos' ) ? "" : strtolower($sLetra[0])) . "%";

$where = $campo . " LIKE '{$letra1}' OR " . $campo . " LIKE '{$letra2}'";

$listado = Contacto::listar($bd, $where);
  • Generar la presentación del listado (XHTML) para ser enviado hacia el cliente para que sea ubicado en el contenedor apropiado de la página web.
echo "<table><tbody>";

echo "<tr><td class='RegistroTitulo'>Apellidos</td>" .
     "<td class='RegistroTitulo'>Nombres</td>" .
     "<td class='RegistroTitulo'>Correo</td>" .
     "<td class='RegistroTitulo'>FNacimiento</td></tr>";

foreach($listado as $registro)
{
    $id          = $registro['id'];
    $nombres     = urldecode($registro['nombres']);
    $apellidos   = urldecode($registro['apellidos']);
    $correo      = urldecode($registro['correo']);
    $fnacimiento = urldecode($registro['fnacimiento']);

    $e1 = "<a href='#' onClick='enCargarRegistro({$id}); return false;'>";
    $e2 = "</a>";

    echo "<tr><td class='RegistroListado'>{$e1}{$apellidos}{$e2}</td>" .
         "<td class='RegistroListado'>{$e1}{$nombres}{$e2}</td>" .
         "<td class='RegistroListado'>{$e1}{$correo}{$e2}</td>" .
         "<td class='RegistroListado'>{$e1}{$fnacimiento}{$e2}</td></tr>";
}

echo "</tbody></table>";

$bd -> desconectar();

Cargando registros.

El usuario puede seleccionar a uno de los Contactos registrados haciendo clic sobre sus datos en el componente (6), esta acción invoca a la función enCargarRegistro que conociendo el ID del Contacto debe obtener toda su información para ser desplegada en el formulario apropiado.

Esta acción requiere poder manipular de manera discreta e individual a la información retornada por el servidor ya que cada valor corresponderá con un campo diferente del Contacto seleccionado, lo cual es diferente a por ejemplo la acción anterior, la obtención del listado de Contactos, en la cual todo lo obtenido del servidor (XHTML) es transferido directamente y sin procesamiento a un div específico; por este motivo se involucrará el uso de JSON en la respuesta del llamado asíncrono.

En el lado del cliente.

  • La función enCargarRegistro recibe el ID del Contacto a obtener el cual se convierte en parámetro para la acción recuperar.php.
    new Ajax.Request('recuperar.php',
    {
        parameters: 'id=' + id,

        method: 'post',
  • Se especifica que el resultado de esta acción deberá ser del tipo Application/JSON.
        requestHeaders:
        {
            Accept: 'application/json'
        },
  • En caso de fracasar el llamado asíncrono se mostrará un mensaje indicándolo.
        onFailure: function(transport)
        {
            ponMensajeMal('<b>No fue posible recuperar el registro ' + id + '</b>: ' + transport.responseText);
        }
  • En caso de tener éxito el llamado asíncrono se ejecutarán las siguientes actividades.
  • Se evalúa la respuesta del servidor como un objeto JSON convirtiéndo la respuesta en un objeto JavaScript.
            var json = transport.responseText.evalJSON(true);
  • Se llenan los camps del formulario con la información recibida del servidor.
            $('tId').value          = json.id;
            $('tNombres').value     = json.nombres;
            $('tApellidos').value   = json.apellidos;
            $('tCorreo').value      = json.correo;
            $('tFNacimiento').value = json.fechaNacimiento;
  • Se modifica la acción del formulario a Editar ya que se cuenta con un registro seleccionado.
            $('bAccion').value = 'Editar';
  • Se muestra un mensaje de éxito.
            ponMensajeBien('<b>Registro recuperado: ' + json.id + '</b>');

En el lado del servidor.

La lógica de la acción se implementa en el archivo recuperar.php ubicado en la raíz de la aplicación web y en ella se desarrollan las siguientes actividades.

  • Establecer la conexión con la base de datos.
include('config.php');
include_once('basededatos.php');
include_once('contacto.php');

$bd = new BaseDeDatos(BaseDeDatos::crearDSN($bdInfo));
  • Obtener el identificador del Contacto enviado desde el cliente.
$tId = filter_var($_POST['id'], FILTER_SANITIZE_STRING);
  • Crear una instancia de la clase Contacto y a través de ella recuperar la información del contacto especificado por el usuario.
$contacto = new Contacto();

$control = $contacto -> recuperar($bd, $tId);

$bd -> desconectar();
  • A partir de la respuesta obtenida ($control) determinar si la acción fue realizada exitosa o fallidamente y realizar los procedimientos apropiados para informárselo al cliente.
  • En caso de éxito:
    • Enviar al cliente el objeto resultante en formato JSON.
    • Informar al cliente que el tipo de contenido enviado es Application/JSON.
    • Enviar el código HTTP de éxito.
if($control === true)
{
    echo "/*-secure-n";

    echo json_encode($contacto -> toArray());

    echo "n*/";

    header('Content-type: application/json');

    header("Status: 200 OK", false, 200);
}
  • En caso de fracaso:
    • Enviar al usuario el motivo del error con un mensaje informativo.
    • Enviar el código HTTP de fracaso.
else
{
    echo utf8_encode("El registro no pudo ser agregado");

    header("Status: 400 Bad request", false, 400);
}

Ejemplo 2 de Ajax con Prototype: creando el modelo de datos

Introducción.

Teniendo lista la capa de acceso a la base de datos se procede entonces a la creación del modelo de datos de la única estructura de persistencia que tiene el proyecto: la tabla Contacto. La utilidad de este modelo es la de abstraer del resto de la aplicación todo lo concerniente a su lógica especifica de negocio, es decir, especificar todo lo relacionado con su conducta, consulta y persistencia para que el resto de la aplicación se pueda desentender de ella y se modularice al estilo DRY

Creación de la clase contacto.

Para hacer esto se crea el archivo contacto.php en el directorio raíz del proyecto.

Los campos de la clase coinciden con los especificados en la tabla de la base de datos con igual nombre. La versión inicial de la clase se describe a continuación.

<?php

class Contacto
{
    private $id;
    private $nombres;
    private $apellidos;
    private $correo;
    private $fechaNacimiento;

    // Resto de la implementación.
}

?>

Se crean los métodos para modificar desde el exterior a los atributos de la instancia: métodos pon o setters.

    public function ponId($valor)
    {
        $this -> id = $valor;
    }

    public function ponNombres($valor)
    {
        $this -> nombres = $valor;
    }

    public function ponApellidos($valor)
    {
        $this -> apellidos = $valor;
    }

    public function ponCorreo($valor)
    {
        $this -> correo = $valor;
    }

    public function ponFechaDeNacimiento($valor)
    {
        $this -> fechaNacimiento = $valor;
    }

De igual manera se crean los métodos que permiten obtener el valor de los atributos de la instancia: métodos obt o getters.

    public function obtId()
    {
        return $this -> id;
    }

    public function obtNombres()
    {
        return $this -> nombres;
    }

    public function obtApellidos()
    {
        return $this -> apellidos;
    }

    public function obtCorreo()
    {
        return $this -> correo;
    }

    public function obtFechaDeNacimiento()
    {
        return $this -> fechaNacimiento;
    }

Estos métodos son el lugar idóneo para establecer restricciones en el dominio de los valores de los atributos como por ejemplo, evitar que al campo $correo se le asignen direcciones que no cumplan con el formato o que al campo $fechaNacimiento se le asignen fechas futuras entre otros. Por simplicidad de la aplicación de demostración este tipo de validaciones se omiten y se dejan libres para ser desarrolladas por el lector.

Implementación del acceso a la base de datos.

Como se mencionó inicialmente, el acceso a la base de datos no se realiza directamente utilizando las función de PDO provistas por PHP sino que por el contrario, se aprovecha la capa adicional de alto nivel implementada en el capítulo pasado.

Todas las operaciones descritas a continuación hacen referencia a como almacenar, editar, remover y recuperar Contactos (operaciones CRUD) a partir de la información de la instancia de la clase del mismo nombre.

Agregando contactos.

    public function agregar($bd)
    {
        $sql = "INSERT INTO contacto ('nombres', 'apellidos', 'correo', 'fnacimiento') " .
               "VALUES ('{$this -> nombres}', '{$this -> apellidos}', '{$this -> correo}', '{$this -> fechaNacimiento}')";

        return $bd -> ejecutar($sql);
    }

Editando contactos.

    public function editar($bd)
    {
        $sql = "UPDATE contacto SET nombres='{$this -> nombres}', " .
               "apellidos='{$this -> apellidos}', correo='{$this -> correo}', " .
               "fnacimiento='{$this -> fechaNacimiento}' WHERE id='{$this -> id}'";

        return $bd -> ejecutar($sql);
    }

Removiendo contactos.

    public function remover($bd)
    {
        $sql = "DELETE FROM contacto WHERE id='{$this -> id}'";

        return $bd -> ejecutar($sql);
    }

Recuperando contactos.

    public function recuperar($bd, $id)
    {
        $sql = "SELECT id, nombres, apellidos, correo, fnacimiento FROM contacto WHERE id='{$id}'";

        $res = $bd -> consultar($sql) -> fetch();

         if($res === false)
            return false;

        $this -> id              = $res['id'];
        $this -> nombres         = $res['nombres'];
        $this -> apellidos       = $res['apellidos'];
        $this -> correo          = $res['correo'];
        $this -> fechaNacimiento = $res['fnacimiento'];

        return true;
    }

Nótese como todos los métodos del modelo Contacto reciben por parámetro a la instancia de BaseDeDatos a la cual se va a acceder, esto quiere decir que es responsabilidad de la acción que los invoca el establecer la conexión y el validar que esta se haya realizado exitosamente.

También se puede observar que el método recuperar recibe un parámetro adicional al resto de los métodos el cual hace referencia al ID del Contacto que se desea recuperar, con él se realiza la consulta a la base de datos y en éxito se actualizan los campos de la instancia con la información obtenida. El resto de los métodos realizan su cometido: agregar, actualizar y borrar, a partir la información encontrada en sus atributos.

Listando contactos.

Un método adicional que requiere del acceso a la base de datos es el que permite obtener un listado de los contactos registrados filtrado de acuerdo a una serie de condiciones especificadas en ejecución. Este método será útil para implementar el listado de contactos con filtro de búsqueda (5 y 6).

	public static function listar($bd, $where=false)
	{
		$sql = "SELECT id, nombres, apellidos, correo, fnacimiento " .
	               "FROM contacto " .
		       (($where !== false) ? "WHERE " . $where . " " : "") .
		       "ORDER BY apellidos, nombres";

		return $bd -> consultar($sql);
	}

Métodos complementarios.

Un último método que se requiere como complemento para facilitar la manipulación del objeto utilizando JSON es el método toArray que convierte los atributos de la instancia a su correspondiente representación de arreglo, algo muy similar a lo realizado por la función get_object_vars.

    public function toArray()
    {
        return array('id'              => $this -> id,
                     'nombres'         => urldecode($this -> nombres),
                     'apellidos'       => urldecode($this -> apellidos),
                     'correo'          => urldecode($this -> correo),
                     'fechaNacimiento' => urldecode($this -> fechaNacimiento));
    }

Conclusiones.

Con la implementación del modelo que recubre a un mayor nivel las operaciones relacionadas con el acceso a la persistencia de los datos y la lógica del negocio se terminan los preparativos de infraestructura para la implementación de las acciones faltantes de la aplicación de ejemplo a través de accesos asíncronos.

Enlaces.

Ejemplo 2 de Ajax con Prototype: estableciendo la persistencia

Introducción.

Después de crear la estructura general de la aplicación web en JavaScript, el siguiente paso es determinar e implementar como se va a realizar la persistencia de los datos de los contactos del lado del servidor.

Para la aplicación de demostración sólo se necesitará almacenar la información de una única entidad llamada Contacto que incluirá los siguientes campos.

  1. Identificador – código del Contacto, servirá como llave primaria.
  2. Nombres.
  3. Apellidos.
  4. Correo electrónico.
  5. Fecha de nacimiento.

Como motor de base de datos se va a utilizar SQLite el cual para este caso práctico nos trae varias ventajas entre las cuales podemos contar a muy buena integración con PHP, soporte la lenguaje SQL y por ende al modelo relacional y la facilidad de instalación y administración, que incluye evitarnos la necesidad de instalar otro software cliente/servidor como sería el caso con MySQL que es un motor de base de datos de un perfil mucho mayor.

Creación de la base de datos.

Para crear la base de datos se realizan los pasos descritos a continuación.

Se crea el directorio /data bajo la carpeta del proyecto web.

Se descarga la aplicación sqlite.exe (para este ejemplo se utilizó la versión 3.6.6.2) y se copia este archivo en el directorio recién creado.

Se establece al comando la sentencia DDL para la creación de la tabla tal y como se describe a continuación.

CREATE TABLE contacto
(
    id          INTEGER      PRIMARY KEY,
    nombres     VARCHAR(30)  NOT NULL,
    apellidos   VARCHAR(30)  NOT NULL,
    correo      VARCHAR(128) NOT NULL,
    fnacimiento DATE         NOT NULL
)

El comando anterior es grabado en el archivo /data/script.sql para su posterior ejecución.

En una ventana de Símbolo del Sistema de Windows (o Shell de *nix) accedemos al directorio de datos donde se encuentra ahora sqlite.exe y el script.sql y ejecutamos los siguientes comandos del sistema operativo.

cd <ruta>data
sqlite contactos.sqlite < script.sql

La ejecución de la línea anterior se encarga de crear el archivo de la base de datos (contactos.sqlite) y de interpretar el contenido del archivo script.sql con la definición de la base de datos.

El procedimiento descrito anteriormente puede realizarse también utilizando herramientas mas elaboradas que facilitan el desarrollo y administración de bases de datos SQLite, entre las cuales destaco SQLite Manager que es un plugin muy util para Firefox.

Implementación del acceso a la base de datos.

Para realizar el acceso a la base de datos SQLite desde PHP se van a utilizar las clases PDO (PHP Data Objects) y se va a implementar la clase BaseDeDatos para simplificar su acceso desde la aplicación.

Para hacer esto creamos el archivo <ruta>basededatos.php en el directorio raíz del proyecto en el cual se define la clase BaseDeDatos de la siguiente manera.

<?php

class BaseDeDatos
{
    private $enlace;

    public function __construct ($dsn)
    {
        $this -> conectar ($dsn);
    }

    // Resto de la implementación.
}

?>

El atributo de clase $enlace almacenará la referencia a la conexion a la base de datos, mientras que el constructor se encargará de recibir la cadena de conexión (DSN) y él mismo se encarga de gestionar la conexión.

Para hacer esto se apoya en el método estático crearDSN diseñado para facilitar su preparación.

    public static function crearDSN ($informacion)
    {
        return 'sqlite:' . $informacion['ruta'];
    }

Este método espera recibir la $informacion de conexión de la siguiente manera la cual por conveniencia de centralizarla se ubicó en el archivo config.php.

array('ruta' => 'data/contactos.sqlite')

La clase BaseDeDatos incluye también métodos para conectarse (a partir de la información proporcionada por en el DSN) y desconectarse de la fuente de datos.

    public function conectar ($dsn)
    {
        $this -> enlace = new PDO ($dsn);

        return $this -> enlace;
    }

    public function desconectar ()
    {
        $this -> enlace = null;
    }

E incluye además métodos para realizar consultas sobre los datos almacenados.

    public function ejecutar ($consulta)
    {
        return $this -> enlace -> exec ($consulta);
    }

    public function consultar ($consulta)
    {
        return $this -> enlace -> query ($consulta);
    }

La diferencia entre consultar y ejecutar radica en que el primero de ellos se utiliza para obtener información (SELECT) de la base de datos y por ende retorna un conjunto de resultados, mientras que el segundo de ellos se utiliza para modificar información (INSERT, UPDATE, DELETE) y retorna el número de filas afectadas por el comando ejecutado.

Conclusiones.

En este capítulo se estableció todo lo referente a la persistencia de la información de los contactos del proyyecto, incluyendo a una capa adicional de software (la clase BaseDeDatos) que facilitará el manejo del acceso a los datos y proveerá simplicidad si se desea posteriormente modificar el motor de base de datos.

El siguiente paso consistirá en la implementación de la entidad Contacto como tal que utilizará como sustrato lo implementado hasta ahora.

Enlaces.

Ejemplo 2 de Ajax con Prototype: preparando el escenario con JavaScript

Creación de los archivos JavaScript.

Después de crearel nivel de presentación con XHTML y CSS, continuamos con la preparación de la infraestructura funcional de la aplicación utilizando JavaScript.

Para esto descargamos la última versión de Prototype, la cual para este caso era la 1.6.0.3 y lo ubicamos en un nuevo directorio llamado /js bajo el directorio de la aplicación.

En ese mismo directorio creamos el archivo funciones.js donde se van a agregar a continuación las funciones que determinarán el comportamiento del sistema.

Referencia desde el archivo XHTML.

Es necesario incluir a estos dos nuevos archivos (prototype-1.6.0.3.js y funciones.js) en la aplicación para que el navegador los descargue cuando el usuario la acceda.  Para hacer esto se incluye su referencia en la cabecera del archivo demo.php de la siguiente manera.

    <script src="js/prototype-1.6.0.3.js" type="text/javascript"></script>
    <script src="js/funciones.js" type="text/javascript"></script>

Estableciendo el estado inicial.

Lo primero que se debe hacer establecer el manejo de eventos de la aplicación, si se recuerda toda la información que conectaba los componentes XHTML a sus eventos (onclick, onchange, etc.) fue omitida voluntariamente para promover la separación de contenido.  Esto se debe realizar tan pronto como la aplicación es cargada para que cuando el usuario interactúe con ella los eventos ya se encuentren disponibles.  Para esto se utiliza el evento on DOM loaded el cual sucede tan pronto como la estructura (XHTML) de la página ha sido cargada.  Se aprovecha también para llamar a la función que listará a los contactos registrados (6) para que aparezcan tan pronto como el usuario accede al sitio.  Esta función como otras será implementada próximamente.

document.observe('dom:loaded', function()
{
    establecerManejoEventos();

    enActualizarBusqueda(null);
});

Requerimientos funcionales.

Como se mencionó anteriormente, la función establecerManejoEventos especifica que funciones se van a invocar ante cual evento generado y determinar con esto el flujo de la aplicación.  El esquema general de este flujo se basa en las siguientes premisas.

  • Si el usuario desea crear a un nuevo registro (bNuevo) se deberá limpiar el formulario para que se ingresen los nuevos datos y la acción siguiente será la de agregar el registro.
  • Si el usuario presiona el botón de acción (bAccion) su funcionamiento dependerá de si se está agregando a un registro nuevo o si por el contrario se está editando a un registro ya existente.  El texto de la etiqueta define su funcionalidad.
  • Si el usuario presiona el botón de remover (bRemover) se deberá solicitar la remoción del registro y actualizar la lista de contactos registrados la cual ya no deberá incluír al mensaje antes seleccionado.
  • Si el usuario presiona el botón de listar (bListar) se deberá actualizar la lista de contactos registrados.
  • Si el usuario modifica alguno de los filtros de búsqueda (sLetra y sCampo) se deberá actualizar el listado de los contactos registrados mostrando únicamente los que cumplan con el criterio de búsqueda seleccionado.
  • Si el usuario decide editar a un registro específico haciendo click sobre su enlace desde el listado de contactos registrados, sus datos deben ubicarse en el formulario de edición y la próxima acción deberá ser la de editar a dicho registro.
  • En cualquier caso se deberá mostrar un mensaje de éxito (verde) o de error (rojo) al usuario indicándole el estado de su solicitud.

Establecimiento del manejo de eventos.

Para manipular formalmente el flujo de la aplicación a partir de los eventos sucitados, se establecen observadores sobre los diferentes componentes los cuales relacionan a un determinado tipo de eventos sobre ellos con una función JavaScript que será ejecutada cuando sucedan.  Los tipos de eventos utilizados por esta aplicación de demostración son onClick sobre los botones y onChange en los campos de selección.

function establecerManejoEventos()
{
    $('bNuevo').observe('click',   enNuevo);
    $('bAccion').observe('click',  enCrearActualizar);
    $('bRemover').observe('click', enRemover);
    $('bListar').observe('click',  enActualizarBusqueda);
    $('sLetra').observe('change',  enActualizarBusqueda);
    $('sCampo').observe('change',  enActualizarBusqueda);
}

Mostrar mensajes de estado.

El mensaje de estado es un texto que le indica al usuario final el éxito o fracaso de la solicitud que le hizo al sistema.  Para tal fin se definió a mensaje el cual es un div que se encuentra en la parte superior del formulario después de la cabecera con el título.

Las siguientes funciones se utilizan para escribir un texto en el mensaje de estado y para limpiarlo.

function ponMensajeEstado(mensaje)
{
    $('mensaje').update(mensaje);
}

function limpiarMensajeEstado()
{
    ponMensajeEstado('');
}

Mensajes de estado buenos y malos.

Los mensajes de estado indican del éxito o fracaso de un procedimiento, para diferenciarlos visualmente se decidió que los mensajes de éxito se presenten de color verde y los mensajes de error sean de color rojo.  Para esto se crearon dos funciones por separado que facilitarán su presentacion y que a su vez se basan en las funciones recientemente creadas.

function ponMensajeBien(mensaje)
{
    $('mensaje').addClassName('MensajeBien');
    $('mensaje').removeClassName('MensajeMal');

    ponMensajeEstado(mensaje + '.');
}

function ponMensajeMal(mensaje)
{
    $('mensaje').addClassName('MensajeMal');
    $('mensaje').removeClassName('MensajeBien');

    ponMensajeEstado(mensaje + '.');
}

MensajeBien y MensajeMal son dos clases CSS definidas en css/style.css que determinan el formato del texto a desplegarse.  Consulte dicho archivo para complementar su utilización.

Crear un nuevo registro.

Como se mencionó anteriormente, la creación de un nuevo registro involucra tres actividades: limpiar el formulario para que el usuario ingrese la nueva información, modificar el tipo de acción próximo a Agregar y limpiar el mensaje de estado para no confundir al usuario con cualquier mensaje previo.

function enNuevo(evento)
{
    $('bAccion').value      = 'Agregar';

    $('tId').value          = '';
    $('tNombres').value     = '';
    $('tApellidos').value   = '';
    $('tCorreo').value      = '';
    $('tFNacimiento').value = '';

    limpiarMensajeEstado();
}

Modificar la información.

El manejo de una solicitud de acción es levemente mas elaborado ya que puede ser de dos tipos: Agregar a un nuevo registro o Editar a uno ya existente.  Por claridad, la función que maneja el evento va a determinar el tipo de acción solicitada y realizará la invocación de una función específica, con su apropiada implementación.

function enCrearActualizar(evento)
{
    if($('bAccion').value == 'Agregar')
        return enAgregarRegistro(evento);

    if($('bAccion').value == 'Editar')
        return enEditarRegistro(evento);

    alert('La acción especificada es desconocida!');

    limpiarMensajeEstado();

    return false;
}

Manipulación y acceso a los datos.

Las funciones restantes: enAgregarRegistro, enEditarRegistro, enActualizarBusqueda, enCargarRegistro y enRemover que permiten manipular la información de los registros serán implementadas utilizando AJAX, sin embargo su implementación tendrá que esperar un poco ya que antes es necesario determinar e implementar el modelo de datos de la aplicación.

Conclusiones.

En este capítulo se realizó la implementación de la estructura que provee el flujo de eventos de la aplicación en general, esto se realizó en JavaScript y utilizando las facilidades de la librería de Prototype.

Algunas de las funciones de este nivel, las relacionadas con la manipulación de información, quedarán pendientes de implementación hasta que se realice la implementación de la persistencia en un próximo capítulo del tutorial.

La aplicación a este nivel ya es funcional, teniendo en cuenta que por ahora, las funciones sin implementación se limitarán a mostrar en pantalla un mensaje informativo.

Enlaces.