Ici et ailleurs

Le blog d’un joueur … mais pas que le blog de Sky.

Ruby on Rails : Rendering et Layout

Aujourd’hui, nous allons nous attaquer à une partie importante puisque nous allons voir comment gérer l’affichage de nos pages et le fonctionnement des layouts. Je m’appuierai énormément sur les guides Ruby on Rails qui sont une vraie mine d’or et j’essaierai encore une fois d’être clair et assez synthétique.

Pour commencer, nous allons donc reprendre notre NotesController et lui demander de pouvoir nous retourner un fichier XML, et pour ce faire, nous allons devoir étudier les différentes méthodes de rendu de Rails, c’est à dire quelle réponse envoyer à l’utilisateur suite à une requête HTTP. Trois manières de faire donc :

  • render pour demander le rendu de manière simple,
  • redirect_to pour rediriger le client,
  • head pour créer la réponse HTTP de toute pièce (je ne détaillerai pas cette méthode ici).

Je rappelle que si aucune méthode de rendu n’est précisée dans le contrôleur, comme c’était le cas dans notre premier contrôleur, Rails appelle par convention la vue associée à l’action.

Avant de commencer

Nous allons modifier notre fichier /config/routes.rb pour lui dire que notre page par défaut est désormais la liste de nos notes. Pour cela, commencez par supprimer le fichier /public/index.html puis modifiez le fichier routes.rb en ajoutant avant notre règle resources : notes :

root :to => "notes#index"

La méthode render

Sûrement la méthode que nous utiliserons le plus. Commençons par voir comment fonctionne cet appel en lui demandant de ne rien afficher. Direction la méthode index de notre NotesController.

def index
	@notes = Note.all
	render :nothing => true
end

Lancez votre serveur Rails et dirigez vous vers http://localhost:3000/notes, plus rien ne s’affiche, magique ! Avant de continuer, nous allons créer une nouvelle page HTML dans notre dossier /app/views/notes/ que nous appellerons new.html.erb :

<!DOCTYPE html>
<html>
<head>
  <title>Notes : Cr&eacute;ation d'une nouvelle note</title>
</head>
<body>

<h2>Cr&eacute;ation d'une nouvelle note</h2>

</body>
</html>

Appeler la vue d’une autre action

Notamment dans le cas où la mise à jour d’une donnée échoue afin d’afficher à nouveau le formulaire d’édition par exemple. Toujours d’une manière très simple, remplaçons notre appel à render :

def index
	@notes = Note.all
	render :new # render "new" marche aussi
end

Retournez sur http://localhost:3000/notes, vous devriez normalement voir notre page nouvellement créée.

Cet appel va donc chercher le rendu associé à l’action new au sein du même contrôleur. Notre méthode n’étant pas définie, par défaut, Rails va chercher la vue associé, en l’occurence new.html.erb.

Point très important, en demandant l’affichage lié à l’action new, aucun code de la méthode new ne sera exécuté, Rails détermine juste la vue à afficher et c’est tout, si vous avez des variables à initialiser, il faudra le faire dans l’action qui effectue l’appel à render.

Afficher une vue appartenant à un autre contrôleur

Prenons un exemple. Vous avez un contrôleur AdminController et vous souhaitez que l’action index de ce contrôleur affiche la liste des notes (donc le template /app/views/notes/index.html.erb), il suffira d’écrire dans ce contrôleur :

def index
	@notes = Note.all
	render "notes/index"
end

Afficher une vue arbitraire

Il peut arriver que vous souhaitiez afficher une page arbitraire, ne nécessitant pas de contrôleur. Dans ce cas il suffira de préciser l’emplacement de cette page de cette manière :

def index
	@notes = Note.all
	render "/public/500.html"
end

Rails détermine qu’il s’agit d’un fichier grâce au premier “/“, ne l’oubliez donc pas.

Retourner du XML, du JSON ou …

Très pratique pour du webservice par exemple, Rails peut renvoyer autre chose que du HTML: du simple texte grâce à render :text => “Du texte”, mais aussi du XML et du JSON. Rien de plus simple, il suffit de préciser la ressource que l’on souhaite formater. Prenons notre contrôleur NotesController :

