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
The app object.
@api private
The logger object.
Values to be presented.
The view object being presented.
Public Class Methods
# 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
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
@api private
# 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
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
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
# 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
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
# 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
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
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
@see View#after
# File lib/pakyow/presenter/presenter.rb, line 307 def after(view) tap do @view.after(view) end end
@see View#append
# File lib/pakyow/presenter/presenter.rb, line 291 def append(view) tap do @view.append(view) end end
@see View#before
# File lib/pakyow/presenter/presenter.rb, line 315 def before(view) tap do @view.before(view) end end
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
@see View#clear
# File lib/pakyow/presenter/presenter.rb, line 339 def clear tap do @view.clear end end
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
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
@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
@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
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
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
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
Returns all forms.
@api private
# File lib/pakyow/presenter/presenter.rb, line 100 def forms @view.forms.map { |form| presenter_for(form) } end
Pakyow::Presenter::Presentable#method_missing
# 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
@see View#prepend
# File lib/pakyow/presenter/presenter.rb, line 299 def prepend(view) tap do @view.prepend(view) end end
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
# 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
@see View#remove
# File lib/pakyow/presenter/presenter.rb, line 331 def remove tap do @view.remove end end
@see View#replace
# File lib/pakyow/presenter/presenter.rb, line 323 def replace(view) tap do @view.replace(view) end end
Pakyow::Presenter::Presentable#respond_to_missing?
# File lib/pakyow/presenter/presenter.rb, line 365 def respond_to_missing?(name, include_private = false) @view.respond_to?(name, include_private) || super end
Returns the title value from the view being presented.
# File lib/pakyow/presenter/presenter.rb, line 127 def title @view.title&.text end
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
# File lib/pakyow/presenter/presenter.rb, line 378 def to_html(output = String.new) @view.object.to_html(output, context: self) end
# File lib/pakyow/presenter/presenter.rb, line 382 def to_s @view.to_s end
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
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
@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
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
Yields self
.
# File lib/pakyow/presenter/presenter.rb, line 159 def with tap do yield self end end
@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
# 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
# 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
# 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
# File lib/pakyow/presenter/presenter.rb, line 570 def internal_presentable?(key) key.to_s.start_with?("__") end
# File lib/pakyow/presenter/presenter.rb, line 574 def object_presents?(object, key) key == plural_channeled_binding_name || key == singular_channeled_binding_name end
# 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
# File lib/pakyow/presenter/presenter.rb, line 566 def present?(key, object) !internal_presentable?(key) && (object_presents?(object, key) || plug_presents?(object, key)) end
# 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
# 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
# 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
# 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