with type tag rails couldn change application jquery ruby-on-rails ajax forms

type - rails jquery ajax



Campos de forma dinĂ¡mica discretos en Rails con jQuery (6)

Estoy tratando de superar el obstáculo de los campos de formas dinámicas en Rails; esto parece ser algo que el framework no maneja con mucha gracia. También estoy usando jQuery en mi proyecto. Tengo jRails instalado, pero prefiero escribir el código AJAX discretamente donde sea posible.

Mis formularios son bastante complejos, dos o tres niveles de anidación no son inusuales. El problema que estoy teniendo es generar los identificadores de formulario correctos, ya que dependen mucho del contexto del generador de formularios. Necesito poder agregar nuevos campos de forma dinámica o eliminar registros existentes en una relación has_many , y estoy completamente perdido.

Cada ejemplo que he visto hasta ahora ha sido feo de una forma u otra. El tutorial Ryan Bates requiere RJS, lo que da como resultado un javascript obsoleto bastante feo en el marcado, y parece haber sido escrito antes de los atributos anidados. He visto un ejemplo de ese ejemplo con jQuery discreto, pero simplemente no entiendo lo que está haciendo y no he podido hacerlo funcionar en mi proyecto.

¿Alguien puede dar un ejemplo simple de cómo se hace esto? ¿Esto es posible incluso respetando la convención RESTful de los controladores?

Andy ha publicado un excelente ejemplo de eliminación de un registro existente. ¿Alguien puede dar un ejemplo de cómo crear nuevos campos con los atributos correctos? No he podido averiguar cómo hacerlo con formularios anidados.


Creé un plugin jQuery discreto para agregar campos de manera dinámica_para objetos para Rails 3. Es muy fácil de usar, solo descargue el archivo js. Casi ninguna configuración. Simplemente sigue las convenciones y estarás listo.

https://github.com/kbparagua/numerous.js

No es súper flexible pero hará el trabajo.


Dado que nadie ha ofrecido una respuesta a esto, incluso después de una recompensa, finalmente he logrado hacer que esto funcione. ¡Esto no se suponía que fuera un stumper! Esperemos que esto sea más fácil de hacer en Rails 3.0.

El ejemplo de Andy es una buena manera de eliminar registros directamente, sin enviar un formulario al servidor. En este caso particular, lo que realmente estoy buscando es una forma de agregar / eliminar campos dinámicamente antes de hacer una actualización a un formulario anidado. Este es un caso ligeramente diferente, porque a medida que se eliminan los campos, en realidad no se eliminan hasta que se envía el formulario. Probablemente terminaré usando ambos dependiendo de la situación.

He basado mi implementación en fork -forms-examples de Tim Riley en github.

Primero configure los modelos y asegúrese de que sean compatibles con los atributos anidados:

class Person < ActiveRecord::Base has_many :phone_numbers, :dependent => :destroy accepts_nested_attributes_for :phone_numbers, :reject_if => lambda { |p| p.values.all?(&:blank?) }, :allow_destroy => true end class PhoneNumber < ActiveRecord::Base belongs_to :person end

Cree una vista parcial para los campos del formulario PhoneNumber:

<div class="fields"> <%= f.text_field :description %> <%= f.text_field :number %> </div>

A continuación, escriba una vista de edición básica para el modelo de persona:

<% form_for @person, :builder => LabeledFormBuilder do |f| -%> <%= f.text_field :name %> <%= f.text_field :email %> <% f.fields_for :phone_numbers do |ph| -%> <%= render :partial => ''phone_number'', :locals => { :f => ph } %> <% end -%> <%= f.submit "Save" %> <% end -%>

Esto funcionará al crear un conjunto de campos de plantilla para el modelo PhoneNumber que podemos duplicar con javascript. Crearemos métodos de ayuda en la app/helpers/application_helper.rb para esto:

def new_child_fields_template(form_builder, association, options = {}) options[:object] ||= form_builder.object.class.reflect_on_association(association).klass.new options[:partial] ||= association.to_s.singularize options[:form_builder_local] ||= :f content_tag(:div, :id => "#{association}_fields_template", :style => "display: none") do form_builder.fields_for(association, options[:object], :child_index => "new_#{association}") do |f| render(:partial => options[:partial], :locals => { options[:form_builder_local] => f }) end end end def add_child_link(name, association) link_to(name, "javascript:void(0)", :class => "add_child", :"data-association" => association) end def remove_child_link(name, f) f.hidden_field(:_destroy) + link_to(name, "javascript:void(0)", :class => "remove_child") end

Ahora agregue estos métodos auxiliares a la edición parcial:

