netzke/netzke-core

View on GitHub
lib/netzke/core/client_code.rb

Summary

Maintainability
A
0 mins
Test Coverage
module Netzke::Core
  # Client class definition and instantiation.
  module ClientCode
    extend ActiveSupport::Concern

    module ClassMethods
      # Configures client class
      # Example:
      #
      #   client_class do |c|
      #     # c is an instance of ClientClassConfig
      #     c.title = "My title"
      #     c.require :extra_js
      #     # ...etc
      #   end
      #
      # For more details see {Netzke::Core::ClientClassConfig}
      def client_class &block
        raise ArgumentError, "client_class called without block" unless block_given?
        @configure_blocks ||= []
        @configure_blocks << [block, dir(caller.first)]
      end

      # Class-level client class config.
      # Note: late evaluation of `client_class` blocks allows us using class-level configs in those blocks, e.g.:
      #
      #     class ConfigurableOnClassLevel < Netzke::Base
      #       class_attribute :title
      #       self.title = "Default"
      #       client_class do |c|
      #         c.title = self.title
      #       end
      #     end
      #
      #     ConfigurableOnClassLevel.title = "Overridden"
      def client_class_config
        return @client_class_config if @client_class_config

        @client_class_config = Netzke::Core::ClientClassConfig.new(self, called_from)

        (@configure_blocks || []).each do |block, dir|
          @client_class_config.dir = dir
          block.call(@client_class_config) if block
        end

        @client_class_config
      end

      # Path to the dir with this component/extension's extra code (ruby modules, scripts, stylesheets)
      def dir(cllr)
        %Q(#{cllr.split(".rb:").first})
      end

      # Converts given string to a string that returns itself as its JSON-encoded form for given string. See
      # {ClientCode#l}
      def l(str)
       Netzke::Core::JsonLiteral.new(str)
      end
    end

    # Builds {#js_config} used for instantiating a client class. Override it when you need to extend/modify the config for the JS component intance. It's *not* being called when the *server* class is being instantiated (e.g. to process an endpoint call). With other words, it's only being called before a component is first being loaded in the browser. so, it's ok to do heavy stuf fhere, like building a tree panel nodes from the database.
    def configure_client(c)
      c.merge!(normalized_config)

      %w[id item_id path netzke_components endpoints xtype alias i18n netzke_plugins].each do |thing|
        js_thing = send(:"js_#{thing}")
        c[thing] = js_thing if js_thing.present?
        c.client_config = client_config.netzke_literalize_keys # because this is what we'll get back from client side as server config, and the keys must be snake_case
      end

      # reset component session
      # TODO: also remove empty hashes from the global session
      component_session.clear
    end

    def js_path
      @path
    end

    # Global id in the component tree, following the double-underscore notation, e.g. +books__config_panel__form+
    def js_id
      @js_id ||= parent.nil? ? @item_id : [parent.js_id, @item_id].join("__")
    end

    def js_xtype
      self.class.client_class_config.xtype
    end

    def js_item_id
      @item_id
    end

    # Ext.createByAlias may be used to instantiate the component.
    def js_alias
      self.class.client_class_config.class_alias
    end

    def js_endpoints
      self.class.endpoints.keys.map{ |p| p.to_s.camelcase(:lower) }
    end

    def js_netzke_plugins
      plugins.map{ |p| p.to_s.camelcase(:lower) }
    end

    # Instance-level client class config. The result of this method (a hash) is converted to a JSON object and passed as options to the constructor of our JavaScript class.
    # Not to be overridden, override {#configure_client} instead.
    def js_config
      @js_config ||= ActiveSupport::OrderedOptions.new.tap{|c| configure_client(c)}
    end

    # Hash containing configuration for all child components to be instantiated at the JS side
    def js_components
      @js_components ||= eagerly_loaded_components.inject({}) do |out, name|
        instance = component_instance(name.to_sym)
        out.merge(name => instance.js_config)
      end
    end

    alias js_netzke_components js_components

    # All the JS-code required by this instance of the component to be instantiated in the browser, excluding cached
    # code.
    # It includes JS-classes for the parents, eagerly loaded child components, and itself.
    def js_missing_code(cached = [])
      code = dependency_classes.inject("") do |r,k|
        cached.include?(k.client_class_config.xtype) ? r : r + k.client_class_config.code_with_dependencies
      end
      code.blank? ? nil : Netzke::Core::DynamicAssets.minify_js(code)
    end

    protected

    # Converts given string to a string that returns itself as its JSON-encoded form for given string. Can sometimes be hande, for example, to pass simple client-side handler inline in the Ruby code:
    #
    #     def configure(c)
    #       super
    #       c.tools = [
    #         { type: :refresh, handler: l("function(){alert('Refresh clicked')}") }
    #       ]
    #     end
    def l(str)
      self.class.l(str)
    end

    # Allows referring to client-side function that will be called in the scope of the component. Handy to specify
    # handlers for tools/actions, or any other functions that have to be passed as configuration to different Ext JS
    # components. Usage:
    #
    #   class MyComponent < Netzke::Base
    #     def configure(c)
    #       super
    #       c.bbar = [{text: 'Export', handler: f(:do_export)}]
    #     end
    #   end
    #
    #   As a result, `MyComponent`'s client-side `doExport` function will be called *in the component's scope*, receiving all the
    #   usual handler parameters from Ext JS.
    #   Read more on how to define client-side functions in `Netzke::Core::ClientClassConfig`.
    def f(name)
      l("function(){var c=Ext.getCmp('#{js_id}'); return c.#{name.to_s.camelize(:lower)}.apply(c, arguments);}")
    end

    private

    # Merges all the translations in the class hierarchy
    # Note: this method can't be moved out to ClientClassConfig, because I18n is loaded only once, when other Ruby classes are evaluated; so, this must remain at instance level.
    def js_i18n
      @js_i18n ||= self.class.netzke_ancestors.inject({}) do |r,klass|
        hsh = klass.client_class_config.translated_properties.inject({}) { |h,t| h.merge(t => I18n.t("#{klass.i18n_id}.#{t}")) }
        r.merge(hsh)
      end
    end
  end
end