Unauffällige dynamische Formularfelder in Rails mit jQuery

Lesezeit: 13 Minuten

Benutzer-Avatar
Adam Lassek

Ich versuche, die Hürde dynamischer Formularfelder in Rails zu überwinden – das scheint etwas zu sein, das das Framework nicht sehr elegant handhabt. Ich verwende auch jQuery in meinem Projekt. Ich habe jRails installiert, aber ich würde den AJAX-Code lieber möglichst unauffällig schreiben.

Meine Formen sind ziemlich komplex, zwei oder drei Verschachtelungsebenen sind keine Seltenheit. Das Problem, das ich habe, ist das Generieren der richtigen Formular-IDs, da sie so stark vom Kontext des Formularerstellers abhängen. Ich muss in der Lage sein, dynamisch neue Felder hinzuzufügen oder vorhandene Datensätze in einem zu löschen has_many Beziehung, und ich bin völlig ratlos.

Jedes Beispiel, das ich bisher gesehen habe, war auf die eine oder andere Weise hässlich. Ryan Bates’ Lernprogramm erfordert RJS, was zu einem ziemlich hässlichen, aufdringlichen Javascript im Markup führt und anscheinend vor verschachtelten Attributen geschrieben wurde. Ich habe eine gesehen Gabel dieses Beispiels mit unauffälligem jQuery, aber ich verstehe einfach nicht, was es tut, und konnte es nicht in meinem Projekt zum Laufen bringen.

Kann jemand ein einfaches Beispiel geben, wie das gemacht wird? Ist dies unter Einhaltung der RESTful-Konvention der Controller überhaupt möglich?


Andy hat ein hervorragendes Beispiel für das Löschen eines vorhandenen Datensatzes gepostet. Kann jemand ein Beispiel für das Erstellen neuer Felder mit den richtigen Attributen geben? Ich konnte nicht herausfinden, wie dies mit verschachtelten Formularen zu tun ist.

  • Haben Sie generell Probleme beim Schreiben von unauffälliger ajaxifizierter jQuery oder ist es spezifisch für verschachtelte Formulare? Wenn Sie unauffällig vorgehen möchten, ist es ein guter Anfang, jRails loszuwerden.

    – Andy Gaskell

    9. November 2009 um 23:09 Uhr

  • Ich muss in der Lage sein, verschachtelte Formulare zu unterstützen, aber ich versuche gerade, die Grundlagen zu verstehen. Ich habe jRails installiert, aber ich verwende es nicht; wie gesagt, ich will es unauffällig machen, und ich möchte lieber keine andere DSL lernen müssen, wenn ich javascript ganz gut kann.

    – Adam Lassek

    10. November 2009 um 0:10 Uhr

Benutzer-Avatar
Adam Lassek

Da niemand eine Antwort darauf angeboten hat, selbst nach einem Kopfgeld, habe ich es endlich geschafft, dies selbst zum Laufen zu bringen. Das sollte kein Stolperer werden! Hoffentlich wird dies in Rails 3.0 einfacher zu bewerkstelligen sein.

Andys Beispiel ist eine gute Möglichkeit, Datensätze direkt zu löschen, ohne ein Formular an den Server zu senden. In diesem speziellen Fall suche ich wirklich nach einer Möglichkeit, Felder dynamisch hinzuzufügen/zu entfernen, bevor eine Aktualisierung an einem verschachtelten Formular vorgenommen wird. Dies ist ein etwas anderer Fall, da die Felder beim Entfernen nicht wirklich gelöscht werden, bis das Formular gesendet wird. Ich werde wahrscheinlich am Ende beide verwenden, je nach Situation.

Ich habe meine Implementierung auf basiert Beispiele für komplexe Formen von Tim Riley Gabel auf github.

Richten Sie zuerst die Modelle ein und stellen Sie sicher, dass sie verschachtelte Attribute unterstützen:

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

Erstellen Sie eine Teilansicht für die Formularfelder der Telefonnummer:

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

Als nächstes schreiben Sie eine grundlegende Bearbeitungsansicht für das Personenmodell:

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

Dies funktioniert, indem wir eine Reihe von Vorlagenfeldern für das PhoneNumber-Modell erstellen, die wir mit Javascript duplizieren können. Wir erstellen Hilfsmethoden in app/helpers/application_helper.rb dafür:

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, "https://stackoverflow.com/questions/1704142/javascript:void(0)", :class => "add_child", :"data-association" => association)
end

def remove_child_link(name, f)
  f.hidden_field(:_destroy) + link_to(name, "https://stackoverflow.com/questions/1704142/javascript:void(0)", :class => "remove_child")
end

Fügen Sie nun diese Hilfsmethoden zum Bearbeitungsteil hinzu:

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

