Слогер Создать блог
Разработка

Rails уже поставляется с class_names, но я всё равно написал clsx

Разбираемся, почему встроенного хелпера для CSS-классов в Rails иногда недостаточно и как альтернатива решает проблемы в реальных проектах.

Когда два года назад я опубликовал небольшую библиотеку для условных CSS-классов, первым комментарием был вопрос Адриана Поли: «А в чём главное отличие от встроенного хелпера class_names в Rails?» Честно признаюсь: я тогда не знал, что class_names существует. Я написал гем для того, что уже было в Rails. Пошёл посмотрел, решил, что разница в производительности, и оставил как есть. Но этот ответ не давал мне покоя два года. Вот настоящая история.

Сначала отдадим должное

Если вы используете Rails 6.1 или новее, то можете прямо сейчас собирать строку классов условно — без всяких гемов:

<%= tag.div class: class_names("btn", "btn-primary", active: @active) do %> Click me <% end %>

class_names — это публичный псевдоним для token_list в ActionView::Helpers::TagHelper. Он принимает строки, массивы и хеши, разделяет по пробелам, отбрасывает ложные значения, удаляет дубликаты и возвращает HTML-безопасную строку. Для повседневных задач этого вполне достаточно, и clsx согласен с ним токен в токен:

require "clsx" require "action_view" view = Object.new.extend(ActionView::Helpers::TagHelper) view.class_names("a", "b") # => "a b" Clsx["a", "b"] # => "a b" view.class_names(foo: true, bar: false) # => "foo" Clsx[foo: true, bar: false] # => "foo" view.class_names("a b", "b c") # => "a b c" Clsx["a b", "b c"] # => "a b c" view.class_names("a", ["b", ["c"]]) # => "a b c" Clsx["a", ["b", ["c"]]] # => "a b c"

Если вам нужно только это — используйте встроенный хелпер. Честно. А остальная часть статьи — о четырёх случаях, когда «только этого» оказывается недостаточно.

Причина 1: class_names привязан к ActionView

class_names — это метод в ActionView::Helpers::TagHelper. Ему не нужен живой view, не нужен запрос или буфер вывода. Ему нужен сам модуль. Но модуль — часть ActionView, поэтому, чтобы склеить две строки в Sinatra-приложении или в обычном сервис-объекте, приходится подгружать целый фреймворк и примешивать его вручную:

# В обычном объекте без require class_names не определён: class_names("a", active: true) # => NoMethodError # Приходится подключать ActionView и расширять модулем: require "action_view" view = Object.new.extend(ActionView::Helpers::TagHelper) view.class_names("a", active: true) # => "a"

Именно этот зуд привёл к разделению. Я поместил сборщик классов внутрь Rails-гема, потому что в ту неделю работал в Rails. А потом захотел то же самое в компоненте Phlex и в обычном презентере — и тянуть для этого ActionView показалось абсурдом. В склеивании имён классов нет ничего специфичного для Rails. Поэтому я выделил логику в clsx-ruby — один файл, около 130 строк, ноль зависимостей, а clsx-rails оставил как тонкую прослойку сверху:

require "clsx" is_active, is_disabled = true, false Clsx["btn", "btn-primary", active: is_active, disabled: is_disabled] # => "btn btn-primary active"

Причина 2: пустой результат и поведение с «мусором»

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

view.class_names(nil, false) # => "" (рендерит class="") Clsx[nil, false] # => nil (Rails-хелперы тегов пропускают nil)

Rails пропускает nil-атрибут class, поэтому Clsx[...] никогда не рендерит пустой class="" на компоненте, которому нечего добавить. class_names возвращает "", и это рендерится.

Есть один честный аргумент в другую сторону: class_names возвращает html_safe-строку, а clsx — обычную String. Внутри атрибута class: это неважно — Rails экранирует значение в любом случае, а в токенах классов нечего экранировать. Если вы интерполируете результат clsx где-то ещё в сыром виде, это обычная строка, и она экранируется как положено — безопасное поведение по умолчанию. Но если вам нужен именно html_safe, class_names даёт его, а clsx — нет.

Остальное — надёжность. Никто не пишет такие входные данные намеренно, но какой-нибудь хелпер рано или поздно передаёт сборщику что-то странное, и две библиотеки расходятся в том, что с этим делать:

# Сложное значение как ключ хеша. view.class_names([{ foo: true }, "bar"] => true) # => "[{foo: true}, &quot;bar&quot;]" Clsx[[{ foo: true }, "bar"] => true] # => "foo bar" # Невызванная лямбда и случайный true. view.class_names(proc {}, true) # => "#&lt;Proc:0x...&gt; true" Clsx[proc {}, true] # => nil

class_names преобразует в строку всё, что получает, и экранирует в атрибут; clsx рекурсивно обрабатывает сложный ключ и игнорирует «голый» Proc или true. Когда хелпер admin_link случайно передаёт невызванную лямбду, clsx её отбрасывает, а class_names печатает адрес Proc в ваш HTML.

Полный набор правил

Название — дань уважения: clsx-ruby создан по образцу lukeed's clsx — крошечного JavaScript-пакета, так что модель аргументов знакома разработчикам на React. Поскольку различия выше касаются в основном того, что считается классом и что считается истинным, вот все правила в одном месте. clsx принимает строки, символы, числа, хеши, массивы и любую их вложенность:

Clsx["foo", true && "bar", "baz"] # => "foo bar baz" Clsx[:foo, :"bar-baz"] # => "foo bar-baz" Clsx[1, 2, 3] # => "1 2 3" Clsx[["foo", nil, false, "bar"]] # => "foo bar" Clsx["foo", ["bar", { baz: false }, ["hi", ["there"]]]] # => "foo bar hi there"

Хеш включает каждый ключ, значение которого истинно, в порядке объявления:

Clsx[foo: true, bar: false, baz: 2 > 1] # => "foo baz"

Единственное правило, которое сбивает с толку пришедших из JavaScript: в Ruby ложны только false и nil. Поэтому 0, "", [] и {} — все истинны и сохраняют свой ключ.

Clsx["foo" => 0, bar: []] # => "foo bar"

И два удобства, которых нет в JS-версии: дубликаты удаляются даже в многотокенных строках, а пустой результат — nil, а не "":

Clsx["a", "a"] # => "a" Clsx["a b", "b c"] # => "a b c" Clsx[nil, false] # => nil

По материалам: dev.to. Текст переработан редакцией Слогера.

← На главную

Рекламное место — Конец поста
Реклама · Слогер

Комментарии (0)

Войдите, чтобы комментировать.

Пока нет комментариев. Будьте первым.