class Pakyow::Presenter::Presenter

Presents a view object with dynamic state in context of an app instance. In normal usage you will be interacting with presenters rather than the {View} directly.

Attributes

path[R]
app[R]

The app object.

@api private

logger[R]

The logger object.

presentables[R]

Values to be presented.

view[R]

The view object being presented.

Public Class Methods

new(view, app:, presentables: {}) click to toggle source
# File lib/pakyow/presenter/presenter.rb, line 54
def initialize(view, app:, presentables: {})
  @app, @view, @presentables = app, view, presentables
  @logger = Pakyow.logger
  @called = false
end

Private Class Methods

attach(view) click to toggle source

Attaches renders to a view's doc.

# File lib/pakyow/presenter/presenter.rb, line 636
def attach(view)
  views_with_renders = {}

  renders = @__attached_renders.dup

  # Automatically present exposed values for this view. Doing this dynamically lets us
  # optimize. The alternative is to attach a render to the entire view, which is less
  # performant because the entire structure must be duped.
  #
  view.binding_scopes.map { |binding_node|
    {
      binding_path: [
        binding_node.label(:channeled_binding)
      ]
    }
  }.uniq.each do |binding_render|
    renders << {
      binding_path: binding_render[:binding_path],
      priority: :low,
      block: Proc.new {
        if object.labeled?(:binding) && !object.labeled?(:bound)
          presentables.each do |key, value|
            if present?(key, object)
              present(value); break
            end
          end
        end
      }
    }
  end

  # Setup binding endpoints in a similar way to automatic presentation above.
  #
  Presenters::Endpoint.attach_to_node(view.object, renders)

  renders.each do |render|
    return_value = if node = render[:node]
      view.instance_exec(&node)
    else
      view.find(*render[:binding_path])
    end

    case return_value
    when Array
      return_value.each do |each_value|
        relate_value_to_render(each_value, render, views_with_renders)
      end
    when View, VersionedView
      relate_value_to_render(return_value, render, views_with_renders)
    end
  end

  views_with_renders.values.each do |view_with_renders, renders_for_view|
    attach_to_node = view_with_renders.object

    if attach_to_node.is_a?(StringDoc)
      attach_to_node = attach_to_node.find_first_significant_node(:html)
    end

    if attach_to_node
      renders_for_view.each do |render|
        attach_to_node.transform priority: render[:priority], &render_proc(view_with_renders, render, &render[:block])
      end
    end
  end
end
make(path, **kwargs, &block) click to toggle source

@api private

Calls superclass method
# File lib/pakyow/presenter/presenter.rb, line 593
def make(path, **kwargs, &block)
  path = String.normalize_path(path)
  super(path, path: path, **kwargs, &block)
end
options_for(form_binding, field_binding, options = nil, &block) click to toggle source

Defines options attached to a form binding.

# File lib/pakyow/presenter/presenter.rb, line 705
def options_for(form_binding, field_binding, options = nil, &block)
  form_binding = form_binding.to_sym
  field_binding = field_binding.to_sym

  @__global_options[form_binding] ||= {}
  @__global_options[form_binding][field_binding] = {
    options: options,
    block: block
  }
end
present(binding_name, &block) click to toggle source

Defines a presentation block called when binding_name is presented. If channel is provided, the block will only be called for that channel.

# File lib/pakyow/presenter/presenter.rb, line 620
def present(binding_name, &block)
  (@__presentation_logic[binding_name.to_sym] ||= []) << {
    block: block
  }
end
relate_value_to_render(value, render, state) click to toggle source
# File lib/pakyow/presenter/presenter.rb, line 753
def relate_value_to_render(value, render, state)
  final_value = case value
  when View, VersionedView
    value
  else
    View.new(value.to_s)
  end

  # Group the renders by node and view type.
  #
  (state["#{final_value.object.object_id}::#{final_value.class}"] ||= [final_value, []])[1] << render
end
render(*binding_path, node: nil, priority: :default, &block) click to toggle source

Defines a render to attach to a node.

