glimmer-dsl-web 0.0.8 → 0.0.10

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.8
1
+ 0.0.10
@@ -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.8 ruby lib
5
+ # stub: glimmer-dsl-web 0.0.10 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "glimmer-dsl-web".freeze
9
- s.version = "0.0.8".freeze
9
+ s.version = "0.0.10".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-03"
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,15 +31,18 @@ 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",
34
35
  "lib/glimmer-dsl-web/samples/hello/hello_content_data_binding.rb",
35
36
  "lib/glimmer-dsl-web/samples/hello/hello_data_binding.rb",
36
37
  "lib/glimmer-dsl-web/samples/hello/hello_form.rb",
38
+ "lib/glimmer-dsl-web/samples/hello/hello_glimmer_component_helper/address_form.rb",
37
39
  "lib/glimmer-dsl-web/samples/hello/hello_input_date_time.rb",
38
40
  "lib/glimmer-dsl-web/samples/hello/hello_world.rb",
39
41
  "lib/glimmer-dsl-web/vendor/jquery.js",
40
42
  "lib/glimmer/config/opal_logger.rb",
41
43
  "lib/glimmer/data_binding/element_binding.rb",
42
44
  "lib/glimmer/dsl/web/bind_expression.rb",
45
+ "lib/glimmer/dsl/web/component_expression.rb",
43
46
  "lib/glimmer/dsl/web/content_data_binding_expression.rb",
44
47
  "lib/glimmer/dsl/web/data_binding_expression.rb",
45
48
  "lib/glimmer/dsl/web/dsl.rb",
@@ -50,8 +53,10 @@ Gem::Specification.new do |s|
50
53
  "lib/glimmer/dsl/web/property_expression.rb",
51
54
  "lib/glimmer/dsl/web/select_expression.rb",
52
55
  "lib/glimmer/dsl/web/shine_data_binding_expression.rb",
56
+ "lib/glimmer/helpers/glimmer_helper.rb",
53
57
  "lib/glimmer/util/proc_tracker.rb",
54
58
  "lib/glimmer/web.rb",
59
+ "lib/glimmer/web/component.rb",
55
60
  "lib/glimmer/web/element_proxy.rb",
56
61
  "lib/glimmer/web/event_proxy.rb",
57
62
  "lib/glimmer/web/listener_proxy.rb"
@@ -66,8 +71,8 @@ Gem::Specification.new do |s|
66
71
  s.add_runtime_dependency(%q<glimmer>.freeze, ["~> 2.7.6".freeze])
67
72
  s.add_runtime_dependency(%q<glimmer-dsl-xml>.freeze, ["~> 1.3.2".freeze])
68
73
  s.add_runtime_dependency(%q<glimmer-dsl-css>.freeze, ["~> 1.2.2".freeze])
69
- s.add_runtime_dependency(%q<opal>.freeze, ["= 1.4.1".freeze])
70
- s.add_runtime_dependency(%q<opal-rails>.freeze, ["= 2.0.2".freeze])
74
+ s.add_runtime_dependency(%q<opal>.freeze, ["= 1.8.2".freeze])
75
+ s.add_runtime_dependency(%q<opal-rails>.freeze, ["= 2.0.3".freeze])
71
76
  s.add_runtime_dependency(%q<opal-async>.freeze, ["~> 1.4.0".freeze])
72
77
  s.add_runtime_dependency(%q<opal-jquery>.freeze, ["~> 0.4.6".freeze])
73
78
  s.add_runtime_dependency(%q<to_collection>.freeze, [">= 2.0.1".freeze, "< 3.0.0".freeze])
@@ -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
@@ -8,6 +8,7 @@ require 'glimmer/dsl/web/bind_expression'
8
8
  require 'glimmer/dsl/web/data_binding_expression'
9
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
@@ -15,6 +16,7 @@ module Glimmer
15
16
  Engine.add_dynamic_expressions(
16
17
  Web,
17
18
  %w[
19
+ component
18
20
  listener
19
21
  data_binding
20
22
  property
@@ -0,0 +1,32 @@
1
+ module GlimmerHelper
2
+ class << self
3
+ def next_id_number
4
+ @next_id_number ||= 0
5
+ @next_id_number += 1
6
+ end
7
+ end
8
+
9
+ def glimmer_component(component_asset_path, *component_args)
10
+ component_file = component_asset_path.split('/').last
11
+ component_class_name = component_file.classify
12
+ next_id_number = GlimmerHelper.next_id_number
13
+ component_id = "glimmer_component_#{next_id_number}"
14
+ component_script_container_id = "glimmer_component_script_container_#{next_id_number}"
15
+ component_args_json = JSON.dump(component_args)
16
+ opal_script = <<~Opal
17
+ require 'glimmer-dsl-web'
18
+ Document.ready? do
19
+ component_args_json = '#{component_args_json}'
20
+ component_args = JSON.parse(component_args_json)
21
+ component_args << {} if !component_args.last.is_a?(Hash)
22
+ component_args.last[:parent] = "##{component_id}"
23
+ #{component_class_name}.render(*component_args)
24
+ end
25
+ Opal
26
+ content_tag(:div, id: component_script_container_id, class: ['glimmer_component_script_container', "#{component_file}_script_container"]) do
27
+ content_tag(:div, '', id: component_id, class: ['glimmer_component', component_file]) +
28
+ javascript_include_tag(component_asset_path, "data-turbolinks-track": "reload") +
29
+ content_tag(:script, raw(opal_script), type: 'text/ruby')
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,319 @@
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, *args)
98
+ options = args.last.is_a?(Hash) ? args.last.slice(:parent, :custom_parent_dom_element, :brand_new) : {}
99
+ rendered_component.render(**options)
100
+ rendered_component
101
+ end
102
+ end
103
+
104
+ # This module was only created to prevent Glimmer from checking method_missing first
105
+ module GlimmerSupersedable
106
+ def method_missing(method_name, *args, &block)
107
+ Glimmer::DSL::Engine.interpret(method_name, *args, &block)
108
+ rescue
109
+ super(method_name, *args, &block)
110
+ end
111
+ end
112
+
113
+ class << self
114
+ def included(klass)
115
+ if !klass.ancestors.include?(GlimmerSupersedable)
116
+ klass.extend(ClassMethods)
117
+ klass.include(Glimmer)
118
+ klass.prepend(GlimmerSupersedable)
119
+ Glimmer::Web::Component.add_component_namespaces_for(klass)
120
+ end
121
+ end
122
+
123
+ def for(underscored_component_name)
124
+ extracted_namespaces = underscored_component_name.
125
+ to_s.
126
+ split(/__/).map do |namespace|
127
+ namespace.camelcase(:upper)
128
+ end
129
+ Glimmer::Web::Component.component_namespaces.each do |base|
130
+ extracted_namespaces.reduce(base) do |result, namespace|
131
+ if !result.constants.include?(namespace)
132
+ namespace = result.constants.detect {|c| c.to_s.upcase == namespace.to_s.upcase } || namespace
133
+ end
134
+ begin
135
+ constant = result.const_get(namespace)
136
+ return constant if constant&.respond_to?(:ancestors) &&
137
+ (
138
+ constant&.ancestors&.to_a.include?(Glimmer::Web::Component) ||
139
+ # TODO checking GlimmerSupersedable as a hack because when a class is loaded twice (like when loading samples
140
+ # by reloading ruby files), it loses its Glimmer::Web::Component ancestor as a bug in Opal
141
+ # but somehow the prepend module remains
142
+ constant&.ancestors&.to_a.include?(GlimmerSupersedable)
143
+ )
144
+ constant
145
+ rescue => e
146
+ # Glimmer::Config.logger.debug {"#{e.message}\n#{e.backtrace.join("\n")}"}
147
+ result
148
+ end
149
+ end
150
+ end
151
+ raise "#{underscored_component_name} has no Glimmer web component class!"
152
+ rescue => e
153
+ Glimmer::Config.logger.debug {e.message}
154
+ Glimmer::Config.logger.debug {"#{e.message}\n#{e.backtrace.join("\n")}"}
155
+ nil
156
+ end
157
+
158
+ def add_component_namespaces_for(klass)
159
+ Glimmer::Web::Component.namespaces_for_class(klass).drop(1).each do |namespace|
160
+ Glimmer::Web::Component.component_namespaces << namespace
161
+ end
162
+ end
163
+
164
+ def namespaces_for_class(m)
165
+ return [m] if m.name.nil?
166
+ namespace_constants = m.name.split(/::/).map(&:to_sym)
167
+ namespace_constants.reduce([Object]) do |output, namespace_constant|
168
+ output += [output.last.const_get(namespace_constant)]
169
+ end[1..-1].uniq.reverse
170
+ end
171
+
172
+ def component_namespaces
173
+ @component_namespaces ||= reset_component_namespaces
174
+ end
175
+
176
+ def reset_component_namespaces
177
+ @component_namespaces = Set[Object, Glimmer::Web]
178
+ end
179
+ end
180
+ # <- end of class methods
181
+
182
+
183
+ attr_reader :markup_root, :parent, :args, :options
184
+ alias parent_proxy parent
185
+
186
+ def initialize(parent, args, options, &content)
187
+ @parent = parent
188
+ options = args.delete_at(-1) if args.is_a?(Array) && args.last.is_a?(Hash)
189
+ if args.is_a?(Hash)
190
+ options = args
191
+ args = []
192
+ end
193
+ options ||= {}
194
+ @args = args
195
+ options ||= {}
196
+ @options = self.class.options.merge(options)
197
+ @content = Util::ProcTracker.new(content) if content
198
+ execute_hooks('before_render')
199
+ markup_block = self.class.instance_variable_get("@markup_block")
200
+ raise Glimmer::Error, 'Invalid Glimmer web component for having no markup! Please define markup block!' if markup_block.nil?
201
+ @markup_root = instance_exec(&markup_block)
202
+ @markup_root.options[:parent] = options[:parent] if options[:parent]
203
+ @parent ||= @markup_root.parent
204
+ raise Glimmer::Error, 'Invalid Glimmer web component for having an empty markup! Please fill markup block!' if @markup_root.nil?
205
+ execute_hooks('after_render')
206
+ end
207
+
208
+ # Subclasses may override to perform post initialization work on an added child
209
+ def post_initialize_child(child)
210
+ # No Op by default
211
+ end
212
+
213
+ def can_handle_observation_request?(observation_request)
214
+ observation_request = observation_request.to_s
215
+ result = false
216
+ if observation_request.start_with?('on_updated_')
217
+ property = observation_request.sub(/^on_updated_/, '')
218
+ result = can_add_observer?(property)
219
+ end
220
+ result || markup_root&.can_handle_observation_request?(observation_request)
221
+ end
222
+
223
+ def handle_observation_request(observation_request, block)
224
+ observation_request = observation_request.to_s
225
+ if observation_request.start_with?('on_updated_')
226
+ property = observation_request.sub(/^on_updated_/, '') # TODO look into eliminating duplication from above
227
+ add_observer(DataBinding::Observer.proc(&block), property) if can_add_observer?(property)
228
+ else
229
+ markup_root.handle_observation_request(observation_request, block)
230
+ end
231
+ end
232
+
233
+ def can_add_observer?(attribute_name)
234
+ has_instance_method?(attribute_name) || has_instance_method?("#{attribute_name}?") || @markup_root.can_add_observer?(attribute_name)
235
+ end
236
+
237
+ def add_observer(observer, attribute_name)
238
+ if has_instance_method?(attribute_name)
239
+ super(observer, attribute_name)
240
+ else
241
+ @markup_root.add_observer(observer, attribute_name)
242
+ end
243
+ end
244
+
245
+ def has_attribute?(attribute_name, *args)
246
+ has_instance_method?(attribute_setter(attribute_name)) ||
247
+ @markup_root.has_attribute?(attribute_name, *args)
248
+ end
249
+
250
+ def set_attribute(attribute_name, *args)
251
+ if has_instance_method?(attribute_setter(attribute_name))
252
+ send(attribute_setter(attribute_name), *args)
253
+ else
254
+ @markup_root.set_attribute(attribute_name, *args)
255
+ end
256
+ end
257
+
258
+ # This method ensures it has an instance method not coming from Glimmer DSL
259
+ def has_instance_method?(method_name)
260
+ respond_to?(method_name) and
261
+ !markup_root&.respond_to?(method_name) and
262
+ !method(method_name)&.source_location&.first&.include?('glimmer/dsl/engine.rb') and
263
+ !method(method_name)&.source_location&.first&.include?('glimmer/web/element_proxy.rb')
264
+ end
265
+
266
+ def get_attribute(attribute_name)
267
+ if has_instance_method?(attribute_name)
268
+ send(attribute_name)
269
+ else
270
+ @markup_root.get_attribute(attribute_name)
271
+ end
272
+ end
273
+
274
+ def attribute_setter(attribute_name)
275
+ "#{attribute_name}="
276
+ end
277
+
278
+ def render(parent: nil, custom_parent_dom_element: nil, brand_new: false)
279
+ # this method is defined to prevent displaying a harmless Glimmer no keyword error as an annoying useless warning
280
+ @markup_root&.render(parent: parent, custom_parent_dom_element: custom_parent_dom_element, brand_new: brand_new)
281
+ end
282
+
283
+ # Returns content block if used as an attribute reader (no args)
284
+ # Otherwise, if a block is passed, it adds it as content to this Glimmer web component
285
+ def content(&block)
286
+ if block_given?
287
+ Glimmer::DSL::Engine.add_content(self, Glimmer::DSL::Web::ComponentExpression.new, self.class.keyword, &block)
288
+ else
289
+ @content
290
+ end
291
+ end
292
+
293
+ def method_missing(method_name, *args, &block)
294
+ if can_handle_observation_request?(method_name)
295
+ handle_observation_request(method_name, block)
296
+ elsif markup_root.respond_to?(method_name, true)
297
+ markup_root.send(method_name, *args, &block)
298
+ else
299
+ super(method_name, *args, &block)
300
+ end
301
+ end
302
+
303
+ alias local_respond_to? respond_to_missing?
304
+ def respond_to_missing?(method_name, include_private = false)
305
+ super(method_name, include_private) or
306
+ can_handle_observation_request?(method_name) or
307
+ markup_root.respond_to?(method_name, include_private)
308
+ end
309
+
310
+ private
311
+
312
+ def execute_hooks(hook_name)
313
+ self.class.instance_variable_get("@#{hook_name}_blocks")&.each do |hook_block|
314
+ instance_exec(&hook_block)
315
+ end
316
+ end
317
+ end
318
+ end
319
+ end
@@ -1,3 +1,5 @@
1
+ # backtick_javascript: true
2
+
1
3
  # Copyright (c) 2023-2024 Andy Maleh
2
4
  #
3
5
  # Permission is hereby granted, free of charge, to any person obtaining
@@ -223,10 +225,17 @@ module Glimmer
223
225
  end
224
226
  end
225
227
 
226
- def render(custom_parent_dom_element: nil, brand_new: false)
228
+ def render(parent: nil, custom_parent_dom_element: nil, brand_new: false)
229
+ parent_selector = parent
230
+ options[:parent] = parent_selector if !parent_selector.to_s.empty?
231
+ if !options[:parent].to_s.empty?
232
+ # ensure element is orphaned as it is becoming a top-level root element
233
+ @parent&.post_remove_child(self)
234
+ @parent = nil
235
+ end
227
236
  the_parent_dom_element = custom_parent_dom_element || parent_dom_element
228
237
  old_element = dom_element
229
- brand_new = @dom.nil? || old_element.empty? || brand_new
238
+ brand_new = @dom.nil? || old_element.empty? || !options[:parent].to_s.empty? || brand_new
230
239
  build_dom(layout: !custom_parent_dom_element) # TODO handle custom parent layout by passing parent instead of parent dom element
231
240
  if brand_new
232
241
  # TODO make a method attach to allow subclasses to override if needed
@@ -493,7 +502,7 @@ module Glimmer
493
502
  end
494
503
 
495
504
  def respond_to_missing?(method_name, include_private = false)
496
- # TODO consider doing more correct checking of availability of properties/methods using native `` ticks
505
+ # TODO consider doing more correct checking of availability of properties/methods using native ticks
497
506
  property_name = property_name_for(method_name)
498
507
  unnormalized_property_name = unnormalized_property_name_for(method_name)
499
508
  super(method_name, include_private) ||
@@ -506,7 +515,7 @@ module Glimmer
506
515
  end
507
516
 
508
517
  def method_missing(method_name, *args, &block)
509
- # TODO consider doing more correct checking of availability of properties/methods using native `` ticks
518
+ # TODO consider doing more correct checking of availability of properties/methods using native ticks
510
519
  property_name = property_name_for(method_name)
511
520
  unnormalized_property_name = unnormalized_property_name_for(method_name)
512
521
  if method_name.to_s.start_with?('on_')