Tag BuilderがあらゆるプレフィックスのHTML属性をダッシュ区切りで展開できるように
RailsのTag Builderで、dataやaria以外の任意のキーに対してもHashを渡すとダッシュ区切りの属性に展開されるようになりました。Alpine.jsやhtmxなど、独自プレフィックスを使うフロントエンドフレームワークとの連携が宣言的に書けるようになります。
背景
これまでRailsのTag Builderは、data:とaria:キーに限りHashをネストしてダッシュ区切りのHTML属性へ展開する機能を持っていました。StimulusやTurboはdata-プレフィックスに依存するため、この仕様で問題ありませんでした。
しかしAlpine.jsはx-プレフィックス、htmxはhx-プレフィックスを使います。これらのフレームワークを使う場合、hx-post="/clicked"のような属性を個別の文字列キーで指定するか、ヘルパーメソッドを自前で用意する必要がありました。本PRはこの制約を解消し、任意のプレフィックスに対してHash展開を可能にしています。
技術的な変更
tag_optionsメソッドの条件分岐を変更し、Hash展開の対象をdata以外の任意のキーへ拡張しました。同時に、展開をスキップすべきariaと新たに追加されたclassの両キーを明示的に除外しています。
変更前:
type = TAG_TYPES[key]
if type == :data && value.is_a?(Hash)
value.each_pair do |k, v|
next if k.blank? || v.nil?
# ...
end
end
変更後:
type = TAG_TYPES[key]
if type != :aria && type != :class && value.is_a?(Hash)
value.each_pair do |k, v|
next if k.blank? || v.nil?
# ...
end
end
これにより、hx:やx:といった未知のキーはTAG_TYPESに登録されていないためtypeがnilになり、nil != :aria && nil != :classが真となってHash展開が実行されます。既存のdata:はTAG_TYPESに:dataとして登録されており、:data != :aria && :data != :classも真のため、従来どおり展開されます。
合わせて CLASS_PREFIXES と対応するTAG_TYPESエントリが追加され、classキーへのHash渡しは展開対象から除外されるようになりました。これはclassがスペース区切りの文字列として扱われる特殊な属性であるため、ダッシュ展開すると意味が変わってしまうことへの対処です。
実際の利用イメージは以下のとおりです:
tag.button "POST to /clicked", hx: { post: "/clicked", swap: :outerHTML, data: { json: true } }
# => <button hx-post="/clicked" hx-swap="outerHTML" hx-data="{"json":true}">POST to /clicked</button>
ネストしたHashの値がさらにHashである場合(上記のdata: { json: true })はJSON文字列にシリアライズされ、属性値として埋め込まれます。
設計判断
「ホワイトリスト方式」から「ブラックリスト方式」への転換が、この変更の核心的な設計判断です。
変更前はtype == :dataという条件で「dataのみ展開する」というホワイトリスト方式でした。変更後はtype != :aria && type != :classという条件で「ariaとclass以外はすべて展開する」というブラックリスト方式に切り替わっています。この方式により、将来新たなプレフィックスが登場してもTAG_TYPESへの登録なしに自動的に対応できます。
ariaがブラックリストに含まれているのは、aria属性がW3Cの仕様で定義された特定の属性名の集合であり、任意のダッシュ展開とは異なる専用の処理が既に存在するためです。classの除外も同様の理由で、スペース区切りという独自の扱いを保護しています。
まとめ
条件分岐のロジックをホワイトリストからブラックリストに反転させるだけの小さな変更で、dataとariaに限定されていたHash展開がすべての任意キーに開放されました。htmxやAlpine.jsをRailsビューヘルパーと組み合わせる際の記述が大幅にシンプルになり、フレームワーク非依存なHTML属性のネスト記法として機能します。