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.
- 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
|