Хелперы форм

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

После прочтения этого руководства, вы узнаете:

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

Это руководство не является подробной документацией по доступным хелперам форм и их аргументам. Для получения полной информации, обратитесь к документации по Rails API.

1. Разбираемся с простыми формами

Самый простой хелпер форм - это form_tag.

<%= form_tag do %>
  Содержимое формы
<% end %>

При подобном вызове без аргументов, он создает тег <form>, который при отправке сформирует POST-запрос на текущую страницу. Например, предположим текущая страница /home/index, тогда сгенерированный HTML будет выглядеть следующим образом (некоторые разрывы строчек добавлены для читаемости):

<form accept-charset="UTF-8" action="/" method="post">
  <input name="utf8" type="hidden" value="&#x2713;" />
  <input name="authenticity_token" type="hidden" value="J7CBxfHalt49OSHp27hblqK20c9PgwJ108nDHX/8Cts=" />
  Содержимое формы
</form>

Можно увидеть, что HTML содержит элемент input с типом hidden. Этот input важен, поскольку без него форма не может быть успешно отправлена. Скрытый элемент input с атрибутом name utf8 обеспечивает, чтобы браузер правильно относился к кодировке вашей формы, он генерируется для всех форм, у которых action равен "GET" или "POST".

Второй элемент input с именем authenticity_token является особенностью безопасности Rails, называемой защитой от подделки межсайтовых запросов, и хелперы форм генерируют его для каждой формы, у которых action не "GET" (при условии, что эта особенность безопасности включена). Подробнее об этом можно прочитать в Руководстве Ruby On Rails по безопасности.

1.1. Характерная форма поиска

Одной из наиболее простых форм, встречающихся в вебе, является форма поиска. Эта форма содержит:

  • элемент формы с методом "GET",
  • метку для поля ввода,
  • элемент поля ввода текста и
  • элемент отправки.

Чтобы создать эту форму, используем, соответственно, form_tag, label_tag, text_field_tag и submit_tag. Как здесь:

<%= form_tag("/search", method: "get") do %>
  <%= label_tag(:q, "Search for:") %>
  <%= text_field_tag(:q) %>
  <%= submit_tag("Search") %>
<% end %>

Это сгенерирует следующий HTML:

<form accept-charset="UTF-8" action="/search" method="get">
  <input name="utf8" type="hidden" value="&#x2713;" />
  <label for="q">Search for:</label>
  <input id="q" name="q" type="text" />
  <input name="commit" type="submit" value="Search" />
</form>

Для каждого поля ввода формы генерируется атрибут ID из его имени ("q" в примере). Эти ID могут быть очень полезны для стилизации CSS или управления элементами форм с помощью JavaScript.

Кроме text_field_tag и submit_tag имеется похожий хелпер для каждого элемента управления формой в HTML.

Всегда используйте "GET" как метод для форм поиска. Это позволяет пользователям добавлять в закладки определенный поиск и потом возвращаться к нему. В более общем смысле Rails призывает вас использовать правильный метод HTTP для экшна.

1.2. Несколько хэшей в вызовах хелпера формы

Хелпер form_tag принимает 2 аргумента: путь для экшна и хэш опций. Этот хэш определяет метод отправки формы и опции HTML, такие как класс элемента form.

Как в случае с хелпером link_to, аргумент пути не должен быть строкой. Это может быть хэш параметров URL, распознаваемый механизмом маршрутизации Rails, преобразующим хэш в валидный URL. Однако, если оба аргумента для form_tag хэши, можно легко получить проблему, если захотите определить оба. Например, вы напишите так:

form_tag(controller: "people", action: "search", method: "get", class: "nifty_form")
# => '<form accept-charset="UTF-8" action="/people/search?method=get&class=nifty_form" method="post">'

Здесь методы method и class присоединены к строке запроса сгенерированного URL, поскольку, хотя вы и хотели передать два хэша, фактически вы передали один. Чтобы сообщить об этом Ruby, следует выделить первый хэш (или оба хэша) фигурными скобками. Это сгенерирует HTML, который вы ожидаете:

form_tag({controller: "people", action: "search"}, method: "get", class: "nifty_form")
# => '<form accept-charset="UTF-8" action="/people/search" method="get" class="nifty_form">'

1.3. Хелперы для генерации элементов формы

Rails предоставляет ряд хелперов для генерации элементов формы, таких как чекбоксы, текстовые поля, радиокнопки и так далее. Эти простые хелперы с именами, оканчивающимися на _tag (такие как text_field_tag и check_box_tag), генерируют только один элемент <input>. Первый параметр у них это всегда имя поля ввода. Когда форма будет отправлена, имя будет передано вместе с данными формы, и, в свою очередь, помещено в params в контроллере со значением, введенным пользователем для этого поля. Например, если форма содержит <%= text_field_tag(:query) %>, то значение этого поля можно получить в контроллере с помощью params[:query].

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

1.3.1. Чекбоксы

Чекбоксы - это элементы управления формой, которые дают пользователю ряд опций, которые он может включить или выключить:

<%= check_box_tag(:pet_dog) %>
<%= label_tag(:pet_dog, "I own a dog") %>
<%= check_box_tag(:pet_cat) %>
<%= label_tag(:pet_cat, "I own a cat") %>

Это сгенерирует следующее:

<input id="pet_dog" name="pet_dog" type="checkbox" value="1" />
<label for="pet_dog">I own a dog</label>
<input id="pet_cat" name="pet_cat" type="checkbox" value="1" />
<label for="pet_cat">I own a cat</label>

