Генерация динамических дочерних строк в Rails частично

Я реализовал вложенную форму атрибута, используя в качестве руководства вложенные атрибуты Railscast. В результате пользователь может щелкнуть значок, чтобы динамически добавить "дочерние" строки в мое представление.

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

Можно ли сделать это? Если да, то какой подход лучше?

Вот моя последняя попытка.

Простынь has_many Слоты. В режиме редактирования листа я использую конструктор форм листа (sheet) сделать мой слот частичным, а также передать его помощнику link_to_add_fields который отображает ссылку, которая будет генерировать новую строку при нажатии (эта часть работает нормально). Вы заметите, что я тоже пытаюсь пройти sheet к частичному, чтобы я мог позвонить link_to_add_fields оттуда, но это где он ломается:

Вид - edit.html.haml:

= sheet.fields_for :slots do |builder|
  = render 'slots/edit_fields', f: builder, sheet:sheet
= link_to_add_fields image_tag("plus.jpg", size:"18x18", alt:"Plus"), sheet, :slots, 'slots/edit'

Частичное - _edit_fields.html.haml:

- random_id = SecureRandom.uuid
.row.signup{:id => "edit-slot-#{random_id}"}
  .col-md-1
    %span.plus-icon
      = link_to_add_fields image_tag("plus.jpg", size:"18x18", alt:"Plus"), sheet, :slots, 'slots/edit'
    %span.minus-icon
      = image_tag "minus.jpg", size:"18x18", alt:"Minus"
  .col-md-2= f.text_field :label
  ... other fields ...

Вспомогательный метод:

def link_to_add_fields(name, f, association, partial)
  new_object = f.object.send(association).klass.new
  id = new_object.object_id
  fields = f.fields_for(association, new_object, child_index: id) do |builder|
    render(partial.to_s.singularize + "_fields", f: builder, name: name)
  end
  link_to(name, '#', class: "add_fields", data: {id: id, fields: fields.gsub("\n", "")})
end

я получил undefined local variable or method 'sheet' на вызов помощнику из частичного. По сути, мне нужно, чтобы компоновщик форм листа (родительский) был доступен в каждой ссылке для работы помощника. Или мне нужно отказаться от этого подхода и использовать AJAX (тоже пробовал).

ОБНОВИТЬ

После небольшой отладки становится ясно, что sheet становится переданным в частичное. Основная проблема в том, что я, кажется, настраиваю бесконечную рекурсию:

  1. Частичные вызовы link_to_add_fields так что мой + значок может служить ссылкой "добавить ребенка".
  2. link_to_add_fields рендерит частичное, так что поля могут быть созданы, когда + иконка нажата.

Другая проблема, с которой я сталкиваюсь, заключается в том, что при визуализации исходных дочерних элементов они получают последовательные индексы в коллекции атрибутов (0, 1, 2,...). Поэтому, даже если я найду способ визуализировать новые дочерние строки среди оригиналов, я не уверен, как смогу поддерживать порядок детей, когда форма отправляется без большого количества jQuery-гимнастики или чего-то еще.

3 ответа

Решение

Я решил решить эту проблему с помощью jQuery. Не супер элегантный, но очень эффективный. По сути, я просто добавил обработчик кликов для + иконки, которые строили новую строку и вставляли ее по мере необходимости (в основном просто взломали jQuery, необходимый для репликации HTML, созданного Rails). В случае, если мой вариант использования поможет кому-то еще в будущем, вот jQuery (см. Ссылку на imgur в оригинальном сообщении для справки):

