Обзор Action View

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

  • Что такое Action View, и как его использовать вместе с Rails.
  • Как лучше использовать шаблоны, партиалы и макеты.
  • Как использовать локализованные вью.

1. Что такое Action View?

Action View - это V в MVC. Action Controller и Action View работают вместе для обработки веб-запросов. Action Controller отвечает за взаимодействие с уровнем модели (в MVC) и получение данных. Затем Action View отвечает за отрисовку тела ответа на веб-запрос, используя эти данные.

По умолчанию шаблоны Action View (также называемые просто "вью") пишутся с использованием Embedded Ruby (ERB), который позволяет использовать код Ruby внутри HTML-документов.

Action View предоставляет множество вспомогательных методов - хелперов для динамического создания HTML-тегов для форм, дат и строк. Также можно добавлять в ваше приложение собственные хелперы по мере необходимости.

Action View может использовать функции Active Model, такие как to_param и to_partial_path для упрощения кода. Это не означает, что Action View зависит от Active Model. Action View является независимым пакетом, который можно использовать с любой библиотекой Ruby.

2. Использование Action View с Rails

Шаблоны Action View (также называемые "вью") хранятся во вложенных папках внутри директории app/views. Там есть вложенная папка, соответствующая имени каждого контроллера. Файлы вью внутри этой папки используются для отрисовки конкретных вью в ответ на экшны контроллера.

Например, при использовании генератора скаффолда для создания ресурса article, Rails создает следующие файлы в app/views/articles:

$ bin/rails generate scaffold article
      [...]
      invoke  scaffold_controller
      create    app/controllers/articles_controller.rb
      invoke    erb
      create      app/views/articles
      create      app/views/articles/index.html.erb
      create      app/views/articles/edit.html.erb
      create      app/views/articles/show.html.erb
      create      app/views/articles/new.html.erb
      create      app/views/articles/_form.html.erb
      [...]

Именование файлов следует соглашениям Rails. Их названия совпадают с названиями связанных экшнов контроллера. Например, index.html.erb, edit.html.erb и т.д.

Соблюдая это соглашение, Rails автоматически найдет и отрисует соответствующую вью в конце экшна контроллера, без необходимости его явного указания. Например, экшн index в articles_controller.rb автоматически отрисует вью index.html.erb из директории app/views/articles/. Важны как название файла, так и его расположение.

Итоговый HTML, возвращаемый клиенту, формируется из комбинации файла ERB .html.erb, обертывающего его шаблона макета и всех партиалов, на которые может ссылаться файл ERB. В оставшейся части этого руководства вы найдете более подробную информацию о каждом из трех компонентов: шаблонов (Templates), партиалов (Partials) и макетов (Layouts).

3. Шаблоны

Шаблоны Action View могут быть написаны в разных форматах. Если файл шаблона имеет расширение .erb, он использует встроенный Ruby для создания HTML-ответа. Если шаблон имеет расширение .jbuilder, он использует гем Jbuilder для создания JSON-ответа. А шаблон с расширением .builder использует библиотеку Builder::XmlMarkup для создания XML-ответа.

Rails использует расширение файла для различения различных систем шаблонов. Например, HTML-файл, использующий систему шаблонов ERB, будет иметь расширение .html.erb, а JSON-файл, использующий систему шаблонов Jbuilder, будет иметь расширение .json.jbuilder. Другие библиотеки также могут добавлять свои типы шаблонов и соответствующие им расширения файлов.

3.1. ERB

ERB шаблон - это способ внедрить код Ruby внутрь статического HTML с помощью специальных тегов ERB, таких как <% %> и <%= %>.

При обработке Rails вью в формате ERB, заканчивающихся на .html.erb, он выполняет встроенный код Ruby и заменяет теги ERB на динамический вывод. Это динамическое содержимое объединяется со статической разметкой HTML для формирования окончательного HTML-ответа.

