Chapter 11: jQuery et Ajax

jQuery et Ajax

Ajax

Alors que web2py est principalement pour le développement côté serveur, l'application de base welcome est fournie avec la la librairie de base [jaquery], jQuery calendars (choix de date, choix de datetime et d'heure), et quelques fonctions complémentaires JavaScript basées sur jQuery.

Rien dans web2py ne vous empêche d'utiliser d'autres librairies Ajax telles que Prototype, ExtJS, ou YUI, mais nous avons décidé de packager jQuery car nous le trouvons plus facile à utiliser et plus puissant que les autres librairies équivalentes. Nous trouvons également qu'il convient bien à l'esprit de web2py en étant fonctionnel et concis.

web2py_ajax.html

L'application de base web2py "welcome" inclut un fichier appelé

views/web2py_ajax.html

qui ressemble à cela :

{{
response.files.insert(0,URL('static','js/jquery.js'))
response.files.insert(1,URL('static','css/calenadar.css'))
response.files.insert(2,URL('static','js/calendar.js'))
response.include_meta()
response.include_files()
}}
<script type="text/javascript"><!--
    // These variables are used by the web2py_ajax_init
        // function in web2py.js (which is loaded below).
    var w2p_ajax_confirm_message =
        "{{=T('Are you sure you want to delete this object?')}}";
    var w2p_ajax_date_format = "{{=T('%Y-%m-%d')}}";
    var w2p_ajax_datetime_format = "{{=T('%Y-%m-%d %H:%M:%S')}}";
//--></script>
<script src="{{=URL('static','js/web2py.js')}}"
        type="text/javascript"></script>

Ce fichier est inclus dans le HEAD du "layout.html" par défaut et fournit les services suivants :

  • Inclut "static/jquery.js".
  • Inclut "static/calendar.js" et "static/calendar.css", qui sont utilisés pour le calendrier qui s'affiche.
  • Inclut tous les en-têtes response.meta
  • Inclut tous les response.files (requiert CSS et JS, comme déclarés dans le code)
  • Définit les variables de formulaires et inclut "static/js/web2py.js"

"web2py.js" fait ce qui suit :

  • Définit une fonction ajax (basée sur jQuery $.ajax).
  • Permet de faire un slide descendant pour toute DIV de classe "error" ou tout objet tag de classe "flash"
  • Empêche d'entrer des entier invalides dans des champs INPUT de classe "integer".
  • Empêche d'entrer des flottants invalides dans des champs INPUT de classe "double".
  • Connecte les champs INPUT de type "date" avec un popup pour récupérer la date.
  • Connecte les champs INPUT de type "datetime" avec un popup pour récupérer la datetime.
  • Connecte les champs INPUT de type "time" avec un popup de récupération de time.
  • Définit web2py_ajax_component, un outil très importan qui sera décrit dans le Chapitre 12.
  • Définit web2py_websocket, une fonction qui peut être utilisée pour les websockets HTML5 (non décrites dans ce livre mais lisez les exemples dans la source de "gluon/contrib/websocket__messaging.py").
    websockets
  • Définit les fonctions des calculs d'entropie et validation d'entrée du champ de mot de passe.

Il inclut également les fonctions popup, collapse, et fade pour la rétro-compatibilité

Voici un exemple de comment les autres effets fonctionnent ensemble.

Considérons une application test avec le modèle suivant :

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()

avec ce contrôleur "default.py" :

def index():
    form = SQLFORM(db.child)
    if form.process().accepted:
        response.flash = 'record inserted'
    return dict(form=form)

et la vue suivante "default/index.html" :

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

L'action "index" génère le formulaire suivant :

image

Si un formulaire invalide est soumis, le serveur retourne la page avec un formulaire modifié contenant les messages d'erreur. Les messages d'erreur sont des DIV de classe "error", et étant donné le code ci-dessus web2py.js, les erreurs apparaissent avec un effet slide-down :

image

La couleurs des erreurs est donnée dans le code CSS dans "layout.html".

Le code web2py.js vous évite de taper une valeur invalide dans le champ input. Ceci est fait avant et en supplément, non comme une substitution, de la validation côté serveur.

Le code web2py.js affiche un sélectionneur de date lorsque vous entrez un champ INPUT de classe "date", et il affiche un sélectionneur de datetime lorsque vous entrez un champ INPUT de classe "datetime". Voici un exemple :

