lib/volt/page/bindings/view_binding.rb
require 'volt/page/bindings/base_binding'
require 'volt/page/template_renderer'
require 'volt/page/bindings/view_binding/grouped_controllers'
require 'volt/page/bindings/view_binding/view_lookup_for_path'
require 'volt/page/bindings/view_binding/controller_handler'
module Volt
class ViewBinding < BaseBinding
# @param [String] binding_in_path is the path this binding was rendered from. Used to
# lookup paths in ViewLookupForPath
# @param [String|nil] content_template_path is the path to the template for the content
# provided in the tag.
def initialize(volt_app, target, context, binding_name, binding_in_path, getter, content_template_path = nil)
super(volt_app, target, context, binding_name)
@content_template_path = content_template_path
# Setup the view lookup helper
@view_lookup = Volt::ViewLookupForPath.new(volt_app.templates, binding_in_path)
@current_template = nil
# Run the initial render
@computation = lambda do
# Don't try to render if this has been removed
if @context
# Render
update(*@context.instance_eval(&getter))
end
end.watch!
end
# update is called when the path string changes.
def update(path, section_or_arguments = nil, options = {})
Computation.run_without_tracking do
@options = options
# A blank path needs to load a missing template, otherwise it tries to load
# the same template.
path = path.blank? ? '---missing---' : path
section = nil
@arguments = nil
if section_or_arguments.is_a?(String)
# Render this as a section
section = section_or_arguments
else
# Use the value passed in as the default arguments
@arguments = section_or_arguments
# Include content_template_path in attrs
if @content_template_path
@arguments ||= {}
@arguments[:content_template_path] = @content_template_path
@arguments[:content_controller] = @context
end
end
# Sometimes we want multiple template bindings to share the same controller (usually
# when displaying a :Title and a :Body), this instance tracks those.
if @options && (controller_group = @options[:controller_group])
# Setup the grouped controller for the first time.
@grouped_controller = GroupedControllers.new(controller_group)
end
# If a controller is already starting, but not yet started, then remove it.
remove_starting_controller
full_path, controller_path = @view_lookup.path_for_template(path, section)
if full_path
@starting_controller_handler, generated_new, chain_stopped = create_controller_handler(full_path, controller_path)
# Check if chain was stopped when the action ran
if chain_stopped
# An action stopped the chain. When this happens, we stop running here.
remove_starting_controller
else
# None of the actions stopped the chain
# Wait until the controller is loaded before we actually render.
@waiting_for_load = -> { @starting_controller_handler.controller.loaded? }.watch_until!(true) do
render_next_template(full_path, path)
end
queue_clear_grouped_controller
end
else
# if we don't have a full path, then we have a missing template
render_next_template(full_path, path)
end
end
end
# Called when the next template is ready to render
def render_next_template(full_path, path)
remove_current_controller_and_template
# Switch the current template
@current_controller_handler = @starting_controller_handler
@starting_controller_handler = nil
# Also track the current controller directly
@controller = @current_controller_handler.controller if full_path
render_template(full_path || path)
# rescue => e
# Volt.logger.error("Error during render of template at #{path}: #{e.inspect}")
# Volt.logger.error(e.backtrace)
end
def stop_waiting_for_load
if @waiting_for_load
@waiting_for_load.stop
@waiting_for_load = nil
end
end
# On the next tick, we clear the grouped controller so that any changes to template paths
# will create a new controller and trigger the action.
def queue_clear_grouped_controller
if Volt.in_browser?
# In the browser, we want to keep a grouped controller around during a single run
# of the event loop. To make that happen, we clear it on the next tick.
`setImmediate(function() {`
clear_grouped_controller
`});`
else
# For the backend, clear it immediately
clear_grouped_controller
end
end
def clear_grouped_controller
if @grouped_controller
@grouped_controller.clear
@grouped_controller = nil
end
end
def remove_current_controller_and_template
# Remove existing controller and template and call _removed
if @current_controller_handler
@current_controller_handler.call_action('before', 'remove')
end
if @current_template
@current_template.remove
@current_template = nil
end
if @current_controller_handler
@current_controller_handler.call_action('after', 'remove')
end
if @grouped_controller && @current_controller_handler
# We remove the controller after all of the current rendering is done.
Timers.next_tick do
# Remove a reference for the controller in the group.
@grouped_controller.remove(@current_controller_handler.controller.class)
end
end
@controller = nil
@current_controller_handler = nil
end
def remove_starting_controller
# Clear any previously running wait for loads. This is for when the path changes
# before the view actually renders.
stop_waiting_for_load
if @starting_controller_handler
# Only call the after_..._removed because the dom never loaded.
@starting_controller_handler.call_action('after', 'removed')
@starting_controller_handler = nil
end
end
# Create controller handler loads up a controller inside of the controller handler for the paths
def create_controller_handler(full_path, controller_path)
# If arguments is nil, then an blank SubContext will be created
args = [SubContext.new(@arguments, nil, true)]
# get the controller class and action
controller_class, action = ControllerHandler.get_controller_and_action(controller_path)
generated_new = false
new_controller = proc do
# Mark that we needed to generate a new controller instance (not reused
# from the group)
generated_new = true
# Setup the controller
controller_class.new(@volt_app, *args)
end
# Fetch grouped controllers if we're grouping
if @grouped_controller
# Find the controller in the group, or create it
controller = @grouped_controller.lookup_or_create(controller_class, &new_controller)
else
# Just create the controller
controller = new_controller.call
end
handler = ControllerHandler.fetch(controller, action)
if generated_new
# Call the action
stopped = handler.call_action
controller.instance_variable_set('@chain_stopped', true) if stopped
else
stopped = controller.instance_variable_get('@chain_stopped')
end
[handler, generated_new, stopped]
end
# The context for templates can be either a controller, or the original context.
def render_template(full_path)
@current_template = TemplateRenderer.new(@volt_app, @target, @controller, @binding_name, full_path)
call_ready
end
def call_ready
if @controller
# Set the current section on the controller if it wants so it can manipulate
# the dom if needed.
# Only assign sections for action's, so we don't get AttributeSections bound
# also.
if @controller.respond_to?(:section=)
dom_section = @current_template.dom_section
# Only assign dom sections that can be manipulated via the dom (so not the title for example)
@controller.section = dom_section unless dom_section.is_a?(Volt::AttributeSection)
end
# Call the ready callback on the controller
@current_controller_handler.call_action(nil, 'ready')
end
end
# Called when the binding is removed from the page
def remove
# Cleanup any starting controller
remove_starting_controller
@computation.stop
@computation = nil
remove_current_controller_and_template
super
end
end
end