Хелперы форм

Формы в веб-приложениях - это основной интерфейс для пользовательского ввода. Однако, разметка форм может быстро стать нудной в написании и поддержке из-за необходимости описывать элементы форм и их бесчисленные атрибуты. 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 контроллера со значением, введенным пользователем в это поле. Например, если форма содержит `<%= textfield_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_field(:user, :meeting_time) %>
<%= 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_meeting_time" name="user[meeting_time]" type="datetime" />
<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 установит значение элемента управления равным возвращаемому значению метода объекта и установит подходящее имя поля. Если ваш контроллер определил @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.

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

def new
  @article = Article.new
end

Соответствующая вьюха 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 элементов формы. Атрибут namespace будет префиксом с подчеркиванием в генерируемых для HTML id.
  • Метод form_for предоставляет объект form builder (переменная f).
  • Методы создания элементов управления формой вызываются для объекта form builder f.

Итоговый HTML:

<form accept-charset="UTF-8" action="/articles" method="post" class="nifty_form">
  <input id="article_title" name="article[title]" type="text" />
  <textarea id="article_body" name="article[body]" cols="60" rows="12"></textarea>
  <input name="commit" type="submit" value="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 accept-charset="UTF-8" action="/people" class="new_person" id="new_person" method="post">
  <input id="person_name" name="person[name]" type="text" />
  <input id="contact_detail_phone_number" name="contact_detail[phone_number]" type="text" />
</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: форма, создающая статью, будет иметь 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, инкапсулирующий строку опций:

<%= 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 к нему.

Второй аргумент для options_for_select должен быть точно равен желаемому внутреннему значению. В частности, если значение - число 2, вы не можете передать "2" в options_for_select - вы должны передать 2. Имейте это в виду при использовании значений, извлеченных из хэша params, так как они всегда строковые.

Когда отсутствует :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 первый аргумент - это метод значения, а второй аргумент - метод текста.

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, чем для отдельного элемента ввода.

Как ярлык вы можете добавить [] к имени и опустить опцию :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 вы, возможно, захотите позволить пользователю (в той же самой форме) создать несколько записей адресов (домашний, рабочий и т.д.). Позже, редактируя этого человека, пользователю должно быть доступно добавление, удаление или правка адреса, если необходимо.

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

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

class Person < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :addresses
  accepts_nested_attributes_for :addresses

end

class Address < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  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  # ActiveRecord::Base до Rails 5.0
  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  # ActiveRecord::Base до Rails 5.0
  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).