hanami-view/lib/dry/view/part.rb

207 lines
6.3 KiB
Ruby

require 'dry/equalizer'
require_relative "decorated_attributes"
require_relative "render_environment_missing"
module Dry
class View
# Decorates an exposure value and provides a place to encapsulate
# view-specific behavior alongside your application's domain objects.
#
# @abstract Subclass this and provide your own methods adding view-specific
# behavior. You should not override `#initialize`.
#
# @see https://dry-rb.org/gems/dry-view/parts/
#
# @api public
class Part
# @api private
CONVENIENCE_METHODS = %i[
format
context
render
scope
value
].freeze
include Dry::Equalizer(:_name, :_value, :_render_env)
include DecoratedAttributes
# The part's name. This comes from the exposure supplying the value.
#
# @return [Symbol] name
#
# @api public
attr_reader :_name
# The decorated value. This is the value returned from the exposure.
#
# @overload _value
# Returns the value.
# @overload value
# A convenience alias for `_value`. Is available unless the value itself
# responds to `#value`.
#
# @return [Object] value
#
# @api public
attr_reader :_value
# The current render environment
#
# @return [RenderEnvironment] render environment
#
# @api private
attr_reader :_render_env
# Determins a part name (when initialized without one). Intended for use
# only while unit testing Parts.
#
# @api private
def self.part_name(inflector)
name ? inflector.underscore(inflector.demodulize(name)) : "part"
end
# Returns a new Part instance
#
# @param name [Symbol] part name
# @param value [Object] the value to decorate
# @param render_env [RenderEnvironment] render environment
#
# @api public
def initialize(render_env: RenderEnvironmentMissing.new, name: self.class.part_name(render_env.inflector), value:)
@_name = name
@_value = value
@_render_env = render_env
end
# The template format for the current render environment.
#
# @overload _format
# Returns the format.
# @overload format
# A convenience alias for `#_format.` Is available unless the value
# itself responds to `#format`.
#
# @return [Symbol] format
#
# @api public
def _format
_render_env.format
end
# The context object for the current render environment
#
# @overload _context
# Returns the context.
# @overload context
# A convenience alias for `#_context`. Is available unless the value
# itself responds to `#context`.
#
# @return [Context] context
#
# @api public
def _context
_render_env.context
end
# Renders a new partial with the part included in its locals.
#
# @overload _render(partial_name, as: _name, **locals, &block)
# Renders the partial.
# @overload render(partial_name, as: _name, **locals, &block)
# A convenience alias for `#_render`. Is available unless the value
# itself responds to `#render`.
#
# @param partial_name [Symbol, String] partial name
# @param as [Symbol] the name for the Part to assume in the partial's locals. Default's to the Part's `_name`.
# @param locals [Hash<Symbol, Object>] other locals to provide the partial
#
# @return [String] rendered partial
#
# @api public
def _render(partial_name, as: _name, **locals, &block)
_render_env.partial(partial_name, _render_env.scope({as => self}.merge(locals)), &block)
end
# Builds a new scope with the part included in its locals.
#
# @overload _scope(scope_name = nil, **locals)
# Builds the scope.
# @overload scope(scope_name = nil, **locals)
# A convenience alias for `#_scope`. Is available unless the value
# itself responds to `#scope`.
#
# @param scope_name [Symbol, nil] scope name, used by the scope builder to determine the scope class
# @param locals [Hash<Symbol, Object>] other locals to provide the partial
#
# @return [Dry::View::Scope] scope
#
# @api public
def _scope(scope_name = nil, **locals)
_render_env.scope(scope_name, {_name => self}.merge(locals))
end
# Returns a string representation of the value
#
# @return [String]
#
# @api public
def to_s
_value.to_s
end
# Builds a new a part with the given parameters
#
# This is helpful for manually constructing a new part object that
# maintains the current render environment.
#
# However, using `.decorate` is preferred for declaring attributes that
# should also be decorated as parts.
#
# @see DecoratedAttributes::ClassInterface#decorate
#
# @param klass [Class] part class to use (defaults to the part's class)
# @param name [Symbol] part name (defaults to the part's name)
# @param value [Object] value to decorate (defaults to the part's value)
# @param options[Hash<Symbol, Object>] other options to provide when initializing the new part
#
# @api public
def new(klass = (self.class), name: (_name), value: (_value), **options)
klass.new(
name: name,
value: value,
render_env: _render_env,
**options,
)
end
# Returns a string representation of the part
#
# @return [String]
#
# @api public
def inspect
%(#<#{self.class.name} name=#{_name.inspect} value=#{_value.inspect}>)
end
private
# Handles missing methods. If the `_value` responds to the method, then
# the method will be sent to the value.
def method_missing(name, *args, &block)
if _value.respond_to?(name)
_value.public_send(name, *args, &block)
elsif CONVENIENCE_METHODS.include?(name)
__send__(:"_#{name}", *args, &block)
else
super
end
end
def respond_to_missing?(name, include_private = false)
CONVENIENCE_METHODS.include?(name) || _value.respond_to?(name, include_private) || super
end
end
end
end