Первый параметр у check_box_tag - это, разумеется, имя поля ввода. Второй параметр - это, естественно, значение поля ввода. Это значение будет включено в данные формы (и будет присутствовать в params), когда чекбокс нажат.

1.3.2. Радиокнопки

Радиокнопки, чем-то похожие на чекбоксы, являются элементами управления, которые определяют набор взаимоисключающих опций (т.е. пользователь может выбрать только одну):

<%= radio_button_tag(:age, "child") %>
<%= label_tag(:age_child, "I am younger than 21") %>
<%= radio_button_tag(:age, "adult") %>
<%= label_tag(:age_adult, "I'm over 21") %>

Результат:

<input id="age_child" name="age" type="radio" value="child" />
<label for="age_child">I am younger than 21</label>
<input id="age_adult" name="age" type="radio" value="adult" />
<label for="age_adult">I'm over 21</label>

Как и у check_box_tag, второй параметр для radio_button_tag - это значение поля ввода. Так как эти две радиокнопки имеют одинаковое имя (age), пользователь может выбрать одну, и params[:age] будет содержать или "child", или "adult".

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

1.4. Другие интересные хелперы

Среди других элементов управления формой стоит упомянуть текстовые области и следующие поля: паролей, скрытые, поиска, ввода телефона, даты, времени, цвета, локальных даты-времени, месяца, недели, url, email, числовые и интервалов:

<%= text_area_tag(:message, "Hi, nice site", size: "24x6") %>
<%= password_field_tag(:password) %>
<%= hidden_field_tag(:parent_id, "5") %>
<%= search_field(:user, :name) %>
<%= telephone_field(:user, :phone) %>
<%= date_field(:user, :born_on) %>
<%= datetime_local_field(:user, :graduation_day) %>
<%= month_field(:user, :birthday_month) %>
<%= week_field(:user, :birthday_week) %>
<%= url_field(:user, :homepage) %>
<%= email_field(:user, :address) %>
<%= color_field(:user, :favorite_color) %>
<%= time_field(:task, :started_at) %>
<%= number_field(:product, :price, in: 1.0..20.0, step: 0.5) %>
<%= range_field(:product, :discount, in: 1..100) %>

Результат:

<textarea id="message" name="message" cols="24" rows="6">Hi, nice site</textarea>
<input id="password" name="password" type="password" />
<input id="parent_id" name="parent_id" type="hidden" value="5" />
<input id="user_name" name="user[name]" type="search" />
<input id="user_phone" name="user[phone]" type="tel" />
<input id="user_born_on" name="user[born_on]" type="date" />
<input id="user_graduation_day" name="user[graduation_day]" type="datetime-local" />
<input id="user_birthday_month" name="user[birthday_month]" type="month" />
<input id="user_birthday_week" name="user[birthday_week]" type="week" />
<input id="user_homepage" name="user[homepage]" type="url" />
<input id="user_address" name="user[address]" type="email" />
<input id="user_favorite_color" name="user[favorite_color]" type="color" value="#000000" />
<input id="task_started_at" name="task[started_at]" type="time" />
<input id="product_price" max="20.0" min="1.0" name="product[price]" step="0.5" type="number" />
<input id="product_discount" max="100" min="1" name="product[discount]" type="range" />

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

Поля поиска, ввода телефона, даты, времени, цвета, даты-времени, локальных даты-времени, месяца, недели, url, email, числовые и интервалов - это элементы управления HTML5. Если необходимо, чтобы у вашего приложения была совместимость со старыми браузерами, вам необходим HTML5 polyfill (предоставляемый с помощью CSS и/или JavaScript). Хотя в таких решениях нет недостатка, популярным инструментом на сегодняшний момент является Modernizr, предоставляющий простой способ добавить функциональность, основанной на обнаружении установленных особенностей HTML5.

Если используются поля для ввода пароля (для любых целей), вы можете настроить свое приложение для предотвращения появления их значений в логах приложения. Это можно изучить в Руководстве Ruby On Rails по безопасности.

2. Работаем с объектами модели

2.1. Хелперы объекта модели

Наиболее частыми задачами для форм являются редактирование или создание объекта модели. В то время как хелперы *_tag, конечно, могут быть использованы для этой задачи, они несколько многословны, так как для каждого тега вам придется обеспечить использование правильного имени параметра и установку подходящего значения поля ввода по умолчанию. Rails предоставляет хелперы, адаптированные под эту задачу. У этих хелперов отсутствует суффикс _tag, например, text_field, text_area.

Для этих хелперов первый аргумент - это имя переменной экземпляра, а второй - это имя метода (обычно атрибутного), вызываемого на этом объекта. Rails установит значение value элемента управления input равным возвращаемому значению этого метода для объекта и установит подходящее имя поля ввода. Если ваш контроллер определил @person и имя этой персоны Henry, тогда форма, содержащая:

<%= text_field(:person, :name) %>

выдаст подобный результат

<input id="person_name" name="person[name]" type="text" value="Henry"/>

После подтверждения формы, значение, введенное пользователем, будет храниться в params[:person][:name]. Хэш params[:person] подходит для передачи в Person.new или, если @person - это экземпляр Person, в @person.update. Хотя имя атрибута - наиболее распространен как второй параметр для этих хелперов, он не является обязательным. В вышеприведенном примере, до тех пор пока объекты person имеют методы name и name=, Rails будет удовлетворен.

