rails nested layout

先來解釋一下自己對layout的定義

Layout(模板),在不同的view裡如果有相同的部分的話通常會把共同的地方拆出來放在layout裡,最常看到的應用就是就是把每個view都會看到的navber 跟 footer拆出來放在layout/application裡,然後大家共同使用這個layout。

在某個controller裡如果不想使用預設的application layout的話,也可以自己在app/layout裡定義一個layout,然後在controller裡使用,這樣這個controller裡的所有view都會有共同的地方。

class OrdersController < ApplicationController
    layout "order_layout"
end

以上面的例子來說,如果我的order_layout裡有大部分結構都跟application layout一樣的話該怎麼辦呢?是要把有重複的部分從application裡copy到order_layout嗎?我想這不是一個好的做法也不符合DRY的原則,所以這時候就需要使用nested layout。

What is Nested Layout?

nested layout有點像是Class裡extends的概念,以例子來說就是把order_layout裡跟application layout重複的部分拿掉,然後讓order_layout繼承application layout,這樣order_layout裡也會有application layout的結構。

How to do?

step1

applicationHelper裡加入parent_layoutmethod

ApplicationHelper.rb
module ApplicationHelper  
  def parent_layout(layout)
    @view_flow.set(:layout, output_buffer)
    output = render(:file => "layouts/#{layout}")
    self.output_buffer = ActionView::OutputBuffer.new(output)
  end
end

step2

app/layout/order_layout的最下面加入 <% parent_layout "application" %> ,要記得放在<%= yield %>之後,這是重點!!

app/layout/order_layout.html.erb
<div class="container">
  <%= yield %>
  <div class="sidebar">
    ...
  </div>
</div>
<% parent_layout "application" %>

step3

在controller使用layout

class OrdersController < ApplicationController
    layout "order_layout"
end

結論

這邊上個圖看了會比較清楚

A的部分就是寫在layout/application
B的部分就是寫在layout/order_layout
C的部分就是ordersController index view
這樣在application裡的yield就會去render order_layout,然後在order_layout裡的yield就會去render action所對應到的view。

所以ordersController裡所有action的view都會有A跟B。

補充

在ruby on rails guides裡也有寫到關於nested layout但是是用不同的方法,官方是用content_for的用法。
using-nested-layouts

app/views/layouts/application.html.erb
<html>
<head>
  <title><%= @page_title or "Page Title" %></title>
  <%= stylesheet_link_tag "layout" %>
  <style><%= yield :stylesheets %></style>
</head>
<body>
  <div id="top_menu">Top menu items here</div>
  <div id="menu">Menu items here</div>
  <div id="content"><%= content_for?(:content) ? yield(:content) : yield %></div>
</body>
</html>
app/views/layouts/news.html.erb
<% content_for :stylesheets do %>
  #top_menu {display: none}
  #right_menu {float: right; background-color: yellow; color: black}
<% end %>
<% content_for :content do %>
  <div id="right_menu">Right menu items here</div>
  <%= content_for?(:news_content) ? yield(:news_content) : yield %>
<% end %>
<%= render template: "layouts/application" %>

news 裡render application 然後在把額外的結構抽出來用content_for包住,這樣在render application的時候,就會依照content_for的名稱把內容render到 application裡有指定名稱的yield裡。
但這個方法比較不適合我的需求,覺得這個方法比較適合不同的view想要render 不一樣的東西到application layout的時候。有點像是gretel這個breadcrumbs的用法,只要在view裡寫下要render的breadcrumb就可以在application layout的某個部份render出來。

參考資料

Rails nested layout
Simpler nested layouts in Rails using the parent_layout helper

using-nested-layouts
How to Clean Your Nested Layouts in 5 Minutes

這個好像是方便做nesred layout的gem
A Nested Inheritable Layouts Helpers for Rails

comments powered by Disqus