module NestiveRails # The Nestive LayoutHelper provides a handful of helper methods for use in your layouts and views. # # See the documentation for each individual method for detailed information, but at a high level, # your parent layouts define `area`s of content. You can define an area and optionally add content # to it at the same time using either a String, or a block: # # # app/views/layouts/global.html.erb # # # <%= area :title, "MySite.com" %> # # #
# <%= area :content %> #
# # # # # Your child layouts (or views) inherit and modify the parent by wrapping in an `extends` block # helper. You can then either `append`, `prepend` or `replace` the content that has previously # been assigned to each area by parent layouts. # # The `append`, `prepend` or `replace` helpers are *similar* to Rails' own `content_for`, which # accepts content for the named area with either a String or with a block). They're different to # `content_for` because they're only used modify the content assigned to the area, not retrieve it: # # # app/views/layouts/admin.html.erb # <%= extends :global do %> # <% prepend :title, "Admin :: " %> # <% replace :sidebar do %> #

Quick Links

# # <% end %> # <% end %> # # # app/views/admin/posts/index.html.erb # <%= extends :admin do %> # <% prepend :title, "Posts ::" %> # <% replace :content do %> # Normal view stuff goes here. # <% end %> # <% end %> module LayoutHelper # Declares that the current layour (or view) is inheriting from and extending another layout. # # @param [String] layout # The base name of the file in `layouts/` that you wish to extend (eg `application` for `layouts/application.html.erb`) # # @example Extending the `application` layout to create an `admin` layout # # # app/views/layouts/admin.html.erb # <%= extends :application do %> # ... # <% end %> # # @example Extending the `admin` layout in a view (you'll need to render the view with `layout: nil`) # # # app/controllers/admin/posts_controller.rb # class Admin::PostsController < ApplicationController # # You can disable Rails' layout rendering for all actions # layout nil # # # Or disable Rails' layout rendering per-controller # def index # render layout: nil # end # end # # # app/views/admin/posts/index.html.erb # <%= extends :admin do %> # ... # <% end %> def extends layout, &block # Make sure it's a string layout = layout.to_s # If there's no directory component, presume a plain layout name layout = "layouts/#{layout}" unless layout.include?('/') # Capture the content to be placed inside the extended layout @view_flow.get(:layout).replace capture(&block).to_s render file: layout end # Defines an area of content in your layout that can be modified or replaced by child layouts # that extend it. You can optionally add content to an area using either a String, or a block. # # Areas are declared in a parent layout and modified by a child layout, but since Nestive # allows for multiple levels of inheritance, a child layout can also declare an area for it's # children to modify. # # @example Define an area without adding content to it: # <%= area :sidebar %> # # @example Define an area and add a String of content to it: # <%= area :sidebar, "Some content." %> # # @example Define an area and add content to it with a block: # <%= area :sidebar do %> # Some content. # <% end %> # # @example Define an area in a child layout: # <%= extends :global do %> # <%= area :sidebar do %> # Some content. # <% end %> # <% end %> # # @param [Symbol] name # A unique name to identify this area of content. # # @param [String] content # An optional String of content to add to the area as you declare it. def area name, content=nil, &block content = capture(&block) if block_given? append name, content render_area name end # Appends content to an area previously defined or modified in parent layout(s). You can provide # the content using either a String, or a block. # # @example Appending content with a String # <% append :sidebar, "Some content." %> # # @example Appending content with a block: # <% append :sidebar do %> # Some content. # <% end %> # # @param [Symbol] name # A name to identify the area of content you wish to append to # # @param [String] content # Optionally provide a String of content, instead of a block. A block will take precedence. def append name, content=nil, &block content = capture(&block) if block_given? add_instruction_to_area name, :push, content end # Prepends content to an area previously declared or modified in parent layout(s). You can # provide the content using either a String, or a block. # # @example Prepending content with a String # <% prepend :sidebar, "Some content." %> # # @example Prepending content with a block: # <% prepend :sidebar do %> # Some content. # <% end %> # # @param [Symbol] name # A name to identify the area of content you wish to prepend to # # @param [String] content # Optionally provide a String of content, instead of a block. A block will take precedence. def prepend name, content=nil, &block content = capture(&block) if block_given? add_instruction_to_area name, :unshift, content end # Replaces the content of an area previously declared or modified in parent layout(s). You can # provide the content using either a String, or a block. # # @example Replacing content with a String # <% replace :sidebar, "New content." %> # # @example Replacing content with a block: # <% replace :sidebar do %> # New content. # <% end %> # # @param [Symbol] name # A name to identify the area of content you wish to replace # # @param [String] content # Optionally provide a String of content, instead of a block. A block will take precedence. def replace name, content=nil, &block content = capture(&block) if block_given? add_instruction_to_area name, :replace, [content] end # Purge the content of an area previously declared or modified in parent layout(s). # # @example Purge content # <% purge :sidebar %> # # @param names # A list of area names to purge def purge *names names.each{ |name| replace(name, nil) } end private # We record the instructions (declaring, appending, prepending and replacing) for an area of # content into an array that we can later retrieve and replay. Instructions are stored in an # instance variable Hash `@_area_for`, with each key representing an area name, and each value # an Array of instructions. Each instruction is a two element array containing a instruction # method (eg `:push`, `:unshift`, `:replace`) and a value (content String). # # @_area_for[:sidebar] # => [ [:push,"World"], [:unshift,"Hello"] ] # # Due to the way we extend layouts (render the parent layout after the child), the instructions # are captured in reverse order. `render_area` reversed them and plays them back at rendering # time. # # @example # add_instruction_to_area(:sidebar, :push, "More content.") def add_instruction_to_area name, instruction, value @_area_for ||= {} @_area_for[name] ||= [] @_area_for[name] << [instruction, value] nil end # Take the instructions we've gathered for the area and replay them one after the other on # an empty array. These instructions will push, unshift or replace items into our output array, # which we then join and mark as html_safe. # # These instructions are reversed and replayed when we render the block (rather than as they # happen) due to the way they are gathered by the layout extension process (in reverse). def render_area name [].tap do |output| @_area_for.fetch(name, []).reverse_each do |method_name, content| output.public_send method_name, content end end.join.html_safe end end end