Необходимо передавать имя переменной экземпляра, т.е. :person или "person", а не фактический экземпляр объекта вашей модели.

Rails предоставляет хелперы для отображения ошибок валидации, связанных с объектом модели. Детально они раскрываются в руководстве Отображение ошибок валидации во вьюхе.

2.2. Привязывание формы к объекту

Хотя комфортность несколько улучшилась, она еще далека от совершенства. Если у Person много атрибутов для редактирования, тогда мы должны повторить имя редактируемого объекта много раз. То, что мы хотим сделать, - это как-то привязать форму к объекту модели, что как раз осуществляется с помощью form_for.

Допустим у нас есть контроллер для работы со статьями app/controllers/articles_controller.rb:

def new
  @article = Article.new
end

Соответствующая вьюха app/views/articles/new.html.erb, использующая form_for, выглядит так:

<%= form_for @article, url: {action: "create"}, html: {class: "nifty_form"} do |f| %>
  <%= f.text_field :title %>
  <%= f.text_area :body, size: "60x12" %>
  <%= f.submit "Create" %>
<% end %>

Следует отметить несколько вещей:

  • @article - это фактический объект, который редактируется.
  • Здесь есть одиночный хэш опций. Опции маршрутизации передаются в хэше :url, опции HTML передаются в хэше :html. Также для формы можно предоставить опцию :namespace, чтобы быть уверенным в уникальности атрибутов id элементов формы. Генерируемые для HTML id будут начинаться с префикса, заданного атрибутом namespace, плюс подчеркивание.
  • Метод form_for предоставляет объект form builder (переменная f).
  • Методы создания элементов управления формой вызываются для объекта form builder f.

Итоговый HTML:

<form class="nifty_form" id="new_article" action="/articles" accept-charset="UTF-8" method="post">
  <input name="utf8" type="hidden" value="&#x2713;" />
  <input type="hidden" name="authenticity_token" value="NRkFyRWxdYNfUg7vYxLOp2SLf93lvnl+QwDWorR42Dp6yZXPhHEb6arhDOIWcqGit8jfnrPwL781/xlrzj63TA==" />
  <input type="text" name="article[title]" id="article_title" />
  <textarea name="article[body]" id="article_body" cols="60" rows="12"></textarea>
  <input type="submit" name="commit" value="Create" data-disable-with="Create" />
</form>

Имя, переданное в form_for, контролирует ключ, используемый в params для доступа к значениям формы. В примере имя article, и поэтому все поля ввода имеют имена формы article[attribute_name]. Соответственно, в экшне create хэш params[:article] имеет ключи :title и :body. Подробнее о значении имен полей ввода можно прочитать в разделе про именование параметров.

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

Можно создать подобное привязывание без фактического создания тега <form> с помощью хелпера fields_for. Это полезно для редактирования дополнительных объектов модели в той же форме. Например, если имеется модель Person со связанной моделью ContactDetail, можно создать форму для создания обеих моделей подобным образом:

<%= form_for @person, url: {action: "create"} do |person_form| %>
  <%= person_form.text_field :name %>
  <%= fields_for @person.contact_detail do |contact_detail_form| %>
    <%= contact_detail_form.text_field :phone_number %>
  <% end %>
<% end %>

которая выдаст такой результат:

<form class="new_person" id="new_person" action="/people" accept-charset="UTF-8" method="post">
  <input name="utf8" type="hidden" value="&#x2713;" />
  <input type="hidden" name="authenticity_token" value="bL13x72pldyDD8bgtkjKQakJCpd4A8JdXGbfksxBDHdf1uC0kCMqe2tvVdUYfidJt0fj3ihC4NxiVHv8GVYxJA==" />
  <input type="text" name="person[name]" id="person_name" />
  <input type="text" name="contact_detail[phone_number]" id="contact_detail_phone_number" />
</form>

Объект, предоставляемый fields_for - это form builder, подобный тому, который предоставляется form_for (фактически form_for внутри себя вызывает fields_for).

2.3. Положитесь на идентификацию записи

Модель Article непосредственно доступна пользователям приложения, и таким образом, следуя лучшим рекомендациям разработки на Rails, вы должны объявить ее как ресурс.

resources :articles

Объявление ресурса имеет несколько побочных эффектов. Смотрите Роутинг в Rails для получения более подробной информации по настройке и использованию ресурсов.

Когда работаем с ресурсами RESTful, вызовы form_for могут стать значительно проще, если их основывать на идентификации записи. Вкратце, вы должны всего лишь передать экземпляр модели и позволить Rails выяснить имя модели и остальное:

## Создание новой статьи
# длинный стиль:
form_for(@article, url: articles_path)
# то же самое, короткий стиль (используется идентификация записи):
form_for(@article)

## Редактирование существующей статьи
# длинный стиль:
form_for(@article, url: article_path(@article), html: {method: "patch"})
# короткий стиль:
form_for(@article)

Отметьте, что вызов короткого стиля form_for является практичным, независимо от того, запись новая или уже существует. Идентификация записи достаточно сообразительная, чтобы выяснить, новая ли запись, запрашивая record.new_record?. Она также выбирает правильный путь для подтверждения и имя, основанное на классе объекта.

Rails также автоматически установит надлежащие атрибуты формы class и id: форма, создающая article, будет иметь id и class равные new_article. Если редактируется статья с id=23, то class будет установлен как edit_article, и id - как edit_article_23. Эти атрибуты будут опускаться для краткости далее в остальной части этого руководства.