Sie haben jetzt die js-Vorlagen erstellt. Es wird eine leere Vorlage für jeden Verein eingereicht, aber die :reject_if -Klausel im Modell verwirft sie und lässt nur die vom Benutzer erstellten Felder übrig. Aktualisieren: Ich habe dieses Design überdacht, siehe unten.

Dies ist nicht wirklich AJAX, da über das Laden der Seite und das Absenden des Formulars hinaus keine Kommunikation mit dem Server stattfindet, aber ich konnte ehrlich gesagt keinen Weg finden, dies nachträglich zu tun.

Tatsächlich kann dies eine bessere Benutzererfahrung als AJAX bieten, da Sie nicht für jedes zusätzliche Feld auf eine Serverantwort warten müssen, bis Sie fertig sind.

Schließlich müssen wir dies mit Javascript verdrahten. Fügen Sie Folgendes zu Ihrer Datei „public/javascripts/application.js“ hinzu:

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

Zu diesem Zeitpunkt sollten Sie ein dynamisches Barebone-Formular haben! Das Javascript hier ist wirklich einfach und könnte leicht mit anderen Frameworks ausgeführt werden. Meinen könntest du problemlos ersetzen application.js Code mit Prototyp + Lowpro zum Beispiel. Die Grundidee ist, dass Sie keine gigantischen Javascript-Funktionen in Ihr Markup einbetten und nicht mühsam schreiben müssen phone_numbers=() Funktionen in Ihren Modellen. Alles funktioniert einfach. Hurra!


Nach einigen weiteren Tests bin ich zu dem Schluss gekommen, dass die Vorlagen aus dem verschoben werden müssen <form> Felder. Wenn Sie sie dort lassen, werden sie mit dem Rest des Formulars an den Server zurückgesendet, und das verursacht später nur Kopfschmerzen.

Ich habe dies am Ende meines Layouts hinzugefügt:

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

Und modifizierte die new_child_fields_template Helfer:

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

Jetzt können Sie die entfernen :reject_if Klauseln aus Ihren Modellen und machen Sie sich keine Sorgen mehr über die Rücksendung der Vorlagen.

  • Tolle Antwort. Das einzige, was ich optimiert habe, war, den Entfernungslink ein wenig zu verschieben, sodass ich die Felder mit .remove() anstelle von .hide() entfernen konnte. Auf diese Weise kann ich Gurke + Selen verwenden, um sicherzustellen, dass sie weg sind. Vielen Dank dafür!

    – Mat Schaffer

    7. Dezember 2010 um 17:09 Uhr

  • @matschaffer Ich habe dies tatsächlich geändert, um Moustache.js-Vorlagen zu verwenden, anstatt sie im DOM zu speichern. Wenn Ihr Front-End so ausgefeilt ist, würde ich Sie ermutigen, sich das anzusehen.

    – Adam Lassek

    7. Dezember 2010 um 18:53 Uhr

  • Dies ist eine Einstellung, sich Zeit zu nehmen, um zu antworten, damit der Rest Bescheid weiß, vielen Dank, Adam!

    – Ramón Araujo

    14. März 2012 um 6:43 Uhr

  • @Ramon leider ist das jetzt total veraltet :-/ Eines Tages muss ich es aktualisieren, um zu erklären, wie man Vorlagen verwendet.

    – Adam Lassek

    15. März 2012 um 17:28 Uhr

  • @DGM ja, so mache ich es jetzt.

    – Adam Lassek

    9. Oktober 2012 um 22:31 Uhr

Benutzer-Avatar
Andy Gaskel

Basierend auf Ihrer Antwort auf meinen Kommentar denke ich, dass ein unauffälliger Umgang mit dem Löschen ein guter Anfang ist. Ich werde Product with Scaffolding als Beispiel verwenden, aber der Code wird generisch sein, sodass er in Ihrer Anwendung einfach zu verwenden sein sollte.

Fügen Sie Ihrer Route zunächst eine neue Option hinzu:

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

Und jetzt fügen Sie Ihren Produktansichten eine Löschansicht hinzu:

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

Diese Ansicht wird nur Benutzern mit deaktiviertem JavaScript angezeigt.

Im Products-Controller müssen Sie die Löschaktion hinzufügen.

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

Gehen Sie nun zu Ihrer Indexansicht und ändern Sie den Destroy-Link in diesen:

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

Wenn Sie die App zu diesem Zeitpunkt ausführen und die Liste der Produkte anzeigen, können Sie ein Produkt löschen, aber wir können es für JavaScript-aktivierte Benutzer besser machen. Die dem Löschlink hinzugefügte Klasse wird in unserem JavaScript verwendet.

Dies wird ein ziemlich großer Teil von JavaScript sein, aber es ist wichtig, sich auf den Code zu konzentrieren, der sich mit dem Ajax-Aufruf befasst – dem Code im ajaxSend-Handler und dem „a.delete“-Click-Handler.

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

