2009-11-09 11 views
41

Próbuję przejść przez przeszkodę dynamicznych pól formularzy w Railsach - to wydaje się być czymś, co framework nie radzi sobie z wdziękiem. Używam również jQuery w moim projekcie. Mam zainstalowane jRails, ale wolę pisać kod AJAX dyskretnie tam, gdzie to możliwe.Dyskretne dynamiczne pola formularzy w Railsach z jQuery

Moje formularze są dość złożone, dwa lub trzy poziomy zagnieżdżania nie są niczym niezwykłym. Problem, który mam, generuje poprawne identyfikatory formularzy, ponieważ są one zależne od kontekstu kreatora formularzy. Muszę być w stanie dynamicznie dodawać nowe pola lub usuwać istniejące rekordy w relacji has_many, a ja jestem kompletnie zagubiony.

Każdy przykład, jaki widziałem do tej pory, był brzydki w taki czy inny sposób. Ryan Bates 'tutorial wymaga RJS, co powoduje dość brzydki, natarczywy javascript w znacznikach i wydaje się, że został napisany przed zagnieżdżonymi atrybutami. Widziałem fork tego przykładu z dyskretnym jQuery, ale po prostu nie rozumiem, co robi i nie udało mi się go uruchomić w moim projekcie.

Czy ktoś może podać prosty przykład tego, jak to się robi? Czy jest to możliwe nawet przy respektowaniu restrykcyjnej konwencji kontrolerów?


Andy opublikował doskonały przykład usunięcia istniejącego rekordu, czy ktokolwiek może podać przykład tworzenia nowych pól z poprawnymi atrybutami? Nie byłem w stanie dowiedzieć się, jak to zrobić z zagnieżdżonymi formularzami.

+0

Czy masz problemy z pisaniem dyskretnie sferifikowanej jQuery ogólnie lub czy jest ona specyficzna dla zagnieżdżonych formularzy? Jeśli chcesz dyskretnie pozbyć się jRails, to dobry początek. –

+0

Potrzebuję obsługi form zagnieżdżonych, ale staram się teraz uchwycić podstawy. Mam zainstalowane jRails, ale go nie używam; tak jak powiedziałem, chcę to robić dyskretnie i wolałabym nie musieć się uczyć kolejnego DSL, gdy jestem doskonale dobry w javascript. –

Odpowiedz

53

Ponieważ nikt nie zaoferował odpowiedzi na to pytanie, nawet po bounty, w końcu udało mi się to zrobić samemu. To nie powinno być oszałamiające! Mam nadzieję, że łatwiej będzie to zrobić w Rails 3.0.

Przykład Andy'ego jest dobrym sposobem na usunięcie rekordów bezpośrednio, bez przesyłania formularza do serwera. W tym konkretnym przypadku, czego tak naprawdę szukam, jest sposób dynamicznego dodawania/usuwania pól przed wykonaniem aktualizacji do zagnieżdżonego formularza. Jest to nieco inny przypadek, ponieważ po usunięciu pól nie są one faktycznie usuwane, dopóki formularz nie zostanie przesłany. Prawdopodobnie wykorzystam oba w zależności od sytuacji.

Oparłem moją implementację na widelcu Tim Riley's complex-forms-examples na github.

najpierw skonfigurować modele, i upewnić się, że wspiera zagnieżdżone atrybuty:

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 

stworzyć częściowy widok na pola formularza PhoneNumber za:

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

Następny podstawowego widoku edycji za Osoba model:

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

To będzie działać, tworząc zestaw pól szablonu dla modelu PhoneNumber, który możemy powielić z javascript. Będziemy tworzyć metody pomocnika w app/helpers/application_helper.rb na to:

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 

teraz dodać te metody pomocnika do edycji częściowej:

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

masz teraz szablonów js zrobić. Przekaże pusty szablon dla każdego powiązania, ale klauzula :reject_if w modelu odrzuci je, pozostawiając tylko pola utworzone przez użytkownika. Aktualizacja:Ponownie przemyślałem ten projekt, patrz poniżej.

To naprawdę nie jest AJAX, ponieważ nie ma żadnej komunikacji na serwerze poza ładowaniem strony i wysyłania formularza, ale szczerze nie mogłem znaleźć sposobu, aby to zrobić po fakcie.

W rzeczywistości może to zapewnić lepsze wrażenia użytkownika niż AJAX, ponieważ nie musisz czekać na odpowiedź serwera dla każdego dodatkowego pola, dopóki nie skończysz.