Когда используется STI (single-table inheritance, наследование с единой таблицей) с вашими моделями, нельзя полагаться на идентификацию записей подкласса, если только их родительский класс определен ресурсом. Необходимо явно указывать имя модели, :url и :method.

2.3.1. Работаем с пространствами имен

Если создать пространство имен для маршрутов, form_for также можно изящно сократить. Если в приложении есть пространство имен admin, то

form_for [:admin, @article]

создаст форму, которая передается ArticlesController в пространстве имен admin (передача в admin_article_path(@article) в случае с обновлением). Если у вас несколько уровней пространства имен, тогда синтаксис подобный:

form_for [:admin, :management, @article]

Более подробно о системе маршрутизации Rails и связанным соглашениям смотрите Роутинг в Rails.

2.4. Как формы работают с методами PATCH, PUT или DELETE?

Фреймворк Rails поддерживает стиль RESTful в ваших приложениях, что означает частое использование запросов "PATCH" и "DELETE" (помимо "GET" и "POST"). Однако, большинство браузеров не поддерживают методы, отличные от "GET" и "POST", когда дело доходит до подтверждения форм.

Rails работает с этой проблемой, эмулируя другие методы с помощью POST со скрытым полем, названным "_method", который установлен для отображение желаемого метода:

form_tag(search_path, method: "patch")

результат:

<form accept-charset="UTF-8" action="/search" method="post">
  <input name="_method" type="hidden" value="patch" />
  <input name="utf8" type="hidden" value="&#x2713;" />
  <input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" />
  ...
</form>

При парсинге данных, отправленных с помощью POST, Rails принимает во внимание специальный параметр _method и ведет себя так, как будто бы в нем был определен этот метод HTTP ("PATCH" в этом примере).

3. Легкое создание списков выбора

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

Вот как может выглядеть разметка:

<select name="city_id" id="city_id">
  <option value="1">Lisbon</option>
  <option value="2">Madrid</option>
  ...
  <option value="12">Berlin</option>
</select>

Здесь есть перечень городов, имена которых представлены пользователю. Самому приложению для обработки нужен только их ID, поэтому он используется как атрибут value варианта выбора. Давайте посмотрим, как Rails может нам помочь.

3.1. Теги Select и Option

Наиболее простой хелпер - это select_tag, который, как следует из имени, просто сгенерирует тег SELECT, инкапсулирующий строку option (вариантов выбора):

<%= select_tag(:city_id, '<option value="1">Lisbon</option>...') %>

Это закладывает начало, но пока еще динамически не создает теги вариантов выбора. Вы можете сгенерировать теги option с помощью хелпера options_for_select:

<%= options_for_select([['Lisbon', 1], ['Madrid', 2], ...]) %>

результат:

<option value="1">Lisbon</option>
<option value="2">Madrid</option>
...

Первый аргумент для options_for_select - это вложенный массив, в котором каждый элемент содержит два элемента: текст варианта (название города) и значение варианта (id города). Значение варианта - это то, что будет передано в ваш контроллер. Часто бывает, что значение - это id соответствующего объекта базы данных, но это не всегда так.

Зная это, вы можете комбинировать select_tag и options_for_select для достижения желаемой полной разметки:

<%= select_tag(:city_id, options_for_select(...)) %>

options_for_select позволяет вам предварительно выбрать вариант, передав его значение.

<%= options_for_select([['Lisbon', 1], ['Madrid', 2], ...], 2) %>

результат:

<option value="1">Lisbon</option>
<option value="2" selected="selected">Madrid</option>
...

Всякий раз, когда Rails видит, что внутреннее значение сгенерированного варианта выбора соответствует этому значению, он добавит атрибут selected к этому варианту.

Когда отсутствует :include_blank или :prompt, :include_blank становится true, если атрибут required - true, отображаемый size - 1 и multiple не равен true.

С помощью хэшей можно добавить произвольные атрибуты в option:

<%= options_for_select(
  [
    ['Lisbon', 1, { 'data-size' => '2.8 million' }],
    ['Madrid', 2, { 'data-size' => '3.2 million' }]
  ], 2
) %>

результат:

<option value="1" data-size="2.8 million">Lisbon</option>
<option value="2" selected="selected" data-size="3.2 million">Madrid</option>
...

3.2. Списки выбора для работы с моделями

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

# контроллер:
@person = Person.new(city_id: 2)

# вьюха:
<%= select(:person, :city_id, [['Lisbon', 1], ['Madrid', 2], ...]) %>

Отметьте, что третий параметр - массив вариантов выбора - имеет тот же самый тип аргумента, что мы передавали в options_for_select. Одно из преимуществ заключается в том, что не стоит беспокоиться об предварительном выборе правильного города, если пользователь уже выбрал его - Rails сделает это за вас, прочитав из атрибута @person.city_id.

Как и в других хелперах, если хотите использовать хелпер select в form builder с областью видимостью объекта @person, синтаксис будет такой:

# select в form builder
<%= f.select(:city_id, ...) %>

В хелпер select также можно передать блок:

<%= f.select(:city_id) do %>
  <% [['Lisbon', 1], ['Madrid', 2]].each do |c| -%>
    <%= content_tag(:option, c.first, value: c.last) %>
  <% end %>
<% end %>

