glimmer-dsl-web 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.7
1
+ 0.0.9
@@ -2,16 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: glimmer-dsl-web 0.0.7 ruby lib
5
+ # stub: glimmer-dsl-web 0.0.9 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "glimmer-dsl-web".freeze
9
- s.version = "0.0.7".freeze
9
+ s.version = "0.0.9".freeze
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib".freeze]
13
13
  s.authors = ["Andy Maleh".freeze]
14
- s.date = "2024-01-02"
14
+ s.date = "2024-01-05"
15
15
  s.description = "Glimmer DSL for Web (Ruby in the Browser Web GUI Frontend Library) - Enables frontend GUI development with Ruby by adopting a DSL that follows web-like HTML syntax, enabling the transfer of HTML/CSS/JS skills to Ruby frontend development. This library relies on Opal Ruby.".freeze
16
16
  s.email = "andy.am@gmail.com".freeze
17
17
  s.extra_rdoc_files = [
@@ -31,6 +31,8 @@ Gem::Specification.new do |s|
31
31
  "lib/glimmer-dsl-web/ext/date.rb",
32
32
  "lib/glimmer-dsl-web/ext/exception.rb",
33
33
  "lib/glimmer-dsl-web/samples/hello/hello_button.rb",
34
+ "lib/glimmer-dsl-web/samples/hello/hello_component.rb",
35
+ "lib/glimmer-dsl-web/samples/hello/hello_content_data_binding.rb",
34
36
  "lib/glimmer-dsl-web/samples/hello/hello_data_binding.rb",
35
37
  "lib/glimmer-dsl-web/samples/hello/hello_form.rb",
36
38
  "lib/glimmer-dsl-web/samples/hello/hello_input_date_time.rb",
@@ -39,6 +41,8 @@ Gem::Specification.new do |s|
39
41
  "lib/glimmer/config/opal_logger.rb",
40
42
  "lib/glimmer/data_binding/element_binding.rb",
41
43
  "lib/glimmer/dsl/web/bind_expression.rb",
44
+ "lib/glimmer/dsl/web/component_expression.rb",
45
+ "lib/glimmer/dsl/web/content_data_binding_expression.rb",
42
46
  "lib/glimmer/dsl/web/data_binding_expression.rb",
43
47
  "lib/glimmer/dsl/web/dsl.rb",
44
48
  "lib/glimmer/dsl/web/element_expression.rb",
@@ -50,6 +54,7 @@ Gem::Specification.new do |s|
50
54
  "lib/glimmer/dsl/web/shine_data_binding_expression.rb",
51
55
  "lib/glimmer/util/proc_tracker.rb",
52
56
  "lib/glimmer/web.rb",
57
+ "lib/glimmer/web/component.rb",
53
58
  "lib/glimmer/web/element_proxy.rb",
54
59
  "lib/glimmer/web/event_proxy.rb",
55
60
  "lib/glimmer/web/listener_proxy.rb"
@@ -61,10 +66,13 @@ Gem::Specification.new do |s|
61
66
 
62
67
  s.specification_version = 4
63
68
 
64
- s.add_runtime_dependency(%q<glimmer>.freeze, ["~> 2.4.1".freeze])
69
+ s.add_runtime_dependency(%q<glimmer>.freeze, ["~> 2.7.6".freeze])
65
70
  s.add_runtime_dependency(%q<glimmer-dsl-xml>.freeze, ["~> 1.3.2".freeze])
66
71
  s.add_runtime_dependency(%q<glimmer-dsl-css>.freeze, ["~> 1.2.2".freeze])
72
+ s.add_runtime_dependency(%q<opal>.freeze, ["= 1.4.1".freeze])
73
+ s.add_runtime_dependency(%q<opal-rails>.freeze, ["= 2.0.2".freeze])
67
74
  s.add_runtime_dependency(%q<opal-async>.freeze, ["~> 1.4.0".freeze])
75
+ s.add_runtime_dependency(%q<opal-jquery>.freeze, ["~> 0.4.6".freeze])
68
76
  s.add_runtime_dependency(%q<to_collection>.freeze, [">= 2.0.1".freeze, "< 3.0.0".freeze])
69
77
  s.add_development_dependency(%q<puts_debuggerer>.freeze, [">= 0".freeze])
70
78
  s.add_development_dependency(%q<rake>.freeze, [">= 10.1.0".freeze, "< 14.0.0".freeze])
@@ -72,7 +80,5 @@ Gem::Specification.new do |s|
72
80
  s.add_development_dependency(%q<jeweler>.freeze, [">= 2.3.9".freeze, "< 3.0.0".freeze])
73
81
  s.add_development_dependency(%q<rdoc>.freeze, [">= 6.2.1".freeze, "< 7.0.0".freeze])
74
82
  s.add_development_dependency(%q<opal-rspec>.freeze, ["~> 0.8.0.alpha2".freeze])
75
- s.add_development_dependency(%q<opal-rails>.freeze, ["~> 1.1.2".freeze])
76
- s.add_development_dependency(%q<opal-jquery>.freeze, ["~> 0.4.4".freeze])
77
83
  end
78
84
 
@@ -0,0 +1,30 @@
1
+ require 'glimmer/dsl/parent_expression'
2
+ require 'glimmer/web/component'
3
+
4
+ module Glimmer
5
+ module DSL
6
+ module Web
7
+ class ComponentExpression < Expression
8
+ include ParentExpression
9
+
10
+ def can_interpret?(parent, keyword, *args, &block)
11
+ !!Glimmer::Web::Component.for(keyword)
12
+ end
13
+
14
+ def interpret(parent, keyword, *args, &block)
15
+ custom_widget_class = Glimmer::Web::Component.for(keyword)
16
+ custom_widget_class.new(parent, args, {}, &block)
17
+ end
18
+
19
+ def add_content(parent, keyword, *args, &block)
20
+ # TODO consider avoiding source_location since it does not work in Opal
21
+ if block.source_location && (block.source_location == parent.content&.__getobj__&.source_location)
22
+ parent.content.call(parent) unless parent.content.called?
23
+ else
24
+ super
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,41 @@
1
+ # Copyright (c) 2023-2024 Andy Maleh
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ require 'glimmer/dsl/expression'
23
+
24
+ module Glimmer
25
+ module DSL
26
+ module Web
27
+ class ContentDataBindingExpression < Expression
28
+ def can_interpret?(parent, keyword, *args, &block)
29
+ keyword == 'content' &&
30
+ block_given? &&
31
+ args.size > 0 &&
32
+ parent&.respond_to?(:bind_content)
33
+ end
34
+
35
+ def interpret(parent, keyword, *args, &block)
36
+ parent.bind_content(*args, &block)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,5 +1,4 @@
1
1
  require 'glimmer/dsl/engine'
2
- # Dir[File.expand_path('../*_expression.rb', __FILE__)].each {|f| require f}
3
2
  require 'glimmer/dsl/web/element_expression'
4
3
  require 'glimmer/dsl/web/listener_expression'
5
4
  require 'glimmer/dsl/web/property_expression'
@@ -7,25 +6,21 @@ require 'glimmer/dsl/web/p_expression'
7
6
  require 'glimmer/dsl/web/select_expression'
8
7
  require 'glimmer/dsl/web/bind_expression'
9
8
  require 'glimmer/dsl/web/data_binding_expression'
9
+ require 'glimmer/dsl/web/content_data_binding_expression'
10
10
  require 'glimmer/dsl/web/shine_data_binding_expression'
11
+ require 'glimmer/dsl/web/component_expression'
11
12
 
12
13
  module Glimmer
13
14
  module DSL
14
15
  module Web
15
- # TODO implement all those expressions
16
- # %w[
17
- # listener
18
- # data_binding
19
- # property
20
- # shine_data_binding
21
- # element
22
- # ]
23
16
  Engine.add_dynamic_expressions(
24
17
  Web,
25
18
  %w[
19
+ component
26
20
  listener
27
21
  data_binding
28
22
  property
23
+ content_data_binding
29
24
  shine_data_binding
30
25
  element
31
26
  ]
@@ -1,6 +1,8 @@
1
1
  require 'glimmer/dsl/expression'
2
2
  require 'glimmer/dsl/web/general_element_expression'
3
3
 
4
+ require 'glimmer/web/element_proxy'
5
+
4
6
  module Glimmer
5
7
  module DSL
6
8
  module Web
@@ -8,9 +10,7 @@ module Glimmer
8
10
  include GeneralElementExpression
9
11
 
10
12
  def can_interpret?(parent, keyword, *args, &block)
11
- # TODO automatically pass parent option as element if not passed instead of rejecting elements without a paraent nor root
12
- # TODO raise a proper error if root is an element that is not found (maybe do this in model)
13
- !keyword.to_s.start_with?('on')
13
+ Glimmer::Web::ElementProxy.keyword_supported?(keyword)
14
14
  end
15
15
  end
16
16
  end
@@ -0,0 +1,317 @@
1
+ # Copyright (c) 2023-2024 Andy Maleh
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ require 'glimmer'
23
+ require 'glimmer/error'
24
+ require 'glimmer/util/proc_tracker'
25
+ require 'glimmer/data_binding/observer'
26
+ require 'glimmer/data_binding/observable_model'
27
+
28
+ module Glimmer
29
+ module Web
30
+ module Component
31
+ include DataBinding::ObservableModel
32
+
33
+ module ClassMethods
34
+ include Glimmer
35
+
36
+ # Allows defining convenience option accessors for an array of option names
37
+ # Example: `options :color1, :color2` defines `#color1` and `#color2`
38
+ # where they return the instance values `options[:color1]` and `options[:color2]`
39
+ # respectively.
40
+ # Can be called multiple times to set more options additively.
41
+ # When passed no arguments, it returns list of all option names captured so far
42
+ def options(*new_options)
43
+ new_options = new_options.compact.map(&:to_s).map(&:to_sym)
44
+ if new_options.empty?
45
+ @options ||= {} # maps options to defaults
46
+ else
47
+ new_options = new_options.reduce({}) {|new_options_hash, new_option| new_options_hash.merge(new_option => nil)}
48
+ @options = options.merge(new_options)
49
+ def_option_attr_accessors(new_options)
50
+ end
51
+ end
52
+
53
+ def option(new_option, default: nil)
54
+ new_option = new_option.to_s.to_sym
55
+ new_options = {new_option => default}
56
+ '@options = options.merge(new_options)'
57
+ @options = options.merge(new_options)
58
+ 'def_option_attr_accessors(new_options)'
59
+ def_option_attr_accessors(new_options)
60
+ end
61
+
62
+ def def_option_attr_accessors(new_options)
63
+ new_options.each do |option, default|
64
+ define_method(option) do
65
+ options[:"#{option}"]
66
+ end
67
+ define_method("#{option}=") do |option_value|
68
+ self.options[:"#{option}"] = option_value
69
+ end
70
+ end
71
+ end
72
+
73
+ def before_render(&block)
74
+ @before_render_blocks ||= []
75
+ @before_render_blocks << block
76
+ end
77
+
78
+ def markup(&block)
79
+ @markup_block = block
80
+ end
81
+
82
+ def after_render(&block)
83
+ @after_render_blocks ||= []
84
+ @after_render_blocks << block
85
+ end
86
+
87
+ def keyword
88
+ self.name.underscore.gsub('::', '__')
89
+ end
90
+
91
+ # Returns shortcut keyword to use for this component (keyword minus namespace)
92
+ def shortcut_keyword
93
+ self.name.underscore.gsub('::', '__').split('__').last
94
+ end
95
+
96
+ def render(*args)
97
+ rendered_component = send(keyword)
98
+ rendered_component.render(*args)
99
+ rendered_component
100
+ end
101
+ end
102
+
103
+ # This module was only created to prevent Glimmer from checking method_missing first
104
+ module GlimmerSupersedable
105
+ def method_missing(method_name, *args, &block)
106
+ Glimmer::DSL::Engine.interpret(method_name, *args, &block)
107
+ rescue
108
+ super(method_name, *args, &block)
109
+ end
110
+ end
111
+
112
+ class << self
113
+ def included(klass)
114
+ if !klass.ancestors.include?(GlimmerSupersedable)
115
+ klass.extend(ClassMethods)
116
+ klass.include(Glimmer)
117
+ klass.prepend(GlimmerSupersedable)
118
+ Glimmer::Web::Component.add_component_namespaces_for(klass)
119
+ end
120
+ end
121
+
122
+ def for(underscored_component_name)
123
+ extracted_namespaces = underscored_component_name.
124
+ to_s.
125
+ split(/__/).map do |namespace|
126
+ namespace.camelcase(:upper)
127
+ end
128
+ Glimmer::Web::Component.component_namespaces.each do |base|
129
+ extracted_namespaces.reduce(base) do |result, namespace|
130
+ if !result.constants.include?(namespace)
131
+ namespace = result.constants.detect {|c| c.to_s.upcase == namespace.to_s.upcase } || namespace
132
+ end
133
+ begin
134
+ constant = result.const_get(namespace)
135
+ return constant if constant&.respond_to?(:ancestors) &&
136
+ (
137
+ constant&.ancestors&.to_a.include?(Glimmer::Web::Component) ||
138
+ # TODO checking GlimmerSupersedable as a hack because when a class is loaded twice (like when loading samples
139
+ # by reloading ruby files), it loses its Glimmer::Web::Component ancestor as a bug in Opal
140
+ # but somehow the prepend module remains
141
+ constant&.ancestors&.to_a.include?(GlimmerSupersedable)
142
+ )
143
+ constant
144
+ rescue => e
145
+ # Glimmer::Config.logger.debug {"#{e.message}\n#{e.backtrace.join("\n")}"}
146
+ result
147
+ end
148
+ end
149
+ end
150
+ raise "#{underscored_component_name} has no Glimmer web component class!"
151
+ rescue => e
152
+ Glimmer::Config.logger.debug {e.message}
153
+ Glimmer::Config.logger.debug {"#{e.message}\n#{e.backtrace.join("\n")}"}
154
+ nil
155
+ end
156
+
157
+ def add_component_namespaces_for(klass)
158
+ Glimmer::Web::Component.namespaces_for_class(klass).drop(1).each do |namespace|
159
+ Glimmer::Web::Component.component_namespaces << namespace
160
+ end
161
+ end
162
+
163
+ def namespaces_for_class(m)
164
+ return [m] if m.name.nil?
165
+ namespace_constants = m.name.split(/::/).map(&:to_sym)
166
+ namespace_constants.reduce([Object]) do |output, namespace_constant|
167
+ output += [output.last.const_get(namespace_constant)]
168
+ end[1..-1].uniq.reverse
169
+ end
170
+
171
+ def component_namespaces
172
+ @component_namespaces ||= reset_component_namespaces
173
+ end
174
+
175
+ def reset_component_namespaces
176
+ @component_namespaces = Set[Object, Glimmer::Web]
177
+ end
178
+ end
179
+ # <- end of class methods
180
+
181
+
182
+ attr_reader :markup_root, :parent, :options
183
+ alias parent_proxy parent
184
+
185
+ def initialize(parent, args, options, &content)
186
+ @parent = parent
187
+ options = args.delete_at(-1) if args.is_a?(Array) && args.last.is_a?(Hash)
188
+ if args.is_a?(Hash)
189
+ options = args
190
+ args = []
191
+ end
192
+ options ||= {}
193
+ @args = args
194
+ options ||= {}
195
+ @options = self.class.options.merge(options)
196
+ @content = Util::ProcTracker.new(content) if content
197
+ execute_hooks('before_render')
198
+ markup_block = self.class.instance_variable_get("@markup_block")
199
+ raise Glimmer::Error, 'Invalid Glimmer web component for having no markup! Please define markup block!' if markup_block.nil?
200
+ @markup_root = instance_exec(&markup_block)
201
+ @parent ||= @markup_root.parent
202
+ raise Glimmer::Error, 'Invalid Glimmer web component for having an empty markup! Please fill markup block!' if @markup_root.nil?
203
+ execute_hooks('after_render')
204
+ end
205
+
206
+ # Subclasses may override to perform post initialization work on an added child
207
+ def post_initialize_child(child)
208
+ # No Op by default
209
+ end
210
+
211
+ def can_handle_observation_request?(observation_request)
212
+ observation_request = observation_request.to_s
213
+ result = false
214
+ if observation_request.start_with?('on_updated_')
215
+ property = observation_request.sub(/^on_updated_/, '')
216
+ result = can_add_observer?(property)
217
+ end
218
+ result || markup_root&.can_handle_observation_request?(observation_request)
219
+ end
220
+
221
+ def handle_observation_request(observation_request, block)
222
+ observation_request = observation_request.to_s
223
+ if observation_request.start_with?('on_updated_')
224
+ property = observation_request.sub(/^on_updated_/, '') # TODO look into eliminating duplication from above
225
+ add_observer(DataBinding::Observer.proc(&block), property) if can_add_observer?(property)
226
+ else
227
+ markup_root.handle_observation_request(observation_request, block)
228
+ end
229
+ end
230
+
231
+ def can_add_observer?(attribute_name)
232
+ has_instance_method?(attribute_name) || has_instance_method?("#{attribute_name}?") || @markup_root.can_add_observer?(attribute_name)
233
+ end
234
+
235
+ def add_observer(observer, attribute_name)
236
+ if has_instance_method?(attribute_name)
237
+ super(observer, attribute_name)
238
+ else
239
+ @markup_root.add_observer(observer, attribute_name)
240
+ end
241
+ end
242
+
243
+ def has_attribute?(attribute_name, *args)
244
+ has_instance_method?(attribute_setter(attribute_name)) ||
245
+ @markup_root.has_attribute?(attribute_name, *args)
246
+ end
247
+
248
+ def set_attribute(attribute_name, *args)
249
+ if has_instance_method?(attribute_setter(attribute_name))
250
+ send(attribute_setter(attribute_name), *args)
251
+ else
252
+ @markup_root.set_attribute(attribute_name, *args)
253
+ end
254
+ end
255
+
256
+ # This method ensures it has an instance method not coming from Glimmer DSL
257
+ def has_instance_method?(method_name)
258
+ respond_to?(method_name) and
259
+ !markup_root&.respond_to?(method_name) and
260
+ !method(method_name)&.source_location&.first&.include?('glimmer/dsl/engine.rb') and
261
+ !method(method_name)&.source_location&.first&.include?('glimmer/web/element_proxy.rb')
262
+ end
263
+
264
+ def get_attribute(attribute_name)
265
+ if has_instance_method?(attribute_name)
266
+ send(attribute_name)
267
+ else
268
+ @markup_root.get_attribute(attribute_name)
269
+ end
270
+ end
271
+
272
+ def attribute_setter(attribute_name)
273
+ "#{attribute_name}="
274
+ end
275
+
276
+ def render(*args)
277
+ # this method is defined to prevent displaying a harmless Glimmer no keyword error as an annoying useless warning
278
+ @markup_root&.render(*args)
279
+ end
280
+
281
+ # Returns content block if used as an attribute reader (no args)
282
+ # Otherwise, if a block is passed, it adds it as content to this Glimmer web component
283
+ def content(&block)
284
+ if block_given?
285
+ Glimmer::DSL::Engine.add_content(self, Glimmer::DSL::Web::ComponentExpression.new, self.class.keyword, &block)
286
+ else
287
+ @content
288
+ end
289
+ end
290
+
291
+ def method_missing(method_name, *args, &block)
292
+ if can_handle_observation_request?(method_name)
293
+ handle_observation_request(method_name, block)
294
+ elsif markup_root.respond_to?(method_name, true)
295
+ markup_root.send(method_name, *args, &block)
296
+ else
297
+ super(method_name, *args, &block)
298
+ end
299
+ end
300
+
301
+ alias local_respond_to? respond_to_missing?
302
+ def respond_to_missing?(method_name, include_private = false)
303
+ super(method_name, include_private) or
304
+ can_handle_observation_request?(method_name) or
305
+ markup_root.respond_to?(method_name, include_private)
306
+ end
307
+
308
+ private
309
+
310
+ def execute_hooks(hook_name)
311
+ self.class.instance_variable_get("@#{hook_name}_blocks")&.each do |hook_block|
312
+ instance_exec(&hook_block)
313
+ end
314
+ end
315
+ end
316
+ end
317
+ end
@@ -25,6 +25,10 @@ module Glimmer
25
25
  module Web
26
26
  class ElementProxy
27
27
  class << self
28
+ def keyword_supported?(keyword)
29
+ ELEMENT_KEYWORDS.include?(keyword.to_s)
30
+ end
31
+
28
32
  # Factory Method that translates a Glimmer DSL keyword into a ElementProxy object
29
33
  def for(keyword, parent, args, block)
30
34
  element_type(keyword).new(keyword, parent, args, block)
@@ -67,7 +71,28 @@ module Glimmer
67
71
 
68
72
  Event = Struct.new(:widget, keyword_init: true)
69
73
 
74
+ ELEMENT_KEYWORDS = [
75
+ "a", "abbr", "acronym", "address", "applet", "area", "article", "aside", "audio", "b",
76
+ "base", "basefont", "bdi", "bdo", "bgsound", "big", "blink", "blockquote", "body", "br",
77
+ "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "content", "data",
78
+ "datalist", "dd", "decorator", "del", "details", "dfn", "dir", "div", "dl", "dt",
79
+ "element", "em", "embed", "fieldset", "figcaption", "figure", "font", "footer", "form", "frame",
80
+ "frameset", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup",
81
+ "hr", "html", "i", "iframe", "img", "input", "ins", "isindex", "kbd", "keygen",
82
+ "label", "legend", "li", "link", "listing", "main", "map", "mark", "marquee", "menu",
83
+ "menuitem", "meta", "meter", "nav", "nobr", "noframes", "noscript", "object", "ol", "optgroup",
84
+ "option", "output", "p", "param", "plaintext", "pre", "progress", "q", "rp", "rt",
85
+ "ruby", "s", "samp", "script", "section", "select", "shadow", "small", "source", "spacer",
86
+ "span", "strike", "strong", "style", "sub", "summary", "sup", "table", "tbody", "td",
87
+ "template", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt",
88
+ "u", "ul", "var", "video", "wbr", "xmp",
89
+ ]
90
+
70
91
  GLIMMER_ATTRIBUTES = [:parent]
92
+ PROPERTY_ALIASES = {
93
+ 'inner_html' => 'innerHTML',
94
+ 'outer_html' => 'outerHTML',
95
+ }
71
96
  FORMAT_DATETIME = '%Y-%m-%dT%H:%M'
72
97
  FORMAT_DATE = '%Y-%m-%d'
73
98
  FORMAT_TIME = '%H:%M'
@@ -123,6 +148,11 @@ module Glimmer
123
148
  listeners.each do |event, event_listeners|
124
149
  event_listeners.dup.each(&:unregister)
125
150
  end
151
+ listeners.clear
152
+ data_bindings.each do |element_binding, model_binding|
153
+ element_binding.unregister_all_observables
154
+ end
155
+ data_bindings.clear
126
156
  end
127
157
 
128
158
  # Subclasses can override with their own selector
@@ -193,10 +223,16 @@ module Glimmer
193
223
  end
194
224
  end
195
225
 
196
- def render(custom_parent_dom_element: nil, brand_new: false)
226
+ def render(parent_selector = nil, custom_parent_dom_element: nil, brand_new: false)
227
+ options[:parent] = parent_selector if !parent_selector.to_s.empty?
228
+ if !options[:parent].to_s.empty?
229
+ # ensure element is orphaned as it is becoming a top-level root element
230
+ @parent&.post_remove_child(self)
231
+ @parent = nil
232
+ end
197
233
  the_parent_dom_element = custom_parent_dom_element || parent_dom_element
198
234
  old_element = dom_element
199
- brand_new = @dom.nil? || old_element.empty? || brand_new
235
+ brand_new = @dom.nil? || old_element.empty? || !options[:parent].to_s.empty? || brand_new
200
236
  build_dom(layout: !custom_parent_dom_element) # TODO handle custom parent layout by passing parent instead of parent dom element
201
237
  if brand_new
202
238
  # TODO make a method attach to allow subclasses to override if needed
@@ -423,6 +459,10 @@ module Glimmer
423
459
  event_listener_proxies.clear
424
460
  end
425
461
 
462
+ def data_bindings
463
+ @data_bindings ||= {}
464
+ end
465
+
426
466
  def data_bind(property, model_binding)
427
467
  element_binding_translator = value_converters_for_input_type(type)[:model_to_view]
428
468
  element_binding_parameters = [self, property, element_binding_translator]
@@ -430,6 +470,7 @@ module Glimmer
430
470
  element_binding.call(model_binding.evaluate_property)
431
471
  #TODO make this options observer dependent and all similar observers in element specific data binding handlers
432
472
  element_binding.observe(model_binding)
473
+ data_bindings[element_binding] = model_binding
433
474
  unless model_binding.binding_options[:read_only]
434
475
  # TODO add guards against nil cases for hash below
435
476
  listener_keyword = data_binding_listener_for_element_and_property(keyword, property)
@@ -444,19 +485,36 @@ module Glimmer
444
485
  end
445
486
  end
446
487
 
488
+ # Data-binds the generation of nested content to a model/property (in binding args)
489
+ # consider providing an option to avoid initial rendering without any changes happening
490
+ def bind_content(*binding_args, &content_block)
491
+ # TODO in the future, consider optimizing code by diffing content if that makes sense
492
+ content_binding_work = proc do |*values|
493
+ children.dup.each { |child| child.remove }
494
+ content(&content_block)
495
+ end
496
+ content_binding_observer = Glimmer::DataBinding::Observer.proc(&content_binding_work)
497
+ content_binding_observer.observe(*binding_args)
498
+ content_binding_work.call # TODO inspect if we need to pass args here (from observed attributes) [but it's simpler not to pass anything at first]
499
+ end
500
+
447
501
  def respond_to_missing?(method_name, include_private = false)
448
502
  # TODO consider doing more correct checking of availability of properties/methods using native `` ticks
449
503
  property_name = property_name_for(method_name)
504
+ unnormalized_property_name = unnormalized_property_name_for(method_name)
450
505
  super(method_name, include_private) ||
451
506
  (dom_element && dom_element.length > 0 && Native.call(dom_element, '0').respond_to?(method_name.to_s.camelcase, include_private)) ||
507
+ (dom_element && dom_element.length > 0 && Native.call(dom_element, '0').respond_to?(method_name.to_s, include_private)) ||
452
508
  dom_element.respond_to?(method_name, include_private) ||
453
509
  (!dom_element.prop(property_name).nil? && !dom_element.prop(property_name).is_a?(Proc)) ||
510
+ (!dom_element.prop(unnormalized_property_name).nil? && !dom_element.prop(unnormalized_property_name).is_a?(Proc)) ||
454
511
  method_name.to_s.start_with?('on_')
455
512
  end
456
513
 
457
514
  def method_missing(method_name, *args, &block)
458
515
  # TODO consider doing more correct checking of availability of properties/methods using native `` ticks
459
516
  property_name = property_name_for(method_name)
517
+ unnormalized_property_name = unnormalized_property_name_for(method_name)
460
518
  if method_name.to_s.start_with?('on_')
461
519
  handle_observation_request(method_name, block)
462
520
  elsif dom_element.respond_to?(method_name)
@@ -467,12 +525,22 @@ module Glimmer
467
525
  else
468
526
  dom_element.prop(property_name)
469
527
  end
528
+ elsif !dom_element.prop(unnormalized_property_name).nil? && !dom_element.prop(unnormalized_property_name).is_a?(Proc)
529
+ if method_name.end_with?('=')
530
+ dom_element.prop(unnormalized_property_name, *args)
531
+ else
532
+ dom_element.prop(unnormalized_property_name)
533
+ end
470
534
  elsif dom_element && dom_element.length > 0
535
+ js_args = block.nil? ? args : (args + [block])
471
536
  begin
472
- js_args = block.nil? ? args : (args + [block])
473
537
  Native.call(dom_element, '0').method_missing(method_name.to_s.camelcase, *js_args)
474
538
  rescue Exception => e
475
- super(method_name, *args, &block)
539
+ begin
540
+ Native.call(dom_element, '0').method_missing(method_name.to_s, *js_args)
541
+ rescue Exception => e
542
+ super(method_name, *args, &block)
543
+ end
476
544
  end
477
545
  else
478
546
  super(method_name, *args, &block)
@@ -480,7 +548,12 @@ module Glimmer
480
548
  end
481
549
 
482
550
  def property_name_for(method_name)
483
- method_name.end_with?('=') ? method_name.to_s[0...-1].camelcase : method_name.to_s.camelcase
551
+ attribute_name = method_name.end_with?('=') ? method_name.to_s[0...-1] : method_name.to_s
552
+ PROPERTY_ALIASES[attribute_name] || attribute_name.camelcase
553
+ end
554
+
555
+ def unnormalized_property_name_for(method_name)
556
+ method_name.end_with?('=') ? method_name.to_s[0...-1] : method_name.to_s
484
557
  end
485
558
 
486
559
  def swt_widget