# File lib/pakyow/presenter/presenter.rb, line 600
def render(*binding_path, node: nil, priority: :default, &block)
  if node && !node.is_a?(Proc)
    raise ArgumentError, "Expected `#{node.class}' to be a proc"
  end

  if binding_path.empty? && node.nil?
    node = -> { self }
  end

  @__attached_renders << {
    binding_path: binding_path,
    node: node,
    priority: priority,
    block: block
  }
end
render_proc(_view, _render = nil, &block) click to toggle source
# File lib/pakyow/presenter/presenter.rb, line 718
def render_proc(_view, _render = nil, &block)
  Proc.new do |node, context, string|
    case node
    when StringDoc::MetaNode
      if node.nodes.any?
        returning = node
        presenter = context.presenter_for(
          VersionedView.new(View.from_object(node))
        )
      else
        next node
      end
    when StringDoc::Node
      returning = StringDoc.empty
      returning.append(node)
      presenter = context.presenter_for(
        View.from_object(node)
      )
    end

    presenter.instance_exec(node, context, string, &block); returning
  rescue => error
    if presenter.app.config.presenter.features.streaming
      Pakyow.logger.houston(error)

      presenter.clear
      presenter.attributes[:class] << :"render-failed"
      presenter.view.object.set_label(:failed, true)
      presenter.object
    else
      raise error
    end
  end
end
version(version_name, &block) click to toggle source

Defines a versioning block called when version_name is presented.

# File lib/pakyow/presenter/presenter.rb, line 628
def version(version_name, &block)
  (@__versioning_logic[version_name] ||= []) << {
    block: block
  }
end

Public Instance Methods

==(other) click to toggle source

Returns true if self equals other.

# File lib/pakyow/presenter/presenter.rb, line 347
def ==(other)
  other.is_a?(self.class) && @view == other.view
end
after(view) click to toggle source

@see View#after

# File lib/pakyow/presenter/presenter.rb, line 307
def after(view)
  tap do
    @view.after(view)
  end
end
append(view) click to toggle source

@see View#append

# File lib/pakyow/presenter/presenter.rb, line 291
def append(view)
  tap do
    @view.append(view)
  end
end
before(view) click to toggle source

@see View#before

# File lib/pakyow/presenter/presenter.rb, line 315
def before(view)
  tap do
    @view.before(view)
  end
end
bind(data) click to toggle source

Binds data to the view, using the appropriate binder if available.

# File lib/pakyow/presenter/presenter.rb, line 206
def bind(data)
  tap do
    data = binder_or_data(data)

    if data.is_a?(Binder)
      bind_binder_to_view(data, @view)
    else
      @view.bind(data)
    end

    set_binding_info(data)
    set_endpoint_params(data)
  end
end
clear() click to toggle source

@see View#clear

# File lib/pakyow/presenter/presenter.rb, line 339
def clear
  tap do
    @view.clear
  end
end
component(name) click to toggle source

Returns the component matching name.

# File lib/pakyow/presenter/presenter.rb, line 108
def component(name)
  if found_component = @view.component(name)
    presenter_for(found_component)
  else
    nil
  end
end
components(renderable: false) click to toggle source

Returns all components.

@api private

# File lib/pakyow/presenter/presenter.rb, line 119
def components(renderable: false)
  @view.components(renderable: renderable).map { |component|
    presenter_for(component)
  }
end
endpoint(name) click to toggle source

@api private

# File lib/pakyow/presenter/presenter.rb, line 402
def endpoint(name)
  found = []

  object.each_significant_node(:endpoint) do |endpoint_node|
    if endpoint_node.label(:endpoint) == name.to_sym
      found << endpoint_node
    end
  end

  if found.any?
    if found[0].is_a?(StringDoc::MetaNode)
      presenter_for(View.from_object(found[0]))
    else
      presenter_for(View.from_object(StringDoc::MetaNode.new(found)))
    end
  else
    nil
  end
end
endpoint_action() click to toggle source

@api private

# File lib/pakyow/presenter/presenter.rb, line 423
def endpoint_action
  endpoint_action_node = object.find_first_significant_node(
    :endpoint_action
  ) || object

  presenter_for(View.from_object(endpoint_action_node))
end
find(*names) { |result| ... } click to toggle source