Hier ist das CSS, das Sie auch benötigen:

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

Wir müssen das Authentifizierungstoken mitsenden, wenn wir POST, PUT oder DELETE machen. Fügen Sie diese Zeile unter Ihrem vorhandenen JS-Tag hinzu (wahrscheinlich in Ihrem Layout):

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

Fast fertig. Öffnen Sie Ihren Anwendungscontroller und fügen Sie diese Filter hinzu:

before_filter :correct_safari_and_ie_accept_headers
after_filter :set_xhr_flash

Und die entsprechenden Methoden:

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

Wir müssen Flash-Nachrichten verwerfen, wenn es sich um einen Ajax-Aufruf handelt – andernfalls sehen Sie Flash-Nachrichten aus der “Vergangenheit” bei Ihrer nächsten regulären HTTP-Anfrage. Der zweite Filter ist auch für Webkit- und IE-Browser erforderlich – ich füge diese 2 Filter zu allen meinen Rails-Projekten hinzu.

Alles, was übrig bleibt, ist die Zerstörungsaktion:

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

Und da haben Sie es. Unauffälliges Löschen mit Rails. Es scheint eine Menge Arbeit zu sein, die alle abgetippt sind, aber es ist wirklich nicht so schlimm, wenn man erst einmal losgelegt hat. Das könnte Sie interessieren Railscast zu.

  • Das ist sehr hilfreich, danke. Könnten Sie ein Beispiel posten, wie Sie neue Formularfelder erstellen?

    – Adam Lassek

    10. November 2009 um 5:44 Uhr

  • Haben Sie eine ‘Lösch’-Route mit GET? Bedeutet das nicht Ärger?

    – ScottJ

    18. Januar 2010 um 17:22 Uhr

  • Sie können es nennen, wie Sie wollen (zum Beispiel heißt es in einer meiner Anwendungen ‘confirm_delete’), die GET-Anforderung führt kein tatsächliches Löschen durch. Der Punkt ist, dass Sie eine Ansicht haben, die das Löschformular enthält und den Benutzer auffordert, das Löschen zu bestätigen.

    – Andy Gaskell

    20. Januar 2010 um 15:57 Uhr

Übrigens. Rails hat sich ein wenig geändert, sodass Sie _delete nicht mehr verwenden können, verwenden Sie jetzt _destroy.

def remove_child_link(name, f)
  f.hidden_field(:_destroy) + link_to(name, "https://stackoverflow.com/questions/1704142/javascript:void(0)", :class => "remove_child")
end

Außerdem fand ich es einfacher, nur HTML zu entfernen, das für neue Datensätze ist … also mache ich das

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

  • Guter Fang, mit Blick auf meine Codebasis habe ich diese Änderung tatsächlich selbst vorgenommen, aber vergessen, den Beitrag zu aktualisieren.

    – Adam Lassek

    16. August 2010 um 22:34 Uhr

FYI, Ryan Bates hat jetzt ein Juwel, das wunderbar funktioniert: verschachteltes_formular

Ich habe ein unauffälliges jQuery-Plugin zum dynamischen Hinzufügen von fields_for-Objekten für Rails 3 erstellt. Es ist sehr einfach zu bedienen, laden Sie einfach die js-Datei herunter. Fast keine Konfiguration. Befolgen Sie einfach die Konventionen und Sie können loslegen.

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

Es ist nicht super flexibel, aber es wird die Arbeit erledigen.

  • Ich habe eine Frage zu Ihrem zahlreichen.js-Plugin. Ich poste es hier, weil es die einfachste Möglichkeit ist, es Ihnen zu signalisieren, und vielleicht stoßen andere auf dasselbe Problem und folgen dieser Frage. stackoverflow.com/q/13783927/439756

    – Schalalli

    9. Dezember 2012 um 1:52 Uhr


Benutzer-Avatar
robeedeedada

Ich habe erfolgreich (und ziemlich schmerzlos) verwendet https://github.com/nathanvda/cocoon um meine Formulare dynamisch zu generieren. Geht elegant mit Assoziationen um und die Dokumentation ist sehr einfach. Sie können es auch zusammen mit simple_form verwenden, was für mich besonders nützlich war.

  • Ich habe eine Frage zu Ihrem zahlreichen.js-Plugin. Ich poste es hier, weil es die einfachste Möglichkeit ist, es Ihnen zu signalisieren, und vielleicht stoßen andere auf dasselbe Problem und folgen dieser Frage. stackoverflow.com/q/13783927/439756

    – Schalalli

    9. Dezember 2012 um 1:52 Uhr


1245500cookie-checkUnauffällige dynamische Formularfelder in Rails mit jQuery

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy