Программирование

Преобразование ключей в Rails: подчеркивания в дефисы

Объяснение автоматического преобразования ключей хешей из foo_bar в foo-bar при передаче данных из Ruby в JavaScript в Rails. Механизм и причины.

4 ответа 1 просмотр

Почему в Rails при передаче данных из Ruby в JavaScript ключи хешей с подчеркиваниями (например, ‘foo_bar’) автоматически преобразуются в ключи с дефисами (например, ‘foo-bar’)?

Когда в Ruby on Rails вы передаёте данные из Ruby-контроллера или хелпера в JavaScript через HTML data-атрибуты, ключи хешей автоматически undergo преобразование: foo_bar превращается в foo-bar. Это не баг и не случайность — за этим стоит метод dasherize из ActiveSupport, который вызывается внутри ActionView::Helpers::TagHelper при генерации HTML-атрибутов. Причина проста: HTML-спецификация использует дефисы в составных атрибутах (data-foo-bar), а Ruby предпочитает snake_case. Rails автоматически «мостит» этот разрыв.


Содержание


Механизм преобразования: что происходит под капотом

Допустим, в представлении вы пишете что-то вроде:

ruby
content_tag(:div, "Привет", data: { user_id: 42, foo_bar: "baz" })

На выходе получается:

html
<div data-user-id="42" data-foo-bar="baz">Привет</div>

Кто именно заменяет подчёркивания на дефисы? Цепочка вызовов выглядит так:

  1. content_tag (или tag.div) формирует хеш опций для HTML-элемента.
  2. Внутри вызывается tag_options из модуля ActionView::Helpers::TagHelper, который отвечает за сериализацию хеша Ruby в строку HTML-атрибутов.
  3. Для каждого ключа хеша вызывается метод dasherize из ActiveSupport::Inflector, который заменяет все символы _ на -.

Сам метод dasherize элементарен — он делает именно то, что звучит: foo_bar.dasherize возвращает "foo-bar". Но вызывается он не вами напрямую, а внутри фреймворка при рендеринге тегов.

И это происходит только при генерации HTML-атрибутов. Если вы сериализуете тот же хеш в JSON через to_json, ключи останутся в snake_case — если вы явно не попросите иное.


Почему именно дефисы: логика дизайна

HTML-спецификация исторически использует дефисы в составных именах атрибутов. data-custom-value, aria-hidden, http-equiv — повсеместная практика. Подчёркивания в именах HTML-атрибутов технически допустимы, но считаются плохим стилем и могут вызвать проблемы с некоторыми парсерами.

Rails, будучи фреймворком с сильным мнением (convention over configuration), принимает решение за вас. Вместо того чтобы заставлять каждого разработчика вручную писать data-foo-bar в Ruby-коде (что противоречит snake_case-конвенции Ruby), фреймворк позволяет писать естественно для Ruby и автоматически адаптирует для HTML.

Это проявление более широкого принципа Rails: каждый слой должен использовать свои конвенции, а фреймворк берёт на себя трансляцию между ними. Ruby-код — snake_case, HTML-атрибуты — kebab-case, JavaScript — camelCase. И Rails обеспечивает бесшовное преобразование на границах.


Data-атрибуты как мост между Ruby и JavaScript

Чаще всего эта трансформация всплывает именно при работе с data-* атрибутами, потому что именно они — основной механизм передачи серверных данных в клиентский JavaScript в классических Rails-приложениях.

ruby
# app/views/users/show.html.erb
<%= tag.div id: "user-card",
 data: {
 user_id: @user.id,
 full_name: @user.full_name,
 is_admin: @user.admin?
 } %>

Результат:

html
<div id="user-card" data-user-id="123" data-full-name="Иван Иванов" data-is-admin="false"></div>

А в JavaScript вы обращаетесь к этим данным так:

javascript
const card = document.getElementById('user-card');
const userId = card.dataset.userId; // camelCase в JS!
const fullName = card.dataset.fullName;
const isAdmin = card.dataset.isAdmin === 'true';

Заметьте интересный момент: Rails превращает full_name в data-full-name (через dasherize), а браузер при чтении через dataset автоматически конвертирует обратно — в fullName (camelCase). Получается цепочка из трёх разных конвенций, и каждая ссылка в этой цепочке делает свою работу корректно.

Это поведение детально описано в официальном руководстве Rails по работе с JavaScript, где объясняется, как data-* атрибуты служат связующим звеном между сервером и клиентом.


Как контролировать поведение преобразования

Иногда автоматическое преобразование мешает. Например, если вы передаёте данные в JavaScript-библиотеку, которая ожидает определённый формат ключей. Вот основные способы контроля.

Отключить dasherize для конкретного тега

Если вам нужно передать ключи «как есть» (с подчёркиваниями), можно обойти TagHelper и сформировать HTML-строку вручную:

ruby
tag.div id: "widget",
 data: { foo_bar: "baz" }.map { |k, v| "data-#{k}=#{v}" }.join(" ")

Грязно, но работает для разовых случаев.

Использовать to_json напрямую

Когда вы передаёте данные в JavaScript через инлайн-скрипт или через gon-подобную библиотеку, вы работаете с JSON, а не с HTML-атрибутами — и тут dasherize не участвует:

ruby
<%= javascript_tag do %>
 window.userData = <%= raw({ foo_bar: "baz", nested_key: 123 }.to_json) %>;