Returns a presenter for a view binding.

@see View#find

# File lib/pakyow/presenter/presenter.rb, line 63
def find(*names)
  result = if found_view = @view.find(*names)
    presenter_for(found_view)
  else
    nil
  end

  if result && block_given?
    yield result
  end

  result
end
find_all(*names) click to toggle source

Returns an array of presenters, one for each view binding.

@see View#find_all @api private

# File lib/pakyow/presenter/presenter.rb, line 81
def find_all(*names)
  @view.find_all(*names).map { |view|
    presenter_for(view)
  }
end
form(name) click to toggle source

Returns the named form from the view being presented.

# File lib/pakyow/presenter/presenter.rb, line 89
def form(name)
  if found_form = @view.form(name)
    presenter_for(found_form)
  else
    nil
  end
end
forms() click to toggle source

Returns all forms.

@api private

# File lib/pakyow/presenter/presenter.rb, line 100
def forms
  @view.forms.map { |form|
    presenter_for(form)
  }
end
method_missing(name, *args, &block) click to toggle source
# File lib/pakyow/presenter/presenter.rb, line 351
def method_missing(name, *args, &block)
  if @view.respond_to?(name)
    value = @view.public_send(name, *args, &block)

    if value.equal?(@view)
      self
    else
      value
    end
  else
    super
  end
end
prepend(view) click to toggle source

@see View#prepend

# File lib/pakyow/presenter/presenter.rb, line 299
def prepend(view)
  tap do
    @view.prepend(view)
  end
end
present(data) { |presenter, object| ... } click to toggle source

Transforms the view to match data, then binds, using the appropriate binder if available.

@see View#present

# File lib/pakyow/presenter/presenter.rb, line 225
def present(data)
  tap do
    transform(data, true) do |presenter, binder|
      if block_given?
        yield presenter, binder.object
      end

      unless presenter.view.object.labeled?(:bound) || self.class.__presentation_logic.empty?
        self.class.__presentation_logic[presenter.view.channeled_binding_name].to_a.each do |presentation_logic|
          presenter.instance_exec(binder.object, &presentation_logic[:block])
        end
      end

      if presenter.view.is_a?(VersionedView)
        unless presenter.view.used? || self.class.__versioning_logic.empty?
          # Use global versions.
          #
          presenter.view.names.each do |version|
            self.class.__versioning_logic[version]&.each do |logic|
              if presenter.instance_exec(binder.object, &logic[:block])
                presenter.use(version); break
              end
            end
          end
        end

        # If we still haven't used a version, use one implicitly.
        #
        unless presenter.view.used?
          presenter.use_implicit_version
        end

        # Implicitly use binding props.
        #
        presenter.view.binding_props.map { |binding_prop|
          binding_prop.label(:binding)
        }.uniq.each do |binding_prop_name|
          if found = presenter.view.find(binding_prop_name)
            presenter_for(found).use_implicit_version unless found.used?
          end
        end
      end

      presenter.bind(binder)

      presenter.view.binding_scopes.uniq { |binding_scope|
        binding_scope.label(:binding)
      }.each do |binding_node|
        plural_binding_node_name = Support.inflector.pluralize(binding_node.label(:binding)).to_sym

        if nested_view = presenter.find(binding_node.label(:binding))
          if binder.object.include?(binding_node.label(:binding))
            nested_view.present(binder.object[binding_node.label(:binding)])
          elsif binder.object.include?(plural_binding_node_name)
            nested_view.present(binder.object[plural_binding_node_name])
          else
            nested_view.remove
          end
        end
      end
    end
  end
end
presenter_for(view, type: nil) click to toggle source
# File lib/pakyow/presenter/presenter.rb, line 386
def presenter_for(view, type: nil)
  if view.nil?
    nil
  else
    instance = self.class.new(
      view,
      app: @app,
      presentables: @presentables
    )

    type ||= view.object.label(:presenter_type)
    type ? type.new(instance) : instance
  end
end
remove() click to toggle source

@see View#remove

# File lib/pakyow/presenter/presenter.rb, line 331
def remove
  tap do
    @view.remove
  end
end
replace(view) click to toggle source

@see View#replace

# File lib/pakyow/presenter/presenter.rb, line 323
def replace(view)
  tap do
    @view.replace(view)
  end
end
respond_to_missing?(name, include_private = false) click to toggle source
# File lib/pakyow/presenter/presenter.rb, line 365
def respond_to_missing?(name, include_private = false)
  @view.respond_to?(name, include_private) || super
end
title() click to toggle source

Returns the title value from the view being presented.

# File lib/pakyow/presenter/presenter.rb, line 127
def title
  @view.title&.text
end
title=(value) click to toggle source

Sets the title value on the view.

# File lib/pakyow/presenter/presenter.rb, line 133
def title=(value)
  unless @view.title
    if head_view = @view.head
      title_view = View.new("<title></title>")
      head_view.append(title_view)
    end
  end

  @view.title&.html = strip_tags(value)
end
to_html(output = String.new) click to toggle source
# File lib/pakyow/presenter/presenter.rb, line 378
def to_html(output = String.new)
  @view.object.to_html(output, context: self)
end
to_s() click to toggle source
# File lib/pakyow/presenter/presenter.rb, line 382
def to_s
  @view.to_s
end
transform(data, yield_binder = false) { |presenter_for(current), yield_binder ? binder : object| ... } click to toggle source

Transforms the view to match data.

@see View#transform

# File lib/pakyow/presenter/presenter.rb, line 169
def transform(data, yield_binder = false)
  tap do
    data = Array.ensure(data).reject(&:nil?)

    if data.respond_to?(:empty?) && data.empty?
      if @view.is_a?(VersionedView) && @view.version?(:empty)
        @view.use(:empty); @view.object.set_label(:bound, true)
      else
        remove
      end
    else
      template = @view.soft_copy
      insertable = @view
      current = @view

      data.each do |object|
        binder = binder_or_data(object)

        current.transform(binder)

        if block_given?
          yield presenter_for(current), yield_binder ? binder : object
        end

        unless current.equal?(@view)
          insertable.after(current)
          insertable = current
        end

        current = template.soft_copy
      end
    end
  end
end
use(version) click to toggle source

Uses the view matching version, removing all other versions.

# File lib/pakyow/presenter/presenter.rb, line 146
def use(version)
  @view.use(version)
  self
end
use_implicit_version() click to toggle source

@api private

# File lib/pakyow/presenter/presenter.rb, line 432
def use_implicit_version
  case object
  when StringDoc::MetaNode
    if object.internal_nodes.all? { |node| node.labeled?(:version) && node.label(:version) != VersionedView::DEFAULT_VERSION }
      use(object.internal_nodes.first.label(:version))
    else
      use(:default)
    end
  else
    if versions.all? { |view| view.object.labeled?(:version) && view.object.label(:version) != VersionedView::DEFAULT_VERSION }
      use(versions.first.object.label(:version))
    else
      use(:default)
    end
  end
end
versioned(version) click to toggle source

Returns a presenter for the view matching version.

# File lib/pakyow/presenter/presenter.rb, line 153
def versioned(version)
  presenter_for(@view.versioned(version))
end
with() { |self| ... } click to toggle source

Yields self.

# File lib/pakyow/presenter/presenter.rb, line 159
def with
  tap do
    yield self
  end
end
wrap_data_in_binder(data) click to toggle source

@api private

# File lib/pakyow/presenter/presenter.rb, line 370
def wrap_data_in_binder(data)
  if data.is_a?(Binder)
    data
  else
    binder_for_current_scope(data)
  end
end

Private Instance Methods

bind_binder_to_view(binder, view) click to toggle source
# File lib/pakyow/presenter/presenter.rb, line 470
def bind_binder_to_view(binder, view)
  view.each_binding_prop do |binding|
    value = binder.__value(binding.label(:binding))
    if value.is_a?(BindingParts) && binding_view = view.find(binding.label(:binding))
      value.accept(*binding_view.label(:include).to_s.split(" "))
      value.reject(*binding_view.label(:exclude).to_s.split(" "))

      value.non_content_values(binding_view).each_pair do |key, value_part|
        binding_view.attrs[key] = value_part
      end

      binding_view.object.set_label(:bound, true)
    end
  end

  binder.binding!
  view.bind(binder)
