Recetas Ajax

Mientras web2py se usa principalmente para el desarrollo del lado del servidor, la aplicación de andamiaje welcome viene con la librería básica jQuery 32, calendarios jQuery (seleccionador de fecha, seleccionador de hora y reloj), el menú “superfish.js”, y algunas funciones adicionales JavaScript basadas en jQuery.

Nada en web2py evita que usted use otras librerías Ajax tales como Prototype, ExtJS, o YUI, pero decidimos empacar jQuery porque encontramos que es más fácil de usar y más poderoso que otras librerías equivalentes. También nos dimos cuenta que captura el espíritu de web2py en el sentido de ser funcional y consiso.

web2py_ajax.html

La aplicación web2py andamio “welcome” incluye un archivo llamado

1. views/web2py_ajax.html

Este archivo está incluído al principio del archivo de diseño por defecto “layout.html” y proporciona los siguientes servicios:

  • Incluye static/jquery.js.
  • Incluye, si existen, static/calendar.js y static/calendar.css.
  • Define una función ajax (basada en jQuery $.ajax).
  • Hace que cualquier DIV de clase “error” o cualquier objeto etiqueta de clase “flash” se deslize hacia abajo.
  • Evita que se ingresen enteros inválidos en campos INPUT del tipo “integer”.
  • Evita que se ingresen números flotantes inválidos en campos INPUT del tipo “double”.
  • Conecta campos INPUT del tipo “date” con un selector emergente de fechas.
  • Conecta campos INPUT del tipo “datetime” con un selector emergente de horas.
  • Conecta campos INPUT del tipo “time” con un selector emergente de tiempo.
  • Define web2py_ajax_component, una herramienta muy importante que será descrita en el Capítulo 13.

También incluye las funciones popup, collapse, y fade para compatibilidad con versiones anteriores.

Aquí está un ejemplo de como los otros efectos funcionan bien juntos.

Se considera una aplicación test con el siguiente modelo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
db = DAL("sqlite://db.db")
db.define_table('child',
     Field('name'),
     Field('weight', 'double'),
     Field('birth_date', 'date'),
     Field('time_of_birth', 'time'))
db.child.name.requires=IS_NOT_EMPTY()
db.child.weight.requires=IS_FLOAT_IN_RANGE(0,100)
db.child.birth_date.requires=IS_DATE()
db.child.time_of_birth.requires=IS_TIME()

con este controlador por defecto “default.py”:

1
2
3
4
5
def index():
    form = SQLFORM(db.child)
    if form.accepts(request.vars, session):
        response.flash = 'record inserted'
    return dict(form=form)

y la siguiente vista por defecto “default/index.html”:

1
2
{{extend 'layout.html}}
{{=form}}

La acción “index” genera el siguiente formulario:

form_test_empty

En caso de que se envíe un formulario inválido, el servidor devuelve la página con un formulario modificado que contiene mensajes de error. Los mensajes de error son DIVs de clase “error”, y debido al código web2py_ajax mostrado arriba, los errores aparecen con un efecto de deslizado hacia abajo:

form_test_errors.png

El color de los errores se define en el código CSS dentro de “layout.html”.

El código web2py_ajax evita que usted ingrese un valor inválido en el campo de entrada. Esto se hace antes y además de, no como un substituto para, la validación del lado del servidor.

El código web2py_ajax muestra un selector de fecha cuando usted ingresa un campo de entrada de clase “date”, y muestra un selector de hora y fecha cuando ingresa un campo de entrada del tipo “datetime”. He aquí un ejemplo:

form_test_datetimepicker.png

El código web2py_ajax también muestra el siguiente seleccionador de tiempo cuando se trata de editar un campo de entrada de clase “time”:

form_test_timepicker.png

Luego del envío, el controlador de la acción establece la respuesta flash en el mensaje “record inserted”. El diseño por defecto muestra este mensaje en un DIV con id=”flash”. El código web2py_ajax es responsable de hacer que este DIV aparezca y luego desaparezca cuando se hace clic en él:

form_test_flash.png

Éstos y otros efectos son accesibles programáticamente en las vistas y a través de los ayudantes en los controladores.

Efectos jQuery

Los efectos básicos descritos aquí no requieren archivos adicionales; todo lo que usted necesita ya está incluído en web2py_ajax.html.

Los objetos HTML/XHTML pueden ser identificados por su tipo (por ejemplo un DIV), sus clases, o su id. Por ejemplo:

1
2
<div class="one" id="a">Hello</div>
<div class="two" id="b">World</div>

Ellos pertenecen a la clase “one” y “two” respectivamente. Ellos tienen ids “a” y “b” respectivamente.

En jQuery usted se puede referir al primero con las siguientes notaciones equivalentes parecidas a CSS

jQuery('.one')    // address object by class "one"
jQuery('#a')      // address object by id "a"
jQuery('DIV.one') // address by object of type "DIV" with class "one"
jQuery('DIV #a')  // address by object of type "DIV" with id "a"

y referirse a lo segundo con:

1
2
3
4
jQuery('.two')
jQuery('#b')
jQuery('DIV.two')
jQuery('DIV #b')

o puede referirse a ambos con

1
jQuery('DIV')

Los objetos etiqueta están asociados a eventos, tal como “onclick”. jQuery permite enlazar estos eventos a efectos, por ejemplo “slideToggle”:

1
2
<div class="one" id="a" onclick="jQuery('.two').slideToggle()">Hello</div>
<div class="two" id="b">World</div>

Ahora si usted hace clic en “Hello”, “World” desaparece. Si hace clic otra vez, “World” reaparece. Usted puede hacer que una etiqueta esté oculta por defecto asignándole la clase hidden:

1
2
<div class="one" id="a" onclick="jQuery('.two').slideToggle()">Hello</div>
<div class="two hidden" id="b">World</div>

También puede enlazar acciones a eventos fuera de la etiqueta en sí. El código anterior se puede reescribir de la siguiente manera:

1
2
3
4
5
<div class="one" id="a">Hello</div>
<div class="two" id="b">World</div>
<script>
jQuery('.one').click(function(){jQuery('.two').slideToggle()});
</script>

Los efectos devuelven el objeto que hace la llamada, por lo que pueden encadenarse.

Cuando se hace click, se establece que la función de regreso sea llamada cuando se hace click. Ocurre de manera similar para change, keyup, keydown, mouseover, etc.

Una situación común es la necesidad de ejecutar algún código JavaScript sólo después de que se ha cargado completamente el documento. Ésto usualmente lo hace el atributo onload de BODY pero jQuery provee una forma alternativa que no requiere editar el diseño:

1
2
3
4
5
6
7
<div class="one" id="a">Hello</div>
<div class="two" id="b">World</div>
<script>
jQuery(document).ready(function(){
   jQuery('.one').click(function(){jQuery('.two').slideToggle()});
});
</script>

El cuerpo de la función sin nombre se ejecuta sólo cuando el documento está listo, después que ha sido cargado completamente.

He aquí una lista de nombres de eventos útiles:

Eventos para Formularios

  • onchange: Código a ser ejecutado cuando el elemento cambia
  • onsubmit: Código a ser ejecutado cuando se envía el formulario
  • onreset: Código a ser ejecutado cuando el formulario se reinicia
  • onselect: Código a ser ejecutado cuando se selecciona el elemento
  • onblur: Código a ser ejecutado cuando el elemento pierde el enfoque
  • onfocus: Código a ser ejecutado cuando el elemento obtiene el enfoque

Eventos para teclado

  • onkeydown: Código a ser ejecutado cuando se presiona una tecla
  • onkeypress: Código a ser ejecutado cuando se presiona y suelta una tecla
  • onkeyup: Código a ser ejecutado cuando se suelta una tecla

Eventos para el ratón

  • onclick: Código a ser ejecutado cuando se le da click al ratón
  • ondblclick: Código a ser ejecutado cuando se le da doble click al ratón
  • onmousedown: Código a ser ejecutado cuando se presiona el botón del ratón
  • onmousemove: Código a ser ejecutado cuando se mueve el apuntador del ratón
  • onmouseout: Código a ser ejecutado cuando el apuntador del ratón se mueve fuera de un elemento
  • onmouseover: Código a ser ejecutado cuando el apuntador del ratón se mueve sobre un elemento
  • onmouseup: Código a ser ejecutado cuando se suelta el botón del ratón

He aquí una lista de efectos útiles definidos por jQuery:

Efectos

  • jQuery(...).attr(name): Devuelve el nombre del valor del atributo
  • jQuery(...).attr(name, value): Le asigna el valor al nombre del atributo
  • jQuery(...).show(): Hace el objeto visible
  • jQuery(...).hide(): Hace el objeto oculto
  • jQuery(...).slideToggle(speed, callback): Hace que el objeto se deslice hacia arriba o hacia abajo
  • jQuery(...).slideUp(speed, callback): Hace que un objeto se deslice hacia arriba
  • jQuery(...).slideDown(speed, callback): Hace que un objeto se deslice hacia abajo
  • jQuery(...).fadeIn(speed, callback): Hace que un objeto se aparezca
  • jQuery(...).fadeOut(speed, callback): Hace que un objeto desaparezca

El argumento de velocidad (speed) es usualmente “slow” (lento), “fast” (rápido) o se omite (por defecto). callback es una función opcional que se llama cuando el efecto ha terminado.

Los efectos jQuery también pueden ser fácilmente incrustados en los ayudantes, por ejemplo, en una vista:

{{=DIV('click me!', _onclick="jQuery(this).fadeOut()")}}

jQuery es una librería Ajax muy compacta y consisa; por lo tanto web2py no necesita una capa de abstracción adicional sobre jQuery (excepto por la función ajax que se discute abajo). Las APIs jQuery son accesibles y fácilmente disponible en su forma original cuando se necesitan.

Consulte la documentación para más información respecto a estos efectos y otras APIs jQuery.

La librería jQuery también puede ser extendida usando plugins y Widgets de Interfaz con el Usuario. Este tema no se cubre aquí; vea la ref. (76) para más detalles.

Campos Condicionales en Formas

Una típica aplicación de los efectos jQuery es un formulario que cambia su apariencia basado en el valor de sus campos.

Esto es fácil en web2py porque el ayudante SQLFORM genera formas que son “amigables con CSS”. El formulario contiene una tabla con filas. Cada fila contiene una etiqueta, un campo de entrada, y una tercera columna opcional. Los ítems tienen ids derivados estrictamente del nombre de la tabla y de los nombres de los campos.

La convención es que cada campo de entrada tiene un id tablename_fieldname y está contenido en una fila con id tablename_fieldname__row.

Como ejemplo, cree un formulario de entrada que pregunte por el nombre del contribuyente y el de su cónyuge, pero solo si el/ella son casados.

Cree un aplicación de prueba con el siguiente modelo:

1
2
3
4
5
db = DAL('sqlite://db.db')
db.define_table('taxpayer',
    Field('name'),
    Field('married', 'boolean'),
    Field('spouse_name'))

el siguiente controlador “default.py”:

1
2
3
4
5
def index():
    form = SQLFORM(db.taxpayer)
    if form.accepts(request.vars, session):
        response.flash = 'record inserted'
    return dict(form=form)

Y la siguiente vista “default/index.html”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{{extend 'layout.html'}}
{{=form}}
<script>
jQuery(document).ready(function(){
   jQuery('#taxpayer_spouse_name__row').hide();
   jQuery('#taxpayer_married').change(function(){
        if(jQuery('#taxpayer_married').attr('checked'))
            jQuery('#taxpayer_spouse_name__row').show();
        else jQuery('#taxpayer_spouse_name__row').hide();});
});
</script>

El código en la vista tiene el efecto de ocultar la fila que contiene el nombre del cónyuge:

form_unmarried.png

Cuando el contribuyente marca “married”, el campo de nombre del cónyuge reaparece:

form_married.png

Aquí “taxpayer_married” es la casilla asociada al campo “boolean” “married” de la tabla “taxpayer”. “taxpayer_spouse_name__row” es la fila que contiene el campo de entrada para el “nombre de cónyuge” de la tabla “taxpayer”.

Confirmación al Borrar

Otra útil aplicación es la de requerir confirmación cuando se selecciona una casilla “delete” tal como la casilla delete que aparece en formularios de edición.

Se considera el ejemplo de arriba y agregue la siguiente acción controladora:

1
2
3
4
5
6
def edit():
    row = db.taxpayer[request.args(0)]
    form = SQLFORM(db.taxpayer, row, deletable=True)
    if form.accepts(request.vars, session):
        response.flash = 'record updated'
    return dict(form=form)

Y la vista correspondiente “default/edit.html”

1
2
{{extend 'layout.html'}}
{{=form}}

El argumento deletable=True en el constructor SQLFORM instruye a web2py para que muestre una casilla “delete” en el formulario de edición. Por defecto es False.

“web2py_ajax.html” de web2py incluye el siguiente código:

1
2
3
4
5
6
jQuery(document).ready(function(){
   jQuery('input.delete').attr('onclick',
     'if(this.checked) if(!confirm(
        "{{=T('Sure you want to delete this object?')}}"))
      this.checked=false;');
});

Por convención esta casilla tiene una clase igual a “delete”. El código jQuery de arriba conecta el evento al dar click de esta casilla con un diálogo de confirmación (estándar en JavaScript) y deselecciona la casilla si el contribuyente no confirma:

form_married_delete.png

La función ajax

En web2py_ajax.html, web2py define una función llamada ajax la cual se basa en, pero no debe ser confundida con, la función jQuery $.ajax. La última es mucho más poderosa que la primera, y para su uso, le recomendamos revisar las ref. (32) y ref. (75). Sin embargo, la primera función es suficiente para muchas tareas complejas, y es más fácil de usar.

La función ajax es una función JavaScript que tiene la siguiente sintáxis:

1
ajax(url, [id1, id2, ...], target)

Llama de manera asíncrona el url (primer argumento), pasa el valor de los campos con el id igual a uno de los id de la lista (segundo argumento), luego guarda la respuesta en el HTML interno de la etiqueta con el id igual al objetivo (el tercer argumento).

He aquí un ejemplo del controlador por defecto default:

1
2
3
4
5
def one():
    return dict()

def echo():
    return request.vars.name

y la vista asociada “default/one.html”:

1
2
3
4
5
{{extend 'layout.html'}}
 <form>
    <input name="name" onkeyup="ajax('echo', ['name'], 'target')" />
 </form>
 <div id="target"></div>

Cuando usted escribe algo en el campo de entrada, tan pronto como suelta la tecla (onkeyup), la función ajax es llamada, y el valor del campo id=”name” se pasa a la acción “echo”, la cual envía el texto de regreso a la vista. La función ajax recibe la respuesta y muestra la respuesta eco en el DIV “target” (objetivo).

Objetivo Eval

El tercer argumento de la función ajax puede ser la cadena ”:eval”. Esto significa que la cadena devuelta por el servidor no será incrustada en el documento, sino que más bien será evaluada.

He aquí un ejemplo del controlador por defecto default:

1
2
3
4
def one():
    return dict()
def echo():
    return "jQuery('#target').html(%s);" % repr(request.vars.name)

y la vista asociadad “default/one.html”:

1
2
3
4
5
{{extend 'layout.html'}}
 <form>
    <input name="name" onkeyup="ajax('echo', ['name'], ':eval')" />
 </form>
 <div id="target"></div>

Esto permite respuestas más articuladas que simples cadenas.

Auto-completar

Web2py contiene un widget incorporado para autocompletar, que se describe en el capítulo acerca de los formularios. Aquí se construirá uno mas simple desde cero.

Otra aplicación de la función ajax mostrada arriba es la de autocompletar. Se desea crear un campo de entrada que espere el nombre de un mes y, cuando el visitante escriba un nombre incompleto, autocomplete el mismo a través de una solicitud Ajax. En respuesta, una buzón para autocompletar aparece debajo del campo de entrada.

Esto se puede lograr a través del siguiente controlador por defecto default:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def month_input():
    return dict()
def month_selector():
    if not request.vars.month: return ''
    months = ['January', 'February', 'March', 'April', 'May',
              'June', 'July', 'August', 'September' ,'October',
              'November', 'December']
    month_start = request.vars.month.capitalize()
    selected = [m for m in months if m.startswith(month_start)]
    return DIV(*[DIV(k,
                     _onclick="jQuery('#month').val('%s')" % k,
                     _onmouseover="this.style.backgroundColor='yellow'",
                     _onmouseout="this.style.backgroundColor='white'"
                     ) for k in selected])

y la vista correspondiente “default/month_input.html”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{{extend 'layout.html'}}
<style>
#suggestions { position: relative; }
.suggestions { background: white; border: solid 1px #55A6C8; }
.suggestions DIV { padding: 2px 4px 2px 4px; }
</style>
<form>
 <input type="text" id="month" name="month" style="width: 250px" /><br />
 <div style="position: absolute;" id="suggestions"
      class="suggestions"></div>
</form>
<script>
jQuery("#month").keyup(function(){
      ajax('month_selector', ['month'], 'suggestions')});
</script>

El código jQuery en la vista activa la solicitud Ajax cada vez que el visitante escribe algo en el campo de entrada “month”. El valor del campo de entrada se envía con la solicitud Ajax a la acción “month_selector”. Esta acción encuentra una lista de nombres de meses que comienzan con el texto enviado (seleccionado), construye una lista de DIVs (cada uno conteniendo un nombre sugerido de mes), y devuelve una cadena con los DIVs serializados. La vista muestra el HTML de respuesta en el DIV “suggestions”. La acción “month_selector” genera tanto las sugerencias como el código JavaScript insertado en los DIVs que debe ser ejecutado cuando el visitante hace click en cada sugerencia. Por ejemplo cuando el visitante escribe “Ma” la acción de llamada retorna:

1
2
3
<div onclick="jQuery('#month').val('February')"
     onmouseout="this.style.backgroundColor='white'"
     onmouseover="this.style.backgroundColor='yellow'">February</div>

Y aquí vemos el efecto final:

month_input.png

Si los meses están guardados en una base de datos tal como:

1
db.define_table('month', Field('name'))

Luego simplemente reemplace la acción month_selector con:

def month_input():
    return dict()
def month_selector():
    it not request.vars.month:
        return "
    pattern = request.vars.month.capitalize() + '%'
    selected = [row.name for row in db(db.month.name.like(pattern)).select()]
    return ".join([DIV(k,
                 _onclick="jQuery('#month').val('%s')" % k,
                 _onmouseover="this.style.backgroundColor='yellow'",
                 _onmouseout="this.style.backgroundColor='white'"
                 ).xml() for k in selected])

