glimmer-dsl-web 0.6.0 → 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.6.0
1
+ 0.6.2
@@ -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.6.0 ruby lib
5
+ # stub: glimmer-dsl-web 0.6.2 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "glimmer-dsl-web".freeze
9
- s.version = "0.6.0"
9
+ s.version = "0.6.2"
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-08-24"
14
+ s.date = "2024-09-30"
15
15
  s.description = "Glimmer DSL for Web (Ruby in the Browser Web Frontend Framework) enables building Web Frontends using Ruby in the Browser, as per Matz's recommendation in his RubyConf 2022 keynote speech to replace JavaScript with Ruby. It aims at providing the simplest, most intuitive, most straight-forward, and most productive frontend framework in existence. The framework follows the Ruby way (with DSLs and TIMTOWTDI) and the Rails way (Convention over Configuration) in building Isomorphic Ruby on Rails Applications. It provides a Ruby HTML DSL, which uniquely enables writing both structure code and logic code in one language. It supports both Unidirectional (One-Way) Data-Binding (using <=) and Bidirectional (Two-Way) Data-Binding (using <=>). Dynamic rendering (and re-rendering) of HTML content is also supported via Content Data-Binding. Modular design is supported with Glimmer Web Components, Component Slots, and Component Custom Event Listeners. And, a Ruby CSS DSL is supported with the included Glimmer DSL for CSS. Many samples are demonstrated in the Rails sample app (there is a very minimal Standalone [No Rails] sample app too). You can finally live in pure Rubyland on the Web in both the frontend and backend with Glimmer DSL for Web! This gem relies on Opal Ruby.".freeze
16
16
  s.email = "andy.am@gmail.com".freeze
17
17
  s.extra_rdoc_files = [
@@ -33,6 +33,8 @@ Gem::Specification.new do |s|
33
33
  "lib/glimmer-dsl-web/ext/kernel.rb",
34
34
  "lib/glimmer-dsl-web/samples/hello/hello_button.rb",
35
35
  "lib/glimmer-dsl-web/samples/hello/hello_component.rb",
36
+ "lib/glimmer-dsl-web/samples/hello/hello_component_listeners.rb",
37
+ "lib/glimmer-dsl-web/samples/hello/hello_component_listeners_default_slot.rb",
36
38
  "lib/glimmer-dsl-web/samples/hello/hello_component_slots.rb",
37
39
  "lib/glimmer-dsl-web/samples/hello/hello_content_data_binding.rb",
38
40
  "lib/glimmer-dsl-web/samples/hello/hello_data_binding.rb",
@@ -102,7 +104,7 @@ Gem::Specification.new do |s|
102
104
 
103
105
  s.add_runtime_dependency(%q<glimmer>.freeze, ["~> 2.8.0"])
104
106
  s.add_runtime_dependency(%q<glimmer-dsl-xml>.freeze, ["~> 1.4.0"])
105
- s.add_runtime_dependency(%q<glimmer-dsl-css>.freeze, ["~> 1.5.1"])
107
+ s.add_runtime_dependency(%q<glimmer-dsl-css>.freeze, ["~> 1.5.2"])
106
108
  s.add_runtime_dependency(%q<opal>.freeze, ["= 1.8.2"])
107
109
  s.add_runtime_dependency(%q<opal-rails>.freeze, ["= 2.0.3"])
108
110
  s.add_runtime_dependency(%q<opal-async>.freeze, ["~> 1.4.1"])
@@ -12,16 +12,34 @@ module Glimmer
12
12
  end
13
13
 
14
14
  def interpret(parent, keyword, *args, &block)
15
- custom_widget_class = Glimmer::Web::Component.for(keyword)
16
- custom_widget_class.new(parent, args, {}, &block)
15
+ component_class = Glimmer::Web::Component.for(keyword)
16
+ component_class.new(parent, args, {}, &block)
17
17
  end
18
18
 
19
19
  def add_content(parent, keyword, *args, &block)
20
+ options = args.last.is_a?(Hash) ? args.last : {}
21
+ slot = options[:slot] || options['slot']
22
+ slot = slot.to_s unless slot.nil?
20
23
  # TODO consider avoiding source_location since it does not work in Opal
21
24
  if block.source_location && (block.source_location == parent.content&.__getobj__&.source_location)
22
25
  parent.content.call(parent) unless parent.content.called?
23
26
  else
24
- super(parent, keyword, *args, &block)
27
+ if slot
28
+ if slot == 'markup_root_slot'
29
+ super(parent, keyword, *args, &block)
30
+ else
31
+ slot_element = parent.slot_elements[slot]
32
+ slot_element&.content(&block)
33
+ end
34
+ else
35
+ if parent.default_slot
36
+ slot = parent.default_slot
37
+ slot_element = parent.slot_elements[slot]
38
+ slot_element&.content(&block)
39
+ else
40
+ super(parent, keyword, *args, &block)
41
+ end
42
+ end
25
43
  end
26
44
  parent.post_add_content
27
45
  end
@@ -28,19 +28,25 @@ module Glimmer
28
28
  module Web
29
29
  class ComponentSlotContentExpression < Expression
30
30
  def can_interpret?(parent, keyword, *args, &block)
31
+ component = parent.is_a?(Glimmer::Web::Component) ? parent : parent&.ancestor_component
31
32
  slot = keyword.to_s
32
33
  block_given? &&
33
- parent.respond_to?(:slot_elements) &&
34
+ !component.nil? &&
34
35
  (
35
- parent.slot_elements.keys.include?(slot) ||
36
- parent.slot_elements.keys.include?(slot.to_sym)
36
+ component.slot_elements.keys.include?(slot) ||
37
+ component.slot_elements.keys.include?(slot.to_sym)
37
38
  )
38
39
  end
39
40
 
40
41
  def interpret(parent, keyword, *args, &block)
41
42
  slot = keyword.to_s
42
- slot_element = parent.slot_elements[slot] || parent.slot_elements[slot.to_sym]
43
- slot_element&.content(&block)
43
+ component = parent.is_a?(Glimmer::Web::Component) ? parent : parent.ancestor_component
44
+ if slot == 'markup_root_slot'
45
+ component.content(slot: slot.to_sym, &block)
46
+ else
47
+ slot_element = component.slot_elements[slot] || component.slot_elements[slot.to_sym]
48
+ slot_element.content(&block)
49
+ end
44
50
  end
45
51
  end
46
52
  end
@@ -89,17 +89,26 @@ module Glimmer
89
89
  @after_render = block
90
90
  end
91
91
 
92
- def event(event)
92
+ def event(event_name)
93
93
  @events ||= []
94
- event = event.to_sym
95
- @events << event unless @events.include?(event)
94
+ event_name = event_name.to_sym
95
+ @events << event_name unless @events.include?(event_name)
96
96
  end
97
97
 
98
- def events(*events)
99
- if events.empty?
98
+ def events(*event_names)
99
+ @events ||= []
100
+ if event_names.empty?
100
101
  @events
101
102
  else
102
- events.each { |event| event(event) }
103
+ event_names.each { |event| event(event) }
104
+ end
105
+ end
106
+
107
+ def default_slot(slot_name = nil)
108
+ if slot_name.nil?
109
+ @default_slot
110
+ else
111
+ @default_slot = slot_name.to_s.to_sym
103
112
  end
104
113
  end
105
114
 
@@ -269,7 +278,7 @@ module Glimmer
269
278
  end
270
279
  # <- end of class methods
271
280
 
272
- attr_reader :markup_root, :parent, :args, :options, :style_block, :component_style, :slot_elements, :events
281
+ attr_reader :markup_root, :parent, :args, :options, :style_block, :component_style, :slot_elements, :events, :default_slot
273
282
  alias parent_proxy parent
274
283
 
275
284
  def initialize(parent, args, options, &content)
@@ -286,7 +295,8 @@ module Glimmer
286
295
  @args = args
287
296
  options ||= {}
288
297
  @options = self.class.options.merge(options)
289
- @events = self.class.instance_variable_get("@events")
298
+ @events = self.class.instance_variable_get("@events") || []
299
+ @default_slot = self.class.instance_variable_get("@default_slot")
290
300
  @content = Util::ProcTracker.new(content) if content
291
301
  # @style_blocks = {} # TODO enable when doing bulk head rendering in the future
292
302
  execute_hooks('before_render')
@@ -473,8 +483,15 @@ module Glimmer
473
483
  @content
474
484
  end
475
485
  else
476
- # delegate to GUI DSL ContentExpression
477
- super
486
+ options = args.last.is_a?(Hash) ? args.last : {}
487
+ slot = options[:slot] || options['slot']
488
+ slot = slot.to_sym unless slot.nil?
489
+ if slot
490
+ Glimmer::DSL::Engine.add_content(self, Glimmer::DSL::Web::ComponentExpression.new, self.class.keyword, slot: slot, &block)
491
+ else
492
+ # delegate to GUI DSL ContentExpression
493
+ super
494
+ end
478
495
  end
479
496
  end
480
497
 
@@ -604,16 +604,20 @@ module Glimmer
604
604
 
605
605
  def handle_observation_request(keyword, original_event_listener)
606
606
  if rendered?
607
- listener = ListenerProxy.new(
608
- element: self,
609
- selector: selector,
610
- dom_element: dom_element,
611
- event_attribute: keyword,
612
- original_event_listener: original_event_listener,
613
- )
614
- listener.register
615
- listeners_for(keyword) << listener
616
- listener
607
+ if keyword.start_with?('on_') && !['on_render', 'on_remove'].include?(keyword.to_s)
608
+ (ancestor_component || component)&.handle_observation_request(keyword, original_event_listener)
609
+ else
610
+ listener = ListenerProxy.new(
611
+ element: self,
612
+ selector: selector,
613
+ dom_element: dom_element,
614
+ event_attribute: keyword,
615
+ original_event_listener: original_event_listener,
616
+ )
617
+ listener.register
618
+ listeners_for(keyword) << listener
619
+ listener
620
+ end
617
621
  else
618
622
  enqueue_post_render_method_call('handle_observation_request', keyword, original_event_listener)
619
623
  end
@@ -21,198 +21,162 @@
21
21
 
22
22
  require 'glimmer-dsl-web'
23
23
 
24
- Address = Struct.new(:full_name, :street, :street2, :city, :state, :zip_code, keyword_init: true) do
25
- STATES = {
26
- "AK"=>"Alaska",
27
- "AL"=>"Alabama",
28
- "AR"=>"Arkansas",
29
- "AS"=>"American Samoa",
30
- "AZ"=>"Arizona",
31
- "CA"=>"California",
32
- "CO"=>"Colorado",
33
- "CT"=>"Connecticut",
34
- "DC"=>"District of Columbia",
35
- "DE"=>"Delaware",
36
- "FL"=>"Florida",
37
- "GA"=>"Georgia",
38
- "GU"=>"Guam",
39
- "HI"=>"Hawaii",
40
- "IA"=>"Iowa",
41
- "ID"=>"Idaho",
42
- "IL"=>"Illinois",
43
- "IN"=>"Indiana",
44
- "KS"=>"Kansas",
45
- "KY"=>"Kentucky",
46
- "LA"=>"Louisiana",
47
- "MA"=>"Massachusetts",
48
- "MD"=>"Maryland",
49
- "ME"=>"Maine",
50
- "MI"=>"Michigan",
51
- "MN"=>"Minnesota",
52
- "MO"=>"Missouri",
53
- "MS"=>"Mississippi",
54
- "MT"=>"Montana",
55
- "NC"=>"North Carolina",
56
- "ND"=>"North Dakota",
57
- "NE"=>"Nebraska",
58
- "NH"=>"New Hampshire",
59
- "NJ"=>"New Jersey",
60
- "NM"=>"New Mexico",
61
- "NV"=>"Nevada",
62
- "NY"=>"New York",
63
- "OH"=>"Ohio",
64
- "OK"=>"Oklahoma",
65
- "OR"=>"Oregon",
66
- "PA"=>"Pennsylvania",
67
- "PR"=>"Puerto Rico",
68
- "RI"=>"Rhode Island",
69
- "SC"=>"South Carolina",
70
- "SD"=>"South Dakota",
71
- "TN"=>"Tennessee",
72
- "TX"=>"Texas",
73
- "UT"=>"Utah",
74
- "VA"=>"Virginia",
75
- "VI"=>"Virgin Islands",
76
- "VT"=>"Vermont",
77
- "WA"=>"Washington",
78
- "WI"=>"Wisconsin",
79
- "WV"=>"West Virginia",
80
- "WY"=>"Wyoming"
81
- }
82
-
83
- def state_code
84
- STATES.invert[state]
85
- end
24
+ unless Object.const_defined?(:Address)
25
+ Address = Struct.new(:full_name, :street, :street2, :city, :state, :zip_code, :billing_and_shipping, keyword_init: true) do
26
+ STATES = {
27
+ "AK"=>"Alaska", "AL"=>"Alabama", "AR"=>"Arkansas", "AS"=>"American Samoa", "AZ"=>"Arizona",
28
+ "CA"=>"California", "CO"=>"Colorado", "CT"=>"Connecticut", "DC"=>"District of Columbia", "DE"=>"Delaware",
29
+ "FL"=>"Florida", "GA"=>"Georgia", "GU"=>"Guam", "HI"=>"Hawaii", "IA"=>"Iowa", "ID"=>"Idaho", "IL"=>"Illinois",
30
+ "IN"=>"Indiana", "KS"=>"Kansas", "KY"=>"Kentucky", "LA"=>"Louisiana", "MA"=>"Massachusetts", "MD"=>"Maryland",
31
+ "ME"=>"Maine", "MI"=>"Michigan", "MN"=>"Minnesota", "MO"=>"Missouri", "MS"=>"Mississippi", "MT"=>"Montana",
32
+ "NC"=>"North Carolina", "ND"=>"North Dakota", "NE"=>"Nebraska", "NH"=>"New Hampshire", "NJ"=>"New Jersey",
33
+ "NM"=>"New Mexico", "NV"=>"Nevada", "NY"=>"New York", "OH"=>"Ohio", "OK"=>"Oklahoma", "OR"=>"Oregon",
34
+ "PA"=>"Pennsylvania", "PR"=>"Puerto Rico", "RI"=>"Rhode Island", "SC"=>"South Carolina", "SD"=>"South Dakota",
35
+ "TN"=>"Tennessee", "TX"=>"Texas", "UT"=>"Utah", "VA"=>"Virginia", "VI"=>"Virgin Islands", "VT"=>"Vermont",
36
+ "WA"=>"Washington", "WI"=>"Wisconsin", "WV"=>"West Virginia", "WY"=>"Wyoming"
37
+ }
38
+
39
+ def state_code
40
+ STATES.invert[state]
41
+ end
42
+
43
+ def state_code=(value)
44
+ self.state = STATES[value]
45
+ end
86
46
 
87
- def state_code=(value)
88
- self.state = STATES[value]
89
- end
90
-
91
- def summary
92
- to_h.values.map(&:to_s).reject(&:empty?).join(', ')
47
+ def summary
48
+ string_attributes = to_h.except(:billing_and_shipping)
49
+ summary = string_attributes.values.map(&:to_s).reject(&:empty?).join(', ')
50
+ summary += " (Billing & Shipping)" if billing_and_shipping
51
+ summary
52
+ end
93
53
  end
94
54
  end
95
55
 
96
- # AddressForm Glimmer Web Component (View component)
97
- #
98
- # Including Glimmer::Web::Component makes this class a View component and automatically
99
- # generates a new Glimmer HTML DSL keyword that matches the lowercase underscored version
100
- # of the name of the class. AddressForm generates address_form keyword, which can be used
101
- # elsewhere in Glimmer HTML DSL code as done inside AddressPage below.
102
- class AddressForm
103
- include Glimmer::Web::Component
104
-
105
- option :address
106
-
107
- # Optionally, you can execute code before rendering markup.
108
- # This is useful for pre-setup of variables (e.g. Models) that you would use in the markup.
56
+ unless Object.const_defined?(:AddressForm)
57
+ # AddressForm Glimmer Web Component (View component)
109
58
  #
110
- # before_render do
111
- # end
112
-
113
- # Optionally, you can execute code after rendering markup.
114
- # This is useful for post-setup of extra Model listeners that would interact with the
115
- # markup elements and expect them to be rendered already.
116
- #
117
- # after_render do
118
- # end
59
+ # Including Glimmer::Web::Component makes this class a View component and automatically
60
+ # generates a new Glimmer HTML DSL keyword that matches the lowercase underscored version
61
+ # of the name of the class. AddressForm generates address_form keyword, which can be used
62
+ # elsewhere in Glimmer HTML DSL code as done inside AddressPage below.
63
+ class AddressForm
64
+ include Glimmer::Web::Component
65
+
66
+ option :address
67
+
68
+ # Optionally, you can execute code before rendering markup.
69
+ # This is useful for pre-setup of variables (e.g. Models) that you would use in the markup.
70
+ #
71
+ # before_render do
72
+ # end
73
+
74
+ # Optionally, you can execute code after rendering markup.
75
+ # This is useful for post-setup of extra Model listeners that would interact with the
76
+ # markup elements and expect them to be rendered already.
77
+ #
78
+ # after_render do
79
+ # end
80
+
81
+ # markup block provides the content of the
82
+ markup {
83
+ div {
84
+ div(style: 'display: grid; grid-auto-columns: 80px 260px;') { |address_div|
85
+ label('Full Name: ', for: 'full-name-field')
86
+ input(id: 'full-name-field') {
87
+ value <=> [address, :full_name]
88
+ }
89
+
90
+ label('Street: ', for: 'street-field')
91
+ input(id: 'street-field') {
92
+ value <=> [address, :street]
93
+ }
94
+
95
+ label('Street 2: ', for: 'street2-field')
96
+ textarea(id: 'street2-field') {
97
+ value <=> [address, :street2]
98
+ }
99
+
100
+ label('City: ', for: 'city-field')
101
+ input(id: 'city-field') {
102
+ value <=> [address, :city]
103
+ }
104
+
105
+ label('State: ', for: 'state-field')
106
+ select(id: 'state-field') {
107
+ Address::STATES.each do |state_code, state|
108
+ option(value: state_code) { state }
109
+ end
119
110
 
120
- # markup block provides the content of the
121
- markup {
122
- div {
123
- div(style: 'display: grid; grid-auto-columns: 80px 260px;') { |address_div|
124
- label('Full Name: ', for: 'full-name-field')
125
- input(id: 'full-name-field') {
126
- value <=> [address, :full_name]
127
- }
128
-
129
- label('Street: ', for: 'street-field')
130
- input(id: 'street-field') {
131
- value <=> [address, :street]
132
- }
133
-
134
- label('Street 2: ', for: 'street2-field')
135
- textarea(id: 'street2-field') {
136
- value <=> [address, :street2]
111
+ value <=> [address, :state_code]
112
+ }
113
+
114
+ label('Zip Code: ', for: 'zip-code-field')
115
+ input(id: 'zip-code-field', type: 'number', min: '0', max: '99999') {
116
+ value <=> [address, :zip_code,
117
+ on_write: :to_s,
118
+ ]
119
+ }
120
+
121
+ style {
122
+ r("#{address_div.selector} *") {
123
+ margin '5px'
124
+ }
125
+ r("#{address_div.selector} input, #{address_div.selector} select") {
126
+ grid_column '2'
127
+ }
128
+ }
137
129
  }
138
130
 
139
- label('City: ', for: 'city-field')
140
- input(id: 'city-field') {
141
- value <=> [address, :city]
131
+ div(style: 'margin: 5px') {
132
+ inner_text <= [address, :summary,
133
+ computed_by: address.members + ['state_code'],
134
+ ]
142
135
  }
143
-
144
- label('State: ', for: 'state-field')
145
- select(id: 'state-field') {
146
- Address::STATES.each do |state_code, state|
147
- option(value: state_code) { state }
148
- end
136
+ }
137
+ }
138
+ end
139
+ end
149
140
 
150
- value <=> [address, :state_code]
151
- }
141
+ unless Object.const_defined?(:AddressPage)
142
+ # AddressPage Glimmer Web Component (View component)
143
+ #
144
+ # This View component represents the main page being rendered,
145
+ # as done by its `render` class method below
146
+ class AddressPage
147
+ include Glimmer::Web::Component
148
+
149
+ before_render do
150
+ @shipping_address = Address.new(
151
+ full_name: 'Johnny Doe',
152
+ street: '3922 Park Ave',
153
+ street2: 'PO BOX 8382',
154
+ city: 'San Diego',
155
+ state: 'California',
156
+ zip_code: '91913',
157
+ )
158
+ @billing_address = Address.new(
159
+ full_name: 'John C Doe',
160
+ street: '123 Main St',
161
+ street2: 'Apartment 3C',
162
+ city: 'San Diego',
163
+ state: 'California',
164
+ zip_code: '91911',
165
+ )
166
+ end
167
+
168
+ markup {
169
+ div {
170
+ h1('Shipping Address')
152
171
 
153
- label('Zip Code: ', for: 'zip-code-field')
154
- input(id: 'zip-code-field', type: 'number', min: '0', max: '99999') {
155
- value <=> [address, :zip_code,
156
- on_write: :to_s,
157
- ]
158
- }
172
+ address_form(address: @shipping_address)
159
173
 
160
- style {
161
- r("#{address_div.selector} *") {
162
- margin '5px'
163
- }
164
- r("#{address_div.selector} input, #{address_div.selector} select") {
165
- grid_column '2'
166
- }
167
- }
168
- }
169
-
170
- div(style: 'margin: 5px') {
171
- inner_text <= [address, :summary,
172
- computed_by: address.members + ['state_code'],
173
- ]
174
+ h1('Billing Address')
175
+
176
+ address_form(address: @billing_address)
174
177
  }
175
178
  }
176
- }
177
- end
178
-
179
- # AddressPage Glimmer Web Component (View component)
180
- #
181
- # This View component represents the main page being rendered,
182
- # as done by its `render` class method below
183
- class AddressPage
184
- include Glimmer::Web::Component
185
-
186
- before_render do
187
- @shipping_address = Address.new(
188
- full_name: 'Johnny Doe',
189
- street: '3922 Park Ave',
190
- street2: 'PO BOX 8382',
191
- city: 'San Diego',
192
- state: 'California',
193
- zip_code: '91913',
194
- )
195
- @billing_address = Address.new(
196
- full_name: 'John C Doe',
197
- street: '123 Main St',
198
- street2: 'Apartment 3C',
199
- city: 'San Diego',
200
- state: 'California',
201
- zip_code: '91911',
202
- )
203
179
  end
204
-
205
- markup {
206
- div {
207
- h1('Shipping Address')
208
-
209
- address_form(address: @shipping_address)
210
-
211
- h1('Billing Address')
212
-
213
- address_form(address: @billing_address)
214
- }
215
- }
216
180
  end
217
181
 
218
182
  Document.ready? do