image

Le code web2py.js affiche également le sélectionneur de temps suivant lorsque vous essayez d'éditer un champ INPUT de classe "time" :

image

Lors de la soumission, l'action du contrôleur définit lé réponse flash en message "record inserted". Le layout par défaut affiche ce message dans une DIV avec id="flash". Le code web2py.js est responsable de faire apparaître cette DIV et de la faire disparaitre lorsque vous cliquez dessus :

image

Ceux-là et les autres effets sont accessibles de manière programmable dans les vues et via les helpers dans les contrôleurs.

Effets jQuery

effects

Les effets basiques décrits ici ne requièrent pas de fichier additionnel ; tout ce dont vous avez besoin est déjà inclus dans web2py_ajax.html.

Les objets HTML/XHTML peuvent être identifiés par leur type (par exemple un DIV), leurs classes, ou leur id. Par exemple :

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

Ils appartiennent à la classe "one" et "two" respectivement. Ils ont des ids égaux à "a" et "b" respectivement.

Dans jQuery vous pouvez vous référer au premier avec les notations équivalentes CSS suivantes

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"

et au dernier avec

jQuery('.two')
jQuery('#b')
jQuery('DIV.two')
jQuery('DIV #b')

ou vous référez aux deux avec

jQuery('DIV')

Les objets Tag sont associés aux événements, tels que "onclick". jQuery permet de lier ces événements aux effets, par exemple "slideToggle" :

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

Maintenant si vous cliquez sur "Hello", "World" disparait. Si vous cliquez encore, "World" ré-apparait. Vous pouvez faire un tag caché par défaut en lui donnant la classe "hidden" :

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

Vous pouvez aussi lier des actions aux événements en dehors du tag lui-même. Le code précédent peut être ré-écrit comme suit :

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

Les effets retourne l'objet appelant, afin qu'ils puissent être chaînés.

Alors le click définit la fonction callback à appeler lors du clic. De la même manière pour change, keyup, keydown, mouseover, etc...

Une situation commune est le besoin d'exécuter quelque code JavaScript seulement après que le document entier ait été chargé. C'est habituellement fait par l'attribut onload du BODY mais jQuery fournit une solution alternative qui ne nécessite pas d'éditer le layout :

<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>

Le corps de la fonction non-nommée est exécutée seulement lorsque le document est prêt, après qu'il ait été totalement chargé.

Voici une liste des noms d'événement utiles :

Evénements de formulaires
  • onchange: Script à exécuter lorsqu'un élément change
  • onsubmit: Script à exécuter lorsque le formulaire est soumis
  • onreset: Script à exécuter lorsque le formulaire est réinitialisé
  • onselect: Script à exécuter lorsque l'élément est sélectionné
  • onblur: Script à exécuter lorsque l'élément perd le focus
  • onfocus: Script à exécuter lorsque l'élément obtient le focus
Evénements de clavier
  • onkeydown: Script à exécuter lorsqu'une touche est pressée
  • onkeypress: Script à exécuter lorsqu'une touche est pressée et relâchée
  • onkeyup: Script à exécuter lorsqu'une touche est relâchée
Evénements de souris
  • onclick: Script à exécuter lors d'un clic de souris
  • ondblclick: Script à exécuter lors d'un double clic de souris
  • onmousedown: Script à exécuter lorsque le bouton de la souris est pressé
  • onmousemove: Script à exécuter lorsque le pointeur de la souris bouge
  • onmouseout: Script à exécuter lorsque le pointeur de la souris sort d'un élément
  • onmouseover: Script à exécuter lorsque le pointeur de la souris passe sur un élément
  • onmouseup: Script à exécuter lorsque le bouton de la souris est relâché

Voici une liste des effets utiles définis par jQuery :

Effects
  • jQuery(...).show(): Rend l'objet visible
  • jQuery(...).hide(): Rend l'objet caché
  • jQuery(...).slideToggle(speed, callback): Fait glisser l'objet vers le haut ou le bas
  • jQuery(...).slideUp(speed, callback): Fait glisser l'objet vers le haut
  • jQuery(...).slideDown(speed, callback): Fait glisser l'objet vers le bas
  • jQuery(...).fadeIn(speed, callback): Fait apparaître l'objet
  • jQuery(...).fadeOut(speed, callback): Fait disparaître l'objet