jQuery proporciona un Plugin opcional para Autocompletar con funciones adicionales, pero eso no se discute aqui.

Envío de Formularios Ajax

Aquí consideramos una página que permite al visitante enviar mensajes usando Ajax sin recargar la página completa. Web2py proporciona un mejor mecanismo para hacerlo que el que aquí se describe usando el ayudante LOAD y será descrito en el Capítulo 13, a pesar de esto se quiere mostrar como hacerlo de forma simple usando jQuery.

Contiene un formulario “myform” y un DIV “target”. Cuando se envía el formulario, el servidor puede aceptarlo (y hacer la insersión en la base de datos) o rechazarlo (porque no pasó la validación). La notificación correspondiente se devuelve con la respuesta Ajax y se muestra en el DIV “target”.

Construir una aplicación test con el siguiente modelo:

1
2
3
db = DAL('sqlite://db.db')
db.define_table('post', Field('your_message', 'text'))
db.post.your_message.requires = IS_NOT_EMPTY()

Note que cada mensaje tienen un campo único “your_message” que se requiere para que no estén vacíos.

Edite el controlador default.py y escriba dos acciones:

1
2
3
4
5
6
7
8
def index():
    return dict()
def new_post():
    form = SQLFORM(db.post)
    if form.accepts(request.vars, formname=None):
        return DIV("Message posted")
    elif form.errors:
        return TABLE(*[TR(k, v) for k, v in form.errors.items()])

