41 votes

Discrète dynamique des champs de formulaire dans les Rails avec jQuery

Je suis d'essayer d'obtenir plus de l'obstacle de la dynamique des champs de formulaire dans les Rails -- cela semble être quelque chose que le cadre ne gère pas très gracieusement. Je suis aussi à l'aide de jQuery dans mon projet. J'ai jRails installé, mais je préfère écrire le code AJAX discrètement si possible.

Mes formes sont assez complexes, deux ou trois niveaux d'imbrication ne sont pas inhabituelles. Le problème que je vais avoir est la génération de la forme correcte de l'ids, car ils sont si la personne à charge sur le générateur de formulaire contexte. J'ai besoin d'être en mesure d'ajouter dynamiquement de nouveaux champs ou de supprimer des enregistrements existants dans une has_many relation, et je suis complètement à perte.

Tous les exemples que j'ai vu jusqu'à présent a été laid, d'une manière ou d'une autre. Ryan Bates tutoriel nécessite RJS, ce qui entraîne dans certains assez laid discret javascript dans la balise, et semble avoir été écrit avant imbriquée attributs. J'ai vu une fourche de cet exemple avec discrète jQuery, mais je ne comprends pas ce qu'il fait, et n'ont pas été en mesure de le faire fonctionner dans mon projet.

Quelqu'un peut-il fournir un exemple simple de comment c'est fait? Est-ce même possible tout en respectant le repos de la convention des contrôleurs?


Andy a publié un excellent exemple de la suppression d'un enregistrement existant, quelqu'un peut-il fournir un exemple de création de nouveaux champs avec les attributs corrects? Je n'ai pas été en mesure de comprendre comment faire cela avec des formulaires imbriqués.

53voto

Adam Lassek Points 18918

Puisque personne n'a offert une réponse, même après un bounty, j'ai enfin réussi à obtenir ce travail moi-même. Ce n'était pas censé être une stumper! J'espère que ce sera plus facile de le faire dans les Rails 3.0.

Andy exemple est un bon moyen de supprimer des enregistrements directement, sans l'envoi d'un formulaire vers le serveur. Dans ce cas particulier, ce que je suis vraiment à la recherche d'une façon dynamique d'ajouter/supprimer des champs avant de faire une mise à jour d'un formulaire imbriqué. C'est un cas un peu différent, parce que les champs sont supprimés, ils ne sont pas réellement supprimés jusqu'à ce que le formulaire est soumis. Je vais probablement jusqu'à la fin en utilisant à la fois en fonction de la situation.

J'ai basé mon application sur Tim Riley, complexe-formes-exemples fork sur github.

D'abord configurer les modèles, et assurez-vous qu'ils soutiennent imbriqués les attributs:

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

Créer une vue partielle pour le Numéro de champs de formulaire:

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

Prochaine écriture de base modifier la vue de la Personne modèle:

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

Ce sera le travail par la création d'un ensemble de champs de modèle pour le Numéro de modèle que nous pouvons reproduire avec javascript. Nous allons créer des méthodes d'assistance en app/helpers/application_helper.rb pour ce:

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

Maintenant, ajoutez ces méthodes d'aide à la modification partielle:

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

Vous avez maintenant la js templating fait. Il présentera un modèle vierge pour chaque association, mais l' :reject_if clause dans le modèle seront les ignorer, ne laissant que l'utilisateur créé des champs. Mise à jour: j'ai repensé à cette conception, voir ci-dessous.

Ce n'est pas vraiment AJAX, car il n'y a pas de communication d'aller sur le serveur-delà de la page de chargement et de soumettre le formulaire, mais honnêtement, je ne pouvais pas trouver un moyen de le faire après le fait.

En fait, cela peut fournir une meilleure expérience utilisateur que l'AJAX, puisque vous n'avez pas à attendre une réponse du serveur pour chaque champ supplémentaire jusqu'à ce que vous avez terminé.

Enfin, nous avons besoin de la relier avec javascript. Ajoutez les lignes suivantes à votre " public/javascripts/application.js fichier de:

$(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 ce moment, vous devriez avoir un barebones forme dynamique! Le javascript ici est vraiment simple, et pourrait facilement être fait avec les autres cadres. Vous pouvez facilement remplacer mon application.js code du prototype + lowpro par exemple. L'idée de base est que vous n'êtes pas l'incorporation de gigantesques fonctions javascript dans votre balisage, et vous n'avez pas à écrire fastidieux phone_numbers=() fonctions dans vos modèles. Tout fonctionne, tout simplement. Hourra!


Après quelques tests, j'en ai conclu que les modèles doivent être déplacés hors de l' <form> champs. Les y maintenir signifie qu'ils sont envoyés au serveur avec le reste de la forme, et cela crée des maux de tête plus tard.

J'ai ajouté cette à la fond de ma présentation:

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

Et modifié le new_child_fields_template helper:

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

Maintenant, vous pouvez supprimer l' :reject_if clauses de vos modèles et de cesser de s'inquiéter sur les modèles d'être envoyé.

3voto

Andy Gaskell Points 15264

En fonction de votre réponse à mon commentaire, je pense que la manipulation de la suppression discrètement est un bon endroit pour commencer. Je vais utiliser le Produit avec de l'échafaudage comme un exemple, mais le code sera générique, donc ça devrait être facile à utiliser dans votre application.

Tout d'abord ajouter une nouvelle option pour votre itinéraire:

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

Et maintenant, ajoutez un supprimer la vue de votre Produit points de vue:

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

Ce point de vue ne sera visible que par les utilisateurs ayant désactivé JavaScript.

Dans les Produits de contrôleur, vous aurez besoin d'ajouter l'action de suppression.

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

Allez maintenant à votre indice de visualiser et de modifier les Détruire lien vers ce:

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

Si vous exécutez l'application, à ce stade, et voir la liste des produits que vous serez en mesure de supprimer un produit, mais nous pouvons faire mieux pour le JavaScript utilisateurs. La classe ajoutée à la suppression de lien sera utilisée dans notre code JavaScript.

Ce sera un assez grand morceau de JavaScript, mais il est important de se concentrer sur le code qui traite de la prise de l'appel ajax - le code de la ajaxSend gestionnaire et le " une.supprimer " gestionnaire de clic.

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

Voici le CSS que vous aurez besoin de:

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

Nous avons besoin d'envoyer le long de la auth jeton lorsque nous faisons POST, PUT ou DELETE. Ajoutez cette ligne dans votre JS tag (probablement dans votre mise en page):

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

Presque terminé. Ouvrez votre Application de contrôleur et d'ajouter ces filtres:

before_filter :correct_safari_and_ie_accept_headers
after_filter :set_xhr_flash

Et les méthodes correspondantes:

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

Nous avons besoin de jeter des messages flash si c'est un appel ajax - sinon, vous allez voir des messages flash de le "passé" sur votre prochaine requête http. Le second filtre est également nécessaire pour webkit et IE web - je ajouter ces 2 filtres à tous mes Rails de projets.

Tout ce qui reste est la détruire de l'action:

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

Et là vous l'avez. Discrète, la suppression de Rails. Il semble que beaucoup de travail tout tapé, mais c'est vraiment pas mal une fois que vous aller de l'avant. Vous pourriez être intéressé par ce Railscast trop.

2voto

David Henner Points 21

Par la voie. rails a un peu changé de sorte que vous pouvez ne plus utiliser _delete, maintenant utiliser _destroy.

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

Aussi je l'ai trouvé plus facile de supprimer du code html de nouveaux records... donc ce que je fais

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

2voto

Garrett Lancaster Points 145

Pour info, Ryan Bates maintenant un a une gemme qui fonctionne à merveille: nested_form

1voto

Karl Paragua Points 11

J'ai créé une discrète plugin jQuery pour ajouter dynamiquement fields_for objets pour Rails 3. Il est très facile à utiliser, il suffit de télécharger le fichier js. Presque aucune configuration. Il suffit de suivre les conventions et vous êtes bon pour aller.

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

Il n'est pas super souple, mais il va faire le travail.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X