Comprendre le form_for de Rails

Publié le 2 juillet 2015 par Nicolas Le Chenic | back

Cet article est publié sous licence CC BY-NC-SA

Prêt pour une séance d’occultisme ? Dans cet article de la série destinée aux bases de Ruby On Rails nous allons continuer à étudier la magie de notre framework favori partout où elle se trouve ! Aujourd’hui on s’attaque aux formulaires et plus précisément au form_for.

Rails offre un tas de raccourcis qui aident à développer et le form n’échappe pas à cette règle. Si vous avez commencé avec le scaffold de Rails vous utilisez peut-être form_for sans trop vous soucier de ce qui se passe derrière pourtant…

Ce que génère form_for

Avant d’entrer dans les détails, voyons voir ce qu’un form_for génère pour un formulaire minimaliste de création d’article de blog :

views/blogs/_form.html.erb

<%= form_for(@blog) do |f| %>

  <div class="field">
    <%= f.label :title %><br>
    <%= f.text_field :title %>
  </div>

  <div class="field">
    <%= f.label :content %><br>
    <%= f.text_area :content %>
  </div>

  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

HTML généré

<form class="new_blog" id="new_blog" action="/blogs" accept-charset="UTF-8" method="post">
  <input name="utf8" type="hidden" value="&#x2713;" />
  <input type="hidden" name="authenticity_token" value="RTzn2pxMSZJB3wS7+H0CXYHghQgfjvhFznarusp4ALR2iR8QxCxL5I1HZgPkQwN26YjaxpWb29x2tkT3i6vjqw==" />

  <div class="field">
    <label for="blog_title">Title</label><br>
    <input type="text" name="blog[title]" id="blog_title" />
  </div>

  <div class="field">
    <label for="blog_content">Content</label><br>
    <textarea name="blog[content]" id="blog_content">
    </textarea>
  </div>

  <div class="actions">
    <input type="submit" name="commit" value="Create Blog" />
  </div>
</form>

À première vue rien d’extraordinaire, mais si on regarde de plus près, on observe plusieurs choses.

Une marque étrange à la naissance

À la création de votre formulaire on remarque un input de type hidden qui a pour valeur une clé d’authentification.

<input type="hidden" name="authenticity_token" value="RTzn2pxMSZJB3wS7+H0CXYHghQgfjvhFznarusp4ALR2iR8QxCxL5I1HZgPkQwN26YjaxpWb29x2tkT3i6vjqw==" />

Celle-ci est créée par le form_for pour prévenir de la faille CSRF et oblige le visiteur à posséder le jeton généré lors de l’affichage du formulaire.

Un attribut name inhabituel

  <div class="field">
    <label for="blog_title">Title</label><br>
    <input type="text" name="blog[title]" id="blog_title" />
  </div>

On remarque aussi que nos inputs ont un name composé du nom du modèle sur lequel on agit ainsi que de son attribut entre crochets.

Ainsi Rails va nous simplifier la vie lorsqu’on voudra traiter des objets imbriqués au sein d’un même formulaire.

Et si je ne respecte pas le formulaire magique ?

Admettons que vous souhaitiez réaliser un formulaire sans passer par Rails, vous vous y prendriez sans doute comme ceci :

<form action="/blogs" accept-charset="UTF-8" method="post">

    <label for="blog_title">Title</label><br>
    <input type="text" name="title" id="blog_title" />

    <label for="blog_content">Content</label><br>
    <textarea name="content" id="blog_content">
    </textarea>

    <input type="submit" name="commit" value="Créer un article" />
</form>

Plusieurs problèmes se posent alors, d’abord lors de la soumission, plusieurs messages d’erreurs apparaissent :

Erreur d'authentification

En effet si vous souhaitez interagir avec la base de données, vous aurez besoin d’ajouter une clé d’authentification fournie une fois de plus par Ruby On Rails.

<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>" />

Afin de respecter les conventions de Rails, on nous invite alors à détailler l’attribut name comme vu précédemment.

Erreur d'attribut

Enfin, si je souhaite que mon formulaire HTML représente mon objet actuel j’aurais besoin de préciser l’attribut value comme ci-dessous :

<input type="text" name="blog[title]" id="blog_title" value="<%= @blog.title %>" />

Derrière le rideau

C’est beau la magie, mais on a envie de comprendre “le truc”, on va donc regarder la méthode form_for fournit par Rails. Pour cela on se dirige dans l’action_view qui comme son nom l’indique regroupe ce qui attrait au rendu.

