Rails

partial with layout の微妙な挙動

More than 3 years have passed since last update.

ほとんど同じなんだけど、ちょっと違うpartialがある時に、partialにlayoutを適用するとDRYに書けたりする。

自分の場合は、メインメニューにおいて、ほとんど全てのメニューは一つのpartialでまかなえるのだが、通知メニューにのみ、未読の数字をバッジのようにつけたかった。partialに変数を渡してif分などで出し分けるということもできそうだが、読みずらくなりそうなので、layoutを使うことにした。

ただ、細かい挙動に関してあまりドキュメントが無いのでハマった。content_forで渡すセクションが二つあったが、なかなかその例がなかった。
試行錯誤過程については、ややこしいので、とばしてもらっても構いません。

試行錯誤過程

通知メニュー以外のメニューでは通常通りにpartialをrenderしています。(slimで書いてます)

= render "shared/main_nav__item", path: root_path, label: "Home"

通知メニューでまず最初にやってみたのが、上で使ったpartialをlayoutととして使い、

= render layout: "shared/main_nav__item", path: notifications_path, label: "Notifications"
  content_for :unread_count_active, "unread"
  content_for :unread_count_num, "10"

のように、renderしたい内容をその場でブロックで渡してみることです。ただ、これはlayout側からyieldcontent_forで取り出すことができませんでした。

どうやら、このようにblockで渡した場合は名前付きのcontent_forは使えないようです。

= render layout: "shared/main_nav__item", path: notifications_path, label: "Notifications"
 span= "10"

試しに名前無しで渡してみたら取り出すことができました。ただ、自分の場合は、二つのセクションを渡したかったので、これでは足りません。

次にpartialのファイルを作って、先ほどblockで内容を書き込みます。

content_for :unread_count_active, "unread"
content_for :unread_count_num, "10"

それをrenderします。

= render partial: "shared/unread_count",layout: "shared/main_nav__item",
  locals: {\
    path:   notifications_path,
    label:  "Notifications",
  }

この場合、変数はlocalsにいれる必要があるのに注意が必要です。

これを試した結果、不思議なことがおこります。

未読の数字が通知メニューの右隣のメニューの上に表示されてしまいました。partial側をcontent_forからprovideに換えても同じです。

そして、layout側のyeildcontent_forに置き換えてみると、通知メニューと右隣のメニュー両方に未読の数字が表示されてしまいました。この二つ未読数が表示される現象はrwz/nestiveというサードパーティのレイアウトシステムを使っても発生しました。どうやら、yieldやcontent_forでは呼び出しても中身が消えないような作りになっていて、同じpartialを複数回呼び出すとおかしな挙動になるようです。

また、試しに、二つのセクションで分けて名前をつけて渡していたのを、片方だけにして名前無しで渡すとどうなるかやってみました。そうするとひどい画面になりました。通知メニュー以外の全てのメニュー内に、application layoutでyieldされる内容が出力されてしまいました。また、名前ありでyieldしている部分には名前無しで渡されるている部分が出力されてしまっていて、html的にもぐちゃぐちゃになりました。

どうやら、viewの名前空間とpartialで使われる名前空間は同じようで、通知メニュー以外はviewでセットされる内容を出してしまっているようです。
また、この現象はpartialを指定せずにblockでrender内容を名前無しで渡しても発生しました。ただ、名前ありでyieldしている部分は想定通り何も出力しませんでした。

こうしたら動いた

partial側で、contetn_forを名前ありで使ってlayoutに渡したい内容をセットします。

content_for :unread_count_active, "unread"
content_for :unread_count_num, "10"

application_helperに以下のメソッドを作ります。

def yield_content!(content_key)
  view_flow.content.delete(content_key)
end

layout側でyieldではなく、yield_content!を使います。

= yield_content! :unread_count

こんな感じで。

これでpartialでlayoutに二つのセクションを渡すことができました。

参考

Partial, Layout, Template rendering problems