def index
	@notes = Note.all
	render :xml => @notes
	# render :json => @notes
end

Notre action index nous retournera désormais nos notes au format XML !

J’ai volontairement omis d’autres formes de réponses pour alléger le tutorial.

Alors c’est sympa, mais ce qui le serait encore plus, c’est que notre contrôleur puisse décider quel format de réponse envoyé.

respond_to : faites votre choix

Cette fonctionnalité va nous permettre de pouvoir répondre de différentes manières en fonction de ce que le client attend. Par exemple, toujours dans notre contrôleur, nous allons écrire ceci :

def index
	@notes = Note.all
	respond_to do |format|
		format.html
		format.xml {render :xml => @notes}
	end
end

Si vous accédez à http://localhost:3000/notes, notre page HTML s’affiche normalement. Accédez maintenant à http://localhost:3000/notes.xml, voilà, Rails nous retourne nos notes au format XML.

render et ses paramètres

Cette méthode possède quelques paramètres :

  • :content_type pour préciser le type de contenu renvoyé,
  • :layout pour préciser quel layout utilisé pour le rendu ou false pour n’en utiliser aucun, on y reviendra juste après,
  • :status pour préciser le code HTTP de réponse,
  • :location pour préciser le HTTP location header.

Et dans un exemple, si on ne veut pas utiliser de layout dans le rendu de l’action new:

render :new, :layout => false

Avant d’en venir aux layouts, un petit mot sur redirect_to.

La méthode redirect_to

La méthode redirect_to agit d’une manière totalement différente que render. On a vu que l’appel à render :action va uniquement chercher quelle vue afficher. Un appel à redirect_to va réorienter la requête HTTP vers la page renseignée, c’est à dire que le client va effectuer une nouvelle requête à l’adresse donnée.

Prenons un exemple.

def index
  @notes = Note.all
end

def show
  @note = Note.find_by_id(params[:id])
  if @note.nil?
    render :action => "index"
  end
end

En accédant à l’action show, si aucune note n’est trouvée, on demande d’afficher la vue associée à l’action index. Cependant, la variable @notes n’étant pas initialisée, on aura une erreur. Il faut dans ce cas utiliser la méthode redirect_to, la variable sera effectivement initialisée et tout le code sera exécuté puisqu’une nouvelle requête aura été adressée vers notre action index. On se retrouve donc avec ce code :

def index
  @notes = Note.all
end

def show
  @note = Note.find_by_id(params[:id])
  if @note.nil?
    redirect_to :action => "index"
  end
end

Petit tip intéressant, redirect_to :back, pas besoin de l’expliquer mais ça peut être utile.

Les layouts

Nous allons enfin pouvoir modifier l’apparence de notre application et faire quelque chose de plus esthétique. Commençons par modifier nos deux fichiers HTML (index.html.erb et new.html.erb) afin de leur ajouter un header et un footer.

index.html.erb

<!DOCTYPE html>
<html>
<head>
  <title>Rails Notes : Liste des notes</title>
</head>
<body>
<div class="wrapper">
	<div id="header">
		<h3><%= link_to "Rails Notes", "/" %></h3>
	</div>

	<div id="content">
		<h1>Liste des notes</h1>
		<ul>
		<% @notes.each do |note| %>
			<li><em><%= note.created_at.strftime("%m/%d/%Y") %></em> - <%= link_to(note.description, note_path(note.id)) %></li>
		<% end %>
		</ul>
	</div>

	<div id="footer">
		<p><small>Rails Notes est propuls&eacute; par Ruby on Rails!</small></p>
	</div>
</div>
</body>
</html>

new.html.erb

<!DOCTYPE html>
<html>
<head>
  <title>Rails Notes : Cr&eacute;ation d'une nouvelle note</title>
</head>
<body>
<div class="wrapper">
	<div id="header">
		<h3><%= link_to "Rails Notes", "/" %></h3>
	</div>

	<div id="content">
		<h1>Cr&eacute;er une nouvelle note</h1>
	</div>

	<div id="footer">
		<p><small>Rails Notes est propuls&eacute; par Ruby on Rails!</small></p>
	</div>