Wreszcie musimy połączyć to z javascript. Dodaj poniższe linie do pliku `/ 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; 
    }); 
}); 

Do tego czasu trzeba mieć kadłubków formę dynamiczną! Tutaj javascript jest naprawdę prosty i można go łatwo wykonać za pomocą innych frameworków. Możesz łatwo zastąpić mój kod application.js prototypem + lowpro na przykład. Podstawową ideą jest to, że nie wbudowujesz gigantycznych funkcji javascript w swój znacznik i nie musisz pisać żmudnych phone_numbers=() funkcji w swoich modelach. Wszystko po prostu działa. Brawo!


Po jakimś dalszym badaniom, mam stwierdziła, że ​​szablony muszą być przeniesione z pola <form>. Utrzymanie ich w tym miejscu oznacza, że ​​zostaną odesłani do serwera wraz z resztą formularza, co może później wywołać bóle głowy.

Dodałem to do głębi układ:

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

i zmodyfikowała new_child_fields_template pomocnika:

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 

Teraz można usunąć klauzule :reject_if z modeli i przestać się martwić szablony są odesłane.

+1

Niesamowita odpowiedź. Jedyną rzeczą, którą poprawiłem, było przesunięcie łącza usuwania tak, żebym mógł .remove() pola raczej niż .hide(). W ten sposób mogę użyć Cucumber + Selenium, aby upewnić się, że ich nie ma. Dziękuję bardzo za to! –

+0

@matschaffer Zmieniliśmy to tak, aby używać szablonów Mustache.js zamiast przechowywać je w DOM. Jeśli twój front-end jest tak wyrafinowany, zachęcam do tego. –

+0

To jest postawa, aby znaleźć czas na odpowiedź dla reszty, dzięki Adamowi! –

3

Na podstawie Twojej odpowiedzi na mój komentarz uważam, że usuwanie dyskretnie jest dobrym miejscem do rozpoczęcia. Użyję produktu z rusztowaniem jako przykładu, ale kod będzie ogólny, więc powinien być łatwy w użyciu w twojej aplikacji.

Pierwszy dodać nową opcję do trasy:

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

I teraz dodać Usuń widok do swoich poglądów produktu:

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

Ten widok będzie można zobaczyć tylko przez użytkowników z włączoną obsługę Javascript.

W kontrolerze produktów musisz dodać akcję usuwania.

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

Teraz przejdź do widoku indeksu i zmienić link Destroy do tego:

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

Po uruchomieniu aplikacji w tym momencie i przejrzeć listę produktów będziesz w stanie usunąć produkt, ale możemy zrobić lepiej dla użytkowników obsługujących JavaScript. Klasa dodana do linku do usuwania będzie używana w naszym JavaScript.

To będzie dość duży fragment JavaScript, ale ważne jest, aby skupić się na kodzie, który zajmuje się wywoływaniem ajax'a - kodu w procedurze obsługi ajaxSend i obsłudze kliknięcia "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); 
}; 

Oto CSS musisz też:

.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; 
} 

Musimy wysłać wraz tokenu auth gdy robimy POST, PUT lub DELETE.Dodaj tę linię pod istniejącym tagu JS (prawdopodobnie w układzie):

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

prawie gotowe. Otwórz swój kontroler aplikację i dodać te filtry:

before_filter :correct_safari_and_ie_accept_headers 
after_filter :set_xhr_flash 

i odpowiadające im metody:

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 

Musimy odrzucić wiadomości flash, jeśli jest to wywołanie ajax - w przeciwnym razie zobaczysz wiadomości flash z "przeszłość" na kolejnej zwykłej prośbie http. Drugi filtr jest również wymagany w przypadku przeglądarek Webkit i IE - dodaję te 2 filtry do wszystkich moich projektów Rails.

Pozostaje jest akcja niszczenia:

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 

I tam masz. Dyskretne usuwanie za pomocą Railsów. Wygląda na to, że wszystkie prace zostały spisane, ale nie jest tak źle, gdy już zaczniesz. Możesz być także zainteresowany tym Railscast.

+0

To bardzo pomocne, dziękuję. Czy możesz podać przykład tworzenia nowych pól formularza? –

+1

Masz "usuwać" trasę używając GET? Czy to nie oznacza kłopotów? – ScottJ

+0

Możesz nazwać to, co chcesz (na przykład w jednej z moich aplikacji nazywa się "confirm_delete"), żądanie GET nie powoduje faktycznego usunięcia. Chodzi o to, że masz widok zawierający formularz kasowania i prosi użytkownika o potwierdzenie usunięcia. –

2

Przy okazji. poręcze nieco się zmieniły, więc nie możesz już używać _delete, teraz używaj _destroy.

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

Również łatwiej było po prostu usunąć kod HTML, który jest dla nowych rekordów ... więc mogę to zrobić

$(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

Dobra haczyka, patrząc na moją bazę kodową Sam wprowadziłem tę zmianę, ale zapomniałem zaktualizować wpis. –

2

FYI, Ryan Bates teraz ma klejnot, który działa pięknie: nested_form

+0

Czy ten klejnot jest nadal konserwowany? Chodzi mi o to, czy właściwie można go używać? Widzę "Ostatnie zatwierdzenie 1b0689d z 1 grudnia 2013". Jeśli nie, to jakąkolwiek inną opcję niż kokon? –

1

Stworzyłem dyskretną wtyczkę jQuery do dynamicznego dodawania fields_for obiektów dla Rails 3. Jest bardzo łatwy w użyciu, wystarczy pobrać plik js. Prawie bez konfiguracji. Po prostu postępuj zgodnie z konwencjami i dobrze jest jechać.

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

To nie jest super elastyczny, ale to będzie wykonać zadanie.

+0

Mam pytanie związane z wtyczką numerous.js. Zamieszczam go tutaj, ponieważ jest to najprostszy sposób, aby zasygnalizować to użytkownikowi, a być może inni mogą napotkać ten sam problem i podążać za tym pytaniem. http://stackoverflow.com/q/13783927/439756 – shawalli

1

Udało mi się (i raczej bezboleśnie) użyć https://github.com/nathanvda/cocoon do dynamicznego generowania moich formularzy. Dobrze radzi sobie z powiązaniami, a dokumentacja jest bardzo prosta. Możesz go używać razem z simple_form, co było szczególnie przydatne dla mnie.

Powiązane problemy