end
binder_for_current_scope(data) click to toggle source
# File lib/pakyow/presenter/presenter.rb, line 451
def binder_for_current_scope(data)
  context = if plug = @view.label(:plug)
    @app.plug(plug[:name], plug[:instance])
  else
    @app
  end

  binder = context.state(:binder).find { |possible_binder|
    possible_binder.__object_name.name == @view.label(:binding)
  }

  unless binder
     binder = @app.isolated(:Binder)
     context = @app
  end

  binder.new(data, app: context)
end
binder_or_data(data) click to toggle source
# File lib/pakyow/presenter/presenter.rb, line 489
def binder_or_data(data)
  if data.nil? || data.is_a?(Array) || data.is_a?(Binder)
    data
  else
    wrap_data_in_binder(data)
  end
end
internal_presentable?(key) click to toggle source
# File lib/pakyow/presenter/presenter.rb, line 570
def internal_presentable?(key)
  key.to_s.start_with?("__")
end
object_presents?(object, key) click to toggle source
# File lib/pakyow/presenter/presenter.rb, line 574
def object_presents?(object, key)
  key == plural_channeled_binding_name || key == singular_channeled_binding_name
end
plug_presents?(object, key) click to toggle source
# File lib/pakyow/presenter/presenter.rb, line 578
def plug_presents?(object, key)
  key = key.to_s
  object.labeled?(:plug) &&
    key.start_with?(object.label(:plug)[:key]) &&
    # FIXME: Find a more performant way to do this
    #
    object_presents?(object, key.split("#{object.label(:plug)[:key]}.", 2)[1].to_sym)
end
present?(key, object) click to toggle source
# File lib/pakyow/presenter/presenter.rb, line 566
def present?(key, object)
  !internal_presentable?(key) && (object_presents?(object, key) || plug_presents?(object, key))
end
set_binding_info(data) click to toggle source
# File lib/pakyow/presenter/presenter.rb, line 497
def set_binding_info(data)
  object = if data.is_a?(Binder)
    data.object
  else
    data
  end

  if object && @view.object.labeled?(:binding)
    binding_info = {
      @view.object.label(:binding) => object[:id]
    }

    set_binding_info_for_node(@view.object, binding_info)

    @view.object.each_significant_node(:binding, descend: true) do |binding_node|
      set_binding_info_for_node(binding_node, binding_info)
    end

    @view.object.each_significant_node(:form, descend: true) do |form_node|
      set_binding_info_for_node(form_node, binding_info)
    end
  end
end
set_binding_info_for_node(node, info) click to toggle source
# File lib/pakyow/presenter/presenter.rb, line 521
def set_binding_info_for_node(node, info)
  unless node.labeled?(:binding_info)
    node.set_label(:binding_info, {})
  end

  node.label(:binding_info).merge!(info)
end
set_endpoint_params(data) click to toggle source
# File lib/pakyow/presenter/presenter.rb, line 529
def set_endpoint_params(data)
  object = if data.is_a?(Binder)
    data.object
  else
    data
  end

  if @view.object.labeled?(:endpoint)
    set_endpoint_params_for_node(@view.object, object)
  end

  @view.object.each_significant_node(:endpoint, descend: true) do |endpoint_node|
    set_endpoint_params_for_node(endpoint_node, object)
  end
end
set_endpoint_params_for_node(node, object) click to toggle source
# File lib/pakyow/presenter/presenter.rb, line 545
def set_endpoint_params_for_node(node, object)
  endpoint_object = node.label(:endpoint_object)
  endpoint_params = node.label(:endpoint_params)

  if endpoint_object && endpoint_params
    endpoint_object.params.each do |param|
      if param.to_s.start_with?("#{@view.label(:binding)}_")
        key = param.to_s.split("_", 2)[1].to_sym

        if object.include?(key)
          endpoint_params[param] = object[key]; next
        end
      end

      if object.include?(param)
        endpoint_params[param] = object[param]
      end
    end
  end
end