Подтвердить что ты не робот

Ненузивные поля динамической формы в Rails с jQuery

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

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

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

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


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

4b9b3361

Ответ 1

Поскольку никто не предлагал ответа на этот вопрос, даже после щедрости, мне наконец-то удалось заставить это работать самостоятельно. Это не должно было быть ошеломляющим! Надеюсь, это будет проще сделать в 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 templating. Он отправит пустой шаблон для каждой ассоциации, но предложение :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 прототипом + 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 из своих моделей и перестать беспокоиться о отправке шаблонов.

Ответ 2

Основываясь на вашем ответе на мой комментарий, я думаю, что обработка удаления ненавязчиво - это хорошее место для начала. В качестве примера я буду использовать 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 вам необходимо добавить действие delete.

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

Теперь перейдите в свой индексный указатель и измените ссылку Destroy на это:

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

Нам нужно отправить маркер auth, когда мы делаем 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.

Все, что осталось, - это действие destroy:

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.

Ответ 3

Кстати. рельсы немного изменились, поэтому вы больше не можете использовать _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;
  });
});

Ответ 4

FYI, у Райана Бейтса теперь есть драгоценный камень, который прекрасно работает: nested_form

Ответ 5

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

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

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

Ответ 6

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