L'argument speed est habituellement "slow", "fast" ou oublié (par défaut). La callback est une fonction optionnelle qui est appelée lorsque l'effet est complété.

Les effets jQuery peuvent aussi facilement être embarqués dans les helpers, par exemple, dans une vue :

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

D'autres méthodes utiles et les attributs pour gérer les éléments sélectionnés

Méthodes et attributs
  • jQuery(...).prop(name): Retourne le nom de la valeur de l'attribut
  • jQuery(...).prop(name, value): Définit le nom de l'attribut à la valeur
  • jQuery(...).html(): Sans argument, il retourne le contenu html de l'élément sélectionné, il accepte une chaîne comme argument pour remplacer le contenu du tag.
  • jQuery(...).text(): Sans argument, il retourne le contenu texte de l'élément sélectionné (sans tags), si une chaîne est passée comme argument, il remplace le contenu texte avec les nouvelles données.
  • jQuery(...).css(name, value): Avec un paramètre, il retourne la valeur CSS de l'attribut style spécifié pour les éléments sélectionnés. Avec deux paramètres, il définit une nouvelle valeur pour l'attribut CSS spécifié.
  • jQuery(...).each(function): Il boucle sur les éléments sélectionnés et appelle la fonction avec chaque objet en argument.
  • jQuery(...).index(): Sans arguments, il retourne la valeur index pour le premier élément sélectionné selon ses semblables (i.e, l'index d'un élément LI). Si un élément est passé en argument, il retourne la position de l'élément selon l'ensemble des éléments sélectionnés.
  • jQuery(...).length: Cet attribut retourne le nombre d'éléments sélectionnés.

jQuery est une librairie Ajax compacte et concise ; donc web2py n'a pas besoin d'une couche d'abstraction supplémentaire par-dessus jQuery (sauf pour la fonction ajax présentée ci-après). Les APIs jQuery sont accessibles et disponibles en lecteur dans leur forme native si nécessaire.

Consultez la documentation pour plus d'information sur ces effets et autres APIs jQuery.

La librairie jQuery peut également être étendue en utilisant des plugins et des User Interface Widgets. Ce sujet n'est pas couvert ici ; voir la référence jquery-ui pour des détails.

Les champs conditionnels dans les formulaires

Une application typique d'effets jQuery est un formulaire qui change son apparence basée sur la valeur de ses champs.

C'est simple dans web2py puisque le helper SSQLFORM génère les formulaires qui sont "CSS friendly". Le formulaire contient une table avec des lignes. Chaque ligne contient un label, un champ input, et une troisième colonne optionnelle. Les objets ont des ids dérivés strictement du nom de la table et des noms des champs.

La convention est que tout champ INPUT a un id tablename_fieldname et est contenu dans une ligne avec l'id tablename_fieldname__row.

Comme exemple, créez un formulaire en entrée qui demande pour le nom du payeur de la taxe et pour le nom de l'épouse, mais seulement si il/elle est marié(e).

Créez une application de test avec le modèle suivant :

db = DAL('sqlite://db.db')
db.define_table('taxpayer',
    Field('name'),
    Field('married', 'boolean'),
    Field('spouse_name'))

le contrôleur "default.py" suivant :

def index():
    form = SQLFORM(db.taxpayer)
    if form.process().accepted:
        response.flash = 'record inserted'
    return dict(form=form)

et la vue "default/index.html" suivante :

{{extend 'layout.html'}}
{{=form}}
<script>
jQuery(document).ready(function(){
   if(jQuery('#taxpayer_married').prop('checked'))
        jQuery('#taxpayer_spouse_name__row').show();
   else jQuery('#taxpayer_spouse_name__row').hide();
   jQuery('#taxpayer_married').change(function(){
        if(jQuery('#taxpayer_married').prop('checked'))
            jQuery('#taxpayer_spouse_name__row').show();
        else jQuery('#taxpayer_spouse_name__row').hide();});
});
</script>

Le script dans la vue a l'effet de cacher la ligne contenant le nom d'épouse :

image

Lorsque le payeur coche la case "married", le champ du nom d'épouse ré-apparaît :

image

Ici "taxpayer_married" est la case à cocher associée au champ "boolean" "married de la table "taxpayer". "taxpayer_spouse_name__row" est la ligne contenant le champs d'entrée pour "spouse_name" de la table "taxpayer".

