【Rails】Slimで入れ子になっている要素の親タグのみを分岐させる


Slimで、入れ子になっている子タグはそのままで、親タグのみ切り替えたいケースの書き方でハマったのでメモ。

具体的にはこんな感じのHTMLを出力したいときです。

求めるHTML

<div class="User__list">

  <!-- 選択中ユーザの場合、親タグをaタグではなくdivタグにしたい -->
  <div class="User__listSection--selected">
    <div>[ユーザ名]</div>
    <div>[メールアドレス]</div>
  </div>

  <a class="User__listSection"> <!-- hrefは割愛 -->
    <div>[ユーザ名]</div>
    <div>[メールアドレス]</div>
  </a>

  <a class="User__listSection">
    <div>[ユーザ名]</div>
    <div>[メールアドレス]</div>
  </a>

  ...

</div>

ダメな例

直感的にはこう書きたいところですが、slimには end が無いのでこれだとエラーになってしまいます。

slim
.User__list
  - users.each do |user|
    - if current_user == user
      .User__listSection--selected
    - else
      a.User__listSection
    - end / slimに end は無いのでエラー!
        div #{user.name}
        div #{user.email}

実現できるけど微妙な例

一番素朴にやるとこうなると思います。

slim
.User__list
  - users.each do |user|
    - if current_user == user
      / 選択中ユーザの場合、親タグをaタグではなくdivタグにする
      .User__listSection--selected
        div #{user.name}
        div #{user.email}
    - else
      a.User__listSection
        div #{user.name}
        div #{user.email}

これは入れ子のタグを2回書いていて冗長なのであまり好ましくないですね。メンテナンス性も極めて悪いです。

動的タグを使ってちょっぴりスマートに書いた例

動的タグを使うと入れ子のタグは一度だけ書けば済むようになります。

slim
ruby:
  def user_list_section_tag(current_user, user)
    if current_user == user
      # 選択中ユーザの場合、親タグをaタグではなくdivタグにする
      { tag: 'div', class: 'User__listSection--selected' }
    else
      { tag: 'a', class: 'User__listSection' }
    end
  end

.User__list
  - users.each do |user|
    *user_list_section_tag(current_user, user)
      div #{user.name}
      div #{user.email}

複雑な分岐がある場合は動的タグを使うと良さそうですが、slimの中に急にrubyが出てくるのはちょっと気持ち悪い気がします。

content_tagを使って書いた例

content_tagでタグを生成するようにすれば、slim内にrubyを書かずに済みます。

slim
.User__list
  - users.each do |user|
    - user_list_section_attrs = current_user == user \
      ? { class: "User__listSection--selected" } \
      : { class: "User__listSection" }

    / 選択中ユーザの場合、親タグをaタグではなくdivタグにする
    = content_tag(current_user == user ? :div : :a, '', user_list_section_attrs)
      div #{user.name}
      div #{user.email}

content_tag を使えば確かにRubyを書かずに済みますが、current_user == user が2回出てきたりして、むしろこっちの方が分かりづらいのでは?という気もします。
が、純粋にslimだけで書けるのでこちらの方が僕は良いと感じています。

もっとスマートな方法があればどなたか教えて欲しいです!

追記:tagを使ってもっとスマートに書いた例

@eRy-sk さんに コメント でより良い書き方を教えていただきました!ありがとうございます!

tagsend で生成したいタグ名を分岐して渡します。

slim
.User__list
  - users.each do |user|
    - selected = current_user == user

    / 選択中ユーザの場合、親タグをaタグではなくdivタグにする
    = tag.send(selected ? 'div' : 'a', class: "User__listSection#{'--selected' if selected}")
      div = user.name
      div = user.email

content_tag だと第二引数が悲しいことになりますし、この tag を使う書き方が良さそうですね!

さらに追記: content_tagでもっとスマートに書いた例

@scivola さんに コメント でもっとスマートな書き方を教えていただきました!ありがとうございます!!!

content_tag に渡す引数として、タグ名も含めた配列を分岐して定義しておいて、それを渡すようにします。

slim
.User__list
  - users.each do |user|
    / 選択中ユーザの場合、親タグをaタグではなくdivタグにする
    - if current_user == user
      - args = [:div, class: "User__listSection--selected"]
    - else
      - args = [:a, class: "User__listSection"]
    = content_tag(*args)
      div = user.name
      div = user.email

send を使わなくてよくなりますし、これが現状一番スマートな書き方、というところでしょうか。