Внутри ERB-шаблона Ruby-код можно включать с помощью тегов <% %> и <%= %>. Тег <% %> (без =) используется, когда вы хотите выполнить код Ruby, но не выводить его результат напрямую, например, для условий или циклов. Тег <%= %> используется для кода Ruby, который генерирует вывод, который вы хотите отобразить внутри шаблона. Например, для вывода атрибута модели, как person.name в этом примере:

<h1>Names</h1>
<% @people.each do |person| %>
  Name: <%= person.name %><br>
<% end %>

Цикл настроен с помощью обычных встраиваемых тегов (<% %>), а имя вставлено с помощью выводящих встраиваемых тегов (<%= %>).

Имейте в виду, что функции вроде print и puts не будут отображаться во вью при использовании ERB-шаблонов. Таким образом, следующий код не будет работать:

<%# WRONG %>
Hi, Mr. <% puts "Frodo" %>

В предыдущем примере показано, что комментарии в ERB можно добавлять с помощью тега <%# %>.

Для подавления ведущих и завершающих пробелов вы можете использовать <%- -%> взаимозаменяемо с <% и %>

3.2. Jbuilder

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

Если у вас его нет, вы можете добавить следующую строку в ваш Gemfile:

gem "jbuilder"

Объект Jbuilder с именем json автоматически становится доступным для шаблонов с расширением .jbuilder.

Вот базовый пример:

json.name("Alex")
json.email("alex@example.com")

что произведет:

{
  "name": "Alex",
  "email": "alex@example.com"
}

Больше примеров смотрите в документации Jbuilder.

3.3. Builder

Шаблоны Builder — это более программная альтернатива ERB. Они похожи на JBuilder, но используются для создания XML, а не JSON.

Объект XmlMarkup с именем xml автоматически становится доступным для шаблонов с расширением .builder.

Вот простой пример:

xml.em("emphasized")
xml.em { xml.b("emph & bold") }
xml.a("A Link", "href" => "https://rubyonrails.org")
xml.target("name" => "compile", "option" => "fast")

которые создадут:

<em>emphasized</em>
<em><b>emph &amp; bold</b></em>
<a href="https://rubyonrails.org">A link</a>
<target option="fast" name="compile" />

Любой метод с блоком будет трактован как разметка тега XML с вложенной разметкой в блоке. Например, следующее:

xml.div {
  xml.h1(@person.name)
  xml.p(@person.bio)
}

создаст что-то вроде:

<div>
  <h1>David Heinemeier Hansson</h1>
  <p>A product of Danish Design during the Winter of '79...</p>
</div>

Больше примеров смотрите в документации Builder.

3.4. Компиляция шаблонов

По умолчанию Rails компилирует каждый шаблон в метод для его отрисовки. В среде development при изменении шаблона Rails проверяет время модификации файла и перекомпилирует его.

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

4. Партиалы

Частичные шаблоны, обычно просто называемые "партиалами", - это способ разбить шаблоны вью на более мелкие фрагменты, пригодные для повторного использования. С помощью партиалов вы можете извлечь часть кода из вашего основного шаблона в отдельный меньший файл и отрисовать этот файл в основном шаблоне. Вы также можете передавать данные в файлы партиалов из основного шаблона.

Давайте разберемся на практике с помощью примеров:

4.1. Рендеринг партиалов

Для того, чтобы отрисовать партиал внутри вью, используйте метод render внутри этого вью:

<%= render "product" %>

Этот код будет искать файл с названием _product.html.erb в той же папке, чтобы отрисовать его внутри этой вью. По соглашению, имена файлов партиалов начинаются с подчеркивания. Это помогает отличать их от обычных файлов вью. Однако, при ссылке на партиал внутри вью, само подчеркивание не используется. Это верно даже при ссылке на партиал из другой директории:

<%= render "application/product" %>

Этот код найдет и отрисует файл партиала _product.html.erb в app/views/application/.

4.2. Использование партиалов для упрощения вью

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

<%= render "application/ad_banner" %>

