Recommend Me


Vendredi 30 mars 2007

Comment paginer, trier et filtrer un tableau avec Ajax et Rails

Si vous recherchez une solution simple et efficace, allez donc consulter l’excellent tutoriel du même nom sur le site de Julien. Toute la mécanique y est clairement expliquée - merci à lui.

J’ai eu le même besoin il y a peu de temps, mais ma problèmatique comprenait un paramètre supplémentaire. A savoir mon tableau était lui même composé de données venant de deux tables différentes. Or la fonction paginate de Rails ne sait par défaut que travailler sur une seule table. Heureusement une solution existe via la méthode paginate_by_sql dont vous trouverez les détails sur le blog de Phil Bogle. Faisons donc une adaptation sur la base du tutoriel de Julien…

Installation et configuration de l’application

Bon, je vais partir du principe que ce n’est pas votre première application développée avec Rails. Je vais donc aller un peu vite.

Commencez par créer une nouvelle application :

$ rails AjaxTablePlus

$ cd AjaxTablePlus

Dans cet exemple, nous allons utiliser un modèle comprenant deux tables : users et companies. Dans la première nous placerons des utilisateurs (avec nom et prénom) et nous rattacherons chaque utilisateur à une société comprenant un nom.

Avant cela commençons par configurer la base de donnée. Modifiez le fichier db/database.yml de la façon suivante (ou autrement si vous n’utilisez pas sqlite3) :

development:
  adapter: sqlite3
  database: db/development.db

Vous pouvez maintenant créer un fichier db/schema.rb dans lequel sera définit le schéma de la base :

ActiveRecord::Schema.define() do

  create_table :companies , :force => true do |t|
    t.column :name      , :string , :null => false
  end

  create_table :users , :force => true do |t|
    t.column :company_id  , :integer  , :null => false

    t.column :firstname , :string
    t.column :lastname  , :string
  end

end

Et c’est parti pour la construction du schéma :

$ rake db:schema:load

Sachant que notre modèle comprend deux tables, nous devons donc générer deux fichiers de modèle correspondant :

$ ruby script/generate model company

$ ruby script/generate model user

Sachant que nous avons une relation 1-n entre les tables companies et users il faut alors modifier les fichiers de modèle correspondant. Modifier app/models/company.rb de la façon suivante :

class Company < ActiveRecord::Base
  has_many :users
end

Puis modifiez app/models/user.rb ainsi :

class User < ActiveRecord::Base
  belongs_to :company
end

Il nous faut ensuite un contrôleur (dont le nom importe peu en fait - appelons le table) :

$ ruby script/generate controller table

Création de la vue

Nous allons suivre la méthode de Julien, mais sans oublier que notre contrôleur s’appelle table. Nous garderons list comme nom pour l’action.

En bon élève nous pouvons donc créer un layout app/views/layouts/table.rhtml

<html>
  <head>
    <title>Essai de tableau avec Ajax</title>
    <%= stylesheet_link_tag "style" %>
    <%= javascript_include_tag :defaults %>
  </head>
  <body>

    <div id="content">
      <%= @content_for_layout %>
    </div>

  </body>
</html>

… puis la vue pour l’action list, à savoir app/views/table/list.rhtml :

<h1>Bienvenue dans ce magnifique tableau</h1>

<p>Cette liste est mise &#224; jour en temps de réel depuis la plus large
source de données actuellement accessible en utilisant les
technologies les plus innovantes associées aux effets graphiques
époustouflants du Web 2.0.</p>

<p>Mais faites gaffe, y’a plein de bugs.</p>

<h3>Et voil&#224; la liste…</h3>

<p>
<form name="sform" action="" style="display:inline;">
  <label for="item_name">Rechercher sur le nom, le prénom ou la société  : </label>
  <%= text_field_tag("query", params['query'], :size => 10 ) %>
</form>

<%= image_tag("spinner.gif",
              :align => ‘absmiddle’,
              :border=> 0,
              :id => "spinner",
              :style=>"display: none;" ) %>
</p>

<%= observe_field ‘query’,  :frequency => 2,
         :update => ‘table’,
         :before => "Element.show(’spinner’)",
         :success => "Element.hide(’spinner’)",
         :url => {:action => ‘list’},
         :with => ‘query’ %>

<div id="table">
  <%= render :partial => "items_list" %>
</div>

Pour les détails, je vous renvoie à la version originale du tutoriel.

Création du contrôleur

Dans notre contrôleur, nous ne définirons qu’une seule action : list. Modifions le fichier app/controllers/table_controller.rb :

