Генерация динамических дочерних строк в 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
становится переданным в частичное. Основная проблема в том, что я, кажется, настраиваю бесконечную рекурсию:
- Частичные вызовы
link_to_add_fields
так что мой+
значок может служить ссылкой "добавить ребенка". 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 с новым идентификатором для новой империи. Вы можете быть в состоянии улучшить это. Надеюсь, поможет.