lib/netzke/core/client_code.rb
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