Ненавязчивые динамические поля формы в Rails с помощью jQuery - PullRequest
41 голосов
/ 10 ноября 2009

Я пытаюсь преодолеть барьер динамических полей формы в Rails - кажется, что фреймворк не справляется с этим изящно. Я также использую jQuery в своем проекте. У меня установлены jRails, но я бы предпочел писать код AJAX незаметно, где это возможно.

Мои формы довольно сложны, два или три уровня вложенности не являются необычными. Проблема в том, что я генерирую правильные идентификаторы форм, так как они очень зависят от контекста конструктора форм. Мне нужно иметь возможность динамически добавлять новые поля или удалять существующие записи в отношениях has_many, и я полностью в растерянности.

Каждый пример, который я видел до сих пор, был так или иначе безобразен. учебник Райана Бейтса требует RJS, что приводит к некоторому довольно уродливому навязчивому javascript в разметке и, кажется, было написано перед вложенными атрибутами. Я видел fork этого примера с ненавязчивым jQuery, но я просто не понимаю, что он делает, и не смог заставить его работать в моем проекте.

Может ли кто-нибудь привести простой пример того, как это делается? Возможно ли это даже при соблюдении RESTful-соглашения контроллеров?


Энди опубликовал отличный пример удаления существующей записи, кто-нибудь может привести пример создания новых полей с правильными атрибутами? Я не смог понять, как это сделать с помощью вложенных форм.

Ответы [ 6 ]

53 голосов
/ 18 ноября 2009

Поскольку никто не предложил ответа на этот вопрос, даже после щедрости, мне наконец-то удалось заставить это работать самостоятельно. Это не должно было быть глупее! Надеюсь, это будет проще сделать в Rails 3.0.

Пример Энди - это хороший способ удаления записей напрямую, без отправки формы на сервер. В данном конкретном случае мне действительно нужен способ динамического добавления / удаления полей перед обновлением вложенной формы. Это немного другой случай, потому что, поскольку поля удаляются, они фактически не удаляются, пока форма не будет отправлена. Я, вероятно, в конечном итоге буду использовать оба в зависимости от ситуации.

Моя реализация основана на примерах сложных форм Тима Райли fork на github.

Сначала настройте модели и убедитесь, что они поддерживают вложенные атрибуты:

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

Создание частичного представления для полей формы PhoneNumber:

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

Далее напишите базовый вид редактирования для модели Person:

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

Это будет работать путем создания набора полей шаблона для модели PhoneNumber, которые мы можем дублировать с помощью javascript. Мы создадим вспомогательные методы в app/helpers/application_helper.rb для этого:

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

Теперь добавьте эти вспомогательные методы в часть редактирования:

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

Теперь у вас есть готовые шаблоны js. Он будет представлять пустой шаблон для каждой ассоциации, но предложение :reject_if в модели будет отбрасывать их, оставляя только созданные пользователем поля. Обновление: Я переосмыслил этот дизайн, см. Ниже.

Это на самом деле не AJAX, поскольку на сервере не происходит никакой связи, кроме загрузки страницы и отправки формы, но я, честно говоря, не смог найти способ сделать это после этого.

На самом деле это может обеспечить лучшее взаимодействие с пользователем, чем AJAX, поскольку вам не нужно ждать ответа сервера для каждого дополнительного поля, пока вы не закончите.

Наконец, нам нужно связать это с JavaScript. Добавьте следующее в ваш файл `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;
  });
});

К этому времени вы должны иметь динамическую форму barebones! Javascript здесь действительно прост, и его можно легко сделать с помощью других фреймворков. Вы можете легко заменить мой application.js код, например, на prototype + lowpro. Основная идея заключается в том, что вы не встраиваете гигантские функции JavaScript в свою разметку, и вам не нужно писать утомительные функции phone_numbers=() в ваших моделях. Все просто работает. Ура!


После некоторого дальнейшего тестирования я пришел к выводу, что шаблоны должны быть удалены из полей <form>. Хранение их там означает, что они отправляются обратно на сервер с остальной частью формы, и это только позже вызывает головную боль.

Я добавил это в конец моего макета:

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

И изменил хелпер 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

Теперь вы можете удалить предложения :reject_if из ваших моделей и перестать беспокоиться о отправке шаблонов назад.

3 голосов
/ 10 ноября 2009

Исходя из вашего ответа на мой комментарий, я думаю, что ненавязчивая обработка удаления - это хорошее место для начала. В качестве примера я буду использовать Product with scaffolding, но код будет общим, поэтому его будет легко использовать в вашем приложении.

Сначала добавьте новую опцию к вашему маршруту:

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

А теперь добавьте вид удаления к просмотрам вашего продукта:

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

Это представление будут видеть только пользователи с отключенным JavaScript.

В контроллере Products вам нужно добавить действие удаления.

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

Теперь перейдите к просмотру индекса и измените ссылку «Уничтожить» на:

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

Если вы запустите приложение на этом этапе и просмотрите список продуктов, вы сможете удалить продукт, но мы можем добиться большего успеха для пользователей с поддержкой JavaScript. Класс, добавленный к ссылке для удаления, будет использоваться в нашем JavaScript.

Это будет довольно большой кусок JavaScript, но важно сосредоточиться на коде, который связан с выполнением вызова ajax - коде в обработчике ajaxSend и обработчике щелчков «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);
};

Вот CSS, который вам тоже понадобится:

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

Нам нужно отправить токен аутентификации, когда мы делаем POST, PUT или DELETE. Добавьте эту строку под существующим тегом JS (возможно, в вашем макете):

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

Почти готово. Откройте свой контроллер приложений и добавьте следующие фильтры:

before_filter :correct_safari_and_ie_accept_headers
after_filter :set_xhr_flash

и соответствующие методы:

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

Нам нужно отбросить флеш-сообщения, если это ajax-вызов - в противном случае вы увидите флеш-сообщения из «прошлого» при следующем регулярном http-запросе. Второй фильтр также необходим для webkit и браузеров IE - я добавляю эти 2 фильтра ко всем моим проектам Rails.

Осталось только уничтожить:

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

И вот оно у вас. Ненавязчивое удаление с помощью Rails. Кажется, что много работы уже распечатано, но на самом деле все не так плохо, как только вы начинаете. Возможно, вас заинтересует Railscast .

2 голосов
/ 17 января 2012

К вашему сведению, у Райана Бейтса теперь есть самоцвет, который прекрасно работает: nested_form

2 голосов
/ 16 августа 2010

Кстати. Рельсы немного изменились, поэтому вы больше не можете использовать _delete, теперь используйте _destroy.

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

Кроме того, я обнаружил, что проще удалить html для новых записей ... так что я делаю это

$(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;
  });
});
1 голос
/ 21 января 2013

Я успешно (и довольно безболезненно) использовал https://github.com/nathanvda/cocoon для динамического создания моих форм. Обрабатывает ассоциации элегантно и документация очень проста. Вы также можете использовать его вместе с simple_form, что было особенно полезно для меня.

1 голос
/ 25 августа 2012

Я создал ненавязчивый плагин jQuery для динамического добавления fields_for объектов для Rails 3. Его очень просто использовать, просто скачайте файл js. Практически нет конфигурации. Просто следуйте правилам, и все готово.

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

Это не супер гибкий, но он сделает работу.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...