# require 'active_support/core_ext/class/inheritable_attributes' module Netzke # This module takes care of components composition. # # You can define a nested component by calling the +component+ class method: # # component :users, :data_class => "GridPanel", :model => "User" # # The method also accepts a block in case you want access to the component's instance: # # component :books do # {:data_class => "Book", :title => build_title} # end # # To override a component, define a method {component_name}_component, e.g.: # # def books_component # super.merge(:title => "Modified Title") # end 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].split(",") # array of cached xtypes component_name = params[: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. 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 # DEPRECATED in favor of Symbol#component # 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 extend ActiveSupport::Memoizable def items #:nodoc: @items_with_normalized_components end # DEPRECATED in favor of Base.component 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 eager_loaded_components components.reject{|k,v| v[:lazy_loading]} end # DEPRECATED def add_component(aggr) components.merge!(aggr) end # DEPRECATED def remove_component(aggr) if config[:persistent_config] persistence_manager_class.delete_all_for_component("#{global_id}__#{aggr}") end components[aggr] = nil 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 a component based on its "path": e.g. if we have component :component1 which in its turn has component :component2, the path to the latter would be "component1__component2" def component_instance(name, strong_config = {}) composite = self name.to_s.split('__').each do |cmp| cmp = cmp.to_sym component_config = composite.components[cmp] raise ArgumentError, "No child component '#{cmp}' defined for component '#{composite.global_id}'" if component_config.nil? component_class_name = component_config[:class_name] raise ArgumentError, "No class_name specified for component #{cmp} of #{composite.global_id}" if component_class_name.nil? component_class = constantize_class_name(component_class_name) raise ArgumentError, "Unknown constant #{component_class_name}" if component_class.nil? instance_config = weak_children_config.merge(component_config).merge(strong_config).merge(:name => cmp) composite = component_class.new(instance_config, composite) # params: config, parent end composite end memoize :component_instance # for performance # All components that we depend on (used to render all necessary JavaScript and stylesheets) def dependency_classes res = [] eager_loaded_components.keys.each do |aggr| res += component_instance(aggr).dependency_classes end res += self.class.class_ancestors res << self.class res.uniq end # DEPRECATED 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 protected def normalize_components(items) #:nodoc: @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 #:nodoc: normalize_components(config[:items]) if config[:items] end def is_component_config?(c) #:nodoc: !!(c.is_a?(Hash) && c[:class_name]) end end end end