//click handler to add edit row when user clicks on the plus icon
$(document).on('click', '.plus-icon', function (event) {

  //build a new row
  var one_day = 24 * 3600 * 1000;
  var d = new Date();
  var unique_id = d.getTime() % one_day + 10000;            // unique within a day and offset past original IDs
  var new_div =
    $('<div/>', {'class': 'row signup'}).append(
      $('<div/>', {'class': 'col-md-1'}).append(
        $('<span/>', {'class': 'plus-icon'}).append(
          '<img alt="Plus" src="/assets/plus.jpg" width="18" height="18" />'
        )
      ).append(
        $('<span/>', {'class': 'minus-icon'}).append(
          '<img alt="Minus" src="/assets/minus.jpg" width="18" height="18" />'
        )
      )
    ).append(
      $('<div/>', {'class': 'col-md-2'}).append(
        '<input type="text" value="" name="sheet[slots_attributes]['+unique_id+'][label]" id="sheet_slots_attributes_'+unique_id+'_label">'
      )
    ).append(
      $('<div/>', {'class': 'col-md-2'}).append(
        '<input type="text" value="" name="sheet[slots_attributes]['+unique_id+'][name]" id="sheet_slots_attributes_'+unique_id+'_name">'
      )
    ).append(
      $('<div/>', {'class': 'col-md-2'}).append(
        '<input type="text" value="" name="sheet[slots_attributes]['+unique_id+'][email]" id="sheet_slots_attributes_'+unique_id+'_email">'
      )
    ).append(
      $('<div/>', {'class': 'col-md-2'}).append(
        '<input type="text" value="" name="sheet[slots_attributes]['+unique_id+'][phone]" id="sheet_slots_attributes_'+unique_id+'_phone">'
      )
    ).append(
      $('<div/>', {'class': 'col-md-2'}).append(
        '<input type="text" value="" name="sheet[slots_attributes]['+unique_id+'][comments]" id="sheet_slots_attributes_'+unique_id+'_comments">'
      )
    );
  var new_input = 
    '<input id="sheet_slots_attributes_'+unique_id+'_id" type="hidden" name="sheet[slots_attributes]['+unique_id+'][id]" value="'+unique_id+'">';

  //insert new row before clicked row
  $(new_div).insertBefore($(this).parent().parent());
  $(new_input).insertBefore($(this).parent().parent());
});

Я застрял здесь на некоторое время... напоминание о том, что обычно есть несколько способов решить проблему, и важно не зацикливаться на одной идее. Спасибо всем за ваши предложения и вклад.

undefined local variable or method 'sheet'

Это вызвано тем, как вы рендерите частичное.

= render 'slots/edit_fields', f: builder, sheet:sheet

Недостаточно для передачи переменных в частичное. Тебе нужно:

= render partial: 'slots/edit_fields', locals: {f: builder, sheet:sheet}

Это сделает f доступным в вашей части.

Это звучит удивительно близко к проблеме, которую мне пришлось решить несколько месяцев назад.

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

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

<%= form_for @user do |f| %>
<%= render layout: "users/partials/fields",
                 locals: {f: f} do %>

<b>Empires</b><br>
  <% n = 0 %>
  <%= f.fields_for :empires do |empire_form, index| %>
    <% n += 1 %>
    <%= empire_form.label :name, class: 'ms-indent' %>
    <%= empire_form.text_field :name, class: 'form_control' %>
    <%= empire_form.label :_destroy, 'Delete' %>
    <%= empire_form.check_box :_destroy %>
    <br>
  <% end %>

  <!-- Empty field for new Empire -->
  <b class='ms-indent'>Name</b>
    <input class="form_control" value="" name="user[empires_attributes][<%=n+1%>][name]" id="user_empires_attributes_0_name" type="text"> <b>new</b>
  <br>

<% end %>
  <%= f.submit "Save changes", class: "btn btn-primary" %>
<% end %>

Интересным является счетчик для получения количества существующих империй (я не могу вспомнить, почему я просто не использовал размер или длину) и использование "n+1" в поле ввода html, чтобы гарантировать, что новая империя была включена в форму отправлено с правильным идентификатором. Обратите внимание на использование тега ввода html, у Rails нет эквивалента.

[Просто дополнительная заметка в свете комментария DickieBoys. Этот подход "n+1" работает, но может быть заменен случайным числом и все еще работает. Я собираюсь поэкспериментировать с этим позже, но это работает, поэтому я не спешу.]

Частичные "поля" выглядят так, обратите внимание на "урожайность" в середине.

<%= f.label :name %>
<%= f.text_field :name, class: 'form-control', autofocus: true %>

<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>

<%= yield %>

<%= f.label :password %>
<%= f.password_field :password, class: 'form-control', value: "" %>

<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>

Часть 'fields' содержит блок do-end, содержащий код моей империи. Партиал включает в себя этот блок, в котором говорится "yield".

Наконец, обновить код контроллера.

def update
    @user = User.find(params[:id])
    updated = false
    begin
      updated = @user.update_attributes(user_params)
    rescue ActiveRecord::DeleteRestrictionError
      flash[:warning] = "Empire contains ships. Delete these first."
    end 
    if updated
      flash[:success] = "Profile updated"
      redirect_to @user
    else
      render 'edit'
    end
  end

Я не буду притворяться, что это единственное решение, и даже не обязательно лучшее. Но это заняло несколько дней (Google и Stackru подвели меня!) И с тех пор работает надежно. Ключ был в том, чтобы отбросить попытки найти решение Rails для добавления новой империи и просто использовать html с новым идентификатором для новой империи. Вы можете быть в состоянии улучшить это. Надеюсь, поможет.

Другие вопросы по тегам