<h1>Products</h1>

<p>Here are a few of our fine products:</p>
<% @products.each do |product| %>
  <%= render partial: "product", locals: { product: product } %>
<% end %>

<%= render "application/footer" %>

Здесь партиалы _ad_banner.html.erb и _footer.html.erb могут содержать контент, который используется на многих страницах вашего приложения. Вам не нужно видеть детали этих разделов, когда вы фокусируетесь на странице продуктов.

В приведенном выше примере также используется партиал _product.html.erb. Этот партиал содержит детали для отрисовки отдельного продукта и используется для отрисовки каждого продукта из коллекции @products.

4.3. Передача данных в партиалы с помощью опции locals

При отрисовке партиала вы можете передавать данные из основной вью в партиал. Для этого используется хэш опций locals:. Каждый ключ в опции locals: доступен как локальная переменная партиала:

<%# app/views/products/show.html.erb %>

<%= render partial: "product", locals: { my_product: @product } %>

<%# app/views/products/_product.html.erb %>

<%= tag.div id: dom_id(my_product) do %>
  <h1><%= my_product.name %></h1>
<% end %>

"Локальная переменная партиала" - это переменная, которая существует только внутри данного партиала и доступна только из него. В приведенном выше примере my_product является локальной переменной партиала. Ей было присвоено значение @product при передаче в партиал из исходной вью.

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

Поскольку locals - это хэш, вы можете передавать несколько переменных по мере необходимости, например locals: { my_product: @product, my_reviews: @reviews }.

Однако, если шаблон ссылается на переменную, которая не передана в представление как часть опции locals:, шаблон выдаст ошибку ActionView::Template::Error:

<%# app/views/products/_product.html.erb %>

<%= tag.div id: dom_id(my_product) do %>
  <h1><%= my_product.name %></h1>

  <%# => raises ActionView::Template::Error for `product_reviews` %>
  <% product_reviews.each do |review| %>
    <%# ... %>
  <% end %>
<% end %>

4.4. Использование local_assigns

Каждый партиал имеет метод под названием local_assigns. Вы можете использовать этот метод для доступа к ключам, переданным через опцию locals:. Если партиал был отрисован без установленного :some_key, значение local_assigns[:some_key] внутри партиала будет равно nil.

Например, product_reviews будет иметь значение nil в приведенном ниже примере, поскольку в locals: установлено только значение product:

<%# app/views/products/show.html.erb %>

<%= render partial: "product", locals: { product: @product } %>

<%# app/views/products/_product.html.erb %>

<% local_assigns[:product]          # => "#<Product:0x0000000109ec5d10>" %>
<% local_assigns[:product_reviews]  # => nil %>

Одним из вариантов использования local_assigns является необязательная передача локальной переменной, а затем условное выполнение действия в партиале в зависимости от того, установлена ли локальная переменная. Например:

<% if local_assigns[:redirect] %>
  <%= form.hidden_field :redirect, value: true %>
<% end %>

Еще один пример из _blob.html.erb Active Storage. В данном случае размер устанавливается в зависимости от того, установлена ли локальная переменная in_gallery при отрисовке партиала, который содержит эту строку:

<%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>

4.5. render без опций partial и locals

В предыдущих примерах render принимает 2 опции: partial и locals. Однако, если вам нужны только эти опции, вы можете пропустить ключи partial и locals и указать только значения.

Например, вместо:

<%= render partial: "product", locals: { product: @product } %>

Можно написать:

<%= render "product", product: @product %>

Вы также можете использовать эту краткую запись, основанную на соглашениях:

<%= render @product %>

Этот код будет искать партиал с названием _product.html.erb в app/views/products/. Дополнительно, он передаст локальную переменную product со значением @product.

4.6. Опции as и object

По умолчанию, объекты, передаваемые в шаблон, находятся в локальной переменной с тем же именем, что и шаблон. Таким образом, при условии, что:

<%= render @product %>