</div>
</body>
</html>

Et là on se dit qu’on écrit exactement la même chose dans les deux fichiers et que seul le contenu change, pas top. Voilà où vont nous être utile les layouts, nous allons définir ce que nous pourrions appeler un “template” et ainsi éviter d’écrire du code redondant. Ainsi, avant le rendu, Rails incrustera notre contenu dans le layout qui va bien pour nous retourner notre page HTML.

Création de notre layout

Direction le dossier /app/views/layouts/ pour créer un nouveau fichier : notes.html.erb. Dans ce fichier, nous allons donc écrire le code qui reviendra pour chacune de nos pages. Nous nous retrouvons donc avec :

<!DOCTYPE html>
<html>
<head>
  <title>Rails Notes</title>
</head>
<body>
<div class="wrapper">
	<div id="header">
		<h3><%= link_to "Rails Notes", "/" %></h3>
	</div>

	<div id="content">
		<%= yield %>
	</div>

	<div id="footer">
		<p><small>Rails Notes est propuls&eacute; par Ruby on Rails!</small></p>
	</div>
</div>
</body>
</html>

Le contenu de notre page sera inséré par Rails grâce au yield présent dans le template.

Ensuite, faites en sorte qu’il ne reste plus que la section #content dans les fichiers index.html.erb et new.html.erb, par exemple pour new.html.erb, vous devriez avoir ceci :

<h1>Cr&eacute;er une nouvelle note</h1>

En accédant à notre site, on obtient le même résultat seulement maintenant, on évite d’écrire du code redondant. Maintenant on aimerais bien pouvoir changer le titre en fonction de la page sur laquelle on se trouve.

Utiliser content_for

La méthode content_for permet de diviser notre layout en blocs. De cette manière on peut insérer du contenu n’importe où dans le layout et ce, toujours grâce au yield.

Nous allons donc définir nos blocs dans notre layout /app/views/layouts/notes.html.erb :

<!DOCTYPE html>
<html>
<head>
  <%= yield :title %>
</head>
<body>
<div class="wrapper">
	<div id="header">
		<h3><a href="/">Rails <em>Note</em></a></h3>
	</div>

	<div id="content">
		<%= yield %>
	</div>

	<div id="footer">
		<p><small>Rails Notes est propuls&eacute; par Ruby on Rails!</small></p>
	</div>
</div>
</body>
</html>

On a donc défini un nouveau bloc grâce à yield :nom_du_bloc. Maintenant, il suffit de changer le contenu de nos deux pages pour prendre en compte ce bloc :

index.html.erb

<% content_for :title do %>
	<title>Rails Note : Liste des notes</title>
<% end %>

<h1>Liste des notes</h1>
<ul>
<% @notes.each do |note| %>
	<li><em><%= note.created_at.strftime("%m/%d/%Y") %></em> - <%= link_to(note.description, note_path(note.id)) %></li>
<% end %>
</ul>

new.html.erb

<% content_for :title do %>
	<title>Rails Note : Cr&eacute;er une nouvelle note</title>
<% end %>

<h1>Cr&eacute;er une nouvelle note</h1>

Mais alors comment Rails a su qu’il fallait utiliser notre layout et pas un autre ? A aucun moment nous n’avons préciser vouloir utiliser ce layout. Voyons voir ça.

Comment Rails détermine le layout

Et bien au final, c’est assez simple. Lors du rendu appelé par le contrôleur, Rails regarde automatiquement dans le dossier /app/views/layouts si un layout correspond au contrôleur (dans notre cas NotesController doit correspondre à un layout nommé notes).Si c’est le cas, il l’utilise, sinon il utilise le layout application par défaut.

Si toutefois vous souhaitez vous même définir quel layout doit être appelé, plusieurs méthodes s’offrent à vous.

Utiliser le mot clé layout dans le contrôleur

Nous allons voir ça avec des exemples :

Dans cet exemple, toutes les pages de notre contrôleur utiliseront le layout /app/views/layouts/special.html.erb.

class NotesController < ApplicationController
	layout "special"
end

Si vous souhaitez n’utiliser aucun layout par défaut, remplacé le nom du layout par false.