class TableController < ApplicationController
  def list
    items_per_page = 10

    sort = case params[’sort’]
           when "fname"             then "users.firstname"
           when "lname"             then "users.lastname"
           when "company"           then "companies.name"
           when "fname_reverse"     then "users.firstname DESC"
           when "lname_reverse"     then "users.lastname DESC"
           when "company_reverse"   then "companies.name DESC"
           else nil
           end

    query = "SELECT users.firstname, users.lastname, companies.name FROM users, companies WHERE users.company_id = companies.id"
    query_count = "SELECT COUNT(*) FROM users, companies WHERE users.company_id = companies.id"

    unless params[:query].nil?
      query_add = " AND (companies.name LIKE ‘%#{params[:query]}%’ OR users.firstname LIKE ‘%#{params[:query]}%’ OR users.lastname LIKE ‘%#{params[:query]}%’)"
      query = query + query_add
      query_count = query_count + query_add
    end

    unless sort.nil?
      query = query + " ORDER BY #{sort}"
    end

    @total = User.count_by_sql( query_count )
    @items_pages, @items = paginate_by_sql User, query, items_per_page

    if request.xml_http_request?
      render :partial => "items_list", :layout => false
    end
  end
end

Ceci mérite quelques explications.

Nous ne définissons qu’une seule action : list. Je ne rentrerez pas trop dans les détails et me contenterai de mettre en évidence les modifications par rapport à la version originale.

La variable sort permet de définir l’ordre de tri. Notre tableau affichera les prénom et nom de l’utilisateur ainsi que le nom de la société, nous pourrons donc trier le tableau sur ces trois critères, soit respectivement users.firstname, users.lastname et companies.name. Et ce, de façon croissante ou décroissante. Le choix sur l’ordre de tri est récupéré via le paramètre sort.

Sachant que nous avons besoin de faire une jointure entre deux tables, nous devons utiliser des fonctions “_by_sql“. Il nous faut donc une requête SQL. Heu non, deux en fait ! Une pour la récupération des données (c’est query) et une pour compter le nombre de résultats trouvés (c’est query_count)

Si nous recevons un paramètre query, c’est que l’utilisateur à demandé à filtrer la liste. N’étant pas avare, nous partirons du principe que ce filtre est applicable sur chaque colonne de notre tableau. Le filtre est construit dans query_add et ajouté à chaque requête.

Nous terminons la construction de la requête de récupération des données en appliquant l’ordre de tri.

Le comptage du nombre de résultats trouvés se fait en utilisant un classique count_by_sql. Vous remarquerez que nous l’appelons depuis la classe User. Mais cela n’a en fait pas vraiment d’importance et si vous préférez passer par Company, ne vous gênez pas ! Le résultat est stocké dans la variable @total.

La ou nous aurions voulu utiliser un paginate, nous alons devoir utiliser un paginate_by_sql. En effet paginate ne sait travailler qu’à partir d’un seul modèle. Or ici nous avons une jointure. paginate_by_sql propose la même chose que paginate, mais en se basant sur une requête SQL. paginate_by_sql n’est pas une instruction standard de Rails. C’est à vous de la définir. Pour cela ajoutez le morceau de code suivant à la fin du fichier app/controllers/application.rb.

module ActiveRecord
    class Base
        def self.find_by_sql_with_limit(sql, offset, limit)
            sql = sanitize_sql(sql)
            add_limit!(sql, {:limit => limit, :offset => offset})
            find_by_sql(sql)
        end

        def self.count_by_sql_wrapping_select_query(sql)
            sql = sanitize_sql(sql)
            count_by_sql("select count(*) from (#{sql}) as my_table")
        end
   end
end

class ApplicationController < ActionController::Base
    def paginate_by_sql(model, sql, per_page, options={})
       if options[:count]
           if options[:count].is_a? Integer
               total = options[:count]
           else
               total = model.count_by_sql(options[:count])
           end
       else
           total = model.count_by_sql_wrapping_select_query(sql)
       end

       object_pages = Paginator.new self, total, per_page, params[‘page’]
       objects = model.find_by_sql_with_limit(sql, object_pages.current.to_sql[1], per_page)
       return [object_pages, objects]
   end
end

Si vous voulez plus d’explications, voyez avec Phil Bogle.

Création du partiel

Dans la vue app/views/table/list.rhtml, nous avons déclaré faire appel à un layout (désolé, mais partiel je peux pas) Créons donc le fichier app/views/table/_items_list.rhtml :

<% if @total == 0 %>

