Незаметные динамические поля формы в направляющих с jQuery

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

, например,

  • кот/распространенный/lib
  • mywebapp/WEB-INF/lib
41
задан Adam Lassek 11 November 2009 в 17:34
поделиться

3 ответа

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

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

Я основывал свою реализацию на вилке сложных форм Тима Райли на 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;
  });
});

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


После некоторого дальнейшего тестирования я пришел к выводу, что шаблоны необходимо переместить из полей

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

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

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

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

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

53
ответ дан 27 November 2019 в 00:46
поделиться

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

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

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.

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

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.

Все, что осталось, это действие 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
ответ дан 27 November 2019 в 00:46
поделиться

Кстати. rails немного изменился, поэтому вы больше не можете использовать _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;
  });
});
2
ответ дан 27 November 2019 в 00:46
поделиться
Другие вопросы по тегам:

Похожие вопросы: