Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce customizable scope objects #90

Merged
merged 10 commits into from
Dec 18, 2018
40 changes: 18 additions & 22 deletions lib/dry/view/context.rb
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
require "dry/equalizer"

module Dry
module View
class Context
attr_reader :_options, :_part_builder, :_renderer
include Dry::Equalizer(:_options)

attr_reader :_rendering, :_options

def initialize(part_builder: nil, renderer: nil, **options)
@_part_builder = part_builder
@_renderer = renderer
def initialize(rendering: nil, **options)
@_rendering = rendering
@_options = options
end

def bind(part_builder:, renderer:)
self.class.new(
**_options.merge(
part_builder: part_builder,
renderer: renderer,
)
)
def for_rendering(rendering)
return self if rendering == self._rendering

self.class.new(**_options.merge(rendering: rendering))
end

def bound?
!!(_part_builder && _renderer)
def rendering?
!!_rendering
end

def self.decorate(*names, **options)
Expand All @@ -28,15 +28,11 @@ def self.decorate(*names, **options)
define_method name do
attribute = super()

return attribute unless bound? || !attribute

_part_builder.(
name: name,
value: attribute,
renderer: _renderer,
context: self,
**options,
)
if rendering? && attribute
_rendering.part(name, attribute, **options)
else
attribute
end
end
end
end
Expand Down
70 changes: 20 additions & 50 deletions lib/dry/view/controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
require_relative 'path'
require_relative 'rendered'
require_relative 'renderer'
require_relative 'scope'
require_relative 'rendering'
require_relative 'scope_builder'

module Dry
module View
Expand All @@ -33,18 +34,21 @@ class Controller
end
setting :default_context, DEFAULT_CONTEXT

setting :scope

setting :inflector, Dry::Inflector.new

setting :part_builder, PartBuilder
setting :part_namespace

setting :scope_builder, ScopeBuilder
setting :scope_namespace

attr_reader :config
attr_reader :layout_dir
attr_reader :layout_path
attr_reader :template_path

attr_reader :part_builder

attr_reader :exposures

# @api private
Expand All @@ -60,6 +64,11 @@ def self.paths
Array(config.paths).map { |path| Dry::View::Path.new(path) }
end

# @api private
def self.rendering(format: config.default_format, context: config.default_context)
Rendering.prepare(renderer(format), config, context)
end

# @api private
def self.renderer(format)
renderers.fetch(format) {
Expand Down Expand Up @@ -100,38 +109,31 @@ def initialize
@layout_path = "#{layout_dir}/#{config.layout}"
@template_path = config.template

@part_builder = config.part_builder.new(
namespace: config.part_namespace,
inflector: config.inflector,
)

@exposures = self.class.exposures.bind(self)
end

# @api public
def call(format: config.default_format, context: config.default_context, **input)
raise UndefinedTemplateError, "no +template+ configured" unless template_path

renderer = self.class.renderer(format)
context = context.bind(part_builder: part_builder, renderer: renderer)

locals = locals(renderer.chdir(template_path), context, input)

output = renderer.template(template_path, template_scope(renderer, context, locals))
template_rendering = self.class.rendering(format: format, context: context).chdir(template_path)
locals = locals(template_rendering, input)
output = template_rendering.template(template_path, template_rendering.scope(config.scope, locals))

if layout?
output = renderer.template(layout_path, layout_scope(renderer, context, layout_locals(locals))) { output }
layout_rendering = self.class.rendering(format: format, context: context).chdir(layout_path)
output = layout_rendering.template(layout_path, layout_rendering.scope(config.scope, layout_locals(locals))) { output }
end

Rendered.new(output: output, locals: locals)
end

private

def locals(renderer, context, input)
def locals(rendering, input)
exposures.(input) do |value, exposure|
if exposure.decorate?
decorate_local(renderer, context, exposure.name, value, **exposure.options)
if exposure.decorate? && value
rendering.part(exposure.name, value, **exposure.options)
else
value
end
Expand All @@ -147,38 +149,6 @@ def layout_locals(locals)
def layout?
!!config.layout
end

def layout_scope(renderer, context, locals = EMPTY_LOCALS)
scope(renderer.chdir(layout_dir), context, locals)
end

def template_scope(renderer, context, locals)
scope(renderer.chdir(template_path), context, locals)
end

def scope(renderer, context, locals = EMPTY_LOCALS)
Scope.new(
renderer: renderer,
context: context,
locals: locals,
)
end

def decorate_local(renderer, context, name, value, **options)
if value
# Decorate truthy values only
part_builder.(
name: name,
value: value,
renderer: renderer,
context: context,
namespace: config.part_namespace,
**options,
)
else
value
end
end
end
end
end
15 changes: 0 additions & 15 deletions lib/dry/view/missing_renderer.rb

This file was deleted.

48 changes: 17 additions & 31 deletions lib/dry/view/part.rb
Original file line number Diff line number Diff line change
@@ -1,27 +1,22 @@
require 'dry-equalizer'
require 'dry/view/scope'
require 'dry/view/missing_renderer'

module Dry
module View
class Part
CONVENIENCE_METHODS = %i[
context
render
scope
value
].freeze

include Dry::Equalizer(:_name, :_value, :_part_builder, :_context, :_renderer)
include Dry::Equalizer(:_name, :_value, :_rendering)

attr_reader :_name

attr_reader :_value

attr_reader :_context

attr_reader :_renderer

attr_reader :_part_builder
attr_reader :_rendering

attr_reader :_decorated_attributes

Expand All @@ -37,17 +32,20 @@ def self.decorated_attributes
@decorated_attributes ||= {}
end

def initialize(name:, value:, part_builder: Dry::View::PartBuilder.new, renderer: MissingRenderer.new, context: nil)
def initialize(name:, value:, rendering:)
@_name = name
@_value = value
@_context = context
@_renderer = renderer
@_part_builder = part_builder
@_rendering = rendering

@_decorated_attributes = {}
end

def _render(partial_name, as: _name, **locals, &block)
_renderer.partial(partial_name, _render_scope(as, locals), &block)
_rendering.partial(partial_name, _rendering.scope({as => self}.merge(locals)), &block)
end

def _scope(scope_name = nil, **locals)
_rendering.scope(scope_name, {_name => self}.merge(locals))
end

def to_s
Expand All @@ -58,15 +56,17 @@ def new(klass = (self.class), name: (_name), value: (_value), **options)
klass.new(
name: name,
value: value,
context: _context,
renderer: _renderer,
part_builder: _part_builder,
rendering: _rendering,
**options,
)
end

private

def _context
_rendering.context
end

def method_missing(name, *args, &block)
if self.class.decorated_attributes.key?(name)
_resolve_decorated_attribute(name)
Expand All @@ -85,28 +85,14 @@ def respond_to_missing?(name, include_private = false)
d.key?(name) || c.include?(name) || _value.respond_to?(name, include_private) || super
end

def _render_scope(name, **locals)
Scope.new(
locals: locals.merge(name => self),
context: _context,
renderer: _renderer,
)
end

def _resolve_decorated_attribute(name)
_decorated_attributes.fetch(name) {
attribute = _value.__send__(name)

_decorated_attributes[name] =
if attribute
# Decorate truthy attributes only
_part_builder.(
name: name,
value: attribute,
renderer: _renderer,
context: _context,
**self.class.decorated_attributes[name],
)
_rendering.part(name, attribute, **self.class.decorated_attributes[name])
end
}
end
Expand Down
42 changes: 31 additions & 11 deletions lib/dry/view/part_builder.rb
Original file line number Diff line number Diff line change
@@ -1,40 +1,56 @@
require 'dry/inflector'
require 'dry/equalizer'
require_relative 'part'

module Dry
module View
class PartBuilder
include Dry::Equalizer(:namespace)

attr_reader :namespace
attr_reader :inflector
attr_reader :rendering

def initialize(namespace: nil, inflector: Dry::Inflector.new)
def initialize(namespace: nil, rendering: nil)
@namespace = namespace
@inflector = inflector
@rendering = rendering
end

def call(name:, value:, renderer:, context:, **options)
def for_rendering(rendering)
return self if rendering == self.rendering

self.class.new(namespace: namespace, rendering: rendering)
end

def rendering?
!!rendering
end

def call(name, value, **options)
builder = value.respond_to?(:to_ary) ? :build_collection_part : :build_part

send(builder, name: name, value: value, renderer: renderer, context: context, **options)
send(builder, name, value, **options)
end

private

def build_part(name:, value:, renderer:, context:, **options)
def build_part(name, value, **options)
klass = part_class(name: name, **options)

klass.new(name: name, value: value, part_builder: self, renderer: renderer, context: context)
klass.new(
name: name,
value: value,
rendering: rendering,
)
end

def build_collection_part(name:, value:, renderer:, context:, **options)
def build_collection_part(name, value, **options)
collection_as = collection_options(name: name, **options)[:as]
item_name, item_as = collection_item_options(name: name, **options).values_at(:name, :as)

arr = value.to_ary.map { |obj|
build_part(name: item_name, value: obj, renderer: renderer, context: context, **options.merge(as: item_as))
build_part(item_name, obj, **options.merge(as: item_as))
}

build_part(name: name, value: arr, renderer: renderer, context: context, **options.merge(as: collection_as))
build_part(name, arr, **options.merge(as: collection_as))
end

def collection_options(name:, **options)
Expand Down Expand Up @@ -93,6 +109,10 @@ def resolve_part_class(name:, fallback_class:)
fallback_class
end
end

def inflector
rendering.inflector
end
end
end
end
Loading