<p>Aucun objet trouvé…</p>

<% else %>

<p>Nombre d’objets trouvés : <b><%= @total %></b></p>

<p>
<% if @items_pages.page_count > 1 %>
Page&#160;:
<%= pagination_links_remote @items_pages %>
<% end %>
</p>

<table>
  <thead>
    <tr>
      <td <%= sort_td_class_helper "fname" %>>
        <%= sort_link_helper "Prénom", "fname" %>
      </td>
      <td <%= sort_td_class_helper "lname" %>>
        <%= sort_link_helper "Nom", "lname" %>
      </td>
      <td <%= sort_td_class_helper "company" %>>
        <%= sort_link_helper "Société", "company" %>
      </td>
    </tr>
  </thead>
  <tbody>
    <% @items.each do |i| %>
    <tr class="odd">
      <td><%= i.firstname %></td>
      <td><%= i.lastname %></td>
      <td><%= i.name %></td>
    </tr>
    <% end %>
  </tbody>
</table>

<% end %>

Pour la suite nous suivrons mot à mot le tutoriel de Julien.

Assistant de pagination

Nous avons tout d’abord un test déterminant si le nombre total d’objets trouvés est supérieur à zéro. Si c’est le cas, on affiche ce nombre ainsi qu’un paragraphe qui sera vide si la pagination de notre tableau ne comporte qu’une page.

S’il y a plus d’une page de résultats, nous devons afficher les liens de pagination permettant de naviguer d’une page à l’autre. Rails fournit des méthodes très utiles pour générer ces liens, mais nous allons devoir les personnaliser un petit peu. Pour cela, nous allons créer un helper.

Un helper (ou assistant) est une fonction Ruby aidant à générer la vue. L’objectif de créer un helper est de séparer le code de ces fonctions de la vue elle-même, tout en rendant ce code réutilisable par différentes vues.

Nos assistants seront tous situés dans le fichier app/helpers/table_helper.rb. Chaque méthode de ce fichier sera accessible depuis notre vue. Si nous avions voulu rendre ces méthodes accessibles par l’ensemble des vues de notre application, il aurait fallu les placer dans application_helper.rb.

Bref, assez de bla bla, voici le code de notre assistant pagination_links_remote :

def pagination_links_remote(paginator)
  page_options = {:window_size => 1}
  pagination_links_each(paginator, page_options) do |n|
    options = {
      :url => {:action => ‘list’, :params => params.merge({:page => n})},
      :update => ‘table’,
      :before => "Element.show(’spinner’)",
      :success => "Element.hide(’spinner’)"
    }
    html_options = {:href => url_for(:action => ‘list’, :params => params.merge({:page => n}))}
    link_to_remote(n.to_s, options, html_options)
  end
end

Cette méthode prend comme argument un objet de type paginator. Il s’agit d’un objet Rails qui contient toutes les informations relatives à l’état de notre pagination (nombre de pages, page courante, etc.).

Nous définissons ensuite un hash nommé page_options qui contient un seul élément window_size. Ce paramètre indique à Rails le nombre de pages à afficher autour de la page courante dans les liens de pagination. Ainsi, si window_size est égal à un, on aura quelque chose comme ça :

1 … 5 6 7 … 13

Et si window_size égale deux :

1 … 4 5 6 7 8 … 13

Nous pourrions dès lors faire un appel à la fonction pagination_links, qui génèrerait directement le code XHTML affichant nos liens. Le problème est que cette fonction crée des liens “classiques”, pas des liens “Ajax”. Nous allons donc devoir redéfinir ces liens nous-mêmes, ce qui est accompli par la méthode pagination_links_each.

Cette méthode traverse l’ensemble des pages qui doivent être afichées sous forme de lien et leur applique le bloc qui lui a été passé en argument. Notre bloc définit ici deux types d’options :

  • options sert pour la génération des liens Ajax. Son contenu est très similaire à celui des options de l’élément observe_field. La seule réelle nouveauté est l’appel à @params.merge, qui va ajouter les paramètres de la requêtre actuelle au lien généré tout en remplaçant un paramètre page éventuel par le numéro de page du bloc.
  • html_options quant à lui sert à définir les options de génération des liens XHTML “classiques”, histoire que la pagination fonctionne si javascrip n’est pas disponible.
    Puis, un simple appel à la fonction link_to_remote génèrera le XHTML complet correspondant au lien de pagination du numéro de page traité par le bloc, incluant à la fois la partie javascript et la partie XHTML href.

