Scopes
A scope is the object that determines which methods are available to use from within the template. The standard scope provides access to template locals (exposed values), partial rendering, as well as the building of custom scopes.
With a custom scope, you can add your own behavior around a template and its particular set of locals. These, along with parts, allow for most view logic to move away from templates and into classes you can reuse, refactor according to typical object oriented approaches, as well as test in isolation.
Defining a scope class
To provide custom scope behavior, define your own scope classes in a common namespace (e.g. Scopes
) and configure that as your view's scope_namespace
:
class MyView < Dry::View
config.scope_namespace = Scopes
end
Each scope class must inherit from Dry::View::Scope
:
module Scopes
class MediaPlayer < Dry::View::Scope
end
end
Building scopes
Build a scope by using the #scope
method from within a template, or on a part or scope object.
scope(:media_player)
Scopes can be passed their own set of locals:
scope(:media_player, item: audio_file)
Scope class resolution
Scope classes are looked up based on the configured scope_namespace
and the name you pass to #scope
.
So for a scope_namespace
of Scopes
and scope built as :media_player
, the Scopes::MediaPlayer
class will be looked up.
If a matching scope class cannot be found, the standard Dry::View::Scope
class will be used.
Rendering partials
You can render a partial using a scope with the standard #render
method:
scope(:media_player, item: audio_file).render(:media_player)
This rendered partial will have access to all the scope's methods, as well as its locals (see below).
The scope will infer the partial name by rendering without any arguments:
scope(:media_player, item: audio_file).render
This will use the scope's name for the name of the partial. In the example above, this is the equivalent of calling #render(:media_player)
.
You can also render partials from within your scope class' own methods:
class MediaPlayer < Dry::View::Scope
def audio_player_html
render(:audio_player)
end
end
Accessing locals
From within a scope class, or a template rendered with that scope, you can access the locals by their names.
For example, from a template:
<!-- e.g. accessing the `item` when rendered via scope(:media_player, item: audio_file).render -->
<%= item.title %>
Or from a custom scope class:
class MediaPlayer < Dry::View::Scope
def display_title
# `item` is a local
"#{item.title} (#{item.duration})"
end
end
You can also access the full hash of locals via #_locals
(or #locals
as a convenience, provided there is no local named locals
).
This is useful for providing default values for locals that may not explicitly be passed when the scope is built:
class MediaPlayer < Dry::View::Scope
def show_artwork?
locals.fetch(:show_artwork, true)
end
end
Accessing the context
In your scope classes, you can access the context object as #_context
(or #context
as a convenience, provided there is no local named context
).
Scopes also delegate missing methods to the context object (provided there is no local with that name).
For example:
class MediaPlayer < Dry::View::Scope
def image_urls
# item is a local, and asset_path is a method defined on the context object
[item.image_url, asset_path("standard-media-artwork.png")]
end
end
Memoizing methods
You may choose to memoize expensive operations within a scope to ensure they only run once.
Configuring a scope for a whole view
Aside from building custom scopes explicitly, you can also specify a scope to be used when a view renders its own template.
You can specify the scope as a direct class reference:
class MyView < Dry::View
config.template = "my_view"
config.scope = Scopes::MyView
end
Or if you have a scope namepace configured, you can use a symbolic name and a matching scope will be looked up:
class MyView < Dry::View
config.template = "my_view"
config.scope_namespace = Scopes
config.scope = :my_view
end
Providing a custom scope builder
To fully customize scope lookup and initialization, you can provide a replacement scope builder:
class MyView < Dry::View
config.scope_builder = MyScopeBuilder
end
Your scope builder must conform to the following interface:
#initialize(namespace: nil, render_env: nil)
#for_render_env(render_env)
#call(name = nil, locals)
You can also inherit from Dry::View::ScopeBuilder
and override any of its methods, if you want to customize just a particular aspect of the standard behavior.