Confirmation lors d'une suppression

confirmation

Une autre application utile est de demander confirmation lorsque l'on coche une case à cocher "delete" telle que la case à cocher de suppression qui apparaît lors de l'édition de formulaires.

Considérons l'exemple ci-dessus et ajoutons l'action contrôleur suivante :

def edit():
    row = db.taxpayer[request.args(0)]
    form = SQLFORM(db.taxpayer, row, deletable=True)
    if form.process().accepted:
        response.flash = 'record updated'
    return dict(form=form)

et la vue correspondante "default/edit.html"

{{extend 'layout.html'}}
{{=form}}
deletable

L'argument deletable=True dans le constructeur de SQLFORM indique à web2py d'afficher une case à cocher "delete" dans le formulaire d'édition. Il est à False par défaut.

"web2py.js" inclut le code suivant :

jQuery(document).ready(function(){
   jQuery('input.delete').prop('onclick',
     'if(this.checked) if(!confirm(
        "{{=T('Sure you want to delete this object?')}}"))
      this.checked=false;');
});

Par convention, cette case à cocher a une classe égale à "delete". Le code jQuery ci-dessus connecte l'événement onclick de cette case avec une boite de dialogue de confirmation (standard en JavaScript) et décoche la case si le payer de taxe ne confirme pas :

image

La fonction ajax

Dans web2py.js, web2py définit une fonction appelée ajax qui est basée sur, mais ne devrait pas être confondue avec, la fonctione jQuery $.ajax. La dernière est bien plus puissante que la première, et pour son usage, nous vous référons à [jquery] et [jquery-b]. Cependant, la première fonction est suffisante pour beaucoup de tâches complètes, et est facile à utiliser.

La fonction ajax est une fonction JavaScript qui a la syntaxe suivante :

ajax(url, [name1, name2, ...], target)

Cela appelle de manière asynchrone l'url (premier argument), passe la valeur du champ en entrée avec le nom égal à l'un des noms dans la liste (second argument), stocke ensuite la réponse dans le innerHTML du tag avec l'id égal à target (troisième argument).

Voici un exemple d'un contrôleur default :

def one():
    return dict()

def echo():
    return request.vars.name

et la vue associée :

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

Lorsque vous tapez quelque chose dans le champ INPUT, dès que vous relâchez une touche (onkeyup), la fonction ajax est appelée, et la valeur du champ name="name" est pasée à l'action "echo", qui renvoie le texte à la vue. La fonction ajax reçoit la réponse et affiche la réponse echo dans le DIV "target".

Cible eval

Le troisième argument de la fonction ajax peut être une chaîne ":eval". Cela signifie que la chaîne retournée par le serveur ne sera pas embarquée dans le document mais sera évaluée à la place.

Voici un exemple d'un contrôleur default :

def one():
    return dict()

def echo():
    return "jQuery('#target').html(%s);" % repr(request.vars.name)

et la vue associée "default/one.html" :

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

Ceci permet des réponses plus complexes qui peuvent mettre à jour de multiples cibles.

Auto-complétion

web2py contient un widget d'auto-complétion intégré, décrit dans le chapitre sur les formulaires. Nous en construirons un ici plus simple de zéro.

Une autre application de la fonction ci-dessus ajax est l'auto-complétion. Nous souhaitons ici créer un champ input qui attend un nom de mois, et, lorsque le visiteur entre un nom incomplet, effectue l'auto-complétion via une requête Ajax. En réponse, une liste d'auto-complétion apparaît sous le champ d'entrée.

Ceci peut être fait via le contrôleur suivant default :

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])

et la vue correspondante "default/month_input.html" :