Dans celui-ci, le layout devra être déterminé au runtime, c’est à dire qu’une méthode portant le nom du symbole devra être implémentée pour définir la valeur de ce même symbole.

class NotesController < ApplicationController
	layout :layout_name

	private
	def layout_name
		@current_user.special? ? "special" : "notes" # Ceci est une expression ternaire, un équivalent du if / else
	end
end

Et dans ce dernier exemple, nous précisons quelles méthodes doivent utiliser le layout précisé grâce aux options :only et :except qui prennent tous les deux un nom de méthode ou un tableau en paramètre.

class NotesController < ApplicationController
	layout "special", :only => [:index, :show]
end

En l’occurence, le layout special sera utilisé uniquement pour les méthodes index et show.

Préciser l’option layout dans render

On l’avait vu un peu plus haut, on peut préciser quel layout utilisé directement en paramètre de la méthode render. Si on reprend notre méthode index :

respond_to do |format|
	format.html {render :layout => false}
	format.xml {render :xml => @notes}
end

Avant de continuer, pensez à supprimer tout ce que vous auriez pu rajouter, nous allons garder notre layout notes.

Voilà qui ne me semble pas trop mal, seulement notre application reste “ugly”, il va nous falloir lui donner un bon coup de pinceau.

Les Asset Tags

Ces derniers sont des helpers qui nous permettent de générer du HTML plus rapidement (toutes les infos ici). Je ne vais pas tous les présenter ici, mais voici la liste :

  • auto_discovery_link_tag : permet de lier automatiquement aux flux RSS ou ATOM,
  • javascript_include_tag : permet d’inclure du javascript,
  • stylesheet_link_tag : permet d’inclure les feuilles de style,
  • image_tag : permet de générer le code HTML pour inclure une image,
  • video_tag : permet de générer le code HTML pour inclure une vidéo,
  • audio_tag : permet de générer le code HTML pour inclure de l’audio.

Là où ça devient intéressant, c’est que si vous ne précisez pas le chemin complet, Rails va chercher dans les dossiers qui vont bien, vous aidant ainsi à garder votre application bien organisée :

  • /public/javascripts,
  • /public/stylesheets,
  • /public/images,
  • /public/videos,
  • /public/audios.

Une affaire de goût

Commençons par créer notre feuille de style /public/stylesheets/style.css. Nous allons tout de suite utiliser les Asset Tags que nous avons vu plus haut pour lier notre feuille de style dans notre layout. Direction donc /app/views/layouts/note.html.erb, et rajoutez entre les balises head, en dessous de notre bloc yield :title, la ligne suivante :

<%= stylesheet_link_tag "style", "http://fonts.googleapis.com/css?family=Kristi" %>

On inclut non seulement notre feuille de style mais en plus une autre feuille de style de Google Font qu’on utilisera par la suite.

Maintenant, il ne nous reste plus qu’à remplir le contenu de notre feuille de style. J’ai dû modifier quelque peu nos fichiers pour coller au style que je voulais donner, voici au final les sources :

/app/views/notes/index.html.erb

<% content_for :title do %>
	<title>Rails Note : Liste des notes</title>
<% end %>

<h1 class="float-left">Liste des notes</h1>
<a id="add-note" href="<%= new_note_url %>">+ cr&eacute;er une note</a>

<ul id="notes">
<% @notes.each do |note| %>
	<li><em><%= note.created_at.strftime("%m.%d.%Y") %></em> - <%= link_to(note.description, note_path(note.id)) %></li>
<% end %>
</ul>

/public/stylesheets/style.css

body {
	border-top: 5px solid #eee;
	color: #222;
	font-family: arial, sans-serif;
	font-size: 16px;
	margin: 0;
	padding: 0;
}

a:link, a:visited {
	color: #2b95f1;
}

a:hover {
	color: #222;
}

p {
	line-height: 1.5em;
	margin-bottom: 1.5em;
}

h1 {
	font-size: 1.5em;
}

h1.float-left {
	margin-top: 0;
}

.wrapper {
	margin: 0 auto;
	width: 560px;
}

