glimmer-dsl-web 0.7.2 → 0.7.3
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 +4 -0
- data/README.md +469 -373
- data/VERSION +1 -1
- data/glimmer-dsl-web.gemspec +3 -2
- data/lib/glimmer-dsl-web/samples/hello/hello_component_attribute_data_binding.rb +127 -0
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6cdfd633318946532548053f4ab89f31e423280ae73ba9505b598b0e60695680
|
4
|
+
data.tar.gz: e95ac60a8077a1157abaf6da13cf0f7adab1f3f72fb727bb9cf270ae373cab2e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c77c3cc7a2eabae100de6dd48e5797f8ce28774c4da17e6324bfe61fd8ece5d3e7b811538eabef1989f31de589e2ce64a1bbf122998c963158e844f33ca73987
|
7
|
+
data.tar.gz: a9f1a3ca8a39beae60602248b395621cadf2ae15362be300ff34f2d1d7d9c1464dcdab0614f31838388ae7a3e556abf0437f372642566827bd2ec72ba831934d
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## 0.7.3
|
4
|
+
|
5
|
+
- Hello, Component Attribute Data-Binding! Sample: `require 'glimmer-dsl-web/samples/hello/hello_component_attribute_data_binding.rb'`
|
6
|
+
|
3
7
|
## 0.7.2
|
4
8
|
|
5
9
|
- Hello, Component Attribute Listeners! Sample: `require 'glimmer-dsl-web/samples/hello/hello_component_attribute_listeners.rb'`
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# [<img src="https://raw.githubusercontent.com/AndyObtiva/glimmer/master/images/glimmer-logo-hi-res.png" height=85 />](https://github.com/AndyObtiva/glimmer) Glimmer DSL for Web 0.7.
|
1
|
+
# [<img src="https://raw.githubusercontent.com/AndyObtiva/glimmer/master/images/glimmer-logo-hi-res.png" height=85 />](https://github.com/AndyObtiva/glimmer) Glimmer DSL for Web 0.7.3 (Beta)
|
2
2
|
## Ruby-in-the-Browser Web Frontend Framework
|
3
3
|
### The "Rails" of Frontend Frameworks!!! ([Fukuoka Award Winning](https://andymaleh.blogspot.com/2025/01/glimmer-dsl-for-web-wins-in-fukuoka.html))
|
4
4
|
#### Finally, Ruby Developer Productivity, Happiness, and Fun in the Frontend!!!
|
@@ -1353,6 +1353,7 @@ Learn more about the differences between various [Glimmer](https://github.com/An
|
|
1353
1353
|
- [Hello, Component Listeners!](#hello-component-listeners)
|
1354
1354
|
- [Hello, Component Listeners (Default Slot)!](#hello-component-listeners-default-slot)
|
1355
1355
|
- [Hello, Component Attribute Listeners!](#hello-component-attribute-listeners)
|
1356
|
+
- [Hello, Component Attribute Data-Binding!](#hello-component-attribute-data-binding)
|
1356
1357
|
- [Hello, glimmer_component Rails Helper!](#hello-glimmer_component-rails-helper)
|
1357
1358
|
- [Hello, Paragraph!](#hello-paragraph)
|
1358
1359
|
- [Hello, Style!](#hello-style)
|
@@ -1417,7 +1418,7 @@ rails new glimmer_app_server
|
|
1417
1418
|
Add the following to `Gemfile`:
|
1418
1419
|
|
1419
1420
|
```
|
1420
|
-
gem 'glimmer-dsl-web', '~> 0.7.
|
1421
|
+
gem 'glimmer-dsl-web', '~> 0.7.3'
|
1421
1422
|
```
|
1422
1423
|
|
1423
1424
|
Run:
|
@@ -2995,367 +2996,357 @@ Glimmer HTML DSL Ruby code in the frontend:
|
|
2995
2996
|
```ruby
|
2996
2997
|
require 'glimmer-dsl-web'
|
2997
2998
|
|
2998
|
-
|
2999
|
-
|
3000
|
-
|
3001
|
-
|
3002
|
-
|
3003
|
-
|
3004
|
-
|
3005
|
-
|
3006
|
-
|
3007
|
-
|
3008
|
-
|
3009
|
-
|
3010
|
-
|
3011
|
-
|
3012
|
-
|
3013
|
-
|
3014
|
-
|
3015
|
-
|
3016
|
-
|
3017
|
-
|
3018
|
-
|
3019
|
-
|
3020
|
-
|
3021
|
-
|
3022
|
-
|
3023
|
-
|
3024
|
-
|
3025
|
-
|
3026
|
-
|
3027
|
-
|
3028
|
-
|
3029
|
-
|
3030
|
-
|
3031
|
-
|
3032
|
-
|
3033
|
-
|
3034
|
-
|
3035
|
-
|
3036
|
-
|
3037
|
-
|
3038
|
-
|
3039
|
-
|
3040
|
-
|
3041
|
-
|
3042
|
-
|
3043
|
-
|
3044
|
-
|
3045
|
-
|
3046
|
-
|
3047
|
-
|
3048
|
-
|
3049
|
-
|
3050
|
-
|
3051
|
-
|
3052
|
-
|
3053
|
-
|
3054
|
-
|
3055
|
-
|
3056
|
-
}
|
3057
|
-
|
3058
|
-
def state_code
|
3059
|
-
STATES.invert[state]
|
3060
|
-
end
|
3061
|
-
|
3062
|
-
def state_code=(value)
|
3063
|
-
self.state = STATES[value]
|
3064
|
-
end
|
2999
|
+
Address = Struct.new(:full_name, :street, :street2, :city, :state, :zip_code, keyword_init: true) do
|
3000
|
+
STATES = {
|
3001
|
+
"AK"=>"Alaska",
|
3002
|
+
"AL"=>"Alabama",
|
3003
|
+
"AR"=>"Arkansas",
|
3004
|
+
"AS"=>"American Samoa",
|
3005
|
+
"AZ"=>"Arizona",
|
3006
|
+
"CA"=>"California",
|
3007
|
+
"CO"=>"Colorado",
|
3008
|
+
"CT"=>"Connecticut",
|
3009
|
+
"DC"=>"District of Columbia",
|
3010
|
+
"DE"=>"Delaware",
|
3011
|
+
"FL"=>"Florida",
|
3012
|
+
"GA"=>"Georgia",
|
3013
|
+
"GU"=>"Guam",
|
3014
|
+
"HI"=>"Hawaii",
|
3015
|
+
"IA"=>"Iowa",
|
3016
|
+
"ID"=>"Idaho",
|
3017
|
+
"IL"=>"Illinois",
|
3018
|
+
"IN"=>"Indiana",
|
3019
|
+
"KS"=>"Kansas",
|
3020
|
+
"KY"=>"Kentucky",
|
3021
|
+
"LA"=>"Louisiana",
|
3022
|
+
"MA"=>"Massachusetts",
|
3023
|
+
"MD"=>"Maryland",
|
3024
|
+
"ME"=>"Maine",
|
3025
|
+
"MI"=>"Michigan",
|
3026
|
+
"MN"=>"Minnesota",
|
3027
|
+
"MO"=>"Missouri",
|
3028
|
+
"MS"=>"Mississippi",
|
3029
|
+
"MT"=>"Montana",
|
3030
|
+
"NC"=>"North Carolina",
|
3031
|
+
"ND"=>"North Dakota",
|
3032
|
+
"NE"=>"Nebraska",
|
3033
|
+
"NH"=>"New Hampshire",
|
3034
|
+
"NJ"=>"New Jersey",
|
3035
|
+
"NM"=>"New Mexico",
|
3036
|
+
"NV"=>"Nevada",
|
3037
|
+
"NY"=>"New York",
|
3038
|
+
"OH"=>"Ohio",
|
3039
|
+
"OK"=>"Oklahoma",
|
3040
|
+
"OR"=>"Oregon",
|
3041
|
+
"PA"=>"Pennsylvania",
|
3042
|
+
"PR"=>"Puerto Rico",
|
3043
|
+
"RI"=>"Rhode Island",
|
3044
|
+
"SC"=>"South Carolina",
|
3045
|
+
"SD"=>"South Dakota",
|
3046
|
+
"TN"=>"Tennessee",
|
3047
|
+
"TX"=>"Texas",
|
3048
|
+
"UT"=>"Utah",
|
3049
|
+
"VA"=>"Virginia",
|
3050
|
+
"VI"=>"Virgin Islands",
|
3051
|
+
"VT"=>"Vermont",
|
3052
|
+
"WA"=>"Washington",
|
3053
|
+
"WI"=>"Wisconsin",
|
3054
|
+
"WV"=>"West Virginia",
|
3055
|
+
"WY"=>"Wyoming"
|
3056
|
+
}
|
3065
3057
|
|
3066
|
-
|
3067
|
-
|
3068
|
-
|
3058
|
+
def state_code
|
3059
|
+
STATES.invert[state]
|
3060
|
+
end
|
3061
|
+
|
3062
|
+
def state_code=(value)
|
3063
|
+
self.state = STATES[value]
|
3064
|
+
end
|
3065
|
+
|
3066
|
+
def summary
|
3067
|
+
to_h.values.map(&:to_s).reject(&:empty?).join(', ')
|
3069
3068
|
end
|
3070
3069
|
end
|
3071
3070
|
|
3072
|
-
|
3073
|
-
|
3074
|
-
|
3075
|
-
|
3076
|
-
|
3077
|
-
|
3078
|
-
|
3079
|
-
|
3080
|
-
include Glimmer::Web::Component
|
3081
|
-
|
3082
|
-
option :address
|
3083
|
-
|
3084
|
-
markup {
|
3085
|
-
div {
|
3086
|
-
div(style: {display: :grid, grid_auto_columns: '80px 260px'}) { |address_div|
|
3087
|
-
label('Full Name: ', for: 'full-name-field')
|
3088
|
-
input(id: 'full-name-field') {
|
3089
|
-
value <=> [address, :full_name]
|
3090
|
-
}
|
3091
|
-
|
3092
|
-
label('Street: ', for: 'street-field')
|
3093
|
-
input(id: 'street-field') {
|
3094
|
-
value <=> [address, :street]
|
3095
|
-
}
|
3096
|
-
|
3097
|
-
label('Street 2: ', for: 'street2-field')
|
3098
|
-
textarea(id: 'street2-field') {
|
3099
|
-
value <=> [address, :street2]
|
3100
|
-
}
|
3101
|
-
|
3102
|
-
label('City: ', for: 'city-field')
|
3103
|
-
input(id: 'city-field') {
|
3104
|
-
value <=> [address, :city]
|
3105
|
-
}
|
3106
|
-
|
3107
|
-
label('State: ', for: 'state-field')
|
3108
|
-
select(id: 'state-field') {
|
3109
|
-
Address::STATES.each do |state_code, state|
|
3110
|
-
option(value: state_code) { state }
|
3111
|
-
end
|
3071
|
+
# AddressForm Glimmer Web Component (View component)
|
3072
|
+
#
|
3073
|
+
# Including Glimmer::Web::Component makes this class a View component and automatically
|
3074
|
+
# generates a new Glimmer HTML DSL keyword that matches the lowercase underscored version
|
3075
|
+
# of the name of the class. AddressForm generates address_form keyword, which can be used
|
3076
|
+
# elsewhere in Glimmer HTML DSL code as done inside HelloComponentListeners below.
|
3077
|
+
class AddressForm
|
3078
|
+
include Glimmer::Web::Component
|
3112
3079
|
|
3113
|
-
|
3114
|
-
|
3115
|
-
|
3116
|
-
|
3117
|
-
|
3118
|
-
|
3119
|
-
|
3120
|
-
|
3121
|
-
}
|
3122
|
-
|
3123
|
-
style {
|
3124
|
-
r("#{address_div.selector} *") {
|
3125
|
-
margin '5px'
|
3126
|
-
}
|
3127
|
-
r("#{address_div.selector} input, #{address_div.selector} select") {
|
3128
|
-
grid_column '2'
|
3129
|
-
}
|
3130
|
-
}
|
3080
|
+
option :address
|
3081
|
+
|
3082
|
+
markup {
|
3083
|
+
div {
|
3084
|
+
div(style: {display: :grid, grid_auto_columns: '80px 260px'}) { |address_div|
|
3085
|
+
label('Full Name: ', for: 'full-name-field')
|
3086
|
+
input(id: 'full-name-field') {
|
3087
|
+
value <=> [address, :full_name]
|
3131
3088
|
}
|
3132
3089
|
|
3133
|
-
|
3134
|
-
|
3135
|
-
|
3136
|
-
]
|
3090
|
+
label('Street: ', for: 'street-field')
|
3091
|
+
input(id: 'street-field') {
|
3092
|
+
value <=> [address, :street]
|
3137
3093
|
}
|
3138
|
-
|
3139
|
-
|
3140
|
-
|
3141
|
-
|
3142
|
-
|
3143
|
-
|
3144
|
-
|
3145
|
-
|
3146
|
-
|
3147
|
-
|
3148
|
-
|
3149
|
-
|
3150
|
-
|
3151
|
-
|
3152
|
-
|
3153
|
-
def expand(instant: false)
|
3154
|
-
self.instant_transition = instant
|
3155
|
-
self.collapsed = false
|
3156
|
-
end
|
3157
|
-
|
3158
|
-
def collapse(instant: false)
|
3159
|
-
self.instant_transition = instant
|
3160
|
-
self.collapsed = true
|
3161
|
-
end
|
3162
|
-
end
|
3163
|
-
|
3164
|
-
include Glimmer::Web::Component
|
3165
|
-
|
3166
|
-
events :expanded, :collapsed
|
3167
|
-
|
3168
|
-
option :title
|
3169
|
-
|
3170
|
-
attr_reader :presenter
|
3171
|
-
|
3172
|
-
before_render do
|
3173
|
-
@presenter = Presenter.new
|
3174
|
-
end
|
3175
|
-
|
3176
|
-
markup {
|
3177
|
-
section {
|
3178
|
-
# Unidirectionally data-bind the class inclusion of 'collapsed' to the @presenter.collapsed boolean attribute,
|
3179
|
-
# meaning if @presenter.collapsed changes to true, the CSS class 'collapsed' is included on the element,
|
3180
|
-
# and if it changes to false, the CSS class 'collapsed' is removed from the element.
|
3181
|
-
class_name(:collapsed) <= [@presenter, :collapsed]
|
3182
|
-
class_name(:instant_transition) <= [@presenter, :instant_transition]
|
3183
|
-
|
3184
|
-
header(title, class: 'accordion-section-title') {
|
3185
|
-
onclick do |event|
|
3186
|
-
@presenter.toggle_collapsed
|
3187
|
-
if @presenter.collapsed
|
3188
|
-
notify_listeners(:collapsed)
|
3189
|
-
else
|
3190
|
-
notify_listeners(:expanded)
|
3191
|
-
end
|
3094
|
+
|
3095
|
+
label('Street 2: ', for: 'street2-field')
|
3096
|
+
textarea(id: 'street2-field') {
|
3097
|
+
value <=> [address, :street2]
|
3098
|
+
}
|
3099
|
+
|
3100
|
+
label('City: ', for: 'city-field')
|
3101
|
+
input(id: 'city-field') {
|
3102
|
+
value <=> [address, :city]
|
3103
|
+
}
|
3104
|
+
|
3105
|
+
label('State: ', for: 'state-field')
|
3106
|
+
select(id: 'state-field') {
|
3107
|
+
Address::STATES.each do |state_code, state|
|
3108
|
+
option(value: state_code) { state }
|
3192
3109
|
end
|
3110
|
+
|
3111
|
+
value <=> [address, :state_code]
|
3193
3112
|
}
|
3194
3113
|
|
3195
|
-
|
3196
|
-
|
3197
|
-
|
3198
|
-
|
3199
|
-
|
3200
|
-
|
3201
|
-
|
3202
|
-
|
3203
|
-
|
3204
|
-
|
3205
|
-
|
3206
|
-
|
3207
|
-
|
3208
|
-
|
3209
|
-
|
3210
|
-
r('.accordion-section-title::before') {
|
3211
|
-
content '"▼"'
|
3212
|
-
position :absolute
|
3213
|
-
font_size 0.5.em
|
3214
|
-
top 10
|
3215
|
-
left 0
|
3216
|
-
}
|
3217
|
-
|
3218
|
-
r('.accordion-section-content') {
|
3219
|
-
height 246
|
3220
|
-
overflow :hidden
|
3221
|
-
transition 'height 0.5s linear'
|
3222
|
-
}
|
3223
|
-
|
3224
|
-
r("#{component_element_selector}.instant_transition .accordion-section-content") {
|
3225
|
-
transition 'initial'
|
3226
|
-
}
|
3227
|
-
|
3228
|
-
r("#{component_element_selector}.collapsed .accordion-section-title::before") {
|
3229
|
-
content '"►"'
|
3114
|
+
label('Zip Code: ', for: 'zip-code-field')
|
3115
|
+
input(id: 'zip-code-field', type: 'number', min: '0', max: '99999') {
|
3116
|
+
value <=> [address, :zip_code,
|
3117
|
+
on_write: :to_s,
|
3118
|
+
]
|
3119
|
+
}
|
3120
|
+
|
3121
|
+
style {
|
3122
|
+
r("#{address_div.selector} *") {
|
3123
|
+
margin '5px'
|
3124
|
+
}
|
3125
|
+
r("#{address_div.selector} input, #{address_div.selector} select") {
|
3126
|
+
grid_column '2'
|
3127
|
+
}
|
3128
|
+
}
|
3230
3129
|
}
|
3231
3130
|
|
3232
|
-
|
3233
|
-
|
3131
|
+
div(style: {margin: 5}) {
|
3132
|
+
inner_text <= [address, :summary,
|
3133
|
+
computed_by: address.members + ['state_code'],
|
3134
|
+
]
|
3234
3135
|
}
|
3235
3136
|
}
|
3236
|
-
|
3137
|
+
}
|
3237
3138
|
end
|
3238
3139
|
|
3239
|
-
|
3240
|
-
class
|
3241
|
-
|
3140
|
+
class AccordionSection
|
3141
|
+
class Presenter
|
3142
|
+
attr_accessor :collapsed, :instant_transition
|
3242
3143
|
|
3243
|
-
|
3144
|
+
def toggle_collapsed(instant: false)
|
3145
|
+
self.instant_transition = instant
|
3146
|
+
self.collapsed = !collapsed
|
3147
|
+
end
|
3244
3148
|
|
3245
|
-
|
3246
|
-
|
3247
|
-
|
3248
|
-
|
3249
|
-
|
3250
|
-
|
3251
|
-
|
3252
|
-
|
3253
|
-
|
3254
|
-
|
3255
|
-
|
3149
|
+
def expand(instant: false)
|
3150
|
+
self.instant_transition = instant
|
3151
|
+
self.collapsed = false
|
3152
|
+
end
|
3153
|
+
|
3154
|
+
def collapse(instant: false)
|
3155
|
+
self.instant_transition = instant
|
3156
|
+
self.collapsed = true
|
3157
|
+
end
|
3158
|
+
end
|
3159
|
+
|
3160
|
+
include Glimmer::Web::Component
|
3256
3161
|
|
3257
|
-
|
3258
|
-
accordion_section.presenter.collapse(instant: true) if accordion_section_number != 1
|
3162
|
+
events :expanded, :collapsed
|
3259
3163
|
|
3260
|
-
|
3261
|
-
on_expanded do
|
3262
|
-
other_accordion_sections = accordion_sections.reject {|other_accordion_section| other_accordion_section == accordion_section }
|
3263
|
-
other_accordion_sections.each { |other_accordion_section| other_accordion_section.presenter.collapse }
|
3264
|
-
notify_listeners(:accordion_section_expanded, accordion_section_number)
|
3265
|
-
end
|
3164
|
+
option :title
|
3266
3165
|
|
3267
|
-
|
3268
|
-
|
3269
|
-
|
3270
|
-
|
3166
|
+
attr_reader :presenter
|
3167
|
+
|
3168
|
+
before_render do
|
3169
|
+
@presenter = Presenter.new
|
3170
|
+
end
|
3171
|
+
|
3172
|
+
markup {
|
3173
|
+
section {
|
3174
|
+
# Unidirectionally data-bind the class inclusion of 'collapsed' to the @presenter.collapsed boolean attribute,
|
3175
|
+
# meaning if @presenter.collapsed changes to true, the CSS class 'collapsed' is included on the element,
|
3176
|
+
# and if it changes to false, the CSS class 'collapsed' is removed from the element.
|
3177
|
+
class_name(:collapsed) <= [@presenter, :collapsed]
|
3178
|
+
class_name(:instant_transition) <= [@presenter, :instant_transition]
|
3179
|
+
|
3180
|
+
header(title, class: 'accordion-section-title') {
|
3181
|
+
onclick do |event|
|
3182
|
+
@presenter.toggle_collapsed
|
3183
|
+
if @presenter.collapsed
|
3184
|
+
notify_listeners(:collapsed)
|
3185
|
+
else
|
3186
|
+
notify_listeners(:expanded)
|
3271
3187
|
end
|
3272
3188
|
end
|
3273
3189
|
}
|
3190
|
+
|
3191
|
+
div(slot: :section_content, class: 'accordion-section-content')
|
3274
3192
|
}
|
3275
|
-
|
3193
|
+
}
|
3194
|
+
|
3195
|
+
style {
|
3196
|
+
r('.accordion-section-title') {
|
3197
|
+
font_size 2.em
|
3198
|
+
font_weight :bold
|
3199
|
+
cursor :pointer
|
3200
|
+
padding_left 20
|
3201
|
+
position :relative
|
3202
|
+
margin_block_start 0.33.em
|
3203
|
+
margin_block_end 0.33.em
|
3204
|
+
}
|
3205
|
+
|
3206
|
+
r('.accordion-section-title::before') {
|
3207
|
+
content '"▼"'
|
3208
|
+
position :absolute
|
3209
|
+
font_size 0.5.em
|
3210
|
+
top 10
|
3211
|
+
left 0
|
3212
|
+
}
|
3213
|
+
|
3214
|
+
r('.accordion-section-content') {
|
3215
|
+
height 246
|
3216
|
+
overflow :hidden
|
3217
|
+
transition 'height 0.5s linear'
|
3218
|
+
}
|
3219
|
+
|
3220
|
+
r("#{component_element_selector}.instant_transition .accordion-section-content") {
|
3221
|
+
transition 'initial'
|
3222
|
+
}
|
3223
|
+
|
3224
|
+
r("#{component_element_selector}.collapsed .accordion-section-title::before") {
|
3225
|
+
content '"►"'
|
3226
|
+
}
|
3227
|
+
|
3228
|
+
r("#{component_element_selector}.collapsed .accordion-section-content") {
|
3229
|
+
height 0
|
3230
|
+
}
|
3231
|
+
}
|
3276
3232
|
end
|
3277
3233
|
|
3278
|
-
|
3279
|
-
|
3280
|
-
|
3281
|
-
|
3282
|
-
|
3283
|
-
|
3284
|
-
|
3285
|
-
|
3286
|
-
|
3287
|
-
|
3288
|
-
|
3234
|
+
class Accordion
|
3235
|
+
include Glimmer::Web::Component
|
3236
|
+
|
3237
|
+
events :accordion_section_expanded, :accordion_section_collapsed
|
3238
|
+
|
3239
|
+
markup {
|
3240
|
+
# given that no slots are specified, nesting content under the accordion component
|
3241
|
+
# in consumer code adds content directly inside the markup root div.
|
3242
|
+
div { |accordion|
|
3243
|
+
# on render, all accordion sections would have been added by consumers already, so we can
|
3244
|
+
# attach listeners to all of them by re-opening their content with `.content { ... }` block
|
3245
|
+
on_render do
|
3246
|
+
accordion_section_elements = accordion.children
|
3247
|
+
accordion_sections = accordion_section_elements.map(&:component)
|
3248
|
+
accordion_sections.each_with_index do |accordion_section, index|
|
3249
|
+
accordion_section_number = index + 1
|
3250
|
+
|
3251
|
+
# ensure only the first section is expanded
|
3252
|
+
accordion_section.presenter.collapse(instant: true) if accordion_section_number != 1
|
3253
|
+
|
3254
|
+
accordion_section.content {
|
3255
|
+
on_expanded do
|
3256
|
+
other_accordion_sections = accordion_sections.reject {|other_accordion_section| other_accordion_section == accordion_section }
|
3257
|
+
other_accordion_sections.each { |other_accordion_section| other_accordion_section.presenter.collapse }
|
3258
|
+
notify_listeners(:accordion_section_expanded, accordion_section_number)
|
3259
|
+
end
|
3260
|
+
|
3261
|
+
on_collapsed do
|
3262
|
+
notify_listeners(:accordion_section_collapsed, accordion_section_number)
|
3263
|
+
end
|
3264
|
+
}
|
3265
|
+
end
|
3289
3266
|
end
|
3290
|
-
|
3291
|
-
|
3292
|
-
|
3267
|
+
}
|
3268
|
+
}
|
3269
|
+
end
|
3270
|
+
|
3271
|
+
# HelloComponentListeners Glimmer Web Component (View component)
|
3272
|
+
#
|
3273
|
+
# This View component represents the main page being rendered,
|
3274
|
+
# as done by its `render` class method below
|
3275
|
+
class HelloComponentListeners
|
3276
|
+
class Presenter
|
3277
|
+
attr_accessor :status_message
|
3293
3278
|
|
3294
|
-
|
3295
|
-
@
|
3296
|
-
@shipping_address = Address.new(
|
3297
|
-
full_name: 'Johnny Doe',
|
3298
|
-
street: '3922 Park Ave',
|
3299
|
-
street2: 'PO BOX 8382',
|
3300
|
-
city: 'San Diego',
|
3301
|
-
state: 'California',
|
3302
|
-
zip_code: '91913',
|
3303
|
-
)
|
3304
|
-
@billing_address = Address.new(
|
3305
|
-
full_name: 'John C Doe',
|
3306
|
-
street: '123 Main St',
|
3307
|
-
street2: 'Apartment 3C',
|
3308
|
-
city: 'San Diego',
|
3309
|
-
state: 'California',
|
3310
|
-
zip_code: '91911',
|
3311
|
-
)
|
3312
|
-
@emergency_address = Address.new(
|
3313
|
-
full_name: 'Mary Doe',
|
3314
|
-
street: '2038 Ipswitch St',
|
3315
|
-
street2: 'Suite 300',
|
3316
|
-
city: 'San Diego',
|
3317
|
-
state: 'California',
|
3318
|
-
zip_code: '91912',
|
3319
|
-
)
|
3279
|
+
def initialize
|
3280
|
+
@status_message = "Accordion section 1 is expanded!"
|
3320
3281
|
end
|
3321
|
-
|
3322
|
-
|
3323
|
-
|
3324
|
-
|
3325
|
-
|
3326
|
-
|
3327
|
-
|
3328
|
-
|
3329
|
-
|
3330
|
-
|
3331
|
-
|
3332
|
-
|
3333
|
-
|
3334
|
-
|
3335
|
-
|
3336
|
-
|
3337
|
-
|
3338
|
-
|
3339
|
-
|
3340
|
-
|
3341
|
-
|
3342
|
-
|
3343
|
-
|
3344
|
-
|
3282
|
+
end
|
3283
|
+
|
3284
|
+
include Glimmer::Web::Component
|
3285
|
+
|
3286
|
+
before_render do
|
3287
|
+
@presenter = Presenter.new
|
3288
|
+
@shipping_address = Address.new(
|
3289
|
+
full_name: 'Johnny Doe',
|
3290
|
+
street: '3922 Park Ave',
|
3291
|
+
street2: 'PO BOX 8382',
|
3292
|
+
city: 'San Diego',
|
3293
|
+
state: 'California',
|
3294
|
+
zip_code: '91913',
|
3295
|
+
)
|
3296
|
+
@billing_address = Address.new(
|
3297
|
+
full_name: 'John C Doe',
|
3298
|
+
street: '123 Main St',
|
3299
|
+
street2: 'Apartment 3C',
|
3300
|
+
city: 'San Diego',
|
3301
|
+
state: 'California',
|
3302
|
+
zip_code: '91911',
|
3303
|
+
)
|
3304
|
+
@emergency_address = Address.new(
|
3305
|
+
full_name: 'Mary Doe',
|
3306
|
+
street: '2038 Ipswitch St',
|
3307
|
+
street2: 'Suite 300',
|
3308
|
+
city: 'San Diego',
|
3309
|
+
state: 'California',
|
3310
|
+
zip_code: '91912',
|
3311
|
+
)
|
3312
|
+
end
|
3313
|
+
|
3314
|
+
markup {
|
3315
|
+
div {
|
3316
|
+
h1(style: {font_style: :italic}) {
|
3317
|
+
inner_html <= [@presenter, :status_message]
|
3318
|
+
}
|
3319
|
+
|
3320
|
+
accordion { # any content nested under component directly is added under its markup root div element
|
3321
|
+
accordion_section(title: 'Shipping Address') {
|
3322
|
+
section_content { # contribute elements to section_content slot declared in AccordionSection component
|
3323
|
+
address_form(address: @shipping_address)
|
3345
3324
|
}
|
3346
|
-
|
3347
|
-
|
3348
|
-
|
3349
|
-
|
3325
|
+
}
|
3326
|
+
|
3327
|
+
accordion_section(title: 'Billing Address') {
|
3328
|
+
section_content {
|
3329
|
+
address_form(address: @billing_address)
|
3350
3330
|
}
|
3351
|
-
|
3352
|
-
|
3353
|
-
|
3331
|
+
}
|
3332
|
+
|
3333
|
+
accordion_section(title: 'Emergency Address') {
|
3334
|
+
section_content {
|
3335
|
+
address_form(address: @emergency_address)
|
3354
3336
|
}
|
3355
3337
|
}
|
3338
|
+
|
3339
|
+
# on_accordion_section_expanded listener matches event :accordion_section_expanded declared in Accordion component
|
3340
|
+
on_accordion_section_expanded { |accordion_section_number|
|
3341
|
+
@presenter.status_message = "Accordion section #{accordion_section_number} is expanded!"
|
3342
|
+
}
|
3343
|
+
|
3344
|
+
on_accordion_section_collapsed { |accordion_section_number|
|
3345
|
+
@presenter.status_message = "Accordion section #{accordion_section_number} is collapsed!"
|
3346
|
+
}
|
3356
3347
|
}
|
3357
3348
|
}
|
3358
|
-
|
3349
|
+
}
|
3359
3350
|
end
|
3360
3351
|
|
3361
3352
|
Document.ready? do
|
@@ -3768,59 +3759,55 @@ Glimmer HTML DSL Ruby code in the frontend:
|
|
3768
3759
|
```ruby
|
3769
3760
|
require 'glimmer-dsl-web'
|
3770
3761
|
|
3771
|
-
|
3772
|
-
|
3773
|
-
|
3774
|
-
|
3775
|
-
|
3776
|
-
|
3777
|
-
|
3778
|
-
|
3779
|
-
|
3780
|
-
|
3781
|
-
|
3782
|
-
|
3783
|
-
|
3784
|
-
|
3785
|
-
|
3786
|
-
|
3787
|
-
|
3788
|
-
|
3789
|
-
value <=> [self, :selected_address_type]
|
3790
|
-
}
|
3762
|
+
class AddressTypeSelector
|
3763
|
+
include Glimmer::Web::Component
|
3764
|
+
|
3765
|
+
attribute :address_types, default: []
|
3766
|
+
attribute :selected_address_type
|
3767
|
+
|
3768
|
+
before_render do
|
3769
|
+
self.selected_address_type ||= address_types.first
|
3770
|
+
end
|
3771
|
+
|
3772
|
+
markup {
|
3773
|
+
select { |select_element|
|
3774
|
+
address_types.each do |address_type|
|
3775
|
+
option(value: address_type) { address_type }
|
3776
|
+
end
|
3777
|
+
|
3778
|
+
# Bidirectionally data-bind select value to selected_address_type attribute on self (component)
|
3779
|
+
value <=> [self, :selected_address_type]
|
3791
3780
|
}
|
3792
|
-
|
3793
|
-
|
3794
|
-
|
3795
|
-
|
3796
|
-
|
3797
|
-
|
3781
|
+
}
|
3782
|
+
|
3783
|
+
style {
|
3784
|
+
r(component_element_selector) {
|
3785
|
+
font_size 2.em
|
3786
|
+
margin_left 5.px
|
3798
3787
|
}
|
3799
|
-
|
3788
|
+
}
|
3800
3789
|
end
|
3801
3790
|
|
3802
|
-
|
3803
|
-
|
3804
|
-
|
3805
|
-
|
3806
|
-
|
3807
|
-
|
3808
|
-
|
3809
|
-
|
3810
|
-
|
3811
|
-
|
3812
|
-
|
3813
|
-
|
3814
|
-
|
3815
|
-
|
3816
|
-
|
3817
|
-
|
3818
|
-
|
3819
|
-
end
|
3820
|
-
}
|
3791
|
+
class AddressTypeSelectorPage
|
3792
|
+
include Glimmer::Web::Component
|
3793
|
+
|
3794
|
+
markup {
|
3795
|
+
div {
|
3796
|
+
h1('Address type for delivery:', style: {display: :inline})
|
3797
|
+
|
3798
|
+
address_type_selector(address_types: ['Home', 'Work', 'Other']) {
|
3799
|
+
# We can listen to the updates of any attribute/option in a Glimmer Web Component
|
3800
|
+
# on_{attribute_name}_update do execute code when component attribute/option with attribute_name is updated
|
3801
|
+
# This is an alternative to using Component Listeners, which require that the component explicitly calls notify_listeners,
|
3802
|
+
# whereas Component Attribute Listeners get tracked automatically, but depend on a specific attribute
|
3803
|
+
# The trade-off is Component Listeners provide more flexibility when needed as they are not bound to specific attributes,
|
3804
|
+
# but often Component Attribute Listeners are good enough as a solution for certain problems.
|
3805
|
+
on_selected_address_type_update do |address_type|
|
3806
|
+
$$.alert("You selected the address type: #{address_type}")
|
3807
|
+
end
|
3821
3808
|
}
|
3822
3809
|
}
|
3823
|
-
|
3810
|
+
}
|
3824
3811
|
end
|
3825
3812
|
|
3826
3813
|
Document.ready? do
|
@@ -3836,6 +3823,115 @@ Screenshot:
|
|
3836
3823
|
|
3837
3824
|

|
3838
3825
|
|
3826
|
+
#### Hello, Component Attribute Data-Binding!
|
3827
|
+
|
3828
|
+
[lib/glimmer-dsl-web/samples/hello/hello_component_attribute_data_binding.rb](/lib/glimmer-dsl-web/samples/hello/hello_component_attribute_data_binding.rb)
|
3829
|
+
|
3830
|
+
```ruby
|
3831
|
+
require 'glimmer-dsl-web'
|
3832
|
+
|
3833
|
+
class AddressTypePresenter
|
3834
|
+
attr_accessor :address_types, :selected_address_type
|
3835
|
+
|
3836
|
+
def initialize(address_types:, selected_address_type: nil)
|
3837
|
+
@address_types = address_types
|
3838
|
+
@selected_address_type = selected_address_type
|
3839
|
+
@selected_address_type ||= address_types.first
|
3840
|
+
end
|
3841
|
+
end
|
3842
|
+
|
3843
|
+
class AddressTypeSelect
|
3844
|
+
include Glimmer::Web::Component
|
3845
|
+
|
3846
|
+
attribute :address_types, default: []
|
3847
|
+
attribute :selected_address_type
|
3848
|
+
|
3849
|
+
markup {
|
3850
|
+
select { |select_element|
|
3851
|
+
address_types.each do |address_type|
|
3852
|
+
option(value: address_type) { address_type }
|
3853
|
+
end
|
3854
|
+
|
3855
|
+
value <=> [self, :selected_address_type]
|
3856
|
+
}
|
3857
|
+
}
|
3858
|
+
|
3859
|
+
style {
|
3860
|
+
r(component_element_selector) {
|
3861
|
+
font_size 2.em
|
3862
|
+
margin_left 5.px
|
3863
|
+
}
|
3864
|
+
}
|
3865
|
+
end
|
3866
|
+
|
3867
|
+
class AddressTypeRadioGroup
|
3868
|
+
include Glimmer::Web::Component
|
3869
|
+
|
3870
|
+
attribute :address_types, default: []
|
3871
|
+
attribute :selected_address_type
|
3872
|
+
|
3873
|
+
markup {
|
3874
|
+
div { |select_element|
|
3875
|
+
address_types.each do |address_type|
|
3876
|
+
input_id = "radio_address_type_#{address_type}"
|
3877
|
+
input(id: input_id, type: 'radio', name: 'radio_address_type', value: address_type) {
|
3878
|
+
checked <=> [self, :selected_address_type,
|
3879
|
+
on_read: ->(address_type_string_value) { address_type_string_value == address_type },
|
3880
|
+
on_write: ->(radio_input_boolean_value) { radio_input_boolean_value ? address_type : selected_address_type },
|
3881
|
+
]
|
3882
|
+
}
|
3883
|
+
label(for: input_id) { address_type }
|
3884
|
+
end
|
3885
|
+
}
|
3886
|
+
}
|
3887
|
+
|
3888
|
+
style {
|
3889
|
+
r(component_element_selector) {
|
3890
|
+
font_size 2.em
|
3891
|
+
margin_top 10.px
|
3892
|
+
}
|
3893
|
+
r("#{component_element_selector} label") {
|
3894
|
+
margin_left 2.px
|
3895
|
+
margin_right 10.px
|
3896
|
+
}
|
3897
|
+
}
|
3898
|
+
end
|
3899
|
+
|
3900
|
+
class AddressTypeComponentAttributeDataBindingPage
|
3901
|
+
include Glimmer::Web::Component
|
3902
|
+
|
3903
|
+
before_render do
|
3904
|
+
@presenter = AddressTypePresenter.new(address_types: ['Home', 'Work', 'Other'])
|
3905
|
+
end
|
3906
|
+
|
3907
|
+
markup {
|
3908
|
+
div {
|
3909
|
+
h1('Address type for delivery:')
|
3910
|
+
|
3911
|
+
address_type_select(address_types: @presenter.address_types, selected_address_type: @presenter.selected_address_type) {
|
3912
|
+
# Not only can we data-bind attributes on basic HTML elements,
|
3913
|
+
# but we can also data-bind custom attributes on Glimmer Web Components
|
3914
|
+
selected_address_type <=> [@presenter, :selected_address_type]
|
3915
|
+
}
|
3916
|
+
|
3917
|
+
address_type_radio_group(address_types: @presenter.address_types, selected_address_type: @presenter.selected_address_type) {
|
3918
|
+
# Not only can we data-bind attributes on basic HTML elements,
|
3919
|
+
# but we can also data-bind custom attributes on Glimmer Web Components
|
3920
|
+
selected_address_type <=> [@presenter, :selected_address_type]
|
3921
|
+
}
|
3922
|
+
}
|
3923
|
+
}
|
3924
|
+
end
|
3925
|
+
|
3926
|
+
Document.ready? do
|
3927
|
+
AddressTypeComponentAttributeDataBindingPage.render
|
3928
|
+
end
|
3929
|
+
```
|
3930
|
+
|
3931
|
+
Screenshot:
|
3932
|
+
|
3933
|
+

|
3934
|
+
|
3839
3935
|
#### Hello, glimmer_component Rails Helper!
|
3840
3936
|
|
3841
3937
|
You may insert a Glimmer component anywhere into a Rails View using
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.7.
|
1
|
+
0.7.3
|
data/glimmer-dsl-web.gemspec
CHANGED
@@ -2,11 +2,11 @@
|
|
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.7.
|
5
|
+
# stub: glimmer-dsl-web 0.7.3 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "glimmer-dsl-web".freeze
|
9
|
-
s.version = "0.7.
|
9
|
+
s.version = "0.7.3".freeze
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
12
12
|
s.require_paths = ["lib".freeze]
|
@@ -33,6 +33,7 @@ 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_attribute_data_binding.rb",
|
36
37
|
"lib/glimmer-dsl-web/samples/hello/hello_component_attribute_listeners.rb",
|
37
38
|
"lib/glimmer-dsl-web/samples/hello/hello_component_listeners.rb",
|
38
39
|
"lib/glimmer-dsl-web/samples/hello/hello_component_listeners_default_slot.rb",
|
@@ -0,0 +1,127 @@
|
|
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?(:AddressTypePresenter)
|
25
|
+
class AddressTypePresenter
|
26
|
+
attr_accessor :address_types, :selected_address_type
|
27
|
+
|
28
|
+
def initialize(address_types:, selected_address_type: nil)
|
29
|
+
@address_types = address_types
|
30
|
+
@selected_address_type = selected_address_type
|
31
|
+
@selected_address_type ||= address_types.first
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
unless Object.const_defined?(:AddressTypeSelect)
|
37
|
+
class AddressTypeSelect
|
38
|
+
include Glimmer::Web::Component
|
39
|
+
|
40
|
+
attribute :address_types, default: []
|
41
|
+
attribute :selected_address_type
|
42
|
+
|
43
|
+
markup {
|
44
|
+
select { |select_element|
|
45
|
+
address_types.each do |address_type|
|
46
|
+
option(value: address_type) { address_type }
|
47
|
+
end
|
48
|
+
|
49
|
+
value <=> [self, :selected_address_type]
|
50
|
+
}
|
51
|
+
}
|
52
|
+
|
53
|
+
style {
|
54
|
+
r(component_element_selector) {
|
55
|
+
font_size 2.em
|
56
|
+
margin_left 5.px
|
57
|
+
}
|
58
|
+
}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
unless Object.const_defined?(:AddressTypeRadioGroup)
|
63
|
+
class AddressTypeRadioGroup
|
64
|
+
include Glimmer::Web::Component
|
65
|
+
|
66
|
+
attribute :address_types, default: []
|
67
|
+
attribute :selected_address_type
|
68
|
+
|
69
|
+
markup {
|
70
|
+
div { |select_element|
|
71
|
+
address_types.each do |address_type|
|
72
|
+
input_id = "radio_address_type_#{address_type}"
|
73
|
+
input(id: input_id, type: 'radio', name: 'radio_address_type', value: address_type) {
|
74
|
+
checked <=> [self, :selected_address_type,
|
75
|
+
on_read: ->(address_type_string_value) { address_type_string_value == address_type },
|
76
|
+
on_write: ->(radio_input_boolean_value) { radio_input_boolean_value ? address_type : selected_address_type },
|
77
|
+
]
|
78
|
+
}
|
79
|
+
label(for: input_id) { address_type }
|
80
|
+
end
|
81
|
+
}
|
82
|
+
}
|
83
|
+
|
84
|
+
style {
|
85
|
+
r(component_element_selector) {
|
86
|
+
font_size 2.em
|
87
|
+
margin_top 10.px
|
88
|
+
}
|
89
|
+
r("#{component_element_selector} label") {
|
90
|
+
margin_left 2.px
|
91
|
+
margin_right 10.px
|
92
|
+
}
|
93
|
+
}
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
unless Object.const_defined?(:AddressTypeComponentAttributeDataBindingPage)
|
98
|
+
class AddressTypeComponentAttributeDataBindingPage
|
99
|
+
include Glimmer::Web::Component
|
100
|
+
|
101
|
+
before_render do
|
102
|
+
@presenter = AddressTypePresenter.new(address_types: ['Home', 'Work', 'Other'])
|
103
|
+
end
|
104
|
+
|
105
|
+
markup {
|
106
|
+
div {
|
107
|
+
h1('Address type for delivery:')
|
108
|
+
|
109
|
+
address_type_select(address_types: @presenter.address_types, selected_address_type: @presenter.selected_address_type) {
|
110
|
+
# Not only can we data-bind attributes on basic HTML elements,
|
111
|
+
# but we can also data-bind custom attributes on Glimmer Web Components
|
112
|
+
selected_address_type <=> [@presenter, :selected_address_type]
|
113
|
+
}
|
114
|
+
|
115
|
+
address_type_radio_group(address_types: @presenter.address_types, selected_address_type: @presenter.selected_address_type) {
|
116
|
+
# Not only can we data-bind attributes on basic HTML elements,
|
117
|
+
# but we can also data-bind custom attributes on Glimmer Web Components
|
118
|
+
selected_address_type <=> [@presenter, :selected_address_type]
|
119
|
+
}
|
120
|
+
}
|
121
|
+
}
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
Document.ready? do
|
126
|
+
AddressTypeComponentAttributeDataBindingPage.render
|
127
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: glimmer-dsl-web
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.7.
|
4
|
+
version: 0.7.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andy Maleh
|
@@ -266,6 +266,7 @@ files:
|
|
266
266
|
- lib/glimmer-dsl-web/ext/kernel.rb
|
267
267
|
- lib/glimmer-dsl-web/samples/hello/hello_button.rb
|
268
268
|
- lib/glimmer-dsl-web/samples/hello/hello_component.rb
|
269
|
+
- lib/glimmer-dsl-web/samples/hello/hello_component_attribute_data_binding.rb
|
269
270
|
- lib/glimmer-dsl-web/samples/hello/hello_component_attribute_listeners.rb
|
270
271
|
- lib/glimmer-dsl-web/samples/hello/hello_component_listeners.rb
|
271
272
|
- lib/glimmer-dsl-web/samples/hello/hello_component_listeners_default_slot.rb
|