<% end %>

Результат — ключи останутся в snake_case. Если нужен camelCase, используйте опцию camelize:

ruby
{ foo_bar: "baz" }.to_json(camelize: :lower)
# => '{"fooBar":"baz"}'

Кастомный хелпер

Для повторяющихся случаев напишите свой хелпер, который формирует data-атрибуты без dasherize:

ruby
module DataAttrHelper
 def raw_data_attrs(hash)
 hash.transform_keys { |k| "data-#{k}" }
 .map { |k, v| "#{k}=\"#{ERB::Util.html_escape(v)}\"" }
 .join(" ")
 end
end

Подобные подходы активно обсуждаются на Stack Overflow, где разработчики делятся workaround-ами для нестандартных ситуаций.


Практические примеры и подводные камни

Ситуация 1: Stimulus и data-атрибуты

С приходом Stimulus в стандартный стек Rails 7, data-атрибуты стали ещё важнее. Stimulus-контроллеры ссылаются на targets и values через data-*:

ruby
<%= tag.div data: {
 controller: "hello",
 hello_target: "greeting",
 hello_name_value: @user.name
} %>

Результат:

html
<div data-controller="hello" data-hello-target="greeting" data-hello-name-value="Иван"></div>

Здесь dasherize работает как надо — Stimulus ожидает именно data-hello-name-value, а не data-hello_name_value.

Ситуация 2: Массивы в data-атрибутах

ruby
tag.div data: { ids: [1, 2, 3] }
# => <div data-ids="[1,2,3]"></div>

Значение сериализуется через to_json, а ключ — через dasherize. Два разных механизма, два разных пути преобразования. Не путайте их.

Ситуация 3: Вложенные хеши

ruby
tag.div data: { user: { first_name: "Иван", last_name: "Иванов" } }
# => <div data-user="{&quot;first_name&quot;:&quot;Иван&quot;,&quot;last_name&quot;:&quot;Иванов&quot;}"></div>

dasherize применяется только к верхнему уровню ключей. Вложенные ключи остаются в snake_case, потому что они сериализуются в JSON-строку как значение атрибута. Это частый источник путаницы — разработчики ожидают, что first_name тоже превратится в first-name, но этого не происходит.


Источники

  1. Ruby on Rails Guides — Официальное руководство по работе с JavaScript в Rails, включая data-атрибуты: https://guides.rubyonrails.org/working_with_javascript_in_rails.html
  2. ActionView::Helpers::TagHelper — Документация API модуля, отвечающего за генерацию HTML-тегов и преобразование ключей: https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html
  3. ActiveSupport::Inflector — Документация метода dasherize и других методов трансформации строк: https://api.rubyonrails.org/classes/ActiveSupport/Inflector.html
  4. Stack Overflow — Обсуждения сообщества о контроле преобразования ключей при передаче данных в JavaScript: https://stackoverflow.com

Заключение

Автоматическое преобразование foo_barfoo-bar в Rails — это не магия и не ошибка. Это осознанное архитектурное решение, которое позволяет разработчикам писать Ruby-код в snake_case (как принято в Ruby-сообществе), при этом генерируя валидный HTML с kebab-case атрибутами (как принято в HTML). Метод dasherize из ActiveSupport вызывается внутри TagHelper каждый раз, когда Rails формирует HTML-тег из хеша опций. Понимание этого механизма избавляет от множества «странностей» при работе с data-атрибутами, Stimulus-контроллерами и передаче данных в JavaScript. Если поведение по умолчанию вам не подходит — используйте to_json напрямую, пишите кастомные хелперы или формируйте атрибуты вручную.

R

В Ruby on Rails преобразование ключей хешей с подчеркиваниями в дефисы при передаче данных в JavaScript происходит из-за соглашения о именовании в JavaScript. JavaScript традиционно использует camelCase для именования переменных и функций, в то время как Ruby предпочитает snake_case. Rails автоматически преобразует snake_case ключи в camelCase при сериализации данных в JSON для использования в JavaScript. Это поведение реализовано через хелпер to_json, который применяет преобразование ключей для лучшей совместимости между двумя языками.

R

Преобразование ключей хешей с подчеркиваниями в дефисы в Rails при передаче данных в JavaScript регулируется опцией :camelize в методе to_json. По умолчанию, Rails использует :camelize => true, что приводит к преобразованию ключей вида ‘foo_bar’ в ‘FooBar’. Однако, если вы хотите получить именно ‘foo-bar’, вы можете использовать опцию :dashify => true или настроить сериализацию JSON вручную. Это поведение было введено для согласования с соглашениями об именовании в JavaScript, где camelCase является стандартом.

D

Преобразование ключей хешей из snake_case в camelCase при передаче данных из Ruby в JavaScript в Rails - это стандартное поведение, которое можно контролировать. В Rails 5+ вы можете использовать опцию camelize: false в методе to_json, чтобы отключить это преобразование. Например: my_hash.to_json(camelize: false) вернет JSON с исходными snake_case ключами. Это полезно, когда вам нужно сохранить точное соответствие ключей между Ruby и JavaScript кодом.

Авторы
R
Команда разработчиков
R
Разработчики
D
Программисты
Источники
Документация
Документация API
Stack Overflow / Платформа вопросов и ответов
Платформа вопросов и ответов
Проверено модерацией
НейроОтветы
Модерация