La primera acción lo único que hace es devolver una vista.

La segunda acción es la llamada de retorno Ajax. Ésta espera las variables del formulario en request.vars, las procesa y devuelve DIV(“Message posted”) si son exitosas o una tabla TABLE de mensajes de error si fallan.

Ahora edite la vista “default/index.html”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{{extend 'layout.html'}}
<div id="target"></div>
<form id="myform">
  <input name="your_message" id="your_message" />
  <input type="submit" />
</form>
<script>
jQuery('#myform').submit(function() {
  ajax('{{=URL('new_post')}}',
       ['your_message'], 'target');
  return false;
});
</script>

Note como en este ejemplo el formulario es creado manualmente usando HTML, pero es procesado por el SQLFORM en una acción distinta a la que despliega el formulario. El objeto SQLFORM nunca se serializa en HTML. En este caso, SQLFORM.accepts no toma un sesión y establece formname=None, porque elegimos no asignarle ni nombre ni clave al formulario en el formulario manual HTML.

El código al final de la vista conecta el botón de enviar “myform” a una función en línea que envía la entrada INPUT con id=”your_message” usando la función web2py ajax, y muestra la respuesta dentro del DIV con id=”target”.

Votos y Evaluación

Otra aplicación Ajax es la de votos y evaluación de ítems en una página. Aquí consideramos una aplicación que permite a los visitantes votar en imágenes publicadas. La aplicación consta de una única página que muestra las imágenes ordenadas por votos. Le permitiremos a los visitantes votar múltiples veces, aunque este comportamiento es fácil de cambiar si los visitantes son autenticados, llevando cuenta de los votos individuales en la base de datos y asociándolos con el request.env.remote_addr del votante.