При использовании select (или подобных хелперов, таких как collection_select, select_tag), чтобы установить связь belongs_to, необходимо передать имя внешнего ключа (в примере выше city_id), а не само имя связи. Если указать city вместо city_id, Active Record вызовет ошибку в строчках ActiveRecord::AssociationTypeMismatch: City(#17815740) expected, got String(#1138750), когда будет передаваться хэш params в Person.new или update. Можно взглянуть на это по другому, что хелперы форм только редактируют атрибуты. Также стоит побеспокоиться о возможных последствиях безопасности, если разрешить пользователям редактировать внешние ключи напрямую.

3.3. Теги варианта выбора из коллекции произвольных объектов

Генерация тегов вариантов с помощью options_for_select требует, чтобы был создан массив, содержащий текст и значение для каждого варианта. Но что, если есть модель City (вероятно даже модель Active Record) и нужно сгенерировать теги вариантов из коллекции этих объектов? Одним из решений будет сделать вложенный массив с помощью итераций по ним:

<% cities_array = City.all.map { |city| [city.name, city.id] } %>
<%= options_for_select(cities_array) %>

Хотя это и валидное решение, но Rails предоставляет менее сложную альтернативу: options_from_collection_for_select. Этот хелпер принимает коллекцию произвольных объектов и два дополнительных аргумента: имена методов для считывания опций value и text, соответственно:

<%= options_from_collection_for_select(City.all, :id, :name) %>

Как следует из имени хелпера, это генерирует только теги option. Для генерации работающего списка выбора, его необходимо использовать в сочетании с select_tag, как это делалось для options_for_select. Когда работаем с объектами модели, так же как select комбинирует select_tag и options_for_select, collection_select комбинирует select_tag с options_from_collection_for_select.

<%= collection_select(:person, :city_id, City.all, :id, :name) %>

Как и с другими хелперами, если нужно использовать хелпер collection_select в связке с form builder, привязанным к объекту @person, синтаксис будет следующим:

<%= f.collection_select(:city_id, City.all, :id, :name) %>

Напомним, что options_from_collection_for_select в collection_select - то же самое, что и options_for_select в select.

Пары, переданные в options_for_select должны сперва иметь имя, затем id, однако для options_from_collection_for_select первый аргумент - это метод value, а второй аргумент - метод text.

3.4. Выбор часового пояса и страны

Для управления поддержкой часовых поясов в Rails, можно спрашивать своих пользователей, в какой зоне они находятся. Это потребует сгенерировать варианты выбора из списка предопределенных объектов TimeZone, используя collection_select, но вы можете просто использовать хелпер time_zone_select, который уже все это содержит:

<%= time_zone_select(:person, :time_zone) %>

Также есть хелпер time_zone_options_for_select для ручного (и поэтому гибко настраиваемого) способа осуществления этого. Читайте документацию по API, чтобы узнать о доступных аргументах для этих двух методов.

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

4. Использование хелперов даты и времени

Можно выбрать не использовать хелперы форм, генерирующие поля ввода даты и времени HTML5, а использовать альтернативные хелперы даты и времени. Эти хелперы даты и времени отличаются от остальных хелперов форм в двух важных аспектах:

  • Дата и время не представлены отдельным элементом ввода. Вместо них есть несколько, один на каждый компонент (год, месяц, день и т.д.), и поэтому нет одного значения в хэше params с вашими датой и временем.
  • Другие хелперы используют суффикс _tag для обозначения, является ли хелпер скелетным, либо работает на основе объектов модели. Что касается дат и времени, select_date, select_time и select_datetime - это скелетные хелперы, а date_select, time_select и datetime_select - это эквивалентные хелперы объекта модели.

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

4.1. Скелетные хелперы

Семейство хелперов select_* принимает как первый аргумент экземпляр Date, Time или DateTime, который используется как текущее выбранное значение. Можете опустить этот параметр в случае, если используется текущая дата. Например:

<%= select_date Date.today, prefix: :start_date %>

выведет (с опущенными для краткости начальными значениями вариантов)

<select id="start_date_year" name="start_date[year]"> ... </select>
<select id="start_date_month" name="start_date[month]"> ... </select>
<select id="start_date_day" name="start_date[day]"> ... </select>

Эти элементы ввода приведут к тому, что params[:start_date] будет хэшем с ключами :year, :month, :day. Чтобы получить фактический объект Date, Time или DateTime, необходимо извлечь эти значения и передать их в подходящий конструктор, например:

Date.civil(params[:start_date][:year].to_i, params[:start_date][:month].to_i, params[:start_date][:day].to_i)

Опция :prefix это ключ, используемый для получения хэша компонентов даты из хэша params. Здесь она была установлена как start_date, если опущена, то по умолчанию равна date.

4.2. Хелперы объекта модели

select_date не очень хорошо работает с формами, обновляющими или создающими объекты Active Record, так как Active Record ожидает, что каждый элемент хэша params соответствует одному атрибуту. Хелперы объекта модели для даты и времени возвращает параметры со специальными именами, и когда Active Record видит параметры с такими именами, он знает, что они должны быть скомбинированы с другими параметрами, и передает конструктору значения, соответствующему типу столбца. Например:

<%= date_select :person, :birth_date %>

выдаст (с опущенными для краткости начальными значениями вариантов)

<select id="person_birth_date_1i" name="person[birth_date(1i)]"> ... </select>
<select id="person_birth_date_2i" name="person[birth_date(2i)]"> ... </select>
<select id="person_birth_date_3i" name="person[birth_date(3i)]"> ... </select>

что приведет к такому результату в хэше params

{'person' => {'birth_date(1i)' => '2008', 'birth_date(2i)' => '11', 'birth_date(3i)' => '22'}}

Когда это передано в Person.new (или update), Active Record отмечает, что все эти параметры должны быть использованы для конструирования атрибута birth_date и использует суффиксную информацию для определения, в каком порядке должен передать эти параметры в такие функции, как Date.civil.

4.3. Общие опции

Оба семейства хелперов используют одинаковый базовый набор функций для генерации индивидуальных тегов select, таким образом, они оба принимают в основном одинаковые опции. В частности, по умолчанию Rails сгенерирует варианты выбора года как текущий год плюс/минус пять лет. Если это неподходящий интервал, опции :start_year и :end_year переопределяют это. Для полного списка доступных опции обратитесь к документации по API.

Как правило, следует использовать date_select при работе с объектами модели и select_date в иных случаях, например, таких как форма поиска, которая фильтрует результаты по дате.

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

4.4. Индивидуальные компоненты

Иногда необходимо отобразить лишь одиночный компонент даты, такой как год или месяц. Rails предоставляет ряд хелперов для этого, по одному для каждого компонента select_year, select_month, select_day, select_hour, select_minute, select_second. Эти хелперы достаточно простые. По умолчанию они сгенерируют поле ввода, названное по имени компонента времени (например, "year" для select_year, "month" для select_month и т.д.), хотя это может быть переопределено в опции :field_name. Опция :prefix работает так же, как работает для select_date и select_time и имеет такое же значение по умолчанию.

Первый параметр определяет значение даты, которое будет предложено для выбора (соответствующий элемент может быть извлечен из экземпляра Date, Time или DateTime), либо сразу задается числовым значением. Например:

<%= select_year(2011) %>
<%= select_year(Time.now) %>

создаст такой же результат, если сейчас 2011 год, и значение, выбранное пользователем, может быть получено как params[:date][:year].

5. Загрузка файлов

Частой задачей является загрузка некоторого файла, аватарки или файла CSV, содержащего информацию для обработки. Самое важное, это помнить при загрузке файла, что кодирование формы ДОЛЖНО быть установлено как "multipart/form-data". Если используете form_for, это будет выполнено автоматически. Если используете form_tag, нужно установить это самому, как в следующем примере.

Следующие две формы обе загружают файл.

<%= form_tag({action: :upload}, multipart: true) do %>
  <%= file_field_tag 'picture' %>
<% end %>

<%= form_for @person do |f| %>
  <%= f.file_field :picture %>
<% end %>

Rails предоставляет обычную пару хелперов: скелетный file_field_tag и модельно-ориентированный file_field. Единственное отличие от других хелперов в том, что нельзя установить значение по умолчанию для поля загрузки файла, так как в этом нет смысла. Как и следует ожидать, в первом случае загруженный файл находится в params[:picture], а во втором случае - в params[:person][:picture].

5.1. Что имеем загруженным

Объект в хэше params - это экземпляр подкласса IO. В зависимости от размера загруженного файла, это может оказаться либо StringIO, либо File, отраженный на временный файл. В обоих случаях объект будет иметь атрибут original_filename, содержащий имя файла на компьютере пользователя, и атрибут content_type, содержащий тип MIME загруженного файла. Следующий отрывок сохраняет загруженное содержимое в #{Rails.root}/public/uploads под тем же именем, что и исходный файл (при условии, что эта была одна из форм предыдущего примера).

def upload
  uploaded_io = params[:person][:picture]
  File.open(Rails.root.join('public', 'uploads', uploaded_io.original_filename), 'wb') do |file|
    file.write(uploaded_io.read)
  end
end

Как только файл был загружен, появляется множество потенциальных задач, начиная от того, где хранить файлы (на диске, Amazon S3 и т.д.), и как связать их с моделями, до изменения размера файлов изображений и генерации эскизов. Такие частности выходят за рамки данного руководства, но имеется несколько библиотек, разработанных для содействия этому. Две лучших из них - это CarrierWave и Paperclip.

Если пользователь не выбрал файл, соответствующий параметр останется пустой строкой.

5.2. Работа с Ajax

В отличие от других форм, создание формы асинхронной загрузки файла - это не просто передача параметра remote: true в form_for. В Ajax-форме сериализация происходит посредством JavaScript, исполняющимся внутри браузера, и, поскольку JavaScript не может читать файлы с жесткого диска, файл не может быть загружен. Наиболее частым решением является использование невидимого iframe, который служит целью для отправки формы.

6. Настройка Form Builder

Как ранее упоминалось, объект, который передается от form_for и fields_for, - это экземпляр FormBuilder (или его подкласса). Form builder инкапсулирует представление элементов формы для отдельного объекта. Хотя, конечно, можно писать хелперы для своих форм обычным способом, также как можно объявить подкласс FormBuilder и добавить хелперы туда. Например:

<%= form_for @person do |f| %>
  <%= text_field_with_label f, :first_name %>
<% end %>

может быть заменено этим

<%= form_for @person, builder: LabellingFormBuilder do |f| %>
  <%= f.text_field :first_name %>
<% end %>

через определение класса LabellingFormBuilder подобным образом:

class LabellingFormBuilder < ActionView::Helpers::FormBuilder
  def text_field(attribute, options={})
    label(attribute) + super
  end
end

Если это используется часто, можно определить хелпер labeled_form_for который автоматически определяет опцию builder: LabellingFormBuilder:

def labeled_form_for(record, options = {}, &block)
  options.merge! builder: LabellingFormBuilder
  form_for record, options, &block
end

Form builder также определяет, что произойдет, если вы сделаете

<%= render partial: f %>

Если f - это экземпляр FormBuilder, тогда это отрендерит партиал form, установив объект партиала как form builder. Если у form builder есть класс LabellingFormBuilder, тогда вместо него будет отрендерен партиал labelling_form.

7. Понимание соглашений по именованию параметров

Как можно было заметить в предыдущих разделах, значения из форм могут быть на верхнем уровне хэша params или вложены в другой хэш. Например, в стандартном экшне create для модели Person, params[:person] будет обычно хэшем всех атрибутов для создания персоны. Хэш params может также содержать массивы, массивы хэшей и тому подобное.

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

7.1. Базовые структуры

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

<input id="person_name" name="person[name]" type="text" value="Henry"/>

хэш params будет содержать

{'person' => {'name' => 'Henry'}}

и params[:person][:name] получит отправленное значение в контроллере.

Хэши могут быть вложены на столько уровней, сколько требуется, например:

<input id="person_address_city" name="person[address][city]" type="text" value="New York"/>

вернет такой хэш params

{'person' => {'address' => {'city' => 'New York'}}}

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

<input name="person[phone_number][]" type="text"/>
<input name="person[phone_number][]" type="text"/>
<input name="person[phone_number][]" type="text"/>

Что приведет к тому, что params[:person][:phone_number] будет массивом, содержащим введенные телефонные номера.

7.2. Комбинируем их

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

<input name="addresses[][line1]" type="text"/>
<input name="addresses[][line2]" type="text"/>
<input name="addresses[][city]" type="text"/>

Это приведет к тому, что params[:addresses] будет массивом хэшей с ключами line1, line2 и city. Rails начинает накапливать значения в новый хэш, всякий раз, когда он встречает имя поля ввода, уже существующее в текущем хэше.

Однако, имеется ограничение, в то время как хэши могут быть вложены произвольно, является допустимым только один уровень "массивности". Массивы обычно могут быть заменены хэшами; например, вместо массива объектов модели можно иметь хэш объектов модели с ключами, равными их id, индексу массива или любому другому параметру.

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

7.3. Использование хелперов форм

Предыдущие разделы совсем не использовали хелперы Rails. Хотя можно создавать имена полей ввода самому и передавать их напрямую хелперам, таким как text_field_tag, Rails также предоставляет поддержку на более высоком уровне. В вашем распоряжении имеется два инструмента: параметр имени для form_for и fields_for, и опция :index, принимаемая этими хелперами.

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

<%= form_for @person do |person_form| %>
  <%= person_form.text_field :name %>
  <% @person.addresses.each do |address| %>
    <%= person_form.fields_for address, index: address.id do |address_form|%>
      <%= address_form.text_field :city %>
    <% end %>
  <% end %>
<% end %>

Предположим, у кого-то есть два адреса с id 23 и 45, это создаст что-то подобное:

<form accept-charset="UTF-8" action="/people/1" class="edit_person" id="edit_person_1" method="post">
  <input id="person_name" name="person[name]" type="text" />
  <input id="person_address_23_city" name="person[address][23][city]" type="text" />
  <input id="person_address_45_city" name="person[address][45][city]" type="text" />
</form>

Это приведет к тому, что хэш params будет выглядеть так

{'person' => {'name' => 'Bob', 'address' => {'23' => {'city' => 'Paris'}, '45' => {'city' => 'London'}}}}

Rails знает, что все эти поля ввода должны быть частью хэша person, так как fields_for вызывается для первого form builder. Определив опцию :index, сообщается Rails, что вместо именования полей ввода person[address][city], он должен вставить индекс, заключенный в [], между address и city. Это часто бывает полезно, так как тогда просто обнаружить, какая запись Address должна быть изменена. Также можно передавать числа с некоторым другим значением, строками или даже nil (который приведет к созданию параметра в массиве).

Чтобы создать более замысловатые вложения, можно явно указать первую часть имени поля ввода (person[address] в предыдущем примере):

<%= fields_for 'person[address][primary]', address, index: address do |address_form| %>
  <%= address_form.text_field :city %>
<% end %>

создаст такие поля ввода

<input id="person_address_primary_1_city" name="person[address][primary][1][city]" type="text" value="bologna" />

Как правило, конечное имя поля ввода - это сцепление имени, переданного в fields_for/form_for, значения индекса и имени атрибута. Можно также передать опцию :index прямо в хелперы, такие как text_field, но обычно будет меньше повторов, если определить это на уровне form builder, а не для отдельных элементах управления input.

Как ярлык вы можете добавить [] к имени и опустить опцию :index. Это то же самое, что определение index: address, таким образом

<%= fields_for 'person[address][primary][]', address do |address_form| %>
  <%= address_form.text_field :city %>
<% end %>

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

8. Формы к внешним ресурсам

Хелперы форм Rails можно использовать и для создания форм для передачи данных внешнему ресурсу. Однако, иногда необходимо установить authenticity_token для ресурса; это можно осуществить, передав параметр authenticity_token: 'your_external_token' в опциях form_tag:

<%= form_tag 'http://farfar.away/form', authenticity_token: 'external_token' do %>
  Form contents
<% end %>

Иногда при отправке данных внешнему ресурсу, такому как платежный шлюз, поля, которые можно использовать в форме, ограничены внешним API, и генерация authenticity_token нежелательна. Чтобы не посылать токен, просто передайте false в опцию :authenticity_token:

<%= form_tag 'http://farfar.away/form', authenticity_token: false do %>
  Form contents
<% end %>

Та же техника также доступна и для form_for:

<%= form_for @invoice, url: external_url, authenticity_token: 'external_token' do |f| %>
  Form contents
<% end %>

Или, если не хотите создавать поле authenticity_token:

<%= form_for @invoice, url: external_url, authenticity_token: false do |f| %>
  Form contents
<% end %>

9. Создание сложных форм

Многие приложения выходят за рамки простых форм, редактирующих одиночные объекты. Например, при создании Person можно позволить пользователю (в той же самой форме) создать несколько записей адресов (домашний, рабочий и т.д.). При последующем редактировании этого person, пользователю должно быть доступно добавление, удаление или правка адреса, если это необходимо.

9.1. Настройка модели

Active Record предоставляет поддержку на уровне модели с помощью метода accepts_nested_attributes_for:

class Person < ApplicationRecord
  has_many :addresses, inverse_of: :person
  accepts_nested_attributes_for :addresses

end

class Address < ApplicationRecord
  belongs_to :person
end

Это создаст метод addresses_attributes= в Person, позволяющий создавать, обновлять и (опционально) уничтожать адреса.

9.2. Вложенные формы

Следующая форма позволяет пользователю создать Person и связанные с ним адреса.

<%= form_for @person do |f| %>
  Addresses:
  <ul>
    <%= f.fields_for :addresses do |addresses_form| %>
      <li>
        <%= addresses_form.label :kind %>
        <%= addresses_form.text_field :kind %>

        <%= addresses_form.label :street %>
        <%= addresses_form.text_field :street %>
        ...
      </li>
    <% end %>
  </ul>
<% end %>

Когда связь принимает вложенные атрибуты, fields_for рендерит свой блок для каждого элемента связи. В частности, если у person нет адресов, он ничего не рендерит. Обычным паттерном для контроллера является построение одного или более пустых дочерних элементов, чтобы как минимум один набор полей был показан пользователю. Следующий пример покажет 2 набора полей адресов в форме нового person.

def new
  @person = Person.new
  2.times { @person.addresses.build}
end

fields_for вкладывает form builder. Имя параметра будет таким, какое ожидает accepts_nested_attributes_for. К примеру, при создании пользователя с 2 адресами, отправленные параметры будут выглядеть так

{
  'person' => {
    'name' => 'John Doe',
    'addresses_attributes' => {
      '0' => {
        'kind' => 'Home',
        'street' => '221b Baker Street'
      },
      '1' => {
        'kind' => 'Office',
        'street' => '31 Spooner Street'
      }
    }
  }
}

Ключи хэша :addresses_attributes не важны, они всего лишь должны быть различными для каждого адреса.

Если связанный объект уже сохранен, fields_for автоматически генерирует скрытое поле с id сохраненной записи. Это можно отключить, передав include_id: false в fields_for. Возможно, потребуется сделать это, если автоматически сгенерированное поле будет размещено там, где тег input не имеет валидного HTML, или при использовании ORM, когда дочерние элементы не имеют id.

9.3. Контроллер

Как обычно, в контроллере необходим белый список параметров, который вы передаёте в модель:

def create
  @person = Person.new(person_params)
  # ...
end

private
  def person_params
    params.require(:person).permit(:name, addresses_attributes: [:id, :kind, :street])
  end

9.4. Удаление объектов

Можно позволить пользователям удалять связанные объекты, передав allow_destroy: true в accepts_nested_attributes_for

class Person < ApplicationRecord
  has_many :addresses
  accepts_nested_attributes_for :addresses, allow_destroy: true
end

Если хэш атрибутов для объекта содержит ключ _destroy со значением '1' или 'true', тогда объект будет уничтожен. Эта форма позволяет пользователям удалять адреса:

<%= form_for @person do |f| %>
  Addresses:
  <ul>
    <%= f.fields_for :addresses do |addresses_form| %>
      <li>
        <%= addresses_form.check_box :_destroy %>
        <%= addresses_form.label :kind %>
        <%= addresses_form.text_field :kind %>
        ...
      </li>
    <% end %>
  </ul>
<% end %>

Не забудьте обновить белый список параметров в вашем контроллере, а также включить туда поле _destroy:

def person_params
  params.require(:person).
    permit(:name, addresses_attributes: [:id, :kind, :street, :_destroy])
end

9.5. Предотвращение пустых записей

Часто полезно игнорировать наборы полей, которые пользователь не заполнял. Этим можно управлять, передав :reject_if proc в accepts_nested_attributes_for. Этот proc будет вызван для каждого хэша атрибутов, отправляемого формой. Если proc возвращает false, тогда Active Record не создаст связанный объект для этого хэша. Следующий пример пытается создать адрес, если установлен атрибут kind.

class Person < ApplicationRecord
  has_many :addresses
  accepts_nested_attributes_for :addresses, reject_if: lambda {|attributes| attributes['kind'].blank?}
end

Вместо этого для удобства можно передать символ :all_blank, который создаст proc, который отвергнет записи, когда все атрибуты пустые, за исключением любого значения для _destroy.

9.6. Добавление полей на лету

Вместо того, чтобы рендерить несколько наборов полей раньше времени, можно добавить их только тогда, когда пользователь нажимает на кнопку 'Добавить новый адрес'. Rails не предоставляет какой-либо встроенной поддержки для этого. При генерации новых наборов полей следует убедиться, что ключ связанного массива уникальный - наиболее распространенным выбором является текущий JavaScript date (миллисекунды после epoch).