# require 'active_support/core_ext/class/inheritable_attributes' module Netzke module Composition extend ActiveSupport::Concern COMPONENT_METHOD_NAME = "%s_component" included do # Loads a component on browser's request. Every Nettzke component gets this endpoint. # params should contain: # * :cache - an array of component classes cached at the browser # * :id - reference to the component # * :container - Ext id of the container where in which the component will be rendered endpoint :deliver_component do |params| cache = params[:cache].gsub(".", "::").split(",") # array of cached class names (in Ruby) component_name = params.delete(:name).underscore.to_sym component = components[component_name] && component_instance(component_name) if component # inform the component that it's being loaded component.before_load [{ :eval_js => component.js_missing_code(cache), :eval_css => component.css_missing_code(cache) }, { :component_delivered => component.js_config }] else {:feedback => "Couldn't load component '#{component_name}'"} end end end # included module ClassMethods # Defines a nested component. # For example: # # component :users, :data_class => "GridPanel", :model => "User" # # It can also accept a block (receiving as parameter the eventual definition from super class): # # component :books do |orig| # {:data_class => "Book", :title => orig[:title] + ", extended"} # end def component(name, config = {}, &block) register_component(name) config = config.dup config[:class_name] ||= name.to_s.camelize config[:name] = name.to_s method_name = COMPONENT_METHOD_NAME % name if block_given? define_method(method_name, &block) else if superclass.instance_methods.map(&:to_s).include?(method_name) define_method(method_name) do super().merge(config) end else define_method(method_name) do config end end end end # Component's js config used when embedding components as Container's items # (see some_composite.rb for an example) def js_component(name, config = {}) ::ActiveSupport::Deprecation.warn("Using js_component is deprecated. Use Symbol#component instead", caller) config.merge(:component => name) end # Register a component def register_component(name) current_components = read_inheritable_attribute(:components) || [] current_components << name write_inheritable_attribute(:components, current_components.uniq) end # Returns registered components def registered_components read_inheritable_attribute(:components) || [] end end module InstanceMethods def items @items_with_normalized_components end def initial_components {} end # All components for this instance, which includes components defined on class level, and components detected in :items def components @components ||= self.class.registered_components.inject({}){ |res, name| res.merge(name.to_sym => send(COMPONENT_METHOD_NAME % name)) } end def non_late_components components.reject{|k,v| v[:lazy_loading]} end def add_component(aggr) components.merge!(aggr) end def remove_component(aggr) if config[:persistent_config] persistence_manager_class.delete_all_for_component("#{global_id}__#{aggr}") end components[aggr] = nil end # The difference between components and late components is the following: the former gets instantiated together with its composite and is normally *instantly* visible as a part of it (for example, the component in the initially expanded panel in an Accordion). A late component doesn't get instantiated along with its composite. Until it gets requested from the server, it doesn't take any part in its composite's life. An example of late component could be a component that is loaded dynamically into a previously collapsed panel of an Accordion, or a preferences window (late component) for a component (composite) that only gets shown when user wants to edit component's preferences. def initial_late_components {} end def add_late_component(aggr) components.merge!(aggr.merge(:lazy_loading => true)) end # called when the method_missing tries to processes a non-existing component def component_missing(aggr) flash :error => "Unknown component #{aggr} for component #{name}" {:feedback => @flash}.to_nifty_json end # recursively instantiates an component based on its "path": e.g. if we have an component :aggr1 which in its turn has an component :aggr10, the path to the latter would be "aggr1__aggr10" def component_instance(name, strong_config = {}) @cached_component_instances ||= {} @cached_component_instances[name] ||= begin composite = self name.to_s.split('__').each do |aggr| aggr = aggr.to_sym component_config = composite.components[aggr] raise ArgumentError, "No component '#{aggr}' defined for component '#{composite.global_id}'" if component_config.nil? short_component_class_name = component_config[:class_name] raise ArgumentError, "No class_name specified for component #{aggr} of #{composite.global_id}" if short_component_class_name.nil? component_class = constantize_class_name(short_component_class_name) conf = weak_children_config. deep_merge(component_config). deep_merge(strong_config). # we may want to reconfigure the component at the moment of instantiation merge(:name => aggr) composite = component_class.new(conf, composite) # params: config, parent # composite.weak_children_config = weak_children_config # composite.strong_children_config = strong_children_config end composite end end def dependency_classes res = [] non_late_components.keys.each do |aggr| res += component_instance(aggr).dependency_classes end res += self.class.class_ancestors res << self.class res.uniq end ## Dependencies # def dependencies # @dependencies ||= begin # non_late_components_component_classes = non_late_components.values.map{|v| v[:class_name]} # (initial_dependencies + non_late_components_component_classes << self.class.short_component_class_name).uniq # end # end # override this method if you need some extra dependencies, which are not the components def initial_dependencies [] end def js_component(*args) self.class.js_component(*args) end # Returns global id of a component in the hierarchy, based on passed reference that follows # the double-underscore notation. Referring to "parent" is allowed. If going to far up the hierarchy will # result in nil, while referring to a non-existent component will simply provide an erroneous ID. # Example: # parent__parent__child__subchild will traverse the hierarchy 2 levels up, then going down to "child", # and further to "subchild". If such a component exists in the hierarchy, its global id will be returned, otherwise # nil will be returned. def global_id_by_reference(ref) ref = ref.to_s return parent && parent.global_id if ref == "parent" substr = ref.sub(/^parent__/, "") if substr == ref # there's no "parent__" in the beginning return global_id + "__" + ref else return parent.global_id_by_reference(substr) end end # Method dispatcher - instantiates an component and calls the method on it # E.g.: # users__center__get_data # instantiates component "users", and calls "center__get_data" on it # books__move_column # instantiates component "books", and calls "endpoint_move_column" on it def method_missing(method_name, params = {}) component, *action = method_name.to_s.split('__') component = component.to_sym action = !action.empty? && action.join("__").to_sym if action if components[component] # only actions starting with "endpoint_" are accessible endpoint_action = action.to_s.index('__') ? action : "_#{action}_ep_wrapper" component_instance(component).send(endpoint_action, params) else component_missing(component) end else super end end private def normalize_components(items) @component_index ||= 0 @items_with_normalized_components = items.each_with_index.map do |item, i| if is_component_config?(item) component_name = item[:name] || :"#{item[:class_name].underscore.split("/").last}#{@component_index}" @component_index += 1 self.class.component(component_name.to_sym, item) component_name.to_sym.component # replace current item with a reference to component elsif item.is_a?(Hash) item[:items].is_a?(Array) ? item.merge(:items => normalize_components(item[:items])) : item else item end end end def normalize_components_in_items normalize_components(config[:items]) if config[:items] end def is_component_config?(c) !!(c.is_a?(Hash) && c[:class_name]) end end end end