Aquí tenemos un modelo de muestra:

1
2
3
4
db = DAL('sqlite://images.db')
db.define_table('item',
    Field('image', 'upload'),
    Field('votes', 'integer', default=0))

Aquí el controlador por defecto default:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def list_items():
    items = db().select(db.item.ALL, orderby=db.item.votes)
    return dict(items=items)
def download():
    return response.download(request, db)
def vote():
    item = db.item[request.vars.id]
    new_votes = item.votes + 1
    item.update_record(votes=new_votes)
    return str(new_votes)

La acción de descarga es necesaria para permitir a la vista list_items descargar imágenes guardadas en la carpeta “uploads”. La acción votes se usa por la llamada de retorno Ajax.

Aquí está la vista “default/list_items.html”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{{extend 'layout.html'}}
<form><input type="hidden" id="id" value="" /></form>
{{for item in items:}}
<p>
<img src="{{=URL('download', args=item.image)}}"
     width="200px" />
<br />
Votes=<span id="item{{=item.id}}">{{=item.votes}}</span>
[<span onclick="jQuery('#id').val('{{=item.id}}');
       ajax('vote', ['id'], 'item{{=item.id}}');">vote up</span>]
</p>
{{pass}}

Cuando el visitante le da click a “[vote up]” el código JavaScript guarda el item.id en el campo de entrada oculto “id” y envía este valor al servidor a través de una solicitud Ajax. El servidor incrementa el contador de votos para el registro correspondiente y devuelve la nueva cuenta de votos como una cadena. Luego este valor se inserta en el item{{=item.id}} SPAN de destino.

Las llamadas de retorno Ajax pueden ser usadas para hacer cáculos en segundo plano, pero recomendamos usar en cambio cron o un proceso en segundo plano (discutidos en el Capítulo 4), ya que el servidor web impone un tiempo de espera en las tareas. Si el cálculo toma mucho tiempo, el servidor web lo termina. Revise los parámetros de su servidor web para establecer el tiempo de espera.

Contenidos

Tema anterior

Servicios

Próximo tema

Recetas de Implementación

Envíe sus comentarios o correcciones a comunidad@latinuxpress.com

Patrocinado por