Продвинутые формы

Введение

В предыдущих уроках вы научились создавать простые HTML формы в Rails, теперь, с этими знаниями вы сможете намного больше. Случается, что для разработки удобного пользовательского интерфейса необходимо создать форму, которая будет обрабатывать множество вещей за раз (например объектов модели). Пользователям нравится кликать кнопку "Отправить", поэтому лучше бы вам дать им такую возможность.

В данном разделе мы рассмотрим некоторые способы создания форм с возможностью обработки нескольких объектов модели. Также вы научитесь предварительно заполнять (prepopulate) выпадающий список(dropdown) объектами.

Пункты для размышления

Постарайтесь ответить на предложенные вопросы. После выполнения задания попробуйте ответить на них ещё раз

  • Как предварительно заполнить (prepopulate) выпадающее меню данными?
  • В чем разница между хелперами #select_tag и #select ?
  • Какой формат должен иметь массив, который вы передаете методу #options_for_select
  • Для чего нужно использовать вложенные формы?
  • Что нужно изменить в модели, чтобы разрешить вложенным формам создавать новые объекты
  • Как правильно внести вложенные параметры в список разрешенных (whitelist?) в Вашем контроллере?
  • Почему нельзя удалить что-то, просто оставляя поле формы (например флажок) пустым (неотмеченным)?

Предварительное заполнение(Prepopulating) тэгов select Коллекциями.

Rails позволяет несколькими способами создать выпадающие меню, которые содержат какие-то данные уже при загрузке формы (иначе, они были бы бесполезны).

Допустим, вам нужно построить форму "Новый пост" для вашего блога с возможностью выбрать автора из списка пользователей. Для этого вам нужно создать выпадающее меню, которое отправляет ID пользователя, как часть вашего params хэша. Таким образом вы можете заполнить @users в Вашем контроллере постов:

    # app/controllers/posts_controller.rb
    ...
    def new
      @users = User.all
      @post = Post.new
    end
    ...

В "чистом"" HTML это делается путем создания множества тэгов <option> (внутри тэга <select>). Вы можете легко сделать это в Вашем ERB коде, просто итерируясь по коллекции, например если вы хотите выбрать запись из списка для просмотра.

    # app/views/posts/new.html.erb
    ...
    <select name="user_id">
      <%= @users.each do |user| %>
        <option value="<%= user.id %>"><%= user.name %></option>
      <% end %>
    </select>
    ...

Код выше создает выпадающий список с именем каждого пользователя в качестве опции. Ваше #create действие примет атрибут user_id, используя который, вы сможете сопоставить автора и сообщение(пост).

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

#options_for_select ожидает весьма специфичный ввод -- массив массивов, каждый из которых содержит текст и значения для выпадающих опций. Например, options_for_select([["choice1",1],["choice2",2]]) создает пару option-тэгов, по одному для каждого варианта выбора. Это здорово, потому что это именно то, что #select_tag ожидает в качестве второго аргумента. Единственная трудность заключается в том, что необходимо преобразовать коллекцию @users с заполненными объектами User в простой массив, который содержащий только name and value. Это легко сделать при помощи метода #map:

    # app/controllers/posts_controller.rb
    ...
    def new
      @user_options = User.all.map{|u| [ u.name, u.id ] }
      @post = Post.new
    end
    ...

    # app/views/posts/new.html.erb
    ...
    <%= select_tag(:author_id, options_for_select(@user_options)) %>
    ...

Просто передайте в #select_tag имя для выбранного вами значения и коллекцию, вывод программы будет тем же!

Если вы не хотите использовать options_for_select и ваша форма предназначена для построения экземпляра модели (например объекта Post), то в данном случае просто используйте более общий хелпер #select.

    # app/views/posts/new.html.erb
    ...
    <%= select(:post, :author_id, @user_options) %>
    ...

Также вам нужно передавать параметр в :post (что свидетельствует о том, что Ваша форма создает объект Post), чтобы тэг select мог получить имя.. в нашем случае, тэг будет иметь вид <select name="post[author_id]" id="post_author_id">. Это означает(!), что атрибут author_id будет отображаться в params вложенным в хэш post.

Передача :author_id хелперу #select выше представляет собой не только выбранное значение, которое будет вызвано, но также и имя столбца в исходной модели (в нашем случае речь о Post). Поначалу, это может немного раздражать, т.к. вы не сможете просто дать произвольное имя вашему выбору.

Если у вас есть форма #form_for в контексте переменной f, то вам не нужно передавать символ :post выше (он будет получен из f), вместо можно использовать следующий вариант:

    # app/views/posts/new.html.erb
    ...
      <%= f.select(:author_id, @user_options) %>
    ...

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

Вы будете часто использовать выпадающие списки для заполнения связей в модели (например автора в Post). В следующем разделе мы поговорим о том, как создать оба объекта модели используя одну единственную форму.

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

Вложенные Формы

Теперь у вас есть форма для создания одного из объектов User (предположим, для вашего клона приложения Amazon.com), но также вы хотите, чтобы форма создавала один или больше объектов ShippingAdress (которых объект User может иметь несколько). Как сделать форму, которая бы создавала все нужные объекты, но чтобы это не требовало от пользователя многократных нажатий кнопки "Отправить"?

Создание такой формы - многоступенчатый процесс. Нам понадобится контроллер, модель, представление(view) и маршруты(routes)... целая команда MVC! Суть заключается в том, что Ваша форма должна отправить главный объект (например User) как обычно, но данный объект будет включать в себя кучу атрибутов для других объектов, которые вы хотите создать (например объект(ы) ShippingAddress). Ваша модель тоже должна это учитывать. Это создаст не только исходный объект пользователя, но также вложенные объекты, в то же время.

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

