#196 Nested Model Form Part 1
- Download:
- source codeProject Files in Zip (100 KB)
- mp4Full Size H.264 Video (16.1 MB)
- m4vSmaller H.264 Video (11.1 MB)
- webmFull Size VP8 Video (28 MB)
- ogvFull Size Theora Video (21.6 MB)
Allá por 2007 cubrimos durante una serie de episodios la creación de formularios complejos que podían gestionar múltiples modelos en un único formulario, pero esa serie ha quedado desfasada ya. En este episodio vamos a empezar a ver técnicas más modernas para gestionar este tipo de formularios.
La mayor novedad a la hora de enfocar este problema viene dada por el método accepts_nested_attributes_for
que se añadió en Rails 2.3. Vamos a usarlo a lo largo de toda esta serie, de modo que necesitaremos ejecutar la última versión de Rails para poder utilizar esta técnica en nuestras aplicaciones.
Merece la pena repasar la documentación de accepts_nested_attributes_for
donde se muestra cómo utilizar esta función con atributos anidados en una única llamada. Pero esta documentación es menos clara a la hora de explicar cómo utilizarlo en la propia vista, así que nos centraremos en eso.
Nuestra aplicación de encuestas
La aplicación que queremos construir durante esta serie de episodios es una aplicación para realizar encuestas. Esta aplicación tendrá un formulario compuesto para crear y editar encuestas que nos dejará introducir el nombre de la encuesta así como un número de preguntas con un número de respuestas múltiples. El formulario también tendrá enlaces para permitirnos añadir y eliminar preguntas y respuestas dinámicamente de la encuesta.
El formulario compuesto para la creación y edición de encuestas.
Se trata de una asociación profundamente anidada en la que una encuesta tiene muchas preguntas y una pregunta tiene muchas repuestas. En las series anteriores sobre formularios compuestos no era posible crear este tipo de formularios con anidación pero con Rails 2.3 sí se puede.
Empezamos
Vamos a crear nuestra aplicación de encuestas desde cero, así que iremos creando una nueva aplicación Rails denominada surveysays
.
rails surveysays
Vamos a usar dos de los Nifty Generators de Ryan Bates: en primer lugar, el generador de layouts para crear un layout general para la aplicación.
script/generate nifty_layout
Nuestra aplicación tendrá tres modelos: Survey
, Question
y Answer
. Empezaremos con el modelo Survey
, usando el generado nifty_scaffold
para crear un scaffold a juego. En Survey
tan sólo tendremos un atributo llamado name
.
script/generate nifty_scaffold survey name:string
A continuación ejecutaremos las migraciones para crear la tabla de encuestas en la base de datos.
rake db:migrate
Si examinamos nuestra aplicación veremos que tenemos archivos generados automáticamente con el código necesario para poder listar, crear y editar encuestas, y un formulario básico para las encuestas sobre el que podemos empezar a trabajar.
Lo que queremos poner en el formulario son los campos que nos permitirán añadir y eliminar preguntas y respuestas cuando creemos una encuesta. El primer paso será generar el modelo Question
que tendrá un campo survey_id
para relacionarlo con la encuesta y un campo de contenido para albergar el texto de la pregunta.
script/generate model question survey_id:integer content:text
Una vez hecho esto migraremos otra vez la base de datos para crear a tabla de preguntas.
rake db:migrate
A continuación establecemos la relación entre Survey
y Question
en sus modelos.
/app/models/question.rb
class Question < ActiveRecord::Base belongs_to :survey end
/app/models/survey.rb
class Survey < ActiveRecord::Base has_many :questions, :dependent => :destroy end
Obsérvese que en Survey
hemos usado :dependent => :destroy
para que cuando eliminemos una encuesta se eliminen también todas sus preguntas.
En el modelo Survey
vamos a utilizar accepts_nested_attributes_for
para poder gestionar las preguntas a través de Survey
. Con esto podremos crear, actualizar y destruir preguntas cuando actualicemos los atributos de una encuesta.
/app/models/survey.rb
class Survey < ActiveRecord::Base has_many :questions, :dependent => :destroy accepts_nested_attributes_for :questions end
Creación del formulario
Una vez que los modelos Survey
Question
ya están listos, pasaremos al formulario de encuesta. Lo que queremos es añadir al formulario los campos para cada una de las preguntas del fomulario. Podemos usar el método fields_for
para gestionar los ficheros asociados en un formulario, pasándole el nombre del modelo asociado y luego iterando por todas las preguntas y creando un constructor de formularios para cada una de ellas. El constructor mostrará una etiqueta y un área de texto para cada pregunta.
/app/views/survey/_form.html.erb
<% form_for @survey do |f| %> <%= f.error_messages %> <p> <%= f.label :name %><br /> <%= f.text_field :name %> </p> <% f.fields_for :questions do |builder| %> <p> <%= builder.label :content, "Question" %><br /> <%= builder.text_area :content, :rows => 3 %> </p> <% end %> <p><%= f.submit "Submit" %></p> <% end %>
Si recargamos el formulario veremos que tiene el mismo aspecto que antes. Esto es porque una nueva encuesta no tendrá ninguna pregunta asociada que mostrar. En última instancia tendremos que añadir un enlace para añadir preguntas en el formulario pero por ahora solamente crearemos algunas preguntas en la acción new
de SurveyController
.
/app/controllers/surveys_controller.rb
def new @survey = Survey.new 3.times { @survey.questions.build } end
Con esto añadiremos tres preguntas a una nueva encuesta que veremos en el formulario al recargar la página. Ahora podemos rellenar el nombre y las primeras dos preguntas y enviar un nuevo formulario.
Cuando rellenemos el formulario se creará un nuevo registro Survey
, si bien no veremos sus preguntas porque aún no las estamos mostrando en la página. Para corregir esto modificaremos la vista show
de Survey
para ver las preguntas de una encuesta.
/app/views/survey/show.html.erb
<% title "Survey" %> <p> <strong>Name:</strong> <%=h @survey.name %> </p> <ol> <% for question in @survey.questions %> <li><%= h question.content %></li> <% end %> </ol> <p> <%= link_to "Edit", edit_survey_path(@survey) %> | <%= link_to "Destroy", @survey, :confirm => 'Are you sure?', :method => :delete %> | <%= link_to "View All", surveys_path %> </p>
Si recargamos la página de la encuesta veremos las preguntas, con lo que se comprueba que cuando se añadió la encuesta también se almacenaron sus preguntas.
Podemos también editar una encuesta y si cambiamos cualquiera de las preguntas éstas se actualizarán cuando enviemos el formulario.
En la página de arriba vemos que aparecen tres preguntas aunque sólo hemos introducido las primeras dos. Sería mejor que las que se dejasen en blanco fueran eliminadas automáticamente. Para esto podemos usar la opción reject_if
del método accepts_nested_attributes_for
. Este método acepta un lambda al que se le pasa un hash de atributos que podemos usar para rechazar una pregunta si su atributo content
viene vacío.
/app/models/survey.rb
class Survey < ActiveRecord::Base has_many :questions, :dependent => :destroy accepts_nested_attributes_for :questions, :reject_if => lambda { |a| a[:content].blank? } end
Si ahora creamos una nueva encuesta rellenando sólo dos campos de pregunta, sólo se crearán dichas preguntas y no veremos una pregunta vacía al final de la lista.
¿Y si queremos eliminar alguna pregunta cuando estamos editando una encuesta? En la aplicación final queremos tener un enlace para eliminar preguntas pero por ahora vamos a tirar por el camino rápido y utilizaremos una caja de selección que añadiremos en el parcial del formulario de la encuesta, junto con una etiqueta.
/app/views/survey/_form.html.erb
<% form_for @survey do |f| %> <%= f.error_messages %> <p> <%= f.label :name %><br /> <%= f.text_field :name %> </p> <% f.fields_for :questions do |builder| %> <p> <%= builder.label :content, "Question" %><br /> <%= builder.text_area :content, :rows => 3 %> <%= builder.check_box :_destroy %> <%= builder.label :_destroy, "Remove Question" %> </p> <% end %> <p><%= f.submit "Submit" %></p> <% end %>
El truco está en el nombre de atributo para la caja de selección: _destroy
. Cuando tenga un valor true
(cuando haya sido marcada), el registro será eliminado al enviar el formulario.
Paa que esto funcione tenemos que habilitarlo en el modelo Survey
añadiendo en accepts_nested_attributes_for
la opción :allow_destroy => true
.
/apps/models/survey.rb
class Survey < ActiveRecord::Base has_many :questions, :dependent => :destroy accepts_nested_attributes_for :questions, :reject_if => lambda { |a| a[:content].blank? }, :allow_destroy => true end
Si recargamos la página, veremos que ahora tenemos el checkbox de borrado junto a cada pregunta.
...y si marcamos la caja de una de las preguntas, ésta será borrada cuando enviemos el formulario.
Ahora las respuestas
Ya tenemos las preguntas como queremos pero aún nos faltan las respuestas. Empezaremos, al igual que antes, añadiendo el modelo Answer
y configurando su anidamiento. Primero generaremos el modelo.
script/generate model answer question_id:integer content:string
Y luego migraremos la base de datos otra vez.
rake db:migrate
A continuación, configuraremos la relación entre Answer
y Question
.
/app/models/answer.rb
class Answer < ActiveRecord::Base belongs_to :question end
Para Question
tendremos que utilizar accepts_nested_attributes_for
igual que hicimos en el modelo Survey
model.
/app/models/question.rb
class Question < ActiveRecord::Base belongs_to :survey has_many :answers, :dependent => :destroy accepts_nested_attributes_for :answers, :reject_if => lambda { |a| a[:content].blank? }, :allow_destroy => true end
En este formulario podríamos añadir los campos para las respuestas pero el código de la vista del formulario quedará bastante recargado si añadiésemos otro modelo anidado. Por otro lado queremos que la adición de preguntas se haga, más adelante, mediante Javascript. Así que por ambas razones moveremos el código del formulario que muestra cada pregunta a su propio parcial llamado question_fields
.
Después de hacerlo la vista del formulario tendrá este aspecto.
/app/views/surveys/_form.html.erb
<% form_for @survey do |f| %> <%= f.error_messages %> <p> <%= f.label :name %><br /> <%= f.text_field :name %> </p> <% f.fields_for :questions do |builder| %> <%= render 'question_fields', :f => builder %> <% end %> <p><%= f.submit "Submit" %></p> <% end %>
Obsérvese que tan sólo pasamos el nombre del parcial como una cadena a mostrar, haciendo uso de la nueva notación breve que se introdujo en Rails 2.3. También pasamos el builder
al parcial con el nombre f
. En el nuevo parcial question_fields
podemos usar la variable f
para pintar los elementos de formulario de una pregunta.
/app/views/surveys/_question_fields.html.erb
<p> <%= f.label :content, "Question" %><br /> <%= f.text_area :content, :rows => 3 %><br /> <%= f.check_box :_destroy %> <%= f.label :_destroy, "Remove Question" %> </p>
Podemos gestionar los campos de las respuestas de una forma similar y ponerlos en su propio parcial. En el parcial _question_fields
recorremos todas las respuestas de una pregunta y llamaremos a un nuevo parcial llamado _answer_fields
.
/app/views/surveys/_question_fields.html.erb
<p> <%= f.label :content, "Question" %><br /> <%= f.text_area :content, :rows => 3 %><br /> <%= f.check_box :_destroy %> <%= f.label :_destroy, "Remove Question" %> </p> <% f.fields_for :answers do |builder| %> <%= render 'answer_fields', :f => builder %> <% end %>
En nuestro nuevo parcial llamado _answer_fields
pondremos el código para mostrar una respuesta.
/app/views/survey/_answer_fields.html.erb
<p> <%= f.label :content, "Answer" %> <%= f.text_field :content %> <%= f.check_box :_destroy %> <%= f.label :_destroy, "Remove" %> </p>
Igual que antes para poder ver los campos de respuestas en el formulario vamos a modificar la acción new
de SurveyController
para que se creen cuatro respuestas para cada una de las tres nuevas preguntas.
/app/controllers/survey_controller.rb
def new @survey = Survey.new 3.times do question = @survey.questions.build 4.times { question.answers.build } end end
A partir de ahora cuando creemos una encuesta tendremos tres preguntas con cuatro posibles respuestas cada una.
Cuando se envíen los campos del formulario aún no veremos las respuestas pero esto lo resolveremos fácilmente. En la vista show
de una encuesta, añadiremos código para mostrar las respuestas en la sección que recorre cada pregunta .
/app/views/survey/show.html.erb
<ol> <% for question in @survey.questions %> <li><%= h question.content %></li> <ul> <% for answer in question.answers %> <li><%= h answer.content %></li> <% end %> </ul> <% end %> </ol>
Si ahora recargamos la página de la encuesta veremos las preguntas y sus repuestas.
Todavía no hemos terminado puesto que queremos poder añadir o eliminar dinámicamente preguntas y respuestas del formulario usando enlaces. Esto lo veremos en el próximo episodio.