Regardons d’abord la définition de la méthode form_for :

rails/actionview/lib/action_view/helpers/form_helper.rb

def form_for(record, options = {}, &block)
  # Plein de code ruby ...
end

On remarque que celle-ci reçoit trois arguments. Le record, qui est l’objet qu’on passe au form_for, précédemment @blog. Les options avec lesquelles on pourra modifier la méthode. Par exemple :

<%= form_for(@blogs, remote: true, method: :patch) do |f| %>

Enfin le &block, qui désigne le contenu du bloc

<%= form_for @blogs do |f| %>
  <div class="field">
    <%= f.label :title %><br>
    <%= f.text_field :title %>
  </div>

  <div class="field">
    <%= f.label :content %><br>
    <%= f.text_area :content %>
  </div>

  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

Contenu de la méthode

Les premières lignes sont simples, dans un premier temps la méthode form_for soulève une erreur si le bloc n’existe pas, puis elle définit html_options.

def form_for(record, options = {}, &block)
  raise ArgumentError, "Missing block" unless block_given?
  html_options = options[:html] ||= {}

  # Plein de code ruby ...
end

Ensuite, on vérifie le type de record pour déterminer les valeurs qu’on passera dans object_name et dans object qui seront réutilisés par la suite.

  case record
  when String, Symbol
    object_name = record
    object      = nil
  else
    object      = record.is_a?(Array) ? record.last : record
    raise ArgumentError, "First argument in form cannot contain nil or be empty" unless object
    object_name = options[:as] || model_name_from_record_or_class(object).param_key
    apply_form_for_options!(record, object, options)
  end

La méthode model_name_from_record_or_class va nous permettre d’extraire le nom de l’objet disponible depuis celui-ci.

model_name_from_record_or_class(object).param_key

Puis on commence à traiter le rendu avec apply_form_for_options.

apply_form_for_options!(record, object, options)

Les méthodes utiles au rendu du formulaire

builder = instantiate_builder(object_name, object, options)

la méthode instantiate_builder va à la fois instancier la classe souhaitée et construire le formulaire en prenant en compte cette classe et les paramètres passés.

output  = capture(builder, &block)

capture est une méthode qui permet de stocker le template qu’on a constitué jusqu’à présent ainsi que le contenu du bloc dans la variable output qu’on ré-exploitera par la suite.

form_tag_with_body(html_options, output)

Pour finir on concatène les éléments et on ajoute la fermeture du formulaire ! Voyez vous-même :

def form_tag_with_body(html_options, content)
  output = form_tag_html(html_options)
  output << content
  output.safe_concat("</form>")
end

Qu’en est-il du form_tag

Pourquoi utiliser form_tag ?

Le form_for est destiné à interagir avec des ressources, pour cette raison il est contenu dans un bloc qui lui permet de gérer très simplement la création et l’édition de données associées à l’objet, ici @blog.

Cependant, il n’est pas toujours utile d’avoir un formulaire compris dans un bloc, si on souhaite réaliser une barre de recherche un simple GET suffira. C’est la qu’intervient le form_tag qui est à mi-chemin entre notre formulaire HTML et le form_for.

views/blogs/search.html.erb

<%= form_tag(blogs_search_path, :method => "get") do %>
  <%= text_field_tag :title, params[:title], placeholder: "Chercher un article de blog" %>
  <%= submit_tag "Search", :name => nil %>
<% end %>

<% @blogs.each do |blog| %>
  <h1><%= blog.title %></h1>
  <p><%= blog.content %></p>
<% end %>

controllers/blogs_controller.rb

# GET /blogs?title=title
def search
  if params[:title]
    @blogs = Blog.search(params[:title])
  else
    @blogs = Blog.all
  end
end

models/blog.rb

def self.search(title)
  where("title like ?", "%#{title}%")
end

Tableau récapitulatif

Pour finir, voici un petit tableau récapitulatif des avantages des différents formulaires.

Avantages Formulaire HTML form_tag form_for
Facilite la création de balises HTML Non Oui Oui
Génère une clé d'authentification Non Oui Oui
Attributs name : objet[attribut] Non Non Oui
Facilite la création et l'édition Non +/- Oui

J’espère que cet article vous aura aidé à mieux comprendre la magie du form_for.

À très vite pour le prochain article sur la magie de Rails !


L’équipe Synbioz.

Libres d’être ensemble.