Как отправить несколько новых элементов с помощью Rails 3.2 массового назначения

У меня довольно стандартный прецедент. У меня есть родительский объект и список дочерних объектов. Я хочу иметь табличную форму, где я могу редактировать всех детей сразу, как строки в таблице. Я также хочу иметь возможность вставлять одну или несколько новых строк, а в submit их создавать в виде новых записей.

Когда я использую fields_for для рендеринга ряда подформ для вложенных записей, связанных с has-many, rails генерирует имена полей, например. parent[children_attributes][0][fieldname], parent[children_attributes][1][fieldname] и т.д.

Это приводит к тому, что Rack анализирует хеш params, который выглядит следующим образом:

{ "parent" => { 
    "children" => {
      "0" => { ... },
      "1" => { ... } } }

При передаче нового (неустановленного) объекта тот же fields_for будет генерировать имя поля, которое выглядит следующим образом:

parent[children_attributes][][fieldname]

Обратите внимание на [] без индекса в нем.

Это не может быть отправлено в той же форме с полями, содержащими [0], [1] и т.д., потому что Rack запутывается и поднимает

TypeError: expected Array (got Rack::Utils::KeySpaceConstrainedParams)

"ОК", думает я. "Я просто сделаю так, чтобы все поля использовали форму [] вместо формы [index]. Но я не могу понять, как убедить fields_for сделать это последовательно. Даже если я даю ему явный префикс имени поля и объект:

fields_for 'parent[children_attributes][]', child do |f| ...

Пока сохраняется child, он автоматически изменяет имена полей, чтобы они стали, например, parent[children_attributes][0][fieldname], оставляя имена полей для новых записей как parent[children_attributes][][fieldname]. Еще раз, Rack barfs.

Я в недоумении. Как я использую стандартные помощники Rails, такие как fields_for для отправки нескольких новых записей вместе с существующими записями, чтобы они анализировались как массив в параметрах, и чтобы все записи, лишенные идентификаторов, были созданы как новые записи в БД? Мне повезло, и мне просто нужно сгенерировать все имена полей вручную?

Ответ 1

Как отмечали другие, [] должен содержать ключ для новых записей, поскольку в противном случае он смешивает хэш с типом массива. Вы можете установить это с помощью опции child_index в полях_for.

f.fields_for :items, Item.new, child_index: "NEW_ITEM" # ...

Обычно я делаю это с помощью object_id, чтобы убедиться, что он уникален в случае, если есть несколько новых элементов.

item = Item.new
f.fields_for :items, item, child_index: item.object_id # ...

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

def link_to_add_fields(name, f, association)
  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(association.to_s.singularize + "_fields", f: builder)
  end
  link_to(name, '#', class: "add_fields", data: {id: id, fields: fields.gsub("\n", "")})
end

Вы можете использовать его так. Аргументами являются: имя ссылки, родительский шаблон формы и имя ассоциации в родительской модели.

<%= link_to_add_fields "Add Item", f, :items %>

И вот несколько CoffeeScript для прослушивания события клика по этой ссылке, вставки полей и обновления идентификатора объекта с текущим временем, чтобы дать ему уникальный ключ.

jQuery ->
  $('form').on 'click', '.add_fields', (event) ->
    time = new Date().getTime()
    regexp = new RegExp($(this).data('id'), 'g')
    $(this).before($(this).data('fields').replace(regexp, time))
    event.preventDefault()

Этот код взят из этого эпизода RailsCasts Pro, для которого требуется платная подписка. Тем не менее, имеется полный рабочий пример, свободный на GitHub.

Обновление: Я хочу указать, что добавление заполнитель child_index не всегда необходимо. Если вы не хотите использовать JavaScript для динамического вставки новых записей, вы можете их заранее создать:

def new
  @project = Project.new
  3.times { @project.items.build }
end

<%= f.fields_for :items do |builder| %>

Rails автоматически добавит индекс для новых записей, чтобы он просто работал.

Ответ 2

Таким образом, я был недоволен решением, которое я видел чаще всего, что должно было генерировать псевдоиндекс для новых элементов либо на сервере, либо на стороне клиента JS. Это похоже на kludge, особенно в свете того факта, что Rails/Rack отлично умеет анализировать списки элементов, пока все они используют в качестве индекса пустые скобки ([]). Здесь аппроксимация кода, с которым я столкнулся:

# note that this is NOT f.fields_for.
fields_for 'parent[children_attributes][]', child, index: nil do |f|
  f.label :name
  f.text_field :name
  # ...
end

Завершение префикса имени поля с помощью [], в сочетании с опцией index: nil, отключает генерацию индекса Rails, поэтому полезно пытается обеспечить сохранение объектов. Этот фрагмент работает как для новых, так и для сохраненных объектов. Полученные параметры формы, поскольку они последовательно используют [], анализируются в массив в params:

params[:parent][:children_attributes] # => [{"name" => "..."}, {...}]

Метод Parent#children_attributes=, сгенерированный accepts_nested_attributes_for :children, отлично справляется с этим массивом, обновляет измененные записи, добавляет новые (те, у которых отсутствует ключ "id"), и удаляет их с помощью набора "_destroy".

Я все еще обеспокоен тем, что Rails делает это настолько сложным, и мне пришлось вернуться к строковой строке с жестким кодом, вместо того, чтобы использовать, например. f.fields_for :children, index: nil. Для записи, даже делая следующее:

f.fields_for :children, index: nil, child_index: nil do |f| ...

... не удается отключить генерацию индекса поля.

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

EDIT: User @Macario заставил меня понять, почему Rails предпочитает явные индексы в именах полей: как только вы попадаете в три слоя вложенных моделей, должен быть способ распознать, какая модель второго уровня имеет атрибут третьего уровня принадлежит.

Ответ 3

Общим решением является добавление заполнителя в [] и замена его уникальным номером при вставке фрагмента в форму. Временная метка работает большую часть времени.

Ответ 4

Возможно, вам стоит просто обмануть. Поместите новые записи в другой атрибут faux, который является декоратором для фактического.

parent[children_attributes][0][fieldname]
parent[new_children_attributes][][fieldname]

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

Ответ 5

длинный пост удален

У Райана есть эпизод по этому поводу: http://railscasts.com/episodes/196-nested-model-form-revised

Похоже, вам нужно создать уникальный индекс вручную. Для этого Райан использует object_id.

Ответ 6

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

https://gist.github.com/3096634

Предостережения для использования плагина:

  • Поля field_for должны быть обернуты в <fieldset> с атрибутом ассоциации данных, равным плюрализуемому имени модели, и классом 'nested_models'.

  • объект должен быть создан в представлении непосредственно перед вызовом fields_for.

  • поля объекта perse должны быть завернуты в <fieldset> с классом "new", но только если запись является новой (не помните, если я удалил это требование).

  • Флажок для атрибута '_destroy' внутри метки должен существовать, плагин будет использовать текст метки для создания ссылки destroy.

  • Ссылка с классом "add_record" должна существовать в пределах fieldset.nested_models, но вне поля, охватывающего поля модели.

Аплодисменты из этих неприятностей творят чудеса для меня.
После проверки сути эти требования должны быть более четкими. Пожалуйста, дайте мне знать, если вы улучшите код или используете его:).
BTW, я был вдохновлен первым вложенным моделями Ryan Bates.

Ответ 7

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

Ответ 8

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