ruby on rails - que - Cómo enviar múltiples elementos NUEVOS a través de la asignación masiva de Rails 3.2
ruby on rails que es (8)
Tengo un caso de uso bastante estándar. Tengo un objeto principal y una lista de objetos secundarios. Quiero tener una forma tabular donde pueda editar todos los niños a la vez, como filas en la tabla. También deseo poder insertar una o más filas nuevas y, en el caso de enviarlas, crearlas como nuevos registros.
Cuando uso fields_for
para renderizar una serie de fields_for
para registros anidados relacionados por has-many, rails genera nombres de campo, por ejemplo parent[children_attributes][0][fieldname]
, parent[children_attributes][1][fieldname]
y así en.
Esto hace que Rack analice un hash de parámetros que se ve así:
{ "parent" => {
"children" => {
"0" => { ... },
"1" => { ... } } }
Cuando pasa un objeto nuevo (no persistente), el mismo fields_for
generará un nombre de campo que se verá así:
parent[children_attributes][][fieldname]
Tenga en cuenta el []
sin índice en él.
Esto no se puede publicar en la misma forma con los campos que contienen [0]
, [1]
, etc. porque Rack se confunde y aumenta
TypeError: expected Array (got Rack::Utils::KeySpaceConstrainedParams)
"OK", piensa I. "Simplemente me aseguraré de que todos los campos usen el formulario []
lugar del formulario [index]
. Pero no puedo encontrar la manera de convencer a fields_for
de hacer esto de manera consistente. Incluso si doy es un prefijo de nombre de campo explícito y un objeto:
fields_for ''parent[children_attributes][]'', child do |f| ...
Mientras persista el elemento child
, modificará automáticamente los nombres de campo para que se conviertan, por ejemplo, en elementos parent[children_attributes][0][fieldname]
, dejando los nombres de campo para nuevos registros como elemento parent[children_attributes][][fieldname]
. Una vez más, Rack barfs.
Estoy perdido ¿Cómo diablos uso los ayudantes estándar de Rails como fields_for
para enviar múltiples registros nuevos , junto con registros existentes, hacer que se analicen como una matriz en los params, y tener todos los registros sin ID creados como nuevos registros en el DB? ¿No tengo suerte y solo tengo que generar todos los nombres de campo manualmente?
Como han mencionado otros, el []
debe contener una clave para nuevos registros porque de lo contrario está mezclando un hash con un tipo de matriz. Puede configurar esto con la opción child_index en fields_for.
f.fields_for :items, Item.new, child_index: "NEW_ITEM" # ...
Usualmente hago esto usando object_id
para asegurarme de que sea único en caso de que haya múltiples elementos nuevos.
item = Item.new
f.fields_for :items, item, child_index: item.object_id # ...
Aquí hay un método abstracto de ayuda que hace esto. Esto supone que hay un parcial con el nombre de item_fields
que representará.
def link_to_add_fields(name, f, association)
new_object = f.object.send(association).klass.new
id = new_object.object_id
fields = f.fields_for(association, new_object, child_index: id) do |builder|
render(association.to_s.singularize + "_fields", f: builder)
end
link_to(name, ''#'', class: "add_fields", data: {id: id, fields: fields.gsub("/n", "")})
end
Puedes usarlo así Los argumentos son: el nombre del enlace, el generador de formularios del padre y el nombre de la asociación en el modelo principal.
<%= link_to_add_fields "Add Item", f, :items %>
Y aquí hay algunos CoffeeScript para escuchar el evento click de ese enlace, insertar los campos y actualizar la id del objeto con la hora actual para darle una clave única.
jQuery ->
$(''form'').on ''click'', ''.add_fields'', (event) ->
time = new Date().getTime()
regexp = new RegExp($(this).data(''id''), ''g'')
$(this).before($(this).data(''fields'').replace(regexp, time))
event.preventDefault()
Ese código está tomado de este episodio de RailsCasts Pro que requiere una suscripción paga. Sin embargo, hay un ejemplo de trabajo completo disponible gratuitamente en GitHub .
Actualización: quiero señalar que insertar un marcador de posición child_index
no siempre es necesario. Si no desea usar JavaScript para insertar nuevos registros dinámicamente, puede compilarlos de antemano:
def new
@project = Project.new
3.times { @project.items.build }
end
<%= f.fields_for :items do |builder| %>
Rails insertará automáticamente un índice para los nuevos registros, por lo que debería funcionar.
Creo que puede hacer que funcione al incluir la identificación del registro como un campo oculto
Hay una gema llamada capullo para hacer esto, iría por un enfoque de bricolaje más ligero pero fue específicamente creado para este caso.
La solución común es agregar un marcador de posición en [] y reemplazarlo con un número único al insertar el fragmento en el formulario. Timestamp funciona la mayor parte del tiempo.
Me he topado con este caso de usuario en todos mis últimos proyectos, y espero que esto continúe, como señaló julian7, es necesario proporcionar una identificación única dentro de []. En mi opinión, esto se hace mejor a través de js. He estado arrastrando y mejorando un plugin jquery para hacer frente a estas situaciones. Funciona con registros existentes y para agregar nuevos registros, pero espera un cierto margen de beneficio y se degrada graciosamente, aquí está el código y un ejemplo:
https://gist.github.com/3096634
Advertencias para usar el plugin:
La llamada fields_for debe ser envuelta en un
<fieldset>
con un atributo de asociación de datos igual al nombre pluralizado del modelo, y una clase ''nested_models''.un objeto debe construirse en la vista justo antes de llamar a fields_for.
los campos del objeto perse deben estar envueltos en un
<fieldset>
con la clase "new", pero solo si el registro es nuevo (no recuerdo si eliminé este requisito).Debe existir una casilla de verificación para el atributo ''_destroy'' dentro de una etiqueta, el complemento usará el texto de la etiqueta para crear un enlace de destrucción.
Un enlace con la clase ''add_record'' debería existir dentro de fieldset.nested_models pero fuera del fieldset que encierra los campos del modelo.
Aparte de estas molestias, ha estado haciendo maravillas para mí.
Después de verificar la esencia, estos requisitos deben ser más claros. Por favor, avíseme si mejora el código o si lo usa :).
Por cierto, me inspiré en el primer screencast de modelos anidados de Ryan Bates.
Por lo tanto, no estaba contento con la solución que veía con más frecuencia, que era generar un pseudoíndice para nuevos elementos, ya sea en el servidor o en el lado del cliente JS. Esto se siente como un obstáculo, especialmente a la luz del hecho de que Rails / Rack es perfectamente capaz de analizar listas de elementos siempre que todos utilicen corchetes vacíos ( []
) como índice. Aquí hay una aproximación del código con el que terminé:
# note that this is NOT f.fields_for.
fields_for ''parent[children_attributes][]'', child, index: nil do |f|
f.label :name
f.text_field :name
# ...
end
Al finalizar el prefijo de nombre de campo con []
, junto con la opción index: nil
, se inhabilita la generación de índice de Rails que tan útilmente trata de proporcionar para los objetos persistentes. Este fragmento funciona tanto para objetos nuevos como guardados. Los parámetros de formulario resultantes, dado que utilizan sistemáticamente []
, se analizan en una matriz en los params
:
params[:parent][:children_attributes] # => [{"name" => "..."}, {...}]
El método Parent#children_attributes=
generado por accepts_nested_attributes_for :children
ocupa de esta matriz simplemente, actualizando los registros cambiados, agregando nuevos (los que carecen de una clave "id"
) y eliminando los que tienen el conjunto de claves "_destroy"
.
Todavía me molesta que Rails lo haga tan difícil, y que tuve que volver a una cadena de prefijo de nombre de campo codificado en lugar de utilizar, por ejemplo, f.fields_for :children, index: nil
. Para el registro, incluso haciendo lo siguiente:
f.fields_for :children, index: nil, child_index: nil do |f| ...
... no se puede desactivar la generación del índice de campo.
Estoy considerando escribir un parche de Rails para que sea más fácil, pero no sé si a suficientes personas les importa o si incluso sería aceptado.
EDITAR: El usuario @Macario me ha explicado por qué Rails prefiere índices explícitos en los nombres de campo: una vez que ingresas en tres capas de modelos anidados, debe haber una forma de discriminar a qué modelo de segundo nivel pertenece un atributo de tercer nivel.
Tal vez deberías hacer trampa. Coloque los nuevos registros en un atributo falso diferente que sea un decorador para el real.
parent[children_attributes][0][fieldname]
parent[new_children_attributes][][fieldname]
No es bonito, pero debería funcionar. Puede requerir un esfuerzo adicional para respaldar los viajes de ida y vuelta al formulario para errores de validación.
publicación larga eliminada
Ryan tiene un episodio sobre esto: http://railscasts.com/episodes/196-nested-model-form-revised
Parece que necesita generar el índice único de forma manual. Ryan usa el object_id
para esto.