внутри партиала _product.html.erb вы получите переменную экземпляра @product в локальной переменной product, как если бы вы написали:

<%= render partial: "product", locals: { product: @product } %>

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

Например, вместо:

<%= render partial: "product", locals: { product: @item } %>

можно написать:

<%= render partial: "product", object: @item %>

Эта строка присваивает переменную экземпляра @item локальной переменной партиала с именем product. А что, если вы хотите изменить имя локальной переменной с product по умолчанию на что-то другое? Для этого можно использовать опцию :as.

С помощью опции :as вы можете указать другое имя для локальной переменной, например:

<%= render partial: "product", object: @item, as: "item" %>

Это эквивалентно:

<%= render partial: "product", locals: { item: @item } %>

4.7. Рендеринг коллекций

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

Поэтому такой пример для рендеринга всех продуктов:

<% @products.each do |product| %>
  <%= render partial: "product", locals: { product: product } %>
<% end %>

может быть переписан с помощью одной строчки:

<%= render partial: "product", collection: @products %>

Когда партиал вызывается с коллекцией, у отдельных экземпляров партиала есть доступ к рендерящемуся члену коллекции через переменную с именем партиала. В этом случае, поскольку партиал называется _product.html.erb, вы можете использовать product для ссылки на элемент коллекции, который отрисовывается.

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

<%= render @products %>

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

4.8. Разделяющие шаблоны

Также можете определить второй партиал, который будет отрендерен между экземплярами главного партиала, используя опцию :spacer_template:

<%= render partial: @products, spacer_template: "product_ruler" %>

Rails отрендерит партиал _product_ruler.html.erb (без переданных в него данных) между каждой парой партиалов _product.html.erb.

4.9. Переменные счетчика

Rails также предоставляет внутри партиала, вызываемого из коллекции, счетчик. Эта переменная названа по имени партиала с добавлением суффикса _counter. Например, при отрисовке коллекции @products партиал _product.html.erb может получить доступ к переменной product_counter. Эта переменная указывает на количество раз, которое партиал был отрисован внутри ограничивающей его вью, начиная с значения 0 при первой отрисовке.

<%# index.html.erb %>
<%= render partial: "product", collection: @products %>
<%# _product.html.erb %>
<%= product_counter %> # 0 для первого продукта, 1 для второго продукта...

То же самое работает, когда вы изменяете имя локальной переменной с помощью опции as:. Например, если бы вы использовали as: :item в своем коде, тогда счетчик назывался бы item_counter.

Примечание: Следующие два раздела, Строгие локальные переменные и local_assigns с соответствием образцу, посвящены более продвинутым функциям использования партиалов и включены сюда для полноты картины.

4.10. local_assigns с соответствием образцу

Поскольку local_assigns является Hash, он совместим с оператором присваивания с помощью соответствия образцу в Ruby 3.1:

local_assigns => { product:, **options }
product # => "#<Product:0x0000000109ec5d10>"
options # => {}

Когда ключи, кроме :product, присваиваются локальной хэш-переменной для партиала, их можно включить в вызов вспомогательных методов:

<%# app/views/products/_product.html.erb %>

<% local_assigns => { product:, **options } %>

<%= tag.div id: dom_id(product), **options do %>
  <h1><%= product.name %></h1>
<% end %>

<%# app/views/products/show.html.erb %>

<%= render "products/product", product: @product, class: "card" %>
<%# => <div id="product_1" class="card">
  #      <h1>A widget</h1>
  #    </div>
%>

Присваивание с помощью соответствия образцу также поддерживает переименование переменных:

local_assigns => { product: record }
product             # => "#<Product:0x0000000109ec5d10>"
record              # => "#<Product:0x0000000109ec5d10>"
product == record   # => true

Вы также можете условно читать переменную, а затем переходить к значению по умолчанию, если ключ не входит в опцию locals:, используя метод fetch:

<%# app/views/products/_product.html.erb %>