<% form_for @person, :builder => LabeledFormBuilder do |f| -%> <%= f.text_field :name %> <%= f.text_field :email %> <% f.fields_for :phone_numbers do |ph| -%> <%= render :partial => ''phone_number'', :locals => { :f => ph } %> <% end -%> <p><%= add_child_link "New Phone Number", :phone_numbers %></p> <%= new_child_fields_template f, :phone_numbers %> <%= f.submit "Save" %> <% end -%>

Ahora tiene la plantilla js hecha. :reject_if una plantilla en blanco para cada asociación, pero la cláusula :reject_if del modelo los descartará, dejando solo los campos creados por el usuario. Actualización: he repensado este diseño, mira a continuación.

Esto no es realmente AJAX, ya que no hay ninguna comunicación en el servidor más allá de la carga de la página y el formulario de envío, pero honestamente no pude encontrar una manera de hacerlo después del hecho.

De hecho, esto puede proporcionar una mejor experiencia de usuario que AJAX, ya que no tiene que esperar una respuesta del servidor para cada campo adicional hasta que haya terminado.

Finalmente, necesitamos conectar esto con javascript. Agregue lo siguiente a su archivo `public / javascripts / application.js '':

$(function() { $(''form a.add_child'').click(function() { var association = $(this).attr(''data-association''); var template = $(''#'' + association + ''_fields_template'').html(); var regexp = new RegExp(''new_'' + association, ''g''); var new_id = new Date().getTime(); $(this).parent().before(template.replace(regexp, new_id)); return false; }); $(''form a.remove_child'').live(''click'', function() { var hidden_field = $(this).prev(''input[type=hidden]'')[0]; if(hidden_field) { hidden_field.value = ''1''; } $(this).parents(''.fields'').hide(); return false; }); });

¡Para este momento deberías tener una forma dinámica barebones! El javascript aquí es realmente simple, y podría hacerse fácilmente con otros marcos. Podría reemplazar fácilmente mi código application.js con prototype + lowpro por ejemplo. La idea básica es que no está incorporando funciones javascript gigantescas en su marcado, y no tiene que escribir tediosas phone_numbers=() en sus modelos. Todo funciona. ¡Hurra!

Después de algunas pruebas adicionales, he llegado a la conclusión de que las plantillas deben moverse fuera de los campos <form> . Mantenerlos allí significa que se envían de vuelta al servidor con el resto del formulario, y eso simplemente genera dolores de cabeza más adelante.

He agregado esto al final de mi diseño:

<div id="jstemplates"> <%= yield :jstemplates %> </div

Y modificó el helper new_child_fields_template :

def new_child_fields_template(form_builder, association, options = {}) options[:object] ||= form_builder.object.class.reflect_on_association(association).klass.new options[:partial] ||= association.to_s.singularize options[:form_builder_local] ||= :f content_for :jstemplates do content_tag(:div, :id => "#{association}_fields_template", :style => "display: none") do form_builder.fields_for(association, options[:object], :child_index => "new_#{association}") do |f| render(:partial => options[:partial], :locals => { options[:form_builder_local] => f }) end end end end

Ahora puede eliminar las cláusulas :reject_if de sus modelos y dejar de preocuparse por las plantillas que se envían de vuelta.


FYI, Ryan Bates ahora tiene una joya que funciona maravillosamente: nested_form


He utilizado con éxito (y sin dolor) https://github.com/nathanvda/cocoon para generar dinámicamente mis formularios. Maneja las asociaciones de manera elegante y la documentación es muy sencilla. Puedes usarlo junto con simple_form, también, lo cual fue particularmente útil para mí.


Por cierto. rails ha cambiado un poco por lo que ya no puede usar _delete, ahora use _destroy.

def remove_child_link(name, f) f.hidden_field(:_destroy) + link_to(name, "javascript:void(0)", :class => "remove_child") end

También me pareció más fácil eliminar html que es para nuevos registros ... así que hago esto

$(function() { $(''form a.add_child'').click(function() { var association = $(this).attr(''data-association''); var template = $(''#'' + association + ''_fields_template'').html(); var regexp = new RegExp(''new_'' + association, ''g''); var new_id = new Date().getTime(); $(this).parent().before(template.replace(regexp, new_id)); return false; }); $(''form a.remove_child'').live(''click'', function() { var hidden_field = $(this).prev(''input[type=hidden]'')[0]; if(hidden_field) { hidden_field.value = ''1''; } $(this).parents(''.new_fields'').remove(); $(this).parents(''.fields'').hide(); return false; }); });


Según su respuesta a mi comentario, creo que manejar la eliminación discretamente es un buen lugar para comenzar. Usaré Producto con andamios como ejemplo, pero el código será genérico, por lo que debería ser fácil de usar en su aplicación.

Primero agrega una nueva opción a tu ruta:

map.resources :products, :member => { :delete => :get }

Y ahora agregue una vista de eliminación a sus vistas de Producto:

<% title "Delete Product" %> <% form_for @product, :html => { :method => :delete } do |f| %> <h2>Are you sure you want to delete this Product?</h2> <p> <%= submit_tag "Delete" %> or <%= link_to "cancel", products_path %> </p> <% end %>

Esta vista solo será vista por los usuarios con JavaScript deshabilitado.

En el controlador de Productos, deberá agregar la acción de eliminación.

def delete Product.find(params[:id]) end

Ahora vaya a su vista de índice y cambie el enlace Destruir a esto:

<td><%= link_to "Delete", delete_product_path(product), :class => ''delete'' %></td>

Si ejecuta la aplicación en este punto y visualiza la lista de productos, podrá eliminar un producto, pero podemos hacerlo mejor para los usuarios habilitados para JavaScript. La clase agregada al enlace de eliminación se usará en nuestro JavaScript.

Este será un fragmento bastante grande de JavaScript, pero es importante centrarse en el código que trata de hacer la llamada ajax: el código en el manejador ajaxSend y el manejador de clic ''a.delete''.

(function() { var originalRemoveMethod = jQuery.fn.remove; jQuery.fn.remove = function() { if(this.hasClass("infomenu") || this.hasClass("pop")) { $(".selected").removeClass("selected"); } originalRemoveMethod.apply(this, arguments); } })(); function isPost(requestType) { return requestType.toLowerCase() == ''post''; } $(document).ajaxSend(function(event, xhr, settings) { if (isPost(settings.type)) { settings.data = (settings.data ? settings.data + "&" : "") + "authenticity_token=" + encodeURIComponent( AUTH_TOKEN ); } xhr.setRequestHeader("Accept", "text/javascript, application/javascript"); }); function closePop(fn) { var arglength = arguments.length; if($(".pop").length == 0) { return false; } $(".pop").slideFadeToggle(function() { if(arglength) { fn.call(); } $(this).remove(); }); return true; } $(''a.delete'').live(''click'', function(event) { if(event.button != 0) { return true; } var link = $(this); link.addClass("selected").parent().append("<div class=''pop delpop''><p>Are you sure?</p><p><input type=''button'' value=''Yes'' /> or <a href=''#'' class=''close''>Cancel</a></div>"); $(".delpop").slideFadeToggle(); $(".delpop input").click(function() { $(".pop").slideFadeToggle(function() { $.post(link.attr(''href'').substring(0, link.attr(''href'').indexOf(''/delete'')), { _method: "delete" }, function(response) { link.parents("tr").fadeOut(function() { $(this).remove(); }); }); $(this).remove(); }); }); return false; }); $(".close").live(''click'', function() { return !closePop(); }); $.fn.slideFadeToggle = function(easing, callback) { return this.animate({opacity: ''toggle'', height: ''toggle''}, "fast", easing, callback); };

Aquí está el CSS que necesitarás también:

.pop { background-color:#FFFFFF; border:1px solid #999999; cursor:default; display: none; position:absolute; text-align:left; z-index:500; padding: 25px 25px 20px; margin: 0; -webkit-border-radius: 8px; -moz-border-radius: 8px; } a.selected { background-color:#1F75CC; color:white; z-index:100; }

Necesitamos enviar el token de autenticación cuando hagamos POST, PUT o DELETE. Agregue esta línea debajo de su etiqueta JS existente (probablemente en su diseño):

<%= javascript_tag "var AUTH_TOKEN = #{form_authenticity_token.inspect};" if protect_against_forgery? -%>

Casi termino. Abra su controlador de aplicación y agregue estos filtros:

before_filter :correct_safari_and_ie_accept_headers after_filter :set_xhr_flash

Y los métodos correspondientes:

protected def set_xhr_flash flash.discard if request.xhr? end def correct_safari_and_ie_accept_headers ajax_request_types = [''text/javascript'', ''application/json'', ''text/xml''] request.accepts.sort!{ |x, y| ajax_request_types.include?(y.to_s) ? 1 : -1 } if request.xhr? end

Necesitamos descartar los mensajes flash si se trata de una llamada ajax; de lo contrario, verás mensajes flash del "pasado" en tu próxima solicitud HTTP regular. El segundo filtro también es necesario para los navegadores webkit e IE: agrego estos 2 filtros a todos mis proyectos de Rails.

Todo lo que queda es la acción de destruir:

def destroy @product.destroy flash[:notice] = "Successfully destroyed product." respond_to do |format| format.html { redirect_to redirect_to products_url } format.js { render :nothing => true } end end

Y ahí lo tienes. Eliminación discreta con Rails. Parece que todo el trabajo se tiró a máquina, pero en realidad no es tan malo una vez que empiezas. También te puede interesar este Railscast .