netzke-core 0.7.7 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (192) hide show
  1. data/.travis.yml +1 -2
  2. data/CHANGELOG.md +572 -0
  3. data/LICENSE +7 -1
  4. data/README.md +345 -29
  5. data/Rakefile +3 -3
  6. data/app/controllers/netzke_controller.rb +37 -48
  7. data/config/ci/before-travis.sh +3 -4
  8. data/javascripts/base.js +86 -150
  9. data/javascripts/ext.js +180 -210
  10. data/javascripts/{core_extensions.js → js_extensions.js} +0 -0
  11. data/lib/netzke-core.rb +16 -6
  12. data/lib/netzke/base.rb +84 -107
  13. data/lib/netzke/core.rb +7 -41
  14. data/lib/netzke/core/action_config.rb +37 -0
  15. data/lib/netzke/core/actions.rb +123 -0
  16. data/lib/netzke/core/client_class.rb +252 -0
  17. data/lib/netzke/core/component_config.rb +12 -0
  18. data/lib/netzke/core/composition.rb +274 -0
  19. data/lib/netzke/core/config_to_dsl_delegator.rb +69 -0
  20. data/lib/netzke/core/configuration.rb +63 -0
  21. data/lib/netzke/core/css_config.rb +51 -0
  22. data/lib/netzke/core/dynamic_assets.rb +19 -49
  23. data/lib/netzke/{embedding.rb → core/embedding.rb} +4 -6
  24. data/lib/netzke/core/endpoint_response.rb +15 -0
  25. data/lib/netzke/core/javascript.rb +111 -0
  26. data/lib/netzke/core/panel.rb +11 -0
  27. data/lib/netzke/{plugins.rb → core/plugins.rb} +10 -5
  28. data/lib/netzke/core/railz.rb +4 -0
  29. data/lib/netzke/{railz → core/railz}/action_view_ext.rb +22 -18
  30. data/lib/netzke/core/railz/action_view_ext/ext.rb +49 -0
  31. data/lib/netzke/core/railz/controller_extensions.rb +17 -0
  32. data/lib/netzke/core/railz/engine.rb +16 -0
  33. data/lib/netzke/core/railz/routes.rb +10 -0
  34. data/lib/netzke/core/ruby_ext.rb +5 -0
  35. data/lib/netzke/core/ruby_ext/array.rb +23 -0
  36. data/lib/netzke/core/ruby_ext/hash.rb +47 -0
  37. data/lib/netzke/{core_ext → core/ruby_ext}/string.rb +2 -7
  38. data/lib/netzke/core/ruby_ext/symbol.rb +13 -0
  39. data/lib/netzke/{core_ext → core/ruby_ext}/time_with_zone.rb +0 -0
  40. data/lib/netzke/core/services.rb +130 -0
  41. data/lib/netzke/core/session.rb +15 -19
  42. data/lib/netzke/core/state.rb +40 -0
  43. data/lib/netzke/core/stylesheets.rb +48 -0
  44. data/lib/netzke/core/version.rb +2 -2
  45. data/lib/netzke/plugin.rb +8 -11
  46. data/netzke-core.gemspec +69 -59
  47. data/test/core_test_app/Gemfile +2 -20
  48. data/test/core_test_app/Gemfile.lock +65 -74
  49. data/test/core_test_app/app/components/card_component_loader.rb +4 -4
  50. data/test/core_test_app/app/components/component_loader.rb +40 -120
  51. data/test/core_test_app/app/components/component_loader/javascripts/component_loader.js +49 -0
  52. data/test/core_test_app/app/components/component_with_actions.rb +61 -47
  53. data/test/core_test_app/app/components/component_with_custom_css.rb +8 -5
  54. data/test/core_test_app/app/components/component_with_js_mixin.rb +11 -5
  55. data/test/core_test_app/app/components/component_with_js_mixin/javascripts/extra_one.js +1 -1
  56. data/test/core_test_app/app/components/component_with_js_mixin/javascripts/extra_two.js +1 -1
  57. data/test/core_test_app/app/components/component_with_js_mixin/javascripts/method_set_one.js +1 -1
  58. data/test/core_test_app/app/components/component_with_nested_through.rb +2 -2
  59. data/test/core_test_app/app/components/component_with_prebuilt_toolbar_control.rb +12 -0
  60. data/test/core_test_app/app/components/component_with_prebuilt_toolbar_control/javascripts/component_with_prebuilt_toolbar_control.js +12 -0
  61. data/test/core_test_app/app/components/component_with_required_js.rb +24 -0
  62. data/test/core_test_app/app/components/configurable_on_class_level.rb +8 -0
  63. data/test/core_test_app/app/components/dsl_delegated_properties.rb +4 -0
  64. data/test/core_test_app/app/components/dsl_delegated_properties_base.rb +5 -0
  65. data/test/core_test_app/app/components/dynamic_tab_panel/javascripts/dynamic_tab_panel.js +2 -2
  66. data/test/core_test_app/app/components/ext_direct/composite.rb +32 -33
  67. data/test/core_test_app/app/components/ext_direct/details.rb +2 -4
  68. data/test/core_test_app/app/components/ext_direct/selector.rb +20 -22
  69. data/test/core_test_app/app/components/ext_direct/statistics.rb +2 -4
  70. data/test/core_test_app/app/components/extended_component_with_actions.rb +7 -3
  71. data/test/core_test_app/app/components/extended_component_with_js_mixin.rb +7 -4
  72. data/test/core_test_app/app/components/extended_component_with_js_mixin/javascripts/some_method_set.js +1 -1
  73. data/test/core_test_app/app/components/extended_server_caller.rb +20 -14
  74. data/test/core_test_app/app/components/hello_world.rb +23 -0
  75. data/test/core_test_app/app/components/hello_world/javascripts/hello_world.js +12 -0
  76. data/test/core_test_app/app/components/included.js +2 -2
  77. data/test/core_test_app/app/components/kinda_complex_component.rb +1 -3
  78. data/test/core_test_app/app/components/kinda_complex_component/basic_stuff.rb +15 -17
  79. data/test/core_test_app/app/components/kinda_complex_component/extra_stuff.rb +4 -5
  80. data/test/core_test_app/app/components/loader_of_component_with_custom_css.rb +14 -5
  81. data/test/core_test_app/app/components/localized_panel.rb +23 -25
  82. data/test/core_test_app/app/components/multipane_component_loader.rb +19 -20
  83. data/test/core_test_app/app/components/nested_component.rb +4 -5
  84. data/test/core_test_app/app/components/panel_with_plugin.rb +8 -3
  85. data/test/core_test_app/app/components/panel_with_tools.rb +15 -14
  86. data/test/core_test_app/app/components/plugin_with_components.rb +20 -12
  87. data/test/core_test_app/app/components/scoped_components/deep_scoped_components/some_deep_scoped_component.rb +5 -2
  88. data/test/core_test_app/app/components/scoped_components/extended_scoped_component.rb +5 -2
  89. data/test/core_test_app/app/components/scoped_components/some_scoped_component.rb +5 -2
  90. data/test/core_test_app/app/components/server_caller.rb +39 -17
  91. data/test/core_test_app/app/components/server_caller/javascripts/server_caller.js +42 -0
  92. data/test/core_test_app/app/components/server_counter.rb +18 -82
  93. data/test/core_test_app/app/components/server_counter/javascripts/server_counter.js +53 -0
  94. data/test/core_test_app/app/components/simple_authentication_component.rb +46 -0
  95. data/test/core_test_app/app/components/simple_component.rb +8 -3
  96. data/test/core_test_app/app/components/simple_composite.rb +12 -0
  97. data/test/core_test_app/app/components/simple_form_with_file_upload.rb +49 -0
  98. data/test/core_test_app/app/components/simple_panel.rb +2 -2
  99. data/test/core_test_app/app/components/simple_tab_panel.rb +24 -3
  100. data/test/core_test_app/app/components/simple_window.rb +4 -2
  101. data/test/core_test_app/app/components/some_composite.rb +77 -48
  102. data/test/core_test_app/app/components/some_plugin.rb +31 -30
  103. data/test/core_test_app/app/components/stateful_component.rb +46 -0
  104. data/test/core_test_app/app/components/stateful_component_with_shared_state.rb +11 -0
  105. data/test/core_test_app/app/components/window_with_simple_component.rb +14 -0
  106. data/test/core_test_app/app/views/layouts/application.html.erb +1 -1
  107. data/test/core_test_app/app/views/simple_rails/multiple_nested.html.erb +7 -19
  108. data/test/core_test_app/app/views/simple_rails/panel.html.erb +1 -0
  109. data/test/core_test_app/config/database.yml.travis +3 -5
  110. data/test/core_test_app/config/environments/production.rb +1 -1
  111. data/test/core_test_app/config/initializers/netzke.rb +3 -1
  112. data/test/core_test_app/config/locales/en.yml +9 -4
  113. data/test/core_test_app/config/locales/es.yml +4 -2
  114. data/test/core_test_app/config/routes.rb +2 -8
  115. data/test/core_test_app/db/schema.rb +3 -11
  116. data/test/core_test_app/features/actions_and_tools.feature +1 -0
  117. data/test/core_test_app/features/client-server.feature +7 -0
  118. data/test/core_test_app/features/component_loader.feature +13 -13
  119. data/test/core_test_app/features/composition.feature +14 -0
  120. data/test/core_test_app/features/config_to_dsl_delegation.feature +10 -0
  121. data/test/core_test_app/features/file_inclusion.feature +1 -1
  122. data/test/core_test_app/features/i18n.feature +4 -4
  123. data/test/core_test_app/features/js_include.feature +1 -1
  124. data/test/core_test_app/features/persistence.feature +21 -5
  125. data/test/core_test_app/features/step_definitions/generic_steps.rb +14 -0
  126. data/test/core_test_app/features/support/paths.rb +0 -3
  127. data/test/core_test_app/public/images/icons/accept.png +0 -0
  128. data/test/core_test_app/public/images/icons/anchor.png +0 -0
  129. data/test/core_test_app/public/images/icons/tick.png +0 -0
  130. data/test/core_test_app/spec/action_config_spec.rb +15 -0
  131. data/test/core_test_app/spec/{component/actions_spec.rb → actions_spec.rb} +38 -36
  132. data/test/core_test_app/spec/base_spec.rb +35 -0
  133. data/test/core_test_app/spec/client_class_spec.rb +17 -0
  134. data/test/core_test_app/spec/component +0 -0
  135. data/test/core_test_app/spec/composition_spec.rb +118 -0
  136. data/test/core_test_app/spec/core_ext_spec.rb +3 -14
  137. data/test/core_test_app/spec/endpoint_response_spec.rb +17 -0
  138. data/test/core_test_app/spec/javascript_spec.rb +33 -0
  139. data/test/core_test_app/spec/js_class_config_scope.rb +37 -0
  140. data/test/core_test_app/spec/panel_spec.rb +11 -0
  141. data/test/core_test_app/spec/services_spec.rb +16 -0
  142. data/test/core_test_app/spec/state_spec.rb +20 -0
  143. data/test/unit/core_ext_test.rb +0 -53
  144. data/test/unit/netzke_core_test.rb +11 -11
  145. metadata +76 -62
  146. data/CHANGELOG.rdoc +0 -325
  147. data/javascripts/touch.js +0 -58
  148. data/lib/netzke/actions.rb +0 -107
  149. data/lib/netzke/composition.rb +0 -224
  150. data/lib/netzke/config_to_dsl_delegator.rb +0 -43
  151. data/lib/netzke/configuration.rb +0 -195
  152. data/lib/netzke/core/masquerading.rb +0 -34
  153. data/lib/netzke/core_ext.rb +0 -6
  154. data/lib/netzke/core_ext/array.rb +0 -30
  155. data/lib/netzke/core_ext/hash.rb +0 -86
  156. data/lib/netzke/core_ext/symbol.rb +0 -21
  157. data/lib/netzke/ext_component.rb +0 -25
  158. data/lib/netzke/inheritance.rb +0 -31
  159. data/lib/netzke/javascript.rb +0 -382
  160. data/lib/netzke/javascript/scopes.rb +0 -39
  161. data/lib/netzke/railz.rb +0 -4
  162. data/lib/netzke/railz/action_view_ext/ext.rb +0 -64
  163. data/lib/netzke/railz/action_view_ext/touch.rb +0 -52
  164. data/lib/netzke/railz/controller_extensions.rb +0 -33
  165. data/lib/netzke/railz/engine.rb +0 -48
  166. data/lib/netzke/railz/routes.rb +0 -7
  167. data/lib/netzke/services.rb +0 -101
  168. data/lib/netzke/session.rb +0 -54
  169. data/lib/netzke/state.rb +0 -91
  170. data/lib/netzke/stylesheets.rb +0 -65
  171. data/test/core_test_app/app/components/component_with_included_js.rb +0 -16
  172. data/test/core_test_app/app/components/component_with_session_persistence.rb +0 -35
  173. data/test/core_test_app/app/components/deprecated/server_caller.rb +0 -20
  174. data/test/core_test_app/app/components/dynamic_tab_panel.rb +0 -19
  175. data/test/core_test_app/app/components/hello_world_component.rb +0 -31
  176. data/test/core_test_app/app/components/touch/hello_world_component.rb +0 -25
  177. data/test/core_test_app/app/components/touch/server_caller.rb +0 -28
  178. data/test/core_test_app/app/components/touch/simple_carousel.rb +0 -17
  179. data/test/core_test_app/app/controllers/touch_controller.rb +0 -6
  180. data/test/core_test_app/app/helpers/touch_helper.rb +0 -2
  181. data/test/core_test_app/app/views/layouts/touch.html.erb +0 -13
  182. data/test/core_test_app/db/migrate/20110110132720_create_netzke_component_states.rb +0 -20
  183. data/test/core_test_app/features/step_definitions/touch_steps.rb +0 -3
  184. data/test/core_test_app/features/touch.feature +0 -10
  185. data/test/core_test_app/gemfiles/rails3_1.gemfile +0 -16
  186. data/test/core_test_app/gemfiles/rails3_2.gemfile +0 -16
  187. data/test/core_test_app/spec/component/base_spec.rb +0 -36
  188. data/test/core_test_app/spec/component/component_spec.rb +0 -20
  189. data/test/core_test_app/spec/component/composition_spec.rb +0 -132
  190. data/test/core_test_app/spec/component/configuration_spec.rb +0 -61
  191. data/test/core_test_app/spec/component/javascript_spec.rb +0 -16
  192. data/test/core_test_app/spec/component/state_spec.rb +0 -18
@@ -0,0 +1,252 @@
1
+ module Netzke
2
+ module Core
3
+ # This class is responsible of creation of the client (JavaScript) class. It is passed as block parameter to the `js_configure` DSL method:
4
+ #
5
+ # class MyComponent < Netzke::Base
6
+ # js_configure do |c|
7
+ # c.extend = "Ext.form.Panel"
8
+ # end
9
+ # end
10
+ class ClientClass
11
+ attr_accessor :required_files, :base_class, :properties, :mixins, :translated_properties
12
+
13
+ def initialize(klass)
14
+ @klass = klass
15
+ @required_files = []
16
+ @mixins = []
17
+ @properties = {
18
+ extend: extended_class,
19
+ alias: class_alias,
20
+ }
21
+ @properties[:mixins] = ['Netzke.classes.Core.Mixin'] if extending_extjs_component?
22
+ @translated_properties = []
23
+ end
24
+
25
+ # Allows assigning JavaScript prototype properties, including functions:
26
+ #
27
+ # class MyComponent < Netzke::Base
28
+ # js_configure do |c|
29
+ # # this will result in the +title+ property defined on the client class prototype
30
+ # c.title = "My cool component"
31
+ #
32
+ # # this will result in the +onButtonPress+ function defined on the client class prototype
33
+ # c.on_button_press = <<-JS
34
+ # function(){
35
+ # // ...
36
+ # }
37
+ # JS
38
+ # end
39
+ # end
40
+ #
41
+ # An alternative way to define prototype properties is by using "mixins", see {ClientClass#mixin}
42
+ #
43
+ # Class attributes are accessible from inside +js_configure+:
44
+ #
45
+ # class MyComponent < Netzke::Base
46
+ # class_attribute :title
47
+ # self.title = "Some default title"
48
+ # js_configure do |c|
49
+ # c.title = self.title
50
+ # end
51
+ # end
52
+ #
53
+ # Now you can configure your component on a class level like this:
54
+ #
55
+ # # e.g. in Rails initializers
56
+ # MyComponent.title = "New title for all MyComponents"
57
+ #
58
+ # Or using a helper method provided by Netzke:
59
+ #
60
+ # MyComponent.setup do |config|
61
+ # config.title = "New title for all MyComponents"
62
+ # end
63
+ def method_missing(name, *args)
64
+ if name =~ /(.+)=$/
65
+ value = args.first
66
+ @properties[$1.to_sym] = value.is_a?(String) && value =~ /^\s*function/ ? ActiveSupport::JSON::Variable.new(value) : value
67
+ else
68
+ @properties[name.to_sym]
69
+ end
70
+ end
71
+
72
+ # Use it to specify JavaScript files to be loaded *before* this component's JavaScript code. Useful when using external extensions required by this component.
73
+ #
74
+ # It may accept one or more symbols or strings.
75
+ #
76
+ # Symbols will be expanded following a convention, e.g.:
77
+ #
78
+ # class MyComponent < Netzke::Base
79
+ # js_configure do |c|
80
+ # c.require :some_library
81
+ # end
82
+ # end
83
+ #
84
+ # This will "require" a JavaScript file +{component_location}/my_component/javascripts/some_library.js+
85
+ #
86
+ # Strings will be interpreted as full paths to the required JavaScript file:
87
+ #
88
+ # js_configure do |c|
89
+ # c.require "#{File.dirname(__FILE__)}/my_component/one.js", "#{File.dirname(__FILE__)}/my_component/two.js"
90
+ # end
91
+ def require(*args)
92
+ callr = caller.first
93
+
94
+ @required_files |= args.map{ |a| a.is_a?(Symbol) ? expand_require_path(a, callr) : a }
95
+ end
96
+
97
+ # Use it to "mixin" JavaScript objects defined in a separate file. It may accept one or more symbols or strings.
98
+ #
99
+ # Symbols will be expanded following a convention, e.g.:
100
+ #
101
+ # class MyComponent < Netzke::Base
102
+ # js_configure do |c|
103
+ # c.mixin :some_functionality
104
+ # #...
105
+ # end
106
+ # end
107
+ #
108
+ # This will "mixin" a JavaScript object defined in a file named +{component_location}/my_component/javascripts/some_functionality.js+, which way contain something like this:
109
+ #
110
+ # {
111
+ # someProperty: 100,
112
+ # someMethod: function(params){
113
+ # // ...
114
+ # }
115
+ # }
116
+ #
117
+ # Also accepts a string, which will be interpreted as a full path to the file (useful for sharing mixins between classes).
118
+ # With no parameters, will assume :component_class_name_underscored.
119
+ #
120
+ # Also, see defining JavaScript prototype properties with {ClientClass#method_missing}.
121
+ def mixin(*args)
122
+ args << @klass.name.split("::").last.underscore.to_sym if args.empty?
123
+ callr = caller.first
124
+ args.each do |a|
125
+ @mixins << (a.is_a?(Symbol) ? expand_require_path(a, callr) : a)
126
+ end
127
+ end
128
+
129
+ # Defines the "i18n" config property, that is a translation object for this component, such as:
130
+ # i18n: {
131
+ # overwriteConfirm: "Are you sure you want to overwrite preset '{0}'?",
132
+ # overwriteConfirmTitle: "Overwriting preset",
133
+ # deleteConfirm: "Are you sure you want to delete preset '{0}'?",
134
+ # deleteConfirmTitle: "Deleting preset"
135
+ # }
136
+ #
137
+ # E.g.:
138
+ #
139
+ # class MyComponent < Netzke::Base
140
+ # js_configure do |c|
141
+ # c.translate :overwrite_confirm, :overwrite_confirm_title, :delete_confirm, :delete_confirm_title
142
+ # end
143
+ # end
144
+ def translate(*args)
145
+ @translated_properties |= args
146
+ end
147
+
148
+ # The alias, required by Ext.Component, e.g.: widget.helloworld
149
+ def class_alias
150
+ [alias_prefix, xtype].join(".")
151
+ end
152
+
153
+ # Builds this component's xtype
154
+ # E.g.: netzkebasepackwindow, netzkebasepackgridpanel
155
+ def xtype
156
+ @klass.name.gsub("::", "").downcase
157
+ end
158
+
159
+ # Component's JavaScript class declaration.
160
+ # It gets stored in the JS class cache storage (Netzke.classes) at the client side to be reused at the moment of component instantiation.
161
+ def class_code
162
+ res = []
163
+ # Defining the scope if it isn't known yet
164
+ res << %{Ext.ns("#{scope}");} unless scope == default_scope
165
+
166
+ res << class_declaration
167
+
168
+ # Store created class xtype in the cache
169
+ res << %(
170
+ Netzke.cache.push('#{xtype}');
171
+ )
172
+
173
+ res.join("\n")
174
+ end
175
+
176
+ # Top level scope which will be used to scope out Netzke classes
177
+ def default_scope
178
+ "Netzke.classes"
179
+ end
180
+
181
+ # Returns the scope of this component
182
+ # e.g. "Netzke.classes.Netzke.Basepack"
183
+ def scope
184
+ [default_scope, *@klass.name.split("::")[0..-2]].join(".")
185
+ end
186
+
187
+ # Returns the full name of the JavaScript class, including the scopes *and* the common scope, which is 'Netzke.classes'.
188
+ # E.g.: "Netzke.classes.Netzke.Basepack.GridPanel"
189
+ def class_name
190
+ [scope, @klass.name.split("::").last].join(".")
191
+ end
192
+
193
+ # Whether we have to inherit from an Ext JS component, or a Netzke component
194
+ def extending_extjs_component?
195
+ @klass.superclass == Netzke::Base
196
+ end
197
+
198
+ # Returns all required JavaScript files as a string
199
+ def required
200
+ res = ""
201
+
202
+ # Prevent re-including code that was already required by the parent
203
+ # (thus, only require those JS files when require_js was defined in the current class, not in its ancestors)
204
+ # FIXME!
205
+ ((@klass.singleton_methods(false).map(&:to_sym).include?(:include_js) ? include_js : []) + required_files).each do |path|
206
+ f = File.new(path)
207
+ res << f.read << "\n"
208
+ end
209
+
210
+ res
211
+ end
212
+
213
+ # JavaScript code needed for this particulaer class. Includes external JS code and the JS class definition for self.
214
+ def code_with_dependencies
215
+ [required, class_code].join("\n")
216
+ end
217
+
218
+ # Generates declaration of the JS class as direct extension of a Ext component
219
+ def class_declaration
220
+ %(Ext.define('#{class_name}', #{properties_as_string});)
221
+ end
222
+
223
+ # Alias prefix: 'widget' for components, 'plugin' for plugins
224
+ def alias_prefix
225
+ @klass < Netzke::Plugin ? "plugin" : "widget"
226
+ end
227
+
228
+ def mixins_as_string
229
+ mixins.presence && mixins.map do |f|
230
+ as_string = File.read(f)
231
+ as_string.sub!('{', ' ')
232
+ as_string[as_string.rindex('}')] = ' '
233
+ as_string.rstrip
234
+ end.join(",\n")
235
+ end
236
+
237
+ def properties_as_string
238
+ [properties.to_nifty_json.chop, mixins_as_string].compact.join(",\n") + "}"
239
+ end
240
+
241
+ # Default extended class
242
+ def extended_class
243
+ extending_extjs_component? ? "Ext.panel.Panel" : @klass.superclass.js_config.class_name
244
+ end
245
+
246
+ private
247
+ def expand_require_path(sym, callr)
248
+ %Q(#{callr.split(".rb:").first}/javascripts/#{sym}.js)
249
+ end
250
+ end
251
+ end
252
+ end
@@ -0,0 +1,12 @@
1
+ module Netzke::Core
2
+ class ComponentConfig < ActiveSupport::OrderedOptions
3
+ def initialize(name)
4
+ self.name = name.to_s
5
+ end
6
+
7
+ def set_defaults!
8
+ self.item_id ||= name # default item_id
9
+ self.klass ||= name.camelize.constantize # default klass
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,274 @@
1
+ module Netzke::Core
2
+ # Any Netzke component can define child components, which can either be statically nested in the compound layout (e.g. as different regions of the 'border' layout), or dynamically loaded at a request (as is the advanced search panel in Basepack::GridPanel, for example).
3
+ #
4
+ # == Defining a component
5
+ #
6
+ # You can define a child component by calling the +component+ class method which normally requires a block:
7
+ #
8
+ # component :users do |c|
9
+ # c.klass = GridPanel
10
+ # c.model = "User"
11
+ # c.title = "Users"
12
+ # end
13
+ #
14
+ # If no configuration is required, and the component's class name can be derived from its name, then the block can be omitted, e.g.:
15
+ #
16
+ # component :user_grid
17
+ #
18
+ # which is equivalent to:
19
+ #
20
+ # component :user_grid do |c|
21
+ # c.klass = UserGrid
22
+ # end
23
+ #
24
+ # == Overriding a component
25
+ #
26
+ # When overriding a component, the `super` method should be called, with the configuration object passed to it as parameter:
27
+ #
28
+ # component :users do |c|
29
+ # super(c)
30
+ # c.title = "Modified Title"
31
+ # end
32
+ #
33
+ # == Referring to components in layouts
34
+ #
35
+ # A child component can be referred in the layout by using symbols:
36
+ #
37
+ # component :users do |c|
38
+ # c.title = "A Netzke component"
39
+ # end
40
+ #
41
+ # def configure(c)
42
+ # super
43
+ # c.items = [
44
+ # { xtype: :panel, title: "Simple Ext panel" },
45
+ # :users # a Netzke component
46
+ # ]
47
+ # end
48
+ #
49
+ # If an extra (layout) configuration should be provided, a component can be referred to by using the +component+ key in the configuration hash (this can be useful when overriding a layout of a child component):
50
+ #
51
+ # component :tab_one # ...
52
+ # component :tab_two # ...
53
+ #
54
+ # def configure(c)
55
+ # super
56
+ # c.items = [
57
+ # {component: :tab_one, title: "One"},
58
+ # {component: :tab_two, title: "Two"}
59
+ # ]
60
+ # end
61
+ #
62
+ # == Lazily vs eagerly loaded components
63
+ #
64
+ # By default, if a component is not used in the layout, it is lazily loaded, which means that the code for this component is not loaded in the browser until the moment the component gets dynamically loaded by the JavaScript method `netzkeLoadComponent` (see {Netzke::Core::Javascript}). Referring a component in the layout (the `items` property) automatically makes it eagerly loaded. Sometimes it's desired to eagerly load a component without using it directly in the layout (an example can be a window that we need to render instantly without requesting the server). In this case an option `eager_loading` can be set to true:
65
+ #
66
+ # component :eagerly_loaded_window do |c|
67
+ # c.klass = SomeWindowComponent
68
+ # c.eager_loading = true
69
+ # end
70
+ #
71
+ # == Dynamic component loading
72
+ #
73
+ # Child components can be dynamically loaded by using client class' +netzkeLoadComponent+ method (see {javascript/ext.js}[https://github.com/netzke/netzke-core/blob/master/javascripts/ext.js] for inline documentation):
74
+ #
75
+ # == Excluded components
76
+ #
77
+ # You can make a child component inavailable for dynamic loading by using the +excluded+ option. When an excluded component is used in the layout, it will be skipped.
78
+ # This can be used for authorization.
79
+ module Composition
80
+ extend ActiveSupport::Concern
81
+
82
+ COMPONENT_METHOD_NAME = "%s_component"
83
+
84
+
85
+ included do
86
+
87
+ # Returns registered components
88
+ class_attribute :registered_components
89
+ self.registered_components = []
90
+
91
+ # @!method Foobar
92
+ # Loads a component on browser's request. Every Netzke component gets this endpoint.
93
+ # <tt>params</tt> should contain:
94
+ # * <tt>:cache</tt> - an array of component classes cached at the browser
95
+ # * <tt>:id</tt> - reference to the component
96
+ # * <tt>:container</tt> - Ext id of the container where in which the component will be rendered
97
+ endpoint :deliver_component do |params, this|
98
+ cache = params[:cache].split(",") # array of cached xtypes
99
+ component_name = params[:name].underscore.to_sym
100
+ component = components[component_name] && !components[component_name][:excluded] && component_instance(component_name)
101
+
102
+ if component
103
+ js, css = component.js_missing_code(cache), component.css_missing_code(cache)
104
+ this.netzke_eval_js(js) if js.present?
105
+ this.netzke_eval_css(css) if css.present?
106
+
107
+ this.netzke_component_delivered(component.js_config);
108
+ else
109
+ this.netzke_component_delivery_failed(component_name: component_name, msg: "Couldn't load component '#{component_name}'")
110
+ end
111
+ end
112
+
113
+ end # included
114
+
115
+ module ClassMethods
116
+
117
+ # Declares a child (nested) component.
118
+ # @param name [Symbol] component name
119
+ # @param block [Proc] config block
120
+ # @example
121
+ # component :users do |c|
122
+ # c.klass = Netzke::Basepack::Grid
123
+ # c.modul = "User"
124
+ # end
125
+ def component(name, &block)
126
+ self.registered_components |= [name]
127
+
128
+ method_name = COMPONENT_METHOD_NAME % name
129
+ if block_given?
130
+ define_method(method_name, &block)
131
+ else
132
+ define_method(method_name) do |component_config|
133
+ component_config
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ # @return [Hash] component configs by name
140
+ def components
141
+ @components ||= self.class.registered_components.inject({}) do |out, name|
142
+ component_config = Netzke::Core::ComponentConfig.new(name)
143
+ send(COMPONENT_METHOD_NAME % name, component_config)
144
+ component_config.set_defaults!
145
+ if component_config.excluded
146
+ out.merge(name.to_sym => {excluded: true})
147
+ else
148
+ out.merge(name.to_sym => component_config)
149
+ end
150
+ end
151
+ end
152
+
153
+ # @return [Hash] configs of eagerly loaded components by name
154
+ def eagerly_loaded_components
155
+ @eagerly_loaded_components ||= components.select{|k,v| components_in_config.include?(k) || v[:eager_loading]}
156
+ end
157
+
158
+ # @return [Array<Symbol>] components (by name) referred in config (and thus, required to be instantiated)
159
+ def components_in_config
160
+ @components_in_config || (normalize_config || true) && @components_in_config
161
+ end
162
+
163
+ # Called when the method_missing tries to processes a non-existing component. Override when needed.
164
+ def component_missing(aggr)
165
+ flash :error => "Unknown component #{aggr} for #{name}"
166
+ {:feedback => @flash}.to_nifty_json
167
+ end
168
+
169
+ # Recursively instantiates a child 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"
170
+ # @param name [Symbol] component name
171
+ def component_instance(name)
172
+ raise ArgumentError, "No component '#{name.inspect}' defined for '#{self.js_id}'" if !name.present?
173
+
174
+ @component_instance_cache ||= {}
175
+ @component_instance_cache[name] ||= begin
176
+ composite = self
177
+ name.to_s.split('__').each do |cmp|
178
+ cmp = cmp.to_sym
179
+
180
+ component_config = composite.components[cmp]
181
+ raise ArgumentError, "No component '#{cmp}' defined for '#{composite.js_id}'" if component_config.nil? || component_config[:excluded]
182
+
183
+ klass = component_config[:klass]
184
+
185
+ instance_config = component_config.merge(:name => cmp)
186
+
187
+ composite = klass.new(instance_config, composite) # params: config, parent
188
+ end
189
+ composite
190
+ end
191
+ end
192
+
193
+ # @return [Array<Class>] All component classes that we depend on (used to render all necessary javascripts and stylesheets)
194
+ def dependency_classes
195
+ res = []
196
+
197
+ eagerly_loaded_components.keys.each do |aggr|
198
+ res += component_instance(aggr).dependency_classes
199
+ end
200
+
201
+ res += self.class.netzke_ancestors
202
+ res.uniq
203
+ end
204
+
205
+ # JS 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 <tt>nil</tt>, while referring to a non-existent component will simply provide an erroneous ID.
206
+ # For example:
207
+ # <tt>parent__parent__child__subchild</tt> 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 <tt>nil</tt> will be returned.
208
+ # @param ref [Symbol] reference to a child component
209
+ # @return [String] JS id
210
+ def js_id_by_reference(ref)
211
+ ref = ref.to_s
212
+ return parent && parent.js_id if ref == "parent"
213
+ substr = ref.sub(/^parent__/, "")
214
+ if substr == ref # there's no "parent__" in the beginning
215
+ return js_id + "__" + ref
216
+ else
217
+ return parent.js_id_by_reference(substr)
218
+ end
219
+ end
220
+
221
+ protected
222
+
223
+ # During normalization of the config object, this method is being called with each item found (recursively) in there.
224
+ # For example, symbols representing nested child components get replaced with a proper config hash. Same goes for actions.
225
+ # Override to do any additional checks/enhancements. See, for example, +Netzke::Basepack::WrapLazyLoaded+.
226
+ # @return [Object] extended item
227
+ def extend_item(item)
228
+ # in a situation of action and component being equally named, action will take precedence
229
+
230
+ if item.is_a?(Symbol) && item_config = actions[item]
231
+ item = {netzke_action: item}
232
+ elsif item.is_a?(Symbol) && item_config = components[item]
233
+ item = {netzke_component: item}
234
+ end
235
+
236
+ item[:excluded] = true if item_config && item_config[:excluded]
237
+
238
+ if item.is_a?(Hash)
239
+ return nil if item[:excluded] # it'll get compacted away by Array#deep_map
240
+
241
+ # replace the `component` and `action` keys with `netzke_component` and `netzke_action`, which will be looked for at the JS side
242
+ item[:netzke_action] = item.delete(:action) if item[:action]
243
+ item[:netzke_component] = item.delete(:component) if item[:component]
244
+
245
+ @components_in_config << item[:netzke_component] if item[:netzke_component] && item[:eager_loading] != false
246
+ end
247
+
248
+ item
249
+ end
250
+
251
+ private
252
+
253
+ # We'll build a couple of useful instance variables here:
254
+ #
255
+ # +components_in_config+ - an array of components (by name) referred in items
256
+ # +normalized_config+ - a config that has all the config extensions applied
257
+ def normalize_config
258
+ @components_in_config = []
259
+ @normalized_config = config.dup.tap do |c|
260
+ c.each_pair do |k,v|
261
+ c.delete(k) if self.class.server_side_config_options.include?(k.to_sym)
262
+ c[k] = v.deep_map{|el| extend_item(el)} if v.is_a?(Array)
263
+ end
264
+ end
265
+ end
266
+
267
+ # @return [Hash] config with all placeholders (like child components referred by symbols) expanded
268
+ def normalized_config
269
+ # make sure we call normalize_config first
270
+ @normalized_config || (normalize_config || true) && @normalized_config
271
+ end
272
+
273
+ end
274
+ end