glimmer-dsl-web 0.6.0 → 0.6.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +795 -11
- data/VERSION +1 -1
- data/glimmer-dsl-web.gemspec +6 -4
- data/lib/glimmer/dsl/web/component_expression.rb +21 -3
- data/lib/glimmer/dsl/web/component_slot_content_expression.rb +11 -5
- data/lib/glimmer/web/component.rb +27 -10
- data/lib/glimmer/web/element_proxy.rb +14 -10
- data/lib/glimmer-dsl-web/samples/hello/hello_component.rb +141 -177
- data/lib/glimmer-dsl-web/samples/hello/hello_component_listeners.rb +351 -0
- data/lib/glimmer-dsl-web/samples/hello/hello_component_listeners_default_slot.rb +349 -0
- data/lib/glimmer-dsl-web/samples/hello/hello_component_slots.rb +28 -68
- data/lib/glimmer-dsl-web/samples/hello/hello_content_data_binding.rb +3 -2
- data/lib/glimmer-dsl-web/samples/hello/hello_data_binding.rb +28 -71
- data/lib/glimmer-dsl-web/samples/hello/hello_style.rb +3 -3
- data/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/new_todo_form.rb +1 -1
- data/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/todo_input.rb +1 -1
- data/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/todo_list.rb +3 -3
- data/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/todo_list_item.rb +1 -3
- data/lib/glimmer-dsl-web/samples/regular/todo_mvc.rb +2 -2
- metadata +6 -4
@@ -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
|