<% local_assigns.fetch(:related_products, []).each do |related_product| %>
  <%# ... %>
<% end %>

Совмещение оператора присваивания с помощью соответствия образцу в Ruby 3.1 с вызовами Hash#with_defaults позволяет компактно назначать значения по умолчанию локальным переменным в партиале.

<%# app/views/products/_product.html.erb %>

<% local_assigns.with_defaults(related_products: []) => { product:, related_products: } %>

<%= tag.div id: dom_id(product) do %>
  <h1><%= product.name %></h1>

  <% related_products.each do |related_product| %>
    <%# ... %>
  <% end %>
<% end %>

По умолчанию партиалы принимают любые locals в качестве именованных аргументов. Чтобы определить, какие locals принимает партиал, используйте специальный комментарий `locals:. Подробнее об этом можно узнать в разделе Строгие локальные переменные.

4.11. Строгие локальные переменные

Партиалы Action View принимают любое количество locals в качестве именованных аргументов. С помощью специального комментария locals: вы можете контролировать, сколько и какие locals принимает шаблон, устанавливать значения по умолчанию и многое другое.

Ниже приведены примеры использования специального комментария locals:.

<%# app/views/messages/_message.html.erb %>

<%# locals: (message:) -%>
<%= message %>

Вышеуказанный код делает message обязательной локальной переменной. Рендеринг партиала без аргумента локальной переменной :message приведет к ошибке:

render "messages/message"
# => ActionView::Template::Error: missing local: :message for app/views/messages/_message.html.erb

Если установлено значение по умолчанию, оно может быть использовано, если message не передается в locals:.

<%# app/views/messages/_message.html.erb %>

<%# locals: (message: "Hello, world!") -%>
<%= message %>

Рендеринг партиала без локальной переменной :message приведет к использованию значения по умолчанию, установленного в специальном комментарии locals:.

render "messages/message"
# => "Hello, world!"

Рендеринг партиала с локальными переменными, не указанными в специальном комментарии local:, также приведет к ошибке:

render "messages/message", unknown_local: "will raise"
# => ActionView::Template::Error: unknown local: :unknown_local for app/views/messages/_message.html.erb

Вы можете разрешить необязательные аргументы локальных переменных с помощью оператора двойного разбиения **:

<%# app/views/messages/_message.html.erb %>

<%# locals: (message: "Hello, world!", **attributes) -%>
<%= tag.p(message, **attributes) %>

Либо вы можете полностью отключить locals, установив locals: в пустые скобки ().

<%# app/views/messages/_message.html.erb %>

<%# locals: () %>

Рендеринг партиала с любыми аргументами локальных переменных приведет к ошибке:

render "messages/message", unknown_local: "will raise"
# => ActionView::Template::Error: no locals accepted for app/views/messages/_message.html.erb

Action View обработает специальный комментарий locals: во всех шаблонизаторах, которые поддерживают комментарии, начинающиеся с символа #, и прочитает его на любой строке внутри партиала.

Поддерживаются только именованные аргументы. Определение позиционных или блочных аргументов вызовет ошибку Action View во время отрисовки.

5. Макеты

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

Например, в приложении может быть один макет для зарегистрированного пользователя и другой для маркетинговой части сайта. Макет для зарегистрированного пользователя может включать верхнюю панель навигации, которая должна присутствовать во многих экшнах контроллера. Макет продаж для SaaS-приложения может включать верхнюю панель навигации для таких страниц, как "Цены" и "Контакты". Различные макеты могут иметь разное содержимое шапки и колонтитула.

Чтобы найти макет для текущего экшна контроллера, Rails сначала ищет файл в app/views/layouts с тем же базовым именем, что и у контроллера. Например, для рендеринга экшнов из класса ProductsController будет использоваться файл app/views/layouts/products.html.erb.

Если макет, специфичный для контроллера, не существует, Rails использует app/views/layouts/application.html.erb.

Вот пример простого макета в файле application.html.erb:

<!DOCTYPE html>
<html>
<head>
  <title><%= "Your Rails App" %></title>
  <%= csrf_meta_tags %>
  <%= csp_meta_tag %>
  <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
  <%= javascript_importmap_tags %>
</head>
<body>

<nav>
  <ul>
    <li><%= link_to "Home", root_path %></li>
    <li><%= link_to "Products", products_path %></li>
    <!-- Additional navigation links here -->
  </ul>
</nav>

<%= yield %>

<footer>
  <p>&copy; <%= Date.current.year %> Your Company</p>
</footer>

В примере макета выше, содержимое вью будет отрисовано на месте <%= yield %> и окружено тем же содержимым <head>, <nav> и <footer>.

Rails предоставляет дополнительные способы назначения определенных макетов отдельным контроллерам и экшнам. Подробнее о макетах в целом вы можете узнать в руководстве Макеты и рендеринг в Rails.

5.1. Макеты для партиалов

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

Предположим, вы хотите отобразить статью на странице, которая должна быть обернута в div для целей отображения. Сначала вы создадите новую Article:

Article.create(body: 'Partial Layouts are cool!')

В шаблоне show вы отрисуете партиал _article, обернутый в макет box:

<%# app/views/articles/show.html.erb %>
<%= render partial: 'article', layout: 'box', locals: { article: @article } %>

Макет box просто оборачивает партиал _article в div:

<%# app/views/articles/_box.html.erb %>
<div class="box">
  <%= yield %>
</div>

Отметьте, что у макета партиала есть доступ к локальной переменной article, переданной в вызов render, хотя в данном случае она не используется внутри _box.html.erb.

В отличие от макетов приложения, макеты для партиалов по-прежнему имеют префикс подчеркивания в своем названии.

Вы также можете отрисовать блок кода внутри макета партиала вместо использования yield. Например, если бы у вас не было партиала _article, вы могли бы сделать следующее:

<%# app/views/articles/show.html.erb %>
<%= render(layout: 'box', locals: { article: @article }) do %>
  <div>
    <p><%= article.body %></p>
  </div>
<% end %>

Допустим, что мы используем тот же партиал _box, мы получим тот же результат, что и в предыдущем примере.

5.2. Коллекции с макетами для партиалов

При отрисовке коллекций также можно использовать опцию :layout.

<%= render partial: "article", collection: @articles, layout: "special_layout" %>

Макет будет отрисован вместе с партиалом для каждого элемента коллекции. Переменные текущего объекта и счетчика объектов, article и article_counter в приведенном выше примере, также будут доступны в макете, так же как и внутри партиала.

6. Хелперы

Rails предоставляет множество вспомогательных методов для использования с Action View. Эти методы включают:

  • Форматирование дат, строк и чисел
  • Создание ссылок HTML на картинки, видео, таблицы стилей и т.д...
  • Очистку содержимого
  • Создание форм
  • Локализацию содержимого

Подробнее о хелперах можно узнать в руководстве по хелперам Action View и руководстве по хелперам форм Action View.

7. Локализованные вью

В Action View есть возможность рендерить различные шаблоны в зависимости от текущей локали.

Например, предположим, что у вас есть ArticlesController с экшном show. По умолчанию вызов этого экшна отрендерит app/views/articles/show.html.erb. Но если вы установите I18n.locale = :de, то Action View попытается сначала отрендерить шаблон app/views/articles/show.de.html.erb. Если локализованный шаблон отсутствует, будет использована недекорированная версия. Это означает, что не нужно предоставлять локализованные вью для всех случаев, но они будут предпочтительными и будут использоваться, если станут доступны.

Ту же технику можно использовать для локализации страниц ошибок в директории public. Например, установка I18n.locale = :de и создание public/500.de.html и public/404.de.html позволит иметь локализованные страницы ошибок.

Подробнее об API интернационализации Rails (I18n) можно прочитать в руководстве API интернационализации Rails (I18n).