V2.2: Scopes


A scope is the object that determines the methods available to use from within a template. The standard scope provides access to locals, helpers, the context, partial rendering, as well as the building of custom scopes.

You can use a custom scope to add your own behavior around a template and its particular set of locals. Along with parts, this allows you to move your view logic away from templates and into a place where it can be reused and refactored using typical object oriented approaches, as well as tested in isolation.

Defining scope classes

Scopes are kept in the Views::Scopes namespace within your app or slice. For example:

# app/views/scopes/media_player.rb

# auto_register: false

module Bookshelf
  module Views
    module Scopes
      class MediaPlayer < Bookshelf::Views::Scope
      end
    end
  end
end

It’s recommended to define a base scope for the other scopes in the app or slice to inherit:

# app/views/scope.rb

# auto_register: false

module Bookshelf
  module Views
    class Scope < Hanami::View::Scope
    end
  end
end

Building scopes

Build a scope by using the #scope method in a template, a part, or another scope object:

scope(:media_player)

You can also provide locals to the scope:

scope(:media_player, item: audio_file)

When you build a scope, the associated class will be looked up based on the scope’s name.

For example, given the exposure named :media_player, the Views::Scopes::MediaPlayer class will be looked up within your app or slice.

If you have no scope class matching an the scope’s name, then a generic Hanami::View::Scope will be used.

Rendering partials

You can render a partial via a scope with the standard #render method:

scope(:media_player, item: audio_file).render("media/audio_player")

The rendered partial will have access to all the scope’s methods, as well as its locals.

You can also render partials from methods within your scope class:

class MediaPlayer < Bookshelf::Views::Scope
  def render_player
    render("media/audio_player")
  end
end

Accessing locals

Within a scope class, or any partial rendered via that scope, you can access the scope’s locals directly by their names.

For example, from a template:

<!-- Given an `item:` local passed to the scope -->
<%= item.title %>

Or within a scope class:

class MediaPlayer < Bookshelf::Views::Scope
  def display_title
    # `item` is a local
    "#{item.title} (#{item.duration})"
  end
end

You can also access a full hash of locals via #locals (or #_locals as an alias, in case there is a local itself called locals). This is useful for providing default values for locals that may not be provided when the scope is built:

class MediaPlayer < Bookshelf::Views::Scope
  def show_artwork?
    locals.fetch(:show_artwork, true)
  end
end

Accessing the context

You can access the context for the view’s current rendering using the #context method (or #_context as an alias, in case there is a local named context).

Scopes also delegate missing methods to the context object (provided there is no local with that name).

For example:

class MediaPlayer < Bookshelf::Views::Scope
  def item_url
    # `item` is a local, and `routes` is a method on the context
    routes.path(:item, item.id)
  end
end

Memoizing methods

Any given scope object lives from the time of its creation until the end of rendering, so you can memoize expensive operations to ensure they only run once.