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.
@@ -0,0 +1,351 @@
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
+ 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
46
+
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
53
+ end
54
+ end
55
+
56
+ unless Object.const_defined?(:AddressForm)
57
+ # AddressForm Glimmer Web Component (View component)
58
+ #
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 HelloComponentListeners below.
63
+ class AddressForm
64
+ include Glimmer::Web::Component
65
+
66
+ option :address
67
+
68
+ markup {
69
+ div {
70
+ div(style: {display: :grid, grid_auto_columns: '80px 260px'}) { |address_div|
71
+ label('Full Name: ', for: 'full-name-field')
72
+ input(id: 'full-name-field') {
73
+ value <=> [address, :full_name]
74
+ }
75
+
76
+ label('Street: ', for: 'street-field')
77
+ input(id: 'street-field') {
78
+ value <=> [address, :street]
79
+ }
80
+
81
+ label('Street 2: ', for: 'street2-field')
82
+ textarea(id: 'street2-field') {
83
+ value <=> [address, :street2]
84
+ }
85
+
86
+ label('City: ', for: 'city-field')
87
+ input(id: 'city-field') {
88
+ value <=> [address, :city]
89
+ }
90
+
91
+ label('State: ', for: 'state-field')
92
+ select(id: 'state-field') {
93
+ Address::STATES.each do |state_code, state|
94
+ option(value: state_code) { state }
95
+ end
96
+
97
+ value <=> [address, :state_code]
98
+ }
99
+
100
+ label('Zip Code: ', for: 'zip-code-field')
101
+ input(id: 'zip-code-field', type: 'number', min: '0', max: '99999') {
102
+ value <=> [address, :zip_code,
103
+ on_write: :to_s,
104
+ ]
105
+ }
106
+
107
+ style {
108
+ r("#{address_div.selector} *") {
109
+ margin '5px'
110
+ }
111
+ r("#{address_div.selector} input, #{address_div.selector} select") {
112
+ grid_column '2'
113
+ }
114
+ }
115
+ }
116
+
117
+ div(style: {margin: 5}) {
118
+ inner_text <= [address, :summary,
119
+ computed_by: address.members + ['state_code'],
120
+ ]
121
+ }
122
+ }
123
+ }
124
+ end
125
+ end
126
+
127
+ unless Object.const_defined?(:AccordionSection)
128
+ class AccordionSection
129
+ class Presenter
130
+ attr_accessor :collapsed, :instant_transition
131
+
132
+ def toggle_collapsed(instant: false)
133
+ self.instant_transition = instant
134
+ self.collapsed = !collapsed
135
+ end
136
+
137
+ def expand(instant: false)
138
+ self.instant_transition = instant
139
+ self.collapsed = false
140
+ end
141
+
142
+ def collapse(instant: false)
143
+ self.instant_transition = instant
144
+ self.collapsed = true
145
+ end
146
+ end
147
+
148
+ include Glimmer::Web::Component
149
+
150
+ events :expanded, :collapsed
151
+
152
+ option :title
153
+
154
+ attr_reader :presenter
155
+
156
+ before_render do
157
+ @presenter = Presenter.new
158
+ end
159
+
160
+ markup {
161
+ section {
162
+ # Unidirectionally data-bind the class inclusion of 'collapsed' to the @presenter.collapsed boolean attribute,
163
+ # meaning if @presenter.collapsed changes to true, the CSS class 'collapsed' is included on the element,
164
+ # and if it changes to false, the CSS class 'collapsed' is removed from the element.
165
+ class_name(:collapsed) <= [@presenter, :collapsed]
166
+ class_name(:instant_transition) <= [@presenter, :instant_transition]
167
+
168
+ header(title, class: 'accordion-section-title') {
169
+ onclick do |event|
170
+ @presenter.toggle_collapsed
171
+ if @presenter.collapsed
172
+ notify_listeners(:collapsed)
173
+ else
174
+ notify_listeners(:expanded)
175
+ end
176
+ end
177
+ }
178
+
179
+ div(slot: :section_content, class: 'accordion-section-content')
180
+ }
181
+ }
182
+
183
+ style {
184
+ r('.accordion-section-title') {
185
+ font_size 2.em
186
+ font_weight :bold
187
+ cursor :pointer
188
+ padding_left 20
189
+ position :relative
190
+ margin_block_start 0.33.em
191
+ margin_block_end 0.33.em
192
+ }
193
+
194
+ r('.accordion-section-title::before') {
195
+ content '"▼"'
196
+ position :absolute
197
+ font_size 0.5.em
198
+ top 10
199
+ left 0
200
+ }
201
+
202
+ r('.accordion-section-content') {
203
+ height 246
204
+ overflow :hidden
205
+ transition 'height 0.5s linear'
206
+ }
207
+
208
+ r("#{component_element_selector}.instant_transition .accordion-section-content") {
209
+ transition 'initial'
210
+ }
211
+
212
+ r("#{component_element_selector}.collapsed .accordion-section-title::before") {
213
+ content '"►"'
214
+ }
215
+
216
+ r("#{component_element_selector}.collapsed .accordion-section-content") {
217
+ height 0
218
+ }
219
+ }
220
+ end
221
+ end
222
+
223
+ unless Object.const_defined?(:Accordion)
224
+ class Accordion
225
+ include Glimmer::Web::Component
226
+
227
+ events :accordion_section_expanded, :accordion_section_collapsed
228
+
229
+ markup {
230
+ # given that no slots are specified, nesting content under the accordion component
231
+ # in consumer code adds content directly inside the markup root div.
232
+ div { |accordion|
233
+ # on render, all accordion sections would have been added by consumers already, so we can
234
+ # attach listeners to all of them by re-opening their content with `.content { ... }` block
235
+ on_render do
236
+ accordion_section_elements = accordion.children
237
+ accordion_sections = accordion_section_elements.map(&:component)
238
+ accordion_sections.each_with_index do |accordion_section, index|
239
+ accordion_section_number = index + 1
240
+
241
+ # ensure only the first section is expanded
242
+ accordion_section.presenter.collapse(instant: true) if accordion_section_number != 1
243
+
244
+ accordion_section.content {
245
+ on_expanded do
246
+ other_accordion_sections = accordion_sections.reject {|other_accordion_section| other_accordion_section == accordion_section }
247
+ other_accordion_sections.each { |other_accordion_section| other_accordion_section.presenter.collapse }
248
+ notify_listeners(:accordion_section_expanded, accordion_section_number)
249
+ end
250
+
251
+ on_collapsed do
252
+ notify_listeners(:accordion_section_collapsed, accordion_section_number)
253
+ end
254
+ }
255
+ end
256
+ end
257
+ }
258
+ }
259
+ end
260
+ end
261
+
262
+ unless Object.const_defined?(:HelloComponentListeners)
263
+ # HelloComponentListeners Glimmer Web Component (View component)
264
+ #
265
+ # This View component represents the main page being rendered,
266
+ # as done by its `render` class method below
267
+ #
268
+ # Note: check out HelloComponentListenersDefaultSlot for a simpler version that leverages the default slot feature
269
+ class HelloComponentListeners
270
+ class Presenter
271
+ attr_accessor :status_message
272
+
273
+ def initialize
274
+ @status_message = "Accordion section 1 is expanded!"
275
+ end
276
+ end
277
+
278
+ include Glimmer::Web::Component
279
+
280
+ before_render do
281
+ @presenter = Presenter.new
282
+ @shipping_address = Address.new(
283
+ full_name: 'Johnny Doe',
284
+ street: '3922 Park Ave',
285
+ street2: 'PO BOX 8382',
286
+ city: 'San Diego',
287
+ state: 'California',
288
+ zip_code: '91913',
289
+ )
290
+ @billing_address = Address.new(
291
+ full_name: 'John C Doe',
292
+ street: '123 Main St',
293
+ street2: 'Apartment 3C',
294
+ city: 'San Diego',
295
+ state: 'California',
296
+ zip_code: '91911',
297
+ )
298
+ @emergency_address = Address.new(
299
+ full_name: 'Mary Doe',
300
+ street: '2038 Ipswitch St',
301
+ street2: 'Suite 300',
302
+ city: 'San Diego',
303
+ state: 'California',
304
+ zip_code: '91912',
305
+ )
306
+ end
307
+
308
+ markup {
309
+ div {
310
+ h1(style: {font_style: :italic}) {
311
+ inner_html <= [@presenter, :status_message]
312
+ }
313
+
314
+ accordion { # any content nested under component directly is added under its markup root div element
315
+ accordion_section(title: 'Shipping Address') {
316
+ section_content { # contribute elements to section_content slot declared in AccordionSection component
317
+ address_form(address: @shipping_address)
318
+ }
319
+ }
320
+
321
+ accordion_section(title: 'Billing Address') {
322
+ section_content {
323
+ address_form(address: @billing_address)
324
+ }
325
+ }
326
+
327
+ accordion_section(title: 'Emergency Address') {
328
+ section_content {
329
+ address_form(address: @emergency_address)
330
+ }
331
+ }
332
+
333
+ # on_accordion_section_expanded listener matches event :accordion_section_expanded declared in Accordion component
334
+ on_accordion_section_expanded { |accordion_section_number|
335
+ @presenter.status_message = "Accordion section #{accordion_section_number} is expanded!"
336
+ }
337
+
338
+ on_accordion_section_collapsed { |accordion_section_number|
339
+ @presenter.status_message = "Accordion section #{accordion_section_number} is collapsed!"
340
+ }
341
+ }
342
+ }
343
+ }
344
+ end
345
+ end
346
+
347
+ Document.ready? do
348
+ # renders a top-level (root) HelloComponentListeners component
349
+ # Note: check out hello_component_listeners_default_slot.rb for a simpler version that leverages the default slot feature
350
+ HelloComponentListeners.render
351
+ end
@@ -0,0 +1,349 @@
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
+ 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
46
+
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
53
+ end
54
+ end
55
+
56
+ unless Object.const_defined?(:AddressForm)
57
+ # AddressForm Glimmer Web Component (View component)
58
+ #
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 HelloComponentListenersDefaultSlot below.
63
+ class AddressForm
64
+ include Glimmer::Web::Component
65
+
66
+ option :address
67
+
68
+ markup {
69
+ div {
70
+ div(style: {display: :grid, grid_auto_columns: '80px 260px'}) { |address_div|
71
+ label('Full Name: ', for: 'full-name-field')
72
+ input(id: 'full-name-field') {
73
+ value <=> [address, :full_name]
74
+ }
75
+
76
+ label('Street: ', for: 'street-field')
77
+ input(id: 'street-field') {
78
+ value <=> [address, :street]
79
+ }
80
+
81
+ label('Street 2: ', for: 'street2-field')
82
+ textarea(id: 'street2-field') {
83
+ value <=> [address, :street2]
84
+ }
85
+
86
+ label('City: ', for: 'city-field')
87
+ input(id: 'city-field') {
88
+ value <=> [address, :city]
89
+ }
90
+
91
+ label('State: ', for: 'state-field')
92
+ select(id: 'state-field') {
93
+ Address::STATES.each do |state_code, state|
94
+ option(value: state_code) { state }
95
+ end
96
+
97
+ value <=> [address, :state_code]
98
+ }
99
+
100
+ label('Zip Code: ', for: 'zip-code-field')
101
+ input(id: 'zip-code-field', type: 'number', min: '0', max: '99999') {
102
+ value <=> [address, :zip_code,
103
+ on_write: :to_s,
104
+ ]
105
+ }
106
+
107
+ style {
108
+ r("#{address_div.selector} *") {
109
+ margin '5px'
110
+ }
111
+ r("#{address_div.selector} input, #{address_div.selector} select") {
112
+ grid_column '2'
113
+ }
114
+ }
115
+ }
116
+
117
+ div(style: {margin: 5}) {
118
+ inner_text <= [address, :summary,
119
+ computed_by: address.members + ['state_code'],
120
+ ]
121
+ }
122
+ }
123
+ }
124
+ end
125
+ end
126
+
127
+ unless Object.const_defined?(:AccordionSection2)
128
+ # Note: this is similar to AccordionSection in HelloComponentSlots but specifies default_slot for simpler consumption
129
+ class AccordionSection2
130
+ class Presenter
131
+ attr_accessor :collapsed, :instant_transition
132
+
133
+ def toggle_collapsed(instant: false)
134
+ self.instant_transition = instant
135
+ self.collapsed = !collapsed
136
+ end
137
+
138
+ def expand(instant: false)
139
+ self.instant_transition = instant
140
+ self.collapsed = false
141
+ end
142
+
143
+ def collapse(instant: false)
144
+ self.instant_transition = instant
145
+ self.collapsed = true
146
+ end
147
+ end
148
+
149
+ include Glimmer::Web::Component
150
+
151
+ events :expanded, :collapsed
152
+
153
+ default_slot :section_content # automatically insert content in this element slot inside markup
154
+
155
+ option :title
156
+
157
+ attr_reader :presenter
158
+
159
+ before_render do
160
+ @presenter = Presenter.new
161
+ end
162
+
163
+ markup {
164
+ section { # represents the :markup_root_slot to allow inserting content here instead of in default_slot
165
+ # Unidirectionally data-bind the class inclusion of 'collapsed' to the @presenter.collapsed boolean attribute,
166
+ # meaning if @presenter.collapsed changes to true, the CSS class 'collapsed' is included on the element,
167
+ # and if it changes to false, the CSS class 'collapsed' is removed from the element.
168
+ class_name(:collapsed) <= [@presenter, :collapsed]
169
+ class_name(:instant_transition) <= [@presenter, :instant_transition]
170
+
171
+ header(title, class: 'accordion-section-title') {
172
+ onclick do |event|
173
+ @presenter.toggle_collapsed
174
+ if @presenter.collapsed
175
+ notify_listeners(:collapsed)
176
+ else
177
+ notify_listeners(:expanded)
178
+ end
179
+ end
180
+ }
181
+
182
+ div(slot: :section_content, class: 'accordion-section-content')
183
+ }
184
+ }
185
+
186
+ style {
187
+ r('.accordion-section-title') {
188
+ font_size 2.em
189
+ font_weight :bold
190
+ cursor :pointer
191
+ padding_left 20
192
+ position :relative
193
+ margin_block_start 0.33.em
194
+ margin_block_end 0.33.em
195
+ }
196
+
197
+ r('.accordion-section-title::before') {
198
+ content '"▼"'
199
+ position :absolute
200
+ font_size 0.5.em
201
+ top 10
202
+ left 0
203
+ }
204
+
205
+ r('.accordion-section-content') {
206
+ height 246
207
+ overflow :hidden
208
+ transition 'height 0.5s linear'
209
+ }
210
+
211
+ r("#{component_element_selector}.instant_transition .accordion-section-content") {
212
+ transition 'initial'
213
+ }
214
+
215
+ r("#{component_element_selector}.collapsed .accordion-section-title::before") {
216
+ content '"►"'
217
+ }
218
+
219
+ r("#{component_element_selector}.collapsed .accordion-section-content") {
220
+ height 0
221
+ }
222
+ }
223
+ end
224
+ end
225
+
226
+ unless Object.const_defined?(:Accordion)
227
+ class Accordion
228
+ include Glimmer::Web::Component
229
+
230
+ events :accordion_section_expanded, :accordion_section_collapsed
231
+
232
+ markup {
233
+ # given that no slots are specified, nesting content under the accordion component
234
+ # in consumer code adds content directly inside the markup root div.
235
+ div { |accordion| # represents the :markup_root_slot (top-level element)
236
+ # on render, all accordion sections would have been added by consumers already, so we can
237
+ # attach listeners to all of them by re-opening their content with `.content { ... }` block
238
+ on_render do
239
+ accordion_section_elements = accordion.children
240
+ accordion_sections = accordion_section_elements.map(&:component)
241
+ accordion_sections.each_with_index do |accordion_section, index|
242
+ accordion_section_number = index + 1
243
+
244
+ # ensure only the first section is expanded
245
+ accordion_section.presenter.collapse(instant: true) if accordion_section_number != 1
246
+
247
+ accordion_section.content { # re-open content and add component custom event listeners
248
+ on_expanded do
249
+ other_accordion_sections = accordion_sections.reject {|other_accordion_section| other_accordion_section == accordion_section }
250
+ other_accordion_sections.each { |other_accordion_section| other_accordion_section.presenter.collapse }
251
+ notify_listeners(:accordion_section_expanded, accordion_section_number)
252
+ end
253
+
254
+ on_collapsed do
255
+ notify_listeners(:accordion_section_collapsed, accordion_section_number)
256
+ end
257
+ }
258
+ end
259
+ end
260
+ }
261
+ }
262
+ end
263
+ end
264
+
265
+ unless Object.const_defined?(:HelloComponentListenersDefaultSlot)
266
+ # HelloComponentListenersDefaultSlot Glimmer Web Component (View component)
267
+ #
268
+ # This View component represents the main page being rendered,
269
+ # as done by its `render` class method below
270
+ #
271
+ # Note: this is a simpler version of HelloComponentSlots as it leverages the default slot feature
272
+ class HelloComponentListenersDefaultSlot
273
+ class Presenter
274
+ attr_accessor :status_message
275
+
276
+ def initialize
277
+ @status_message = "Accordion section 1 is expanded!"
278
+ end
279
+ end
280
+
281
+ include Glimmer::Web::Component
282
+
283
+ before_render do
284
+ @presenter = Presenter.new
285
+ @shipping_address = Address.new(
286
+ full_name: 'Johnny Doe',
287
+ street: '3922 Park Ave',
288
+ street2: 'PO BOX 8382',
289
+ city: 'San Diego',
290
+ state: 'California',
291
+ zip_code: '91913',
292
+ )
293
+ @billing_address = Address.new(
294
+ full_name: 'John C Doe',
295
+ street: '123 Main St',
296
+ street2: 'Apartment 3C',
297
+ city: 'San Diego',
298
+ state: 'California',
299
+ zip_code: '91911',
300
+ )
301
+ @emergency_address = Address.new(
302
+ full_name: 'Mary Doe',
303
+ street: '2038 Ipswitch St',
304
+ street2: 'Suite 300',
305
+ city: 'San Diego',
306
+ state: 'California',
307
+ zip_code: '91912',
308
+ )
309
+ end
310
+
311
+ markup {
312
+ div {
313
+ h1(style: {font_style: :italic}) {
314
+ inner_html <= [@presenter, :status_message]
315
+ }
316
+
317
+ accordion {
318
+ # any content nested under component directly is added to its markup_root_slot element if no default_slot is specified
319
+ accordion_section2(title: 'Shipping Address') {
320
+ address_form(address: @shipping_address) # automatically inserts content in default_slot :section_content
321
+ }
322
+
323
+ accordion_section2(title: 'Billing Address') {
324
+ address_form(address: @billing_address) # automatically inserts content in default_slot :section_content
325
+ }
326
+
327
+ accordion_section2(title: 'Emergency Address') {
328
+ address_form(address: @emergency_address) # automatically inserts content in default_slot :section_content
329
+ }
330
+
331
+ # on_accordion_section_expanded listener matches event :accordion_section_expanded declared in Accordion component
332
+ on_accordion_section_expanded { |accordion_section_number|
333
+ @presenter.status_message = "Accordion section #{accordion_section_number} is expanded!"
334
+ }
335
+
336
+ on_accordion_section_collapsed { |accordion_section_number|
337
+ @presenter.status_message = "Accordion section #{accordion_section_number} is collapsed!"
338
+ }
339
+ }
340
+ }
341
+ }
342
+ end
343
+ end
344
+
345
+ Document.ready? do
346
+ # renders a top-level (root) HelloComponentListenersDefaultSlot component
347
+ # Note: this is a simpler version of hello_component_slots.rb as it leverages the default slot feature
348
+ HelloComponentListenersDefaultSlot.render
349
+ end