Рассмотрим процесс в общих чертах:

  1. Вам необходимо подготовить модель User, чтобы при передаче ей атрибутов во время создания объекта User, также происходило бы создание одного или нескольких объектов ShippingAddress. Это можно реализовать путем добавления в модель User метода #accepts_nested_attributes_for, который принимает имя связи, например:

        # app/models/user.rb
        class User < ActiveRecord::Base
          has_many :shipping_addresses
          accepts_nested_attributes_for :shipping_addresses
        end
    
  2. Убедитесь, что вы разрешили params включать вложенные атрибуты соответственно включив их в метод контроллера Strong Parameters. Примеры см. по ссылкам в конце урока.

  3. Постройте форму в представлении(view). Используйте метод #fields_for для эффективного создания #form_for внутри уже существующей формы #form_for.

В данном процессе есть несколько новых аспектов. Вы уже видели #fields_for ранее, в Уроке по базовым формам, однако сейчас это может иметь для вас новый смысл, например, когда речь идет о создании формы внутри другой формы (что имеет смысл, т.к. это неявно происходит в #form_for). В данном примере, мы можем создать три "дочерних формы" для объектов ShippingAddress, используя связи, например:

    <%= form_for @user do |f| %>
      ...
      <% 3.times do %>
        <%= f.fields_for @user.shipping_address.build do |addy_form| %>
          ...
          <%= addy_form.text_field :zip_code %>
          ...
        <% end %>
      <% end %>
      <%= f.submit %>
    <% end %>

Обратите внимание что можно (и нужно) создать новый объект shipping_address в контроллере, а не в представлении; это необходимо для демонстрационных целей.

Метод #accepts_nested_attributes_for относительно прост, и документация по нему достаточно содержательна.

В дополнительных материалах по ссылкам в конце урока более подробно рассматривается добавление вложенных параметров в список разрешенных (whitelisting).

Удаление объектов вложенных форм.

Также вы можете уничтожать вложенные формы, для этого сначала нужно установить опцию :allow_destroy в true для метода #accepts_nested_attributes_for, например accepts_nested_attributes_for :shipping_addresses, :allow_destroy => true. Теперь когда вы захотите удалить объект ShippingAddress из формы User, просто внесите ключ _destroy => 1 в отправляемые параметры для ShippingAddress.

Отношения многие-ко-многим (many-to-many)

Если у Вас есть отношение has_many :through, то вам вероятно нужно сделать еще один шаг, указав, что каждая сторона вашего отношения является обратной к другой. Это подробно рассмотрено в этом посте из блога ThoughtBot.

Конструирование своих собственных форм

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

Часто это простейший способ (и, кроме того, хорошая практика пока вы учитесь) начать с самых основ HTML форм. Если вы не понимаете, что происходит в простом HTML (не забудьте про свой CSRF токен), то будете безуспешно пытаться использовать хелперы. Как только вы разберетесь с этими вещами, постепенно можно привносить в работу такие хелперы Rails как #form_tag и #form_for.

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

Simple Form

simple_form это гем за авторством Platformatec, который действительно может облегчить вам жизнь (если вы будете действовать в рамках разумного). Он предоставляет много удобных в использовании функций для создания форм и на сегодняшний день широко используется. Наш материал не покрывает данную тему достаточно подробно (однако она еще будет встречаться в последющих уроках).

При желании вы можете ознакомиться с документацией и просто начать использовать simple_form в Ваших приложениях.

Прочее: пустые поля означают удаление

Иногда, для уже существующей записи, вы можете захотеть отменить выбор в выпадающем списке или снять флаг, но вы хотите, чтобы связанные поля были установлены в значение nil. Обычно, хотя если вы отправляете форму она не включает полей и ваш back-end не знает точно, хотите ли вы удалить эти поля и поэтому ничего не случится. Как вам обойти это?

Попробуйте сделать скрытое поле на Вашей форме (или вложенную форму)с тем же именем, как и checkbox или выпадающий список, но содержит только значение "". Теперь вы получите атрибут, который отобразится в вашем params хэш независимо ни от чего, и вы можете управлять удалением записей, когда вам это нужно.

Иногда хелперы Rails сделают это за Вас, однако, следует убедиться, что именно Ваша форма отправляет c пустыми полями.

Ваше задание

  1. Прочтите Гайд по формам в Rails, в разделе 3.3 рассказывается о предварительном заполнении формы коллекцией объектов
  2. Прочтите тот же гайд по формам в Rails, в разделе 9 рассматривается получение данных из вложенных форм.
  3. Прочтите тот же гайд по формам в Rails, в разделе 7.3 рассказывается про соглашения о параметрах для вложенных форм.
  4. Прочтите запись в блоге Питера Роадса о работе с вложенными формами. Пример охватывает множество вещей, которые мы уже освоили. Также обратите внимание на то, как происходит разрешение вложенных атрибутов в Rails4.

Заключение

Мы рассмотрели два самых распространенных варианта использования сложных форм -- предварительное заполнение формы объектами и создание нескольких объектов из одной единственной формы. Теперь, даже если вы пока не чувствуете себя неуверенно, у Вас есть все, что нужно, чтобы создавать такие формы. Самое время вплотную взяться за проект.

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

Дополнительные ресурсы

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

  • Документация по Simple Form на Github
  • Документация по accepts_nested_attributes_for
  • Пример по вложенным формам на StackOverflow
  • Как использовать inverse_of чтобы разрешить accepts_nested_attributes_for работать с отношением has_many :through
  • Понимание authenticity tokens(токены подлинности) в Rails
  • Почему не стоит явно вписывать секретный токен в продакшене

Поделиться уроком: