glimmer-dsl-web 0.0.10 → 0.0.11

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.10
1
+ 0.0.11
@@ -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.10 ruby lib
5
+ # stub: glimmer-dsl-web 0.0.11 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "glimmer-dsl-web".freeze
9
- s.version = "0.0.10".freeze
9
+ s.version = "0.0.11".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-05"
14
+ s.date = "2024-01-06"
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 = [
@@ -37,7 +37,9 @@ Gem::Specification.new do |s|
37
37
  "lib/glimmer-dsl-web/samples/hello/hello_form.rb",
38
38
  "lib/glimmer-dsl-web/samples/hello/hello_glimmer_component_helper/address_form.rb",
39
39
  "lib/glimmer-dsl-web/samples/hello/hello_input_date_time.rb",
40
+ "lib/glimmer-dsl-web/samples/hello/hello_observer.rb",
40
41
  "lib/glimmer-dsl-web/samples/hello/hello_world.rb",
42
+ "lib/glimmer-dsl-web/samples/regular/button_counter.rb",
41
43
  "lib/glimmer-dsl-web/vendor/jquery.js",
42
44
  "lib/glimmer/config/opal_logger.rb",
43
45
  "lib/glimmer/data_binding/element_binding.rb",
@@ -49,6 +51,7 @@ Gem::Specification.new do |s|
49
51
  "lib/glimmer/dsl/web/element_expression.rb",
50
52
  "lib/glimmer/dsl/web/general_element_expression.rb",
51
53
  "lib/glimmer/dsl/web/listener_expression.rb",
54
+ "lib/glimmer/dsl/web/observe_expression.rb",
52
55
  "lib/glimmer/dsl/web/p_expression.rb",
53
56
  "lib/glimmer/dsl/web/property_expression.rb",
54
57
  "lib/glimmer/dsl/web/select_expression.rb",
@@ -74,7 +77,7 @@ Gem::Specification.new do |s|
74
77
  s.add_runtime_dependency(%q<opal>.freeze, ["= 1.8.2".freeze])
75
78
  s.add_runtime_dependency(%q<opal-rails>.freeze, ["= 2.0.3".freeze])
76
79
  s.add_runtime_dependency(%q<opal-async>.freeze, ["~> 1.4.0".freeze])
77
- s.add_runtime_dependency(%q<opal-jquery>.freeze, ["~> 0.4.6".freeze])
80
+ s.add_runtime_dependency(%q<opal-jquery>.freeze, ["~> 0.5.0".freeze])
78
81
  s.add_runtime_dependency(%q<to_collection>.freeze, [">= 2.0.1".freeze, "< 3.0.0".freeze])
79
82
  s.add_development_dependency(%q<puts_debuggerer>.freeze, [">= 0".freeze])
80
83
  s.add_development_dependency(%q<rake>.freeze, [">= 10.1.0".freeze, "< 14.0.0".freeze])
@@ -21,8 +21,9 @@ module Glimmer
21
21
  if block.source_location && (block.source_location == parent.content&.__getobj__&.source_location)
22
22
  parent.content.call(parent) unless parent.content.called?
23
23
  else
24
- super
24
+ super(parent, keyword, *args, &block)
25
25
  end
26
+ parent.post_add_content
26
27
  end
27
28
  end
28
29
  end
@@ -1,3 +1,24 @@
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
+
1
22
  require 'glimmer/dsl/engine'
2
23
  require 'glimmer/dsl/web/element_expression'
3
24
  require 'glimmer/dsl/web/listener_expression'
@@ -9,6 +30,7 @@ require 'glimmer/dsl/web/data_binding_expression'
9
30
  require 'glimmer/dsl/web/content_data_binding_expression'
10
31
  require 'glimmer/dsl/web/shine_data_binding_expression'
11
32
  require 'glimmer/dsl/web/component_expression'
33
+ require 'glimmer/dsl/web/observe_expression'
12
34
 
13
35
  module Glimmer
14
36
  module DSL
@@ -0,0 +1,42 @@
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/static_expression'
23
+ require 'glimmer/dsl/top_level_expression'
24
+ require 'glimmer/dsl/observe_expression'
25
+ require 'glimmer/web/component'
26
+
27
+ module Glimmer
28
+ module DSL
29
+ module SWT
30
+ class ObserveExpression < StaticExpression
31
+ include TopLevelExpression
32
+ include Glimmer::DSL::ObserveExpression
33
+
34
+ def interpret(parent, keyword, *args, &block)
35
+ observer_registration = super(parent, keyword, *args, &block)
36
+ Glimmer::Web::Component.interpretation_stack.last&.observer_registrations&.push(observer_registration)
37
+ observer_registration
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -15,13 +15,11 @@ module GlimmerHelper
15
15
  component_args_json = JSON.dump(component_args)
16
16
  opal_script = <<~Opal
17
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
18
+ component_args_json = '#{component_args_json}'
19
+ component_args = JSON.parse(component_args_json)
20
+ component_args << {} if !component_args.last.is_a?(Hash)
21
+ component_args.last[:parent] = "##{component_id}"
22
+ #{component_class_name}.render(*component_args)
25
23
  Opal
26
24
  content_tag(:div, id: component_script_container_id, class: ['glimmer_component_script_container', "#{component_file}_script_container"]) do
27
25
  content_tag(:div, '', id: component_id, class: ['glimmer_component', component_file]) +
@@ -176,6 +176,10 @@ module Glimmer
176
176
  def reset_component_namespaces
177
177
  @component_namespaces = Set[Object, Glimmer::Web]
178
178
  end
179
+
180
+ def interpretation_stack
181
+ @interpretation_stack ||= []
182
+ end
179
183
  end
180
184
  # <- end of class methods
181
185
 
@@ -184,6 +188,7 @@ module Glimmer
184
188
  alias parent_proxy parent
185
189
 
186
190
  def initialize(parent, args, options, &content)
191
+ Component.interpretation_stack.push(self)
187
192
  @parent = parent
188
193
  options = args.delete_at(-1) if args.is_a?(Array) && args.last.is_a?(Hash)
189
194
  if args.is_a?(Hash)
@@ -203,6 +208,14 @@ module Glimmer
203
208
  @parent ||= @markup_root.parent
204
209
  raise Glimmer::Error, 'Invalid Glimmer web component for having an empty markup! Please fill markup block!' if @markup_root.nil?
205
210
  execute_hooks('after_render')
211
+
212
+ # TODO adapt for web
213
+ observer_registration_cleanup_listener = proc do
214
+ observer_registrations.compact.each(&:deregister)
215
+ observer_registrations.clear
216
+ end
217
+ @markup_root.handle_observation_request('on_remove', observer_registration_cleanup_listener)
218
+ post_add_content if content.nil?
206
219
  end
207
220
 
208
221
  # Subclasses may override to perform post initialization work on an added child
@@ -210,6 +223,15 @@ module Glimmer
210
223
  # No Op by default
211
224
  end
212
225
 
226
+ def post_add_content
227
+ Component.interpretation_stack.pop
228
+ end
229
+
230
+ # This stores observe keyword registrations of model/attribute observers
231
+ def observer_registrations
232
+ @observer_registrations ||= []
233
+ end
234
+
213
235
  def can_handle_observation_request?(observation_request)
214
236
  observation_request = observation_request.to_s
215
237
  result = false
@@ -139,11 +139,14 @@ module Glimmer
139
139
  @children.dup.each do |child|
140
140
  child.remove
141
141
  end
142
+ on_remove_listeners = listeners_for('on_remove').dup
142
143
  remove_all_listeners
143
144
  dom_element.remove
144
145
  parent&.post_remove_child(self)
145
146
  @removed = true
146
- # listeners_for('widget_removed').each {|listener| listener.call(Event.new(widget: self))}
147
+ on_remove_listeners.each do |listener|
148
+ listener.original_event_listener.call(EventProxy.new(listener: listener))
149
+ end
147
150
  end
148
151
 
149
152
  def remove_all_listeners
@@ -217,10 +220,12 @@ module Glimmer
217
220
  if parent_selector
218
221
  Document.find(parent_selector)
219
222
  else
220
- # TODO consider moving this to initializer
221
223
  options[:parent] ||= 'body'
222
224
  the_element = Document.find(options[:parent])
223
- the_element = Document.find('body') if the_element.length == 0
225
+ if the_element.length == 0
226
+ options[:parent] = 'body'
227
+ the_element = Document.find('body')
228
+ end
224
229
  the_element
225
230
  end
226
231
  end
@@ -433,22 +438,12 @@ module Glimmer
433
438
  end
434
439
 
435
440
  def handle_observation_request(keyword, original_event_listener)
436
- # case keyword
437
- # when 'on_widget_removed'
438
- # listeners_for(keyword.sub(/^on_/, '')) << original_event_listener.to_proc
439
- # else
440
- handle_javascript_observation_request(keyword, original_event_listener)
441
- # end
442
- end
443
-
444
- def handle_javascript_observation_request(keyword, original_event_listener)
445
441
  listener = ListenerProxy.new(
446
442
  element: self,
447
443
  selector: selector,
448
444
  dom_element: dom_element,
449
445
  event_attribute: keyword,
450
- listener: original_event_listener,
451
- original_event_listener: original_event_listener
446
+ original_event_listener: original_event_listener,
452
447
  )
453
448
  listener.register
454
449
  listeners_for(keyword) << listener
@@ -24,9 +24,11 @@ module Glimmer
24
24
  class EventProxy
25
25
  attr_reader :js_event, :listener
26
26
 
27
- def initialize(js_event:, listener:)
28
- @js_event = js_event
27
+ # Instantiates EventProxy
28
+ # When js_event is nil, it is a custom event
29
+ def initialize(listener:, js_event: nil)
29
30
  @listener = listener
31
+ @js_event = js_event
30
32
  end
31
33
 
32
34
  def element = listener.element
@@ -34,6 +36,7 @@ module Glimmer
34
36
  def event_attribute = listener.event_attribute
35
37
 
36
38
  def original_event
39
+ return if js_event.nil?
37
40
  Native(`#{js_event.to_n}.originalEvent`)
38
41
  end
39
42
 
@@ -41,14 +44,14 @@ module Glimmer
41
44
  property_name = method_name.to_s.camelcase
42
45
  super(method_name, include_private) ||
43
46
  js_event.respond_to?(method_name, include_private) ||
44
- `#{property_name} in #{original_event.to_n}`
47
+ (original_event && `#{property_name} in #{original_event.to_n}`)
45
48
  end
46
49
 
47
50
  def method_missing(method_name, *args, &block)
48
51
  property_name = method_name.to_s.camelcase
49
52
  if js_event.respond_to?(method_name, true)
50
53
  js_event.send(method_name, *args, &block)
51
- elsif `#{property_name} in #{original_event.to_n}`
54
+ elsif (original_event && `#{property_name} in #{original_event.to_n}`)
52
55
  original_event[property_name]
53
56
  else
54
57
  super(method_name, *args, &block)
@@ -24,33 +24,34 @@ require 'glimmer/web/event_proxy'
24
24
  module Glimmer
25
25
  module Web
26
26
  class ListenerProxy
27
- attr_reader :element, :event_attribute, :event_name, :dom_element, :selector, :listener, :js_listener, :original_event_listener
27
+ attr_reader :element, :event_attribute, :event_name, :dom_element, :selector, :js_listener, :original_event_listener
28
28
 
29
- def initialize(element:, event_attribute:, dom_element:, selector:, listener:)
29
+ def initialize(element:, event_attribute:, dom_element:, selector:, original_event_listener:)
30
30
  @element = element
31
31
  @event_attribute = event_attribute
32
- @event_name = event_attribute.sub(/^on/, '')
32
+ @event_name = event_attribute.sub(/^on_/, '').sub(/^on/, '')
33
33
  @dom_element = dom_element
34
34
  @selector = selector
35
- @listener = listener
36
- @js_listener = lambda do |js_event|
37
- event = EventProxy.new(js_event: js_event, listener: self)
38
- result = listener.call(event)
39
- result = true if result.nil?
40
- result
35
+ if !event_attribute.start_with?('on_') # custom event
36
+ @js_listener = lambda do |js_event|
37
+ event = EventProxy.new(js_event: js_event, listener: self)
38
+ result = original_event_listener.call(event)
39
+ result = true if result.nil?
40
+ result
41
+ end
41
42
  end
42
43
  @original_event_listener = original_event_listener
43
44
  end
44
45
 
45
46
  def register
46
- @dom_element.on(@event_name, &@js_listener)
47
+ @dom_element.on(@event_name, &@js_listener) unless @js_listener.nil?
47
48
  end
48
49
  alias observe register
49
50
  alias reregister register
50
51
 
51
52
  def unregister
52
53
  # TODO contribute fix to opal to allow passing observer with & to off with selector not specified as nil
53
- @dom_element.off(@event_name, @js_listener)
54
+ @dom_element.off(@event_name, @js_listener) unless @js_listener.nil?
54
55
  @element.listeners_for(@event_attribute).delete(self)
55
56
  end
56
57
  alias unobserve unregister
@@ -0,0 +1,88 @@
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-web'
23
+
24
+ class NumberHolder
25
+ attr_accessor :number
26
+
27
+ def initialize
28
+ self.number = 50
29
+ end
30
+ end
31
+
32
+ class HelloObserver
33
+ include Glimmer::Web::Component
34
+
35
+ before_render do
36
+ @number_holder = NumberHolder.new
37
+ end
38
+
39
+ after_render do
40
+ # observe Model attribute @number_holder.number for changes and update View
41
+ observe(@number_holder, :number) do
42
+ number_string = @number_holder.number.to_s
43
+ @number_input.value = number_string unless @number_input.value == number_string
44
+ @range_input.value = number_string unless @range_input.value == number_string
45
+ end
46
+ # Bidirectional Data-Binding does the same thing automatically
47
+ # Just disable the observe block above as well as the oninput listeners below
48
+ # and enable the `value <=> [@number_holder, :number]` lines to try the data-binding version
49
+ # Learn more about Bidirectional and Unidirectional Data-Binding in hello_data_binding.rb
50
+ end
51
+
52
+ markup {
53
+ div {
54
+ div {
55
+ @number_input = input(type: 'number', value: @number_holder.number, min: 0, max: 100) {
56
+ # oninput listener updates Model attribute @number_holder.number
57
+ oninput do
58
+ @number_holder.number = @number_input.value.to_i
59
+ end
60
+
61
+ # Bidirectional Data-Binding simplifies the implementation significantly
62
+ # by enabling the following line and disabling oninput listeners as well
63
+ # as the after_body observe block observer
64
+ # Learn more about Bidirectional and Unidirectional Data-Binding in hello_data_binding.rb
65
+ # value <=> [@number_holder, :number]
66
+ }
67
+ }
68
+ div {
69
+ @range_input = input(type: 'range', value: @number_holder.number, min: 0, max: 100) {
70
+ # oninput listener updates Model attribute @number_holder.number
71
+ oninput do
72
+ @number_holder.number = @range_input.value.to_i
73
+ end
74
+
75
+ # Bidirectional Data-Binding simplifies the implementation significantly
76
+ # by enabling the following line and disabling oninput listeners as well
77
+ # as the after_body observe block observer
78
+ # Learn more about Bidirectional and Unidirectional Data-Binding in hello_data_binding.rb
79
+ # value <=> [@number_holder, :number]
80
+ }
81
+ }
82
+ }
83
+ }
84
+ end
85
+
86
+ Document.ready? do
87
+ HelloObserver.render
88
+ end
@@ -0,0 +1,36 @@
1
+ require 'glimmer-dsl-web'
2
+
3
+ class Counter
4
+ attr_accessor :count
5
+
6
+ def initialize
7
+ self.count = 0
8
+ end
9
+ end
10
+
11
+ class ButtonCounter
12
+ include Glimmer::Web::Component
13
+
14
+ before_render do
15
+ @counter = Counter.new
16
+ end
17
+
18
+ markup {
19
+ div {
20
+ button {
21
+ # Unidirectional Data-Binding indicating that on every change to @counter.count, the value
22
+ # is read and converted to "Click To Increment: #{value} ", and then automatically
23
+ # copied to button innerText (content) to display to the user
24
+ inner_text <= [@counter, :count,
25
+ on_read: ->(value) { "Click To Increment: #{value} " }
26
+ ]
27
+
28
+ onclick {
29
+ @counter.count += 1
30
+ }
31
+ }
32
+ }
33
+ }
34
+ end
35
+
36
+ ButtonCounter.render
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: glimmer-dsl-web
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.10
4
+ version: 0.0.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Maleh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-05 00:00:00.000000000 Z
11
+ date: 2024-01-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: glimmer
@@ -100,14 +100,14 @@ dependencies:
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: 0.4.6
103
+ version: 0.5.0
104
104
  type: :runtime
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: 0.4.6
110
+ version: 0.5.0
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: to_collection
113
113
  requirement: !ruby/object:Gem::Requirement
@@ -259,7 +259,9 @@ files:
259
259
  - lib/glimmer-dsl-web/samples/hello/hello_form.rb
260
260
  - lib/glimmer-dsl-web/samples/hello/hello_glimmer_component_helper/address_form.rb
261
261
  - lib/glimmer-dsl-web/samples/hello/hello_input_date_time.rb
262
+ - lib/glimmer-dsl-web/samples/hello/hello_observer.rb
262
263
  - lib/glimmer-dsl-web/samples/hello/hello_world.rb
264
+ - lib/glimmer-dsl-web/samples/regular/button_counter.rb
263
265
  - lib/glimmer-dsl-web/vendor/jquery.js
264
266
  - lib/glimmer/config/opal_logger.rb
265
267
  - lib/glimmer/data_binding/element_binding.rb
@@ -271,6 +273,7 @@ files:
271
273
  - lib/glimmer/dsl/web/element_expression.rb
272
274
  - lib/glimmer/dsl/web/general_element_expression.rb
273
275
  - lib/glimmer/dsl/web/listener_expression.rb
276
+ - lib/glimmer/dsl/web/observe_expression.rb
274
277
  - lib/glimmer/dsl/web/p_expression.rb
275
278
  - lib/glimmer/dsl/web/property_expression.rb
276
279
  - lib/glimmer/dsl/web/select_expression.rb