Par exemple, voici ce que l’assistant génère si nous avons deux pages, la première étant actuellement affichée :

<a href="/item/list?page=2" onclick="Element.show(’spinner’); new Ajax.Updater(’table’, ‘/item/list?page=2′, {asynchronous:true, evalScripts:true, onSuccess:function(request){Element.hide(’spinner’)}}); return false;">2</a>

Assistant de tri

Revenons à notre partiel. Après les liens de pagination, nous commençons (enfin) l’affichage de notre tableau. La définition de l’en-tête de la table est un peu compliquée, car c’est là que nous définissons les liens permettant de trier notre tableau selon une colonne ou une autre. Chaque cellule d’en-tête de tableau fait appel à deux helpers.

Le premier assistant sort_td_class_helper, n’est en rien obligatoire. Son unique fonction est d’ajouter un class=”sortup” si la colonne est celle actuellement utilisée pour trier le tableau, et un class=”sortdown” si elle est utilisée pour trier par ordre inverse. La seule utilité de tout ceci est de permettre, via CSS, d’indiquer à l’utilisateur quelle est la colonne actuellement utilisée pour le tri.

Le code n’a vraiment rien de passionnant :

def sort_td_class_helper(param)
  result = ‘class="sortup"’ if params[:sort] == param
  result = ‘class="sortdown"’ if params[:sort] == param + "_reverse"
  return result
end

Nous avons ensuite un second assistant, nommé sort_link_helper.

def sort_link_helper(text, param)
  key = param
  key += "_reverse" if params[:sort] == param
  options = {
      :url => {:action => ‘list’, :params => params.merge({:sort => key, :page => nil})},
      :update => ‘table’,
      :before => "Element.show(’spinner’)",
      :success => "Element.hide(’spinner’)"
  }
  html_options = {
    :title => "Trier selon ce champ",
    :href => url_for(:action => ‘list’, :params => params.merge({:sort => key, :page => nil}))
  }
  link_to_remote(text, options, html_options)
end

Cet assistant est en définitive très similaire à pagination_links_remote, vu précédemment. Il prend deux arguments :

  • text, qui est juste le texte affiché comme titre de colonne et lien ;
  • param, qui est le nom du paramètre de requête associé à cette colonne.

Les deux premières lignes définissent une nouvelle variable, key, qui reçoit le contenu de l’argument param, c’est à dire la clé de tri. La chaîne _reverse est ajoutée à cette clé si param est déjà la clé de tri. Ceci sert à implémenter le tri par ordre croissant et décroissant : si l’utilisateur sélectionne un lien de tri, le tableau sera trié selon la colonne correspondante, par ordre croissant ; si il sélectionne alors à nouveau le même lien, le tri se fera par ordre décroissant, etc. Si vous ne trouvez pas ces explications très claires, jetez un oeil au contrôleur.

Le reste de la fonction définit les options passées au link_to_remote final. Tout ceci est très similaire à ce que nous avons vu pour pagination_links_remote :

  • options est un hash utilisé pour le lien Ajax javascript. Il contient l’url de l’action générant le nouveau XHTML, l’identifiant de l’élément devant être mis à jour, et les actions before et success permettant de montrer/cacher l’image spinner ;
  • html_options est un hash utilisé pour le lien HTML classique. Il génère le contenu de l’attribut href grâce à la fonction url_for de Rails.

C’est fini !

Nous avons désormais passé en revue tous les éléments de notre application. En théorie vous devriez pouvoir voir le résultat en lançant le serveur WebRick intégré à Rails et en vous connectant sur :

http://localhost:3000/item/list

À propos de ce document

Ce document est publié sous licence Creative Commons Attribution.

Merci à Julien pour son tutoriel.

• • •

2 commentaires »

  1. [...] Comment paginer, trier et filtrer un tableau avec Ajax et Rails [...]

    Ping par greg.rubyfr.net»Blog Archive » Truc-On-Rails #3 — Vendredi 30 mars 2007 @ 16:09
  2. hello,

    j’ai eu le même problème mais mais table avait des colonne avec des noms identiques , et je n’ai pas réussi à faire fonctionner ça avec , alors je suis rester sur le tuto de julier et j’ai rajouter un :include[:jointure1, :jointure2 ,:etc]
    et ça fonctionne parfaitement.

    Commentaire par fmh — Samedi 25 octobre 2008 @ 21:01

RSS des commentairesTrackBack URI

Laisser un commentaire

You must be logged in to post a comment.

Powered by: WordPress • Template adapted from the Simple Green' Wench theme - RSS