{{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>

Le script jQuery dans la vue déclenche la requête Ajax chaque fois que le visiteur tape quelque chose dans le champ "month". La valeur du champ d'entrée est soumis avec la requête Ajax à l'action "month_selector". Cette action trouve une liste de noms de mois qui démarrent par le texte soumis (sélectionné), construit une liste de DIVs (chacune contenant un nom de mois suggéré), et retourne les DIVs qui doivent être exécutés lorsque le visiteur clique sur chaque suggestion. Par exemple lorsque le visiteur tape "M" l'action callback retourne :

<div>
     <div onclick="jQuery('#month').val('March')"
          onmouseout="this.style.backgroundColor='white'"
          onmouseover="this.style.backgroundColor='yellow'">March</div>
     <div onclick="jQuery('#month').val('May')"
          onmouseout="this.style.backgroundColor='white'"
          onmouseover="this.style.backgroundColor='yellow'">May</div>
</div>

Voici l'effet final :

image

Si les mois sont stockés dans une table de la base de données telle que :

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

remplacez alors simplement l'action month_selectoravec :

def month_input():
    return dict()

def month_selector():
    if 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 fournit un Plugin Auto-Complete optionnel avec des fonctionnalités supplémentaires, mais ce n'est pas présenté ici.

Soumission de formulaire Ajax

asynchronous

Nous considérons ici une page qui autorise le visiteur à soumettre des messages en utilisant Ajax sans recharger la page complète. En utilisant l'helper LOAD, web2py fournit un meilleur mécanisme pour faire cela que ce qui est décrit ici, il sera expliqué dans le Chapitre 12. Nous voulons ici vous montrer comme il est simple d'utiliser jQuery pour faire cela.

Il contient un formulaire "myform" et un DIV "target". Lorsque le formulaire est soumis, le serveur peut l'accepter (et effectuer une insertion dans la base de données) ou le rejeter (car il n'a pas réussi la validation). La notification correspondante est retournée avec la réponse Ajax et affichée dans le DIV "target".

Construisons une application test avec le modèle suivant :

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

Notez que chaque poste a un champ seul "your_message" qui est requis pour ne pas être vide.

Editez le contrôleur default.py et écrivez deux actions :

def index():
    return dict()

def new_post():
    form = SQLFORM(db.post)
    if form.accepts(request, formname=None):
        return DIV("Message posted")
    elif form.errors:
        return TABLE(*[TR(k, v) for k, v in form.errors.items()])

La première action ne fait rien d'autre que retourner une vue.

La seconde action est le callback Ajax. Il attend des variables de formulaire dans request.vars, les traite et retourne DIV("Message posted") en cas de succès ou une TABLE de messages d'erreur en cas d'échec.

Editons maintenant la vue "default/index/html" :

{{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>

Notez comment dans cet exemple le formulaire est créé manuellement en utilisant l'HTML, mais est géré par SQLFORM dans une action différente que celle qui affiche le formulaire. L'objet SQLFORM n'est jamais sérialisé en HTML. SQLFORM.accepts dans ce cas ne prend pas une session et définit formname=None, car nous choisissons de ne pas définir le nom du formulaire et une clé dans le formualire HTML manuel.

Le script à la fin de la vue connecte le bouton de soumission "myform" à une fonction en ligne qui soumet l'INPUT avec id="your_message" en utilisant la fonction web2py ajax, et affiche la réponse dans le DIV avec id="target".

Voter et noter

Une autre application Ajax est de voter ou noter des objets dans une page. Nous considérons ici une application qui permet aux visiteurs de voter sur les images postées. L'application consiste en une simple page qui affiche les images triées selon leurs votes. Nous autoriseront les visiteurs à voter plusieurs fois, bien qu'il soit facile de changer ce comportement si les visiteurs sont authentifiés, en conservant une trace des votes individuels dans la base de données en les associant avec le request.env.remote_addr du voteur.

Voici un modèle exemple :

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

Voici ici le contrôleur default :

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)

L'action download est nécessaire pour permettre à la vue list_items de télécharger les images stockées dans le dossier "uploads". L'action de vote est utilisée pour la callback Ajax.

Voici la vue "default/list_items.html" :

{{extend 'layout.html'}}

<form><input type="hidden" id="id" name="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}}

Lorsque le visiteur clique sur "[vote up]" le code JavaScript stocke l'item.id dans le champ INPUT caché "id" et soumet cette valeur au serveur via une requête Ajax. Le serveur augmente le compteur de votes pour l'enregistrement correspondant et retourne le nouveau compteur de vote en chaîne. Cette valeur est alors insérée dans la cible SPAN item{{=item.id}} .

Les callbacks Ajax peuvent être utilisés pour effectuer des calculs en arrière-plan, mais nous recommandons d'utiliser cron ou un processus d'arrière-plan plutôt (présenté en chapitre 4), puisque le serveur web force un timeout sur les threads. Si les calculs prennent trop de temps, le serveur web les tuent. Référez-vous aux paramètres de votre serveur web pour définir la valeur de timeout.
 top