2009-11-09 18 views
41

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 muy bien. También estoy usando jQuery en mi proyecto. Tengo jRails instalado, pero preferiría escribir el código AJAX discretamente siempre que sea posible.Campos de formularios dinámicos discretos en Rails con jQuery

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 dinámicamente nuevos campos 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 número tutorial de Ryan Bates requiere RJS, lo que da como resultado un javascript obsoleto bastante feo en el marcado, y parece haberse escrito antes de los atributos anidados. He visto un fork 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 la eliminación de un registro existente, ¿alguien puede dar un ejemplo de la creación de nuevos campos con los atributos correctos? No he podido averiguar cómo hacerlo con formularios anidados.

+0

¿Tiene problemas para escribir jQuery ajaxified no intrusivo en general o es específico para formularios anidados? Si quieres ser discreto, deshacerse de jRails es un buen comienzo. –

+0

Necesito poder admitir formularios anidados, pero estoy tratando de comprender lo básico en este momento. Tengo jRails instalado, pero no lo estoy usando; como dije, quiero hacerlo de forma discreta, y preferiría no tener que aprender otra DSL cuando soy perfectamente bueno en javascript. –

Respuesta

53

Dado que nadie ha ofrecido una respuesta a esto, incluso después de una recompensa, finalmente he logrado hacerlo funcionar yo mismo. ¡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 Tim Riley's complex-forms-examples fork en github.

Primero creó los modelos, y asegurarse de que son compatibles 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 

crear una vista parcial para campos de formulario del número de teléfono:

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

Siguiente escribir una vista básica de edición 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á mediante la creación de un conjunto de campos de plantilla para el modelo PhoneNumber que podemos duplicar con javascript. Vamos a crear métodos de ayuda en 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 añadir estos métodos de ayuda 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 hecho. Enviará una plantilla en blanco para cada asociación, pero la cláusula :reject_if en el modelo los descartará, dejando solo los campos creados por el usuario. Actualización:He repensado este diseño, consulte a continuación.

Esto no es realmente AJAX, ya que no hay ninguna comunicación de pasar al servidor más allá de la carga de la página y el formulario de envío, pero sinceramente no podría 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 tenemos que conectar esto con javascript. Añadir lo siguiente a su fichero `/ javascript/application.js públicos:

$(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; 
    }); 
}); 

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


Después de algunas pruebas más, he concluido que las plantillas necesitan ser movido 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 añadido esto a la parte inferior de mi diseño:

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

y modificó el new_child_fields_template ayudante:

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 retirar las cláusulas :reject_if a partir de modelos y dejar de preocuparse las plantillas que se envían de vuelta.

+1

Awesome answer. Lo único que modifiqué fue mover un poco el enlace de eliminación para poder eliminar los campos en lugar de .hide(). De esta forma puedo usar Cucumber + Selenium para asegurarme de que se hayan ido. ¡Muchas gracias por esto! –

+0

@matschaffer De hecho, he cambiado esto para usar plantillas de Mustache.js en lugar de almacenarlas en el DOM. Si su front-end es así de sofisticado, le recomiendo que investigue eso. –

+0

Esta es una actitud, tomarse el tiempo para responder por el resto, ¡muchas gracias, Adam! –

3

De acuerdo con su respuesta a mi comentario, creo que manejar el borrado de manera discreta 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.

Primera añadir una nueva opción a la ruta:

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

Y ahora añadir una vista de borrar para su producto ha sido visto:

<% 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 %> 

Este punto de vista sólo será visto por los usuarios con JavaScript desactivado.

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

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

Ahora ve a la vista de índice y cambiar el enlace Destruye a esto:

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

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

Esto será una porción 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á la CSS que necesitará 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 a lo largo del token de autenticación cuando hacemos 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 hecho. Abra su controlador de aplicaciones y agregar 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 

Debemos descartar los mensajes de flash si se trata de una llamada AJAX - de lo contrario se verá mensajes flash de la "pasado" en su 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 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 tienen. 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. Usted también podría estar interesado en este Railscast.

+0

Eso es muy útil, gracias. ¿Podría publicar un ejemplo de cómo crea nuevos campos de formulario? –

+1

¿Tiene una ruta 'eliminar' usando GET? ¿No es eso pedir problemas? – ScottJ

+0

Puedes llamarlo como quieras (por ejemplo, en una de mis aplicaciones se llama 'confirm_delete'), la solicitud GET no realiza una eliminación real. El punto es que tiene una vista que contiene el formulario de eliminación y le pide al usuario que confirme la eliminación. –

2

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 de quitar simplemente html que es para los nuevos registros ... así que este

$(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; 
    }); 
}); 
+0

Buena captura, mirando mi código base En realidad, hice este cambio yo mismo, pero olvidé actualizar la publicación. –

2

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

+0

¿Aún se mantiene esta joya? Quiero decir, ¿es seguro usarlo en realidad? Veo 'Latest commit 1b0689d el 1 de diciembre de 2013'. Si no, ¿alguna otra opción que no sea capullo? –

1

Creé un plugin jQuery discreto para agregar dinámicamente fields_for objects for 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 muy flexible, pero que va a hacer el trabajo.

+0

Tengo una pregunta relacionada con el complemento numeros.js. Lo publico aquí porque es la forma más fácil de señalizarlo para usted y quizás otros puedan encontrarse con el mismo problema y estar siguiendo esta pregunta. http://stackoverflow.com/q/13783927/439756 – shawalli

1

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í.

Cuestiones relacionadas