#header h3 {
	border-bottom: 1px solid #eee;
	margin-top: 0.5em;
	padding-bottom: 0.5em;
}

#header h3 a:link, #header h3 a:visited {
	color: #222;
	font-size: 1.6em;
	text-decoration: none;
}

#header h3 a:link em, #header h3 a:visited em {
	color: #2b95f1;
	font-family: 'Kristi', arial, serif;
	font-size: 1.4em;
	font-weight: normal;
}

a#add-note:link, a#add-note:visited {
	background-color: #2b95f1;
	color: #fff;
	float: right;
	font-size: 0.875em;
	margin-top: 0.3em;
	padding: 0.3125em 0.4375em;
	position: relative;
	text-decoration: none;
	-moz-border-radius: 5px;
	-webkit-border-radius: 5px;
}

a#add-note:active {
	top: 1px;
}

ul#notes {
	clear: both;
	list-style-position: inside;
	list-style-type: none;
	padding: 0;
}

ul#notes li {
	background-color: #FFF9D8;
	border: 1px solid #f3e597;
	margin-bottom: 0.5em;
	padding: 0.5em;
}

ul#notes li em {
	color: #c5b143;
	font-size: 0.8em;
}

#footer {
	color: #999;
}

.float-left {float: left;}

Conclusion

Cet article est très long et j’en suis conscient, j’avais prévu autre chose mais finalement, je le garde pour le prochain article. Nous avons vu énormément de choses et j’espère avoir été assez clair, notre application commence enfin à ressembler à quelque chose. Avant de se consacrer à nouveau aux fonctionnalités de notre application, nous parlerons des partials, un autre moyen de séparer nos rendus. Pour ça, rendez-vous au prochain article !

Enfin quelque chose de visuel !

Enfin quelque chose de visuel !

Tags: , , ,

7 commentaires pour “Ruby on Rails : Rendering et Layout”

  1. xan dit :

    Je dois te contacter par mail à propos d’un truc sur RoR (indice : ça commence par w), Sky (at) wefrag.com est ok ?

  2. DjMerguez dit :

    Dense, mais intéressant ! Dans le paragraphe sur redirect_to, tu n’as mis que le code erroné, c’est bien ça ?

  3. Sky dit :

    DjMerguez a dit :
    Dense, mais intéressant ! Dans le paragraphe sur redirect_to, tu n’as mis que le code erroné, c’est bien ça ?

    Exact, j’ai rajouté le code une fois utilisé avec redirect_to, ça me semble plus judicieux, merci pour la remarque.

  4. D0h dit :

    Le MVC, c’est vraiment le top aujourd’hui pour le web.

  5. Mysterius dit :

    Zut, j’ai cru que cet article allait parler de Berlusconi.

  6. Sylario dit :

    Bon, je désespère, j’ai posé ma question dans plusieurs forums Rails et sur stackOverflow, donc je tente ma chance ici

    J’ai :
    model product, qui belong_to un model collection
    model product, qui belong_to un model product_type

    Je veux une vue qui affiche les produits d’une collection ou d’un certain product_type.

    J’ai crée un controller magasin qui ne correspond à aucun modèle.

    J’ai fait une route :

    controller :magasin do
    get “magasin” => “magasin#index”
    end

    et j’ai fait un lien a partir des collections de cette façon :

    link_to collection.nom, magasin_path(collection)

    Et ca me fait un lien “magasin.2″. Impossible de récupérer l’id de collection à l’arriver.
    Comment je fait pour récupérer les données? J’en peux plus ça fait 4 jours que je bloque la dessus je deviens fou.

  7. Sky dit :

    Oups, je réponds sûrement trop tard. Pour l’instant, j’ai pas encore vu les modèles plus que ça, c’est prévu très bientôt donc je ne peux malheureusement pas t’aider (même si je pense que depuis le temps, t’as dû trouver une solution :) ).

Laisser un commentaire

Si vous avez un compte sur WeFrag, connectez-vous pour publier un commentaire.

Vous pouvez, entre autres, utiliser les tags XHTML suivant :
<a href="" title="">...</a>,<b>...</b>,<blockquote cite="">...</blockquote>,<code>...</code>,<i>...</i>