Depuis quelques années le Javascript prends une place de plus en plus prépondérante dans les applications web. Voyons aujourd’hui ce que Rails propose comme mécanismes permettant de simplifier l’introduction de Javascript dans les applications au travers de jquery-ujs.
Comme le laisse supposer son nom, cette bibliothèque dépends de jQuery.
Jquery-ujs
a plus de trois ans et a été introduite dans Rails 3.
L’objectif de la bibliothèque est d’ajouter, de manière discrète en terme de balisage HTML, trois comportements :
Attention, les deux derniers comportements rendront probablement votre site inutilisable à quelqu’un qui n’utilise pas Javascript, que ce soit pour des raisons de sécurité ou encore d’accessibilité. Ce n’est pas systématique mais il faudra être rigoureux pour que ce ne soit pas le cas.
En ajoutant un attribut data-confirm
à un lien, Rails va :
confirm
sur le lien,$.rails.confirm(message)
où message
est la valeur de l’attribut etconfirm:complete
sur le lien.La fonction $.rails.confirm
appelle la fonction window.confirm
. Il est possible
de surcharger $.rails.confirm
avec une fonction de son choix pour, par exemple,
utliser une fenêtre modale stylisée à la place du contrôle par défaut du navigateur.
L’évènement confirm
, s’il est capturé et que son handler retourne false
, va
annuler l’appel à $.rails.confirm
et empêcher le lien d’être suivi.
L’évènement confirm:complete
, s’il est capturé alors son handler aura à
sa disposition la réponse de l’utilisateur :
$('a[data-confirm]').on('confirm:complete', function(event, answer) {
if (answer) {
// User has confirmed the action
} else {
// User has canceled the action
}
});
Il est peu probable qu’utiliser data-confirm
perturbe l’aspect fonctionnel de
votre application. En effet, un utilisateur qui ne dispose pas du javascript
d’activé n’aura tout simplement pas les messages de confirmation.
Rails propose une option dédiée pour son helper link_to
afin de générer
l’attribut data-confirm
.
<%= link_to "Retour", '/', confirm: "Êtes vous sûr ?" %>
Rails utilise les verbes HTTP pour différencier deux routes ayant la même URL.
Les navigateurs ne prennent pas tous en charge tous les verbes. Pour pallier à
ce problème une convention en place depuis longtemps consiste à envoyer le
verbe HTTP souhaité dans un paramètre nommé _method
. Lorsque Rails va recevoir
une requête POST, il va tenter de surcharger le verbe HTTP avec la valeur du
paramètre _method
.
Lorsqu’un formulaire est généré en rails à l’aide de form_tag
et de son
option method
, un champ caché nommé _method
est inséré dans le formulaire.
Pour les liens, Rails propose également une option method
. Plutôt que de
générer du balisage HTML supplémentaire, c’est jquery-ujs
qui va prendre le
relais.
<!-- Exemple du formulaire HTML utilisant le verbe PATCH -->
<form action="/posts/16" method="POST">
<input type="hidden" name="_method" value="PATCH" />
<!-- Plus de champs... -->
<input type="submit" value="Modifier" />
</form>
Les liens qui disposent de l’attribut data-method
seront transformés en
un formulaire caché au moment ou l’on cliquera sur le lien. Ce formulaire
contiendra le champ _method
et ce dernier prendra la valeur de l’attribut
data-method
. C’est ce formulaire qui sera envoyé et le lien ne sera pas
suivi.
<%# Générer un lien utilisant le verbe DELETE %>
<%= link_to "Supprimer", "/posts/32", method: :delete %>
<!-- Formulaire réellement envoyé lors du clic sur le lien -->
<form method="POST" action="/posts/32">
<input name="_method" value="delete" type="hidden" />
</form>
Cette technique ne fonctionne plus dès lors que le navigateur utilisé ne permet pas l’exécution du javascript. En effet, l’URL du lien sans le bon verbe HTTP ne permet pas de trouver la route attendu au sein de Rails.
Le dernier élément offert par jquery-ujs
, et certainement le plus
important, est la mise en place de requêtes asynchrones.
Grâce à l’attribut data-remote
, certains éléments vont déclencher une
requête asynchrone vers le serveur web. Nous allons voir quels autres
attributs sont utilisables, quels effets produisent ces autres attributs
ainsi que quelques exemples.
Les attributs qui sont utilisables en plus de data-remote
sont :
data-type
qui permet de choisir le type réponse attendue parmi xml
, json
, script
, ou html
,data-cross-domain
qui permet de définir l’option crossdomain
(voir la documentation de jQuery) etdata-with-credential
qui permet d’ajouter l’option withCredentials
(à nouveau dans la documentation de jQuery)Ces attributs peuvent être ajoutés sur tous les éléments supportant
data-remote
. Par défaut l’option type
de $.ajax
est GET
et le
type de réponse attendue est non défini (entête Accept
à */*
).
Il existe aussi l’attribut data-disable-with
que l’on va pouvoir
placer sur des champs de formulaire. Ces champs seront désactivés
lorsque le formulaire parent est envoyé en asynchrone et sont
réactivés lorsque la requête se termine.
L’exemple suivant affiche En cours...
à la place de Envoyer
dans le bouton de validation durant le traitement asynchrone du
formulaire. Il désactive également le bouton pendant ce temps
évitant de soumettre deux fois la même requête lors d’un double clic.
<form action="/posts/64" method="POST" data-remote="true">
<!-- Autres champs -->
<input type="submit" value="Envoyer" data-disable-with="En cours..." />
</form>
Selon l’élément qui utilise data-remote
, les éléments utilisés pour
construire la requête diffèrent. Ci-dessous, se trouvent le détails
de ces éléments.
Pour les formulaires, le paramètre type
de $.ajax
est récupéré
depuis l’attribut method
.
L’URL de la requête est extraite de l’attribut action
.
Les données envoyées sont extraites des champs du formulaire grâce à
la fonction serializeArray
de jQuery (doc).
Si vous utilisez un bouton (button
) pour soumettre le formulaire,
sa valeur sera également ajoutée.
Pour les champs de formulaire ou les boutons, le paramètre type
de
$.ajax
est récupéré depuis l’attribut data-method
.
L’URL de la requête est extraite de l’attribut data-url
.
La donnée envoyée est la valeur du champ de formulaire à laquelle est
ajoutée la valeur de l’attribut data-params
s’il est présent. Ce
dernier attribut doit être encodé comme une URL (name=val&foo=bar
).
Pour les liens, le paramètre type
de $.ajax
est récupéré depuis
l’attribut data-method
.
L’URL de la requête est extraite de l’attribut href
.
La donnée envoyée est la valeur de l’attribut data-params
s’il est
présent.
On se place dans le classique exemple du blog. On a une page d’administration qui est une sorte de single page app, c’est à dire que la page est rendue une première fois et chaque action effectuée sur celle-ci modifie la page.
Attention, lorsque vous modifiez les éléments de votre page dynamiquement, il faut être vigilent sur la gestion des évènements. En effet, si vous liez directement les éléments du DOM pour gérer les évènements alors il faudra refaire cette liaison a chaque changement du DOM. Si, au contraire, vous utilisez le mécanisme de propagation des évènements, il vous sera possible modifier le DOM tout en conservant votre gestion d’évènements.
// N'utilisez pas
$('.partial .element').on('click', function() { ... });
// Utilisez plutôt
$(document).on('click', '.partial .element', function() { ... });
-# app/views/posts/index.html.haml
%section
%h2 Créer un nouveau billet
= render partial: "form"
%section
%h2 Liste des billets existants
= render partial: "list"
-# app/views/posts/_form.html.haml
= form_for @post, remote: true, html: { class: 'post' } do |f|
%div{ class: @post.errors[:title] ? "error" : "" }
= f.label :title
= f.text_field :title
%div{ class: @post.errors[:content] ? "error" : "" }
= f.label :content
= f.text_area :content
= f.submit "Envoyer", disable_with: "En cours..."
-# app/views/posts/_list.html.haml
%ul#post-list
- Post.all.each do |post|
= render partial: "list_elem", locals: { post: post }
-# app/views/posts/_list_elem.html.haml
%li.post
= post.title
= link_to "Supprimer", post_path(post), remote: true, method: :delete, confirm: "Êtes vous sûr ?", class: 'delete'
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
respond_to :js, only: [:create]
def index
@post = Post.new
end
def create
@new_post = Post.new(params.require(:post).permit(:title, :content))
@success = @new_post.save
@post = @success ? Post.new : @new_post
end
end
// app/views/posts/create.js.erb
$("form.post").replaceWith("<%= escape_javascript( render( partial: 'form' ) ) %>");
<% if @success %>
$("#post-list").append(
"<%= escape_javascript( render( partial: 'list_elem', locals: { post: @new_post } ) ) %>"
);
window.alert("Votre billet a été créé.");
<% else %>
window.alert("Erreur lors de la création de votre billet.");
<% end %>
Si vous avez suivi correctement vous avez compris que :
Ici, le serveur a répondu avec du javascript. Ce javascript a été exécuté par le client automatiquement par jQuery. Si le serveur avait renvoyé du HTML alors le comportement aurait été différent et on aurait du utiliser les callbacks.
Pour l’exemple, la suppression d’un billet s’appuiera sur les callbacks.
# app/controllers/posts_controller.rb
...
def destroy
post = Post.where(id: params[:id]).first
if post
post.destroy
render nothing: true
else
render partial: 'list', status: 405
end
end
...
// app/assets/javascripts/application.js
$(document).on('ajax:success', '#post-list .post a.delete', function() {
$(this).closest('li').remove();
});
$(document).on('ajax:error', '#post-list .post a.delete', function(event, xhr) {
if (xhr.status == 405) {
$("#post-list").replaceWith(xhr.responseText);
alert("Le post demandé n'a pas été trouvé.");
}
});
Ici, on peut voir comment les callbacks sont utilisés.
Dans le cas où le billet est trouvé, on supprime l’élément de la liste. On évite ainsi de rendre la liste entière des billets.
Dans le cas où le billet n’est pas trouvé, on retourne une réponse avec le statut HTTP 405 et un corps contenant la liste complète des billets. En effet, on présume que la liste des billets du client n’est plus à jour.
Il est très important de bien définir ce qui doit se passer en cas d’erreur. Même si vous n’utilisez pas les callbacks et passez par du JS, comme c’était le cas lors du premier exemple, il est indispensable de prendre en compte le cas ou le serveur ne répond plus.
// app/assets/javascripts/application.js
$(document).on('ajax:error', function(event, xhr) {
if (xhr.status == 500 || xhr.status == 0) {
alert("Une erreur serveur s'est produite.")
}
});
On l’a vu plus tôt, il est possible de personnaliser le champ dataType
de l’appel $.ajax
à l’aide de l’attribut data-type
. Par défaut tout
type de réponse est accepté mais une priorité est laissée aux scripts.
Ainsi, si une action Rails peut répondre à deux formats, html et JS par
exemple, ce sera le JS qui sera prioritaire.
Il est parfois plus pratique de retourner un morceau de HTML plutôt que de générer du javascript.
Prenons le cas de l’édition. On peut vouloir un formulaire d’édition indépendant et, en parallèle, avoir la possibilité d’introduire ce même formulaire au sein d’une autre page.
# app/controllers/posts_controller.rb
def edit
@post = Post.find(params[:id])
respond_to do |format|
format.js { render :edit }
format.html
end
end
Notre action va donc retourner du HTML à la place du Javascript. Dans ces
cas là, il faut informer jQuery qu’il ne devra pas interpréter le résulat
de la requête comme un script. Pour cela on utilise data-type
.
%li.post
= post.title
= link_to "Supprimer", post_path(post), remote: true, method: :delete, confirm: "Êtes vous sûr ?", class: 'delete'
= link_to "Éditer", edit_post_path(post), remote: true, class: 'edit', :'data-type' => 'text'
Ici, on indique que l’on souhaite recevoir du texte depuis le serveur. Si l’on
se contente de ça, jQuery enverra une requête acceptant en priorité le
type text/plain
. Et, dans le cas d’un type de donnéess non pris en compte,
c’est le premier format.js
qui sera utilisé.
Pour s’assurer que Rails choisisse le bon format de réponse, on peut
utiliser l’évènement ajax:beforeSend
pour redéfinir l’entête Accept
.
// app/assets/javascripts/application.js
// Modifier l'entête Accept vers le type Javascript
$(document).on('ajax:beforeSend', '#post-list .post a.edit', function(event, xhr, settings) {
xhr.setRequestHeader('accept', settings.accepts.script)
});
$(document).on('ajax:success', '#post-list .post a.edit', function(event, html) {
$(html).insertBefore($('section:first'))
});
Vous pouvez retrouver le code de ces exemples sur notre dépot ujs-sandbox.
Dans les trois exemples précédents on a vu comment créer et supprimer des billets. Essayez maintenant de traiter la mise à jour d’un billet en gardant à l’idée que la demande peut venir soit d’un formulaire ajouté dynamiquement dans la page d’index soit d’un formulaire seul.
Nous avons fait un tour assez exhaustif de jquery-ujs
. N’oubliez pas de consulter
la liste des callbacks si ce n’est pas déjà fait et n’hésitez pas
à vous plonger directement dans le code. Vous y découvrirez comment
personnaliser le comportement de la bibliothèque au-delà des attributs prévus.
L’équipe Synbioz.
Libres d’être ensemble.