glimmer-dsl-web 0.6.0 → 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,6 +1,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.6.0 (Beta)
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.6.2 (Beta)
2
2
  ## Ruby-in-the-Browser Web Frontend Framework
3
- ### Finally, Ruby Developer Productivity, Happiness, and Fun in the Frontend!!!
3
+ ### The "Rails" of Frontend Frameworks!!!
4
+ #### Finally, Ruby Developer Productivity, Happiness, and Fun in the Frontend!!!
4
5
  [![Gem Version](https://badge.fury.io/rb/glimmer-dsl-web.svg)](http://badge.fury.io/rb/glimmer-dsl-web)
5
6
  [![Join the chat at https://gitter.im/AndyObtiva/glimmer](https://badges.gitter.im/AndyObtiva/glimmer.svg)](https://gitter.im/AndyObtiva/glimmer?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
6
7
 
@@ -12,9 +13,9 @@
12
13
 
13
14
  You can finally have Ruby developer happiness and productivity in the Frontend! No more wasting time splitting your resources across multiple languages, using badly engineered, over-engineered, or premature-optimization-obsessed JavaScript libraries, fighting JavaScript build issues (e.g. webpack), or rewriting Ruby Backend code in Frontend JavaScript. With [Ruby in the Browser](https://www.youtube.com/watch?v=4AdcfbI6A4c), you can have an exponential jump in development productivity (2x or higher), time-to-release (1/2 or less time), cost (1/2 or cheaper), and maintainability (~50% the code that is simpler and more readable) over JavaScript libraries like React, Angular, Ember, Vue, and Svelte, while being able to reuse Backend Ruby code as is in the Frontend for faster interactions when needed. Also, with Frontend Ruby, companies can cut their hiring budget in half by having Backend Ruby Software Engineers do Frontend Development in Ruby! [Ruby in the Browser](https://www.youtube.com/watch?v=4AdcfbI6A4c) finally fulfills every smart highly-productive Rubyist's dream by bringing Ruby productivity fun to Frontend Development, the same productivity fun you had for years and decades in Backend Development.
14
15
 
15
- [Glimmer](https://github.com/AndyObtiva/glimmer) DSL for Web enables building Web Frontends using [Ruby in the Browser](https://www.youtube.com/watch?v=4AdcfbI6A4c), as per [Matz's recommendation in his RubyConf 2022 keynote speech to replace JavaScript with Ruby](https://youtu.be/knutsgHTrfQ?t=789). It supports Rails' principle of the One Person Framework by not requiring any extra developers with JavaScript expertise, yet enabling Ruby (Backend) Software Engineers to develop the Frontend with Ruby code that is better than any JavaScript code produced by JS developers. It aims at providing the simplest, most intuitive, most straight-forward, and most productive frontend framework in existence. The framework follows the Ruby way (with [DSLs](https://martinfowler.com/books/dsl.html) and [TIMTOWTDI](https://en.wiktionary.org/wiki/TMTOWTDI#English)) and the Rails way ([Convention over Configuration](https://rubyonrails.org/doctrine)) in building Isomorphic Ruby on Rails Applications. It provides a Ruby [HTML DSL](#usage) (including full support for [SVG](#hello-svg)), which uniquely enables writing both structure code and logic code in one language. It supports both Unidirectional (One-Way) [Data-Binding](#hello-data-binding) (using `<=`) and Bidirectional (Two-Way) [Data-Binding](#hello-data-binding) (using `<=>`). Dynamic rendering (and re-rendering) of HTML content is also supported via [Content Data-Binding](#hello-content-data-binding). Modular design is supported with [Glimmer Web Components](#hello-component), [Component Slots](#hello-component-slots), and Component Custom Event Listeners. And, a Ruby CSS DSL is supported with the included [Glimmer DSL for CSS](https://github.com/AndyObtiva/glimmer-dsl-css). To automatically convert legacy HTML & CSS code to Glimmer DSL Ruby code, Software Engineers could use the included [`html_to_glimmer`](https://github.com/AndyObtiva/glimmer-dsl-xml#html-to-glimmer-converter) and [`css_to_glimmer`](https://github.com/AndyObtiva/glimmer-dsl-css#css-to-glimmer-converter) commands. Many [samples](#samples) are demonstrated in the [Rails sample app](https://github.com/AndyObtiva/sample-glimmer-dsl-web-rails7-app) (there is a very minimal [Standalone [No Rails] static site sample app](https://github.com/Largo/glimmer-dsl-web-standalone-demo) too). You can finally live in pure Rubyland on the Web in both the frontend and backend with [Glimmer DSL for Web](https://rubygems.org/gems/glimmer-dsl-web)!
16
+ [Glimmer](https://github.com/AndyObtiva/glimmer) DSL for Web enables building Web Frontends using [Ruby in the Browser](https://www.youtube.com/watch?v=4AdcfbI6A4c), as per [Matz's recommendation in his RubyConf 2022 keynote speech to replace JavaScript with Ruby](https://youtu.be/knutsgHTrfQ?t=789). It supports Rails' principle of the One Person Framework by not requiring any extra developers with JavaScript expertise, yet enabling Ruby (Backend) Software Engineers to develop the Frontend with Ruby code that is better than any JavaScript code produced by JS developers. It aims at providing the simplest, most intuitive, most straight-forward, and most productive frontend framework in existence. The framework follows the Ruby way (with [DSLs](https://martinfowler.com/books/dsl.html) and [TIMTOWTDI](https://en.wiktionary.org/wiki/TMTOWTDI#English)) and the Rails way ([Convention over Configuration](https://rubyonrails.org/doctrine)) in building Isomorphic Ruby on Rails Applications. It provides a Ruby [HTML DSL](#usage) (including full support for [SVG](#hello-svg)), which uniquely enables writing both structure code and logic code in one language. It supports both Unidirectional (One-Way) [Data-Binding](#hello-data-binding) (using `<=`) and Bidirectional (Two-Way) [Data-Binding](#hello-data-binding) (using `<=>`). Dynamic rendering (and re-rendering) of HTML content is also supported via [Content Data-Binding](#hello-content-data-binding). Modular design is supported with [Glimmer Web Components](#hello-component), [Component Slots](#hello-component-slots), and [Component Custom Event Listeners](#hello-component-listeners). And, a Ruby CSS DSL is supported with the included [Glimmer DSL for CSS](https://github.com/AndyObtiva/glimmer-dsl-css). To automatically convert legacy HTML & CSS code to Glimmer DSL Ruby code, Software Engineers could use the included [`html_to_glimmer`](https://github.com/AndyObtiva/glimmer-dsl-xml#html-to-glimmer-converter) and [`css_to_glimmer`](https://github.com/AndyObtiva/glimmer-dsl-css#css-to-glimmer-converter) commands. Many [samples](#samples) are demonstrated in the [Rails sample app](https://github.com/AndyObtiva/sample-glimmer-dsl-web-rails7-app) (there is a very minimal [Standalone [No Rails] static site sample app](https://github.com/Largo/glimmer-dsl-web-standalone-demo) too). You can finally live in pure Rubyland on the Web in both the frontend and backend with [Glimmer DSL for Web](https://rubygems.org/gems/glimmer-dsl-web)!
16
17
 
17
- [Glimmer DSL for Web](https://rubygems.org/gems/glimmer-dsl-web) aims to be a very simple Ruby-based drop-in replacement for your existing JavaScript Frontend library (e.g. React, Angular, Vue, Ember, Svelte) or your JavaScript Frontend layer in general. It does not change how your Frontend interacts with the Backend, meaning you can continue to write Rails Backend API endpoints as needed and make HTTP/Ajax requests or read data embedded in elements, but from [Ruby in the Browser](https://www.youtube.com/watch?v=4AdcfbI6A4c). Whatever is possible in JavaScript is possible when using Glimmer DSL for Web as it integrates with any existing JavaScript library. The [Rails sample app](https://github.com/AndyObtiva/sample-glimmer-dsl-web-rails7-app) demonstrates how to [make HTTP calls](https://github.com/AndyObtiva/sample-glimmer-dsl-web-rails7-app/blob/master/app/assets/opal/sample_selector/models/sample_api.rb) and how to [integrate with a JavaScript library](https://github.com/AndyObtiva/sample-glimmer-dsl-web-rails7-app/blob/master/app/views/layouts/application.html.erb) (highlightjs) that performs [code syntax highlighting](https://github.com/AndyObtiva/sample-glimmer-dsl-web-rails7-app/blob/master/app/assets/opal/sample_selector.rb).
18
+ [Glimmer DSL for Web](https://rubygems.org/gems/glimmer-dsl-web) aims to be a very simple Ruby-based drop-in replacement for your existing JavaScript Frontend library (e.g. React, Angular, Vue, Ember, Svelte) or your JavaScript Frontend layer in general. It does not change how your Frontend interacts with the Backend, meaning you can continue to write Rails Backend API endpoints as needed and make HTTP/Ajax requests or read data embedded in elements, but from [Ruby in the Browser](https://www.youtube.com/watch?v=4AdcfbI6A4c). Whatever is possible in JavaScript is possible when using Glimmer DSL for Web as it integrates with any existing JavaScript library. The [Rails sample app](https://github.com/AndyObtiva/sample-glimmer-dsl-web-rails7-app) demonstrates how to [make HTTP calls](https://github.com/AndyObtiva/sample-glimmer-dsl-web-rails7-app/blob/master/app/assets/opal/sample_selector/models/sample_api.rb) and how to [integrate with a JavaScript library](https://github.com/AndyObtiva/sample-glimmer-dsl-web-rails7-app/blob/master/app/views/layouts/application.html.erb) (highlightjs) that performs [code syntax highlighting](https://github.com/AndyObtiva/sample-glimmer-dsl-web-rails7-app/blob/master/app/assets/opal/sample_selector.rb). [Glimmer DSL for Web](https://rubygems.org/gems/glimmer-dsl-web) currently runs on [Opal](https://opalrb.com/) ([Fukuoka Ruby 2023 Award Winner](https://www.digitalfukuoka.jp/topics/228?locale=ja)), a Ruby-to-JavaScript transpiler. In the future, it might support other Frontend Ruby environments, such as [ruby.wasm](https://github.com/ruby/ruby.wasm).
18
19
 
19
20
  After looking through the [samples](#samples) below, read the [FAQ (Frequently Asked Questions)](#faq) to learn more about how Glimmer DSL for Web compares to other approaches/libraries like Hotwire (Turbo), Phlex, ViewComponent, Angular, Vue, React, Svelte, and other JS frameworks.
20
21
 
@@ -1344,6 +1345,8 @@ Learn more about the differences between various [Glimmer](https://github.com/An
1344
1345
  - [Hello, Content Data-Binding!](#hello-content-data-binding)
1345
1346
  - [Hello, Component!](#hello-compoent)
1346
1347
  - [Hello, Component Slots!](#hello-component-slots)
1348
+ - [Hello, Component Listeners!](#hello-compoent-listeners)
1349
+ - [Hello, Component Listeners (Default Slot)!](#hello-compoent-listeners-default-slot)
1347
1350
  - [Hello, glimmer_component Rails Helper!](#hello-glimmer_component-rails-helper)
1348
1351
  - [Hello, Paragraph!](#hello-paragraph)
1349
1352
  - [Hello, Style!](#hello-style)
@@ -1366,7 +1369,7 @@ Learn more about the differences between various [Glimmer](https://github.com/An
1366
1369
 
1367
1370
  ## Prerequisites
1368
1371
 
1369
- [Glimmer DSL for Web](https://rubygems.org/gems/glimmer-dsl-web) will begin by supporting [Opal Ruby](https://opalrb.com/) on [Rails](https://rubyonrails.org/). [Opal](https://opalrb.com/) is a lightweight Ruby to JavaScript transpiler that results in small downloadables compared to WASM. In the future, the project might grow to support [Ruby WASM](https://github.com/ruby/ruby.wasm) as an alternative to [Opal Ruby](https://opalrb.com/) that could be switched to with a simple configuration change.
1372
+ [Glimmer DSL for Web](https://rubygems.org/gems/glimmer-dsl-web) will begin by supporting [Opal Ruby](https://opalrb.com/) on [Rails](https://rubyonrails.org/). [Opal](https://opalrb.com/) ([Fukuoka Ruby 2023 Award Winner](https://www.digitalfukuoka.jp/topics/228?locale=ja)) is a lightweight Ruby to JavaScript transpiler that results in small downloadables compared to WASM. In the future, the project might grow to support [Ruby WASM](https://github.com/ruby/ruby.wasm) as an alternative to [Opal Ruby](https://opalrb.com/) that could be switched to with a simple configuration change.
1370
1373
 
1371
1374
  - Ruby 3.0+
1372
1375
  - Rails 7: [https://github.com/rails/rails](https://github.com/rails/rails)
@@ -1400,7 +1403,7 @@ rails new glimmer_app_server
1400
1403
  Add the following to `Gemfile`:
1401
1404
 
1402
1405
  ```
1403
- gem 'glimmer-dsl-web', '~> 0.6.0'
1406
+ gem 'glimmer-dsl-web', '~> 0.6.2'
1404
1407
  ```
1405
1408
 
1406
1409
  Run:
@@ -1629,7 +1632,7 @@ Disable the `webpacker` gem line in `Gemfile`:
1629
1632
  Add the following to `Gemfile`:
1630
1633
 
1631
1634
  ```ruby
1632
- gem 'glimmer-dsl-web', '~> 0.6.0'
1635
+ gem 'glimmer-dsl-web', '~> 0.6.2'
1633
1636
  ```
1634
1637
 
1635
1638
  Run:
@@ -2867,7 +2870,11 @@ Component slots enables consumers of components to contribute content in designa
2867
2870
  Inside a Glimmer web component, you can designate an element (e.g. `div`) as a component slot by passing the `slot: :slotname` option,
2868
2871
  which enables consumers to contribute elements inside the component slot by opening a `slotname {...}` inside the component content block.
2869
2872
 
2870
- For example, below is a sample that demonstrates how to contribute slot content to `address_header` and `address_footer` in an `address_form` component.
2873
+ If you want content that is added to a component block direclty added to a specific slot by default, you can specify a `default_slot :slot_name`.
2874
+ In that case, if you ever want to add content to the component markup root element, there is a slot that is always available for that, called `:markup_root_slot`.
2875
+ The default slot feature is demonstrated in the [Hello, Component Listeners (Default Slot)!](#hello-component-listeners-default-slot) sample.
2876
+
2877
+ For an example of using component slots, below is a sample that demonstrates how to contribute slot content to `address_header` and `address_footer` in an `address_form` component.
2871
2878
 
2872
2879
  [lib/glimmer-dsl-web/samples/hello/hello_component_slots.rb](/lib/glimmer-dsl-web/samples/hello/hello_component_slots.rb)
2873
2880
 
@@ -3087,6 +3094,783 @@ Screenshot:
3087
3094
 
3088
3095
  ![Hello, Component Slots!](/images/glimmer-dsl-web-samples-hello-hello-component-slots.png)
3089
3096
 
3097
+ #### Hello, Component Listeners!
3098
+
3099
+ Component listeners enable consumers of components to listen to any custom events generated by components. Component supported events are declared with class method invocations `events :eventname1, :eventname2, ...` or `event :eventname`. Consumers can listen to those events by simply nesting `on_eventname do; ... end` matching the name of the declared event inside the component.
3100
+
3101
+ For example, an `AccordionSection` component might generate events `:expanded` and `:collapsed` when a user clicks on the section title to expand it or collapse it. Consumers can then use `on_expanded` and `on_collapsed` to listen to those events.
3102
+
3103
+ [lib/glimmer-dsl-web/samples/hello/hello_component_listeners.rb](/lib/glimmer-dsl-web/samples/hello/hello_component_listeners.rb)
3104
+
3105
+ Glimmer HTML DSL Ruby code in the frontend:
3106
+
3107
+ ```ruby
3108
+ require 'glimmer-dsl-web'
3109
+
3110
+ unless Object.const_defined?(:Address)
3111
+ Address = Struct.new(:full_name, :street, :street2, :city, :state, :zip_code, keyword_init: true) do
3112
+ STATES = {
3113
+ "AK"=>"Alaska",
3114
+ "AL"=>"Alabama",
3115
+ "AR"=>"Arkansas",
3116
+ "AS"=>"American Samoa",
3117
+ "AZ"=>"Arizona",
3118
+ "CA"=>"California",
3119
+ "CO"=>"Colorado",
3120
+ "CT"=>"Connecticut",
3121
+ "DC"=>"District of Columbia",
3122
+ "DE"=>"Delaware",
3123
+ "FL"=>"Florida",
3124
+ "GA"=>"Georgia",
3125
+ "GU"=>"Guam",
3126
+ "HI"=>"Hawaii",
3127
+ "IA"=>"Iowa",
3128
+ "ID"=>"Idaho",
3129
+ "IL"=>"Illinois",
3130
+ "IN"=>"Indiana",
3131
+ "KS"=>"Kansas",
3132
+ "KY"=>"Kentucky",
3133
+ "LA"=>"Louisiana",
3134
+ "MA"=>"Massachusetts",
3135
+ "MD"=>"Maryland",
3136
+ "ME"=>"Maine",
3137
+ "MI"=>"Michigan",
3138
+ "MN"=>"Minnesota",
3139
+ "MO"=>"Missouri",
3140
+ "MS"=>"Mississippi",
3141
+ "MT"=>"Montana",
3142
+ "NC"=>"North Carolina",
3143
+ "ND"=>"North Dakota",
3144
+ "NE"=>"Nebraska",
3145
+ "NH"=>"New Hampshire",
3146
+ "NJ"=>"New Jersey",
3147
+ "NM"=>"New Mexico",
3148
+ "NV"=>"Nevada",
3149
+ "NY"=>"New York",
3150
+ "OH"=>"Ohio",
3151
+ "OK"=>"Oklahoma",
3152
+ "OR"=>"Oregon",
3153
+ "PA"=>"Pennsylvania",
3154
+ "PR"=>"Puerto Rico",
3155
+ "RI"=>"Rhode Island",
3156
+ "SC"=>"South Carolina",
3157
+ "SD"=>"South Dakota",
3158
+ "TN"=>"Tennessee",
3159
+ "TX"=>"Texas",
3160
+ "UT"=>"Utah",
3161
+ "VA"=>"Virginia",
3162
+ "VI"=>"Virgin Islands",
3163
+ "VT"=>"Vermont",
3164
+ "WA"=>"Washington",
3165
+ "WI"=>"Wisconsin",
3166
+ "WV"=>"West Virginia",
3167
+ "WY"=>"Wyoming"
3168
+ }
3169
+
3170
+ def state_code
3171
+ STATES.invert[state]
3172
+ end
3173
+
3174
+ def state_code=(value)
3175
+ self.state = STATES[value]
3176
+ end
3177
+
3178
+ def summary
3179
+ to_h.values.map(&:to_s).reject(&:empty?).join(', ')
3180
+ end
3181
+ end
3182
+ end
3183
+
3184
+ unless Object.const_defined?(:AddressForm)
3185
+ # AddressForm Glimmer Web Component (View component)
3186
+ #
3187
+ # Including Glimmer::Web::Component makes this class a View component and automatically
3188
+ # generates a new Glimmer HTML DSL keyword that matches the lowercase underscored version
3189
+ # of the name of the class. AddressForm generates address_form keyword, which can be used
3190
+ # elsewhere in Glimmer HTML DSL code as done inside HelloComponentListeners below.
3191
+ class AddressForm
3192
+ include Glimmer::Web::Component
3193
+
3194
+ option :address
3195
+
3196
+ markup {
3197
+ div {
3198
+ div(style: {display: :grid, grid_auto_columns: '80px 260px'}) { |address_div|
3199
+ label('Full Name: ', for: 'full-name-field')
3200
+ input(id: 'full-name-field') {
3201
+ value <=> [address, :full_name]
3202
+ }
3203
+
3204
+ label('Street: ', for: 'street-field')
3205
+ input(id: 'street-field') {
3206
+ value <=> [address, :street]
3207
+ }
3208
+
3209
+ label('Street 2: ', for: 'street2-field')
3210
+ textarea(id: 'street2-field') {
3211
+ value <=> [address, :street2]
3212
+ }
3213
+
3214
+ label('City: ', for: 'city-field')
3215
+ input(id: 'city-field') {
3216
+ value <=> [address, :city]
3217
+ }
3218
+
3219
+ label('State: ', for: 'state-field')
3220
+ select(id: 'state-field') {
3221
+ Address::STATES.each do |state_code, state|
3222
+ option(value: state_code) { state }
3223
+ end
3224
+
3225
+ value <=> [address, :state_code]
3226
+ }
3227
+
3228
+ label('Zip Code: ', for: 'zip-code-field')
3229
+ input(id: 'zip-code-field', type: 'number', min: '0', max: '99999') {
3230
+ value <=> [address, :zip_code,
3231
+ on_write: :to_s,
3232
+ ]
3233
+ }
3234
+
3235
+ style {
3236
+ r("#{address_div.selector} *") {
3237
+ margin '5px'
3238
+ }
3239
+ r("#{address_div.selector} input, #{address_div.selector} select") {
3240
+ grid_column '2'
3241
+ }
3242
+ }
3243
+ }
3244
+
3245
+ div(style: {margin: 5}) {
3246
+ inner_text <= [address, :summary,
3247
+ computed_by: address.members + ['state_code'],
3248
+ ]
3249
+ }
3250
+ }
3251
+ }
3252
+ end
3253
+ end
3254
+
3255
+ unless Object.const_defined?(:AccordionSection)
3256
+ class AccordionSection
3257
+ class Presenter
3258
+ attr_accessor :collapsed, :instant_transition
3259
+
3260
+ def toggle_collapsed(instant: false)
3261
+ self.instant_transition = instant
3262
+ self.collapsed = !collapsed
3263
+ end
3264
+
3265
+ def expand(instant: false)
3266
+ self.instant_transition = instant
3267
+ self.collapsed = false
3268
+ end
3269
+
3270
+ def collapse(instant: false)
3271
+ self.instant_transition = instant
3272
+ self.collapsed = true
3273
+ end
3274
+ end
3275
+
3276
+ include Glimmer::Web::Component
3277
+
3278
+ events :expanded, :collapsed
3279
+
3280
+ option :title
3281
+
3282
+ attr_reader :presenter
3283
+
3284
+ before_render do
3285
+ @presenter = Presenter.new
3286
+ end
3287
+
3288
+ markup {
3289
+ section {
3290
+ # Unidirectionally data-bind the class inclusion of 'collapsed' to the @presenter.collapsed boolean attribute,
3291
+ # meaning if @presenter.collapsed changes to true, the CSS class 'collapsed' is included on the element,
3292
+ # and if it changes to false, the CSS class 'collapsed' is removed from the element.
3293
+ class_name(:collapsed) <= [@presenter, :collapsed]
3294
+ class_name(:instant_transition) <= [@presenter, :instant_transition]
3295
+
3296
+ header(title, class: 'accordion-section-title') {
3297
+ onclick do |event|
3298
+ @presenter.toggle_collapsed
3299
+ if @presenter.collapsed
3300
+ notify_listeners(:collapsed)
3301
+ else
3302
+ notify_listeners(:expanded)
3303
+ end
3304
+ end
3305
+ }
3306
+
3307
+ div(slot: :section_content, class: 'accordion-section-content')
3308
+ }
3309
+ }
3310
+
3311
+ style {
3312
+ r('.accordion-section-title') {
3313
+ font_size 2.em
3314
+ font_weight :bold
3315
+ cursor :pointer
3316
+ padding_left 20
3317
+ position :relative
3318
+ margin_block_start 0.33.em
3319
+ margin_block_end 0.33.em
3320
+ }
3321
+
3322
+ r('.accordion-section-title::before') {
3323
+ content '"▼"'
3324
+ position :absolute
3325
+ font_size 0.5.em
3326
+ top 10
3327
+ left 0
3328
+ }
3329
+
3330
+ r('.accordion-section-content') {
3331
+ height 246
3332
+ overflow :hidden
3333
+ transition 'height 0.5s linear'
3334
+ }
3335
+
3336
+ r("#{component_element_selector}.instant_transition .accordion-section-content") {
3337
+ transition 'initial'
3338
+ }
3339
+
3340
+ r("#{component_element_selector}.collapsed .accordion-section-title::before") {
3341
+ content '"►"'
3342
+ }
3343
+
3344
+ r("#{component_element_selector}.collapsed .accordion-section-content") {
3345
+ height 0
3346
+ }
3347
+ }
3348
+ end
3349
+ end
3350
+
3351
+ unless Object.const_defined?(:Accordion)
3352
+ class Accordion
3353
+ include Glimmer::Web::Component
3354
+
3355
+ events :accordion_section_expanded, :accordion_section_collapsed
3356
+
3357
+ markup {
3358
+ # given that no slots are specified, nesting content under the accordion component
3359
+ # in consumer code adds content directly inside the markup root div.
3360
+ div { |accordion|
3361
+ # on render, all accordion sections would have been added by consumers already, so we can
3362
+ # attach listeners to all of them by re-opening their content with `.content { ... }` block
3363
+ on_render do
3364
+ accordion_section_elements = accordion.children
3365
+ accordion_sections = accordion_section_elements.map(&:component)
3366
+ accordion_sections.each_with_index do |accordion_section, index|
3367
+ accordion_section_number = index + 1
3368
+
3369
+ # ensure only the first section is expanded
3370
+ accordion_section.presenter.collapse(instant: true) if accordion_section_number != 1
3371
+
3372
+ accordion_section.content {
3373
+ on_expanded do
3374
+ other_accordion_sections = accordion_sections.reject {|other_accordion_section| other_accordion_section == accordion_section }
3375
+ other_accordion_sections.each { |other_accordion_section| other_accordion_section.presenter.collapse }
3376
+ notify_listeners(:accordion_section_expanded, accordion_section_number)
3377
+ end
3378
+
3379
+ on_collapsed do
3380
+ notify_listeners(:accordion_section_collapsed, accordion_section_number)
3381
+ end
3382
+ }
3383
+ end
3384
+ end
3385
+ }
3386
+ }
3387
+ end
3388
+ end
3389
+
3390
+ unless Object.const_defined?(:HelloComponentListeners)
3391
+ # HelloComponentListeners Glimmer Web Component (View component)
3392
+ #
3393
+ # This View component represents the main page being rendered,
3394
+ # as done by its `render` class method below
3395
+ class HelloComponentListeners
3396
+ class Presenter
3397
+ attr_accessor :status_message
3398
+
3399
+ def initialize
3400
+ @status_message = "Accordion section 1 is expanded!"
3401
+ end
3402
+ end
3403
+
3404
+ include Glimmer::Web::Component
3405
+
3406
+ before_render do
3407
+ @presenter = Presenter.new
3408
+ @shipping_address = Address.new(
3409
+ full_name: 'Johnny Doe',
3410
+ street: '3922 Park Ave',
3411
+ street2: 'PO BOX 8382',
3412
+ city: 'San Diego',
3413
+ state: 'California',
3414
+ zip_code: '91913',
3415
+ )
3416
+ @billing_address = Address.new(
3417
+ full_name: 'John C Doe',
3418
+ street: '123 Main St',
3419
+ street2: 'Apartment 3C',
3420
+ city: 'San Diego',
3421
+ state: 'California',
3422
+ zip_code: '91911',
3423
+ )
3424
+ @emergency_address = Address.new(
3425
+ full_name: 'Mary Doe',
3426
+ street: '2038 Ipswitch St',
3427
+ street2: 'Suite 300',
3428
+ city: 'San Diego',
3429
+ state: 'California',
3430
+ zip_code: '91912',
3431
+ )
3432
+ end
3433
+
3434
+ markup {
3435
+ div {
3436
+ h1(style: {font_style: :italic}) {
3437
+ inner_html <= [@presenter, :status_message]
3438
+ }
3439
+
3440
+ accordion { # any content nested under component directly is added under its markup root div element
3441
+ accordion_section(title: 'Shipping Address') {
3442
+ section_content { # contribute elements to section_content slot declared in AccordionSection component
3443
+ address_form(address: @shipping_address)
3444
+ }
3445
+ }
3446
+
3447
+ accordion_section(title: 'Billing Address') {
3448
+ section_content {
3449
+ address_form(address: @billing_address)
3450
+ }
3451
+ }
3452
+
3453
+ accordion_section(title: 'Emergency Address') {
3454
+ section_content {
3455
+ address_form(address: @emergency_address)
3456
+ }
3457
+ }
3458
+
3459
+ # on_accordion_section_expanded listener matches event :accordion_section_expanded declared in Accordion component
3460
+ on_accordion_section_expanded { |accordion_section_number|
3461
+ @presenter.status_message = "Accordion section #{accordion_section_number} is expanded!"
3462
+ }
3463
+
3464
+ on_accordion_section_collapsed { |accordion_section_number|
3465
+ @presenter.status_message = "Accordion section #{accordion_section_number} is collapsed!"
3466
+ }
3467
+ }
3468
+ }
3469
+ }
3470
+ end
3471
+ end
3472
+
3473
+ Document.ready? do
3474
+ # renders a top-level (root) HelloComponentListeners component
3475
+ HelloComponentListeners.render
3476
+ end
3477
+ ```
3478
+
3479
+ Screenshot:
3480
+
3481
+ ![Hello, Component Listeners!](/images/glimmer-dsl-web-samples-hello-hello-component-listeners.gif)
3482
+
3483
+ #### Hello, Component Listeners (Default Slot)!
3484
+
3485
+ This is a modified simpler version of Hello, Component Listeners! that takes advantage of the Component Default Slot feature.
3486
+
3487
+ If you want content that is added to a component block direclty added to a specific slot by default, you can specify a `default_slot :slot_name`.
3488
+ In that case, if you ever want to add content to the component markup root element, there is a slot that is always available for that, called `:markup_root_slot`.
3489
+ The default slot feature is demonstrated in the [Hello, Component Listeners (Default Slot)!](#hello-component-listeners-default-slot) sample.
3490
+
3491
+ For example, an `AccordionSection` component specifies `default_slot :section_content` to simplify inserting content for consumers of the component.
3492
+
3493
+ [lib/glimmer-dsl-web/samples/hello/hello_component_listeners_default_slot.rb](/lib/glimmer-dsl-web/samples/hello/hello_component_listeners_default_slot.rb)
3494
+
3495
+ Glimmer HTML DSL Ruby code in the frontend:
3496
+
3497
+ ```ruby
3498
+ require 'glimmer-dsl-web'
3499
+
3500
+ unless Object.const_defined?(:Address)
3501
+ Address = Struct.new(:full_name, :street, :street2, :city, :state, :zip_code, keyword_init: true) do
3502
+ STATES = {
3503
+ "AK"=>"Alaska",
3504
+ "AL"=>"Alabama",
3505
+ "AR"=>"Arkansas",
3506
+ "AS"=>"American Samoa",
3507
+ "AZ"=>"Arizona",
3508
+ "CA"=>"California",
3509
+ "CO"=>"Colorado",
3510
+ "CT"=>"Connecticut",
3511
+ "DC"=>"District of Columbia",
3512
+ "DE"=>"Delaware",
3513
+ "FL"=>"Florida",
3514
+ "GA"=>"Georgia",
3515
+ "GU"=>"Guam",
3516
+ "HI"=>"Hawaii",
3517
+ "IA"=>"Iowa",
3518
+ "ID"=>"Idaho",
3519
+ "IL"=>"Illinois",
3520
+ "IN"=>"Indiana",
3521
+ "KS"=>"Kansas",
3522
+ "KY"=>"Kentucky",
3523
+ "LA"=>"Louisiana",
3524
+ "MA"=>"Massachusetts",
3525
+ "MD"=>"Maryland",
3526
+ "ME"=>"Maine",
3527
+ "MI"=>"Michigan",
3528
+ "MN"=>"Minnesota",
3529
+ "MO"=>"Missouri",
3530
+ "MS"=>"Mississippi",
3531
+ "MT"=>"Montana",
3532
+ "NC"=>"North Carolina",
3533
+ "ND"=>"North Dakota",
3534
+ "NE"=>"Nebraska",
3535
+ "NH"=>"New Hampshire",
3536
+ "NJ"=>"New Jersey",
3537
+ "NM"=>"New Mexico",
3538
+ "NV"=>"Nevada",
3539
+ "NY"=>"New York",
3540
+ "OH"=>"Ohio",
3541
+ "OK"=>"Oklahoma",
3542
+ "OR"=>"Oregon",
3543
+ "PA"=>"Pennsylvania",
3544
+ "PR"=>"Puerto Rico",
3545
+ "RI"=>"Rhode Island",
3546
+ "SC"=>"South Carolina",
3547
+ "SD"=>"South Dakota",
3548
+ "TN"=>"Tennessee",
3549
+ "TX"=>"Texas",
3550
+ "UT"=>"Utah",
3551
+ "VA"=>"Virginia",
3552
+ "VI"=>"Virgin Islands",
3553
+ "VT"=>"Vermont",
3554
+ "WA"=>"Washington",
3555
+ "WI"=>"Wisconsin",
3556
+ "WV"=>"West Virginia",
3557
+ "WY"=>"Wyoming"
3558
+ }
3559
+
3560
+ def state_code
3561
+ STATES.invert[state]
3562
+ end
3563
+
3564
+ def state_code=(value)
3565
+ self.state = STATES[value]
3566
+ end
3567
+
3568
+ def summary
3569
+ to_h.values.map(&:to_s).reject(&:empty?).join(', ')
3570
+ end
3571
+ end
3572
+ end
3573
+
3574
+ unless Object.const_defined?(:AddressForm)
3575
+ # AddressForm Glimmer Web Component (View component)
3576
+ #
3577
+ # Including Glimmer::Web::Component makes this class a View component and automatically
3578
+ # generates a new Glimmer HTML DSL keyword that matches the lowercase underscored version
3579
+ # of the name of the class. AddressForm generates address_form keyword, which can be used
3580
+ # elsewhere in Glimmer HTML DSL code as done inside HelloComponentListenersDefaultSlot below.
3581
+ class AddressForm
3582
+ include Glimmer::Web::Component
3583
+
3584
+ option :address
3585
+
3586
+ markup {
3587
+ div {
3588
+ div(style: {display: :grid, grid_auto_columns: '80px 260px'}) { |address_div|
3589
+ label('Full Name: ', for: 'full-name-field')
3590
+ input(id: 'full-name-field') {
3591
+ value <=> [address, :full_name]
3592
+ }
3593
+
3594
+ label('Street: ', for: 'street-field')
3595
+ input(id: 'street-field') {
3596
+ value <=> [address, :street]
3597
+ }
3598
+
3599
+ label('Street 2: ', for: 'street2-field')
3600
+ textarea(id: 'street2-field') {
3601
+ value <=> [address, :street2]
3602
+ }
3603
+
3604
+ label('City: ', for: 'city-field')
3605
+ input(id: 'city-field') {
3606
+ value <=> [address, :city]
3607
+ }
3608
+
3609
+ label('State: ', for: 'state-field')
3610
+ select(id: 'state-field') {
3611
+ Address::STATES.each do |state_code, state|
3612
+ option(value: state_code) { state }
3613
+ end
3614
+
3615
+ value <=> [address, :state_code]
3616
+ }
3617
+
3618
+ label('Zip Code: ', for: 'zip-code-field')
3619
+ input(id: 'zip-code-field', type: 'number', min: '0', max: '99999') {
3620
+ value <=> [address, :zip_code,
3621
+ on_write: :to_s,
3622
+ ]
3623
+ }
3624
+
3625
+ style {
3626
+ r("#{address_div.selector} *") {
3627
+ margin '5px'
3628
+ }
3629
+ r("#{address_div.selector} input, #{address_div.selector} select") {
3630
+ grid_column '2'
3631
+ }
3632
+ }
3633
+ }
3634
+
3635
+ div(style: {margin: 5}) {
3636
+ inner_text <= [address, :summary,
3637
+ computed_by: address.members + ['state_code'],
3638
+ ]
3639
+ }
3640
+ }
3641
+ }
3642
+ end
3643
+ end
3644
+
3645
+ unless Object.const_defined?(:AccordionSection2)
3646
+ # Note: this is similar to AccordionSection in HelloComponentSlots but specifies default_slot for simpler consumption
3647
+ class AccordionSection2
3648
+ class Presenter
3649
+ attr_accessor :collapsed, :instant_transition
3650
+
3651
+ def toggle_collapsed(instant: false)
3652
+ self.instant_transition = instant
3653
+ self.collapsed = !collapsed
3654
+ end
3655
+
3656
+ def expand(instant: false)
3657
+ self.instant_transition = instant
3658
+ self.collapsed = false
3659
+ end
3660
+
3661
+ def collapse(instant: false)
3662
+ self.instant_transition = instant
3663
+ self.collapsed = true
3664
+ end
3665
+ end
3666
+
3667
+ include Glimmer::Web::Component
3668
+
3669
+ events :expanded, :collapsed
3670
+
3671
+ default_slot :section_content # automatically insert content in this element slot inside markup
3672
+
3673
+ option :title
3674
+
3675
+ attr_reader :presenter
3676
+
3677
+ before_render do
3678
+ @presenter = Presenter.new
3679
+ end
3680
+
3681
+ markup {
3682
+ section { # represents the :markup_root_slot to allow inserting content here instead of in default_slot
3683
+ # Unidirectionally data-bind the class inclusion of 'collapsed' to the @presenter.collapsed boolean attribute,
3684
+ # meaning if @presenter.collapsed changes to true, the CSS class 'collapsed' is included on the element,
3685
+ # and if it changes to false, the CSS class 'collapsed' is removed from the element.
3686
+ class_name(:collapsed) <= [@presenter, :collapsed]
3687
+ class_name(:instant_transition) <= [@presenter, :instant_transition]
3688
+
3689
+ header(title, class: 'accordion-section-title') {
3690
+ onclick do |event|
3691
+ @presenter.toggle_collapsed
3692
+ if @presenter.collapsed
3693
+ notify_listeners(:collapsed)
3694
+ else
3695
+ notify_listeners(:expanded)
3696
+ end
3697
+ end
3698
+ }
3699
+
3700
+ div(slot: :section_content, class: 'accordion-section-content')
3701
+ }
3702
+ }
3703
+
3704
+ style {
3705
+ r('.accordion-section-title') {
3706
+ font_size 2.em
3707
+ font_weight :bold
3708
+ cursor :pointer
3709
+ padding_left 20
3710
+ position :relative
3711
+ margin_block_start 0.33.em
3712
+ margin_block_end 0.33.em
3713
+ }
3714
+
3715
+ r('.accordion-section-title::before') {
3716
+ content '"▼"'
3717
+ position :absolute
3718
+ font_size 0.5.em
3719
+ top 10
3720
+ left 0
3721
+ }
3722
+
3723
+ r('.accordion-section-content') {
3724
+ height 246
3725
+ overflow :hidden
3726
+ transition 'height 0.5s linear'
3727
+ }
3728
+
3729
+ r("#{component_element_selector}.instant_transition .accordion-section-content") {
3730
+ transition 'initial'
3731
+ }
3732
+
3733
+ r("#{component_element_selector}.collapsed .accordion-section-title::before") {
3734
+ content '"►"'
3735
+ }
3736
+
3737
+ r("#{component_element_selector}.collapsed .accordion-section-content") {
3738
+ height 0
3739
+ }
3740
+ }
3741
+ end
3742
+ end
3743
+
3744
+ unless Object.const_defined?(:Accordion)
3745
+ class Accordion
3746
+ include Glimmer::Web::Component
3747
+
3748
+ events :accordion_section_expanded, :accordion_section_collapsed
3749
+
3750
+ markup {
3751
+ # given that no slots are specified, nesting content under the accordion component
3752
+ # in consumer code adds content directly inside the markup root div.
3753
+ div { |accordion| # represents the :markup_root_slot (top-level element)
3754
+ # on render, all accordion sections would have been added by consumers already, so we can
3755
+ # attach listeners to all of them by re-opening their content with `.content { ... }` block
3756
+ on_render do
3757
+ accordion_section_elements = accordion.children
3758
+ accordion_sections = accordion_section_elements.map(&:component)
3759
+ accordion_sections.each_with_index do |accordion_section, index|
3760
+ accordion_section_number = index + 1
3761
+
3762
+ # ensure only the first section is expanded
3763
+ accordion_section.presenter.collapse(instant: true) if accordion_section_number != 1
3764
+
3765
+ accordion_section.content { # re-open content and add component custom event listeners
3766
+ on_expanded do
3767
+ other_accordion_sections = accordion_sections.reject {|other_accordion_section| other_accordion_section == accordion_section }
3768
+ other_accordion_sections.each { |other_accordion_section| other_accordion_section.presenter.collapse }
3769
+ notify_listeners(:accordion_section_expanded, accordion_section_number)
3770
+ end
3771
+
3772
+ on_collapsed do
3773
+ notify_listeners(:accordion_section_collapsed, accordion_section_number)
3774
+ end
3775
+ }
3776
+ end
3777
+ end
3778
+ }
3779
+ }
3780
+ end
3781
+ end
3782
+
3783
+ unless Object.const_defined?(:HelloComponentListenersDefaultSlot)
3784
+ # HelloComponentListenersDefaultSlot Glimmer Web Component (View component)
3785
+ #
3786
+ # This View component represents the main page being rendered,
3787
+ # as done by its `render` class method below
3788
+ #
3789
+ # Note: this is a simpler version of HelloComponentSlots as it leverages the default slot feature
3790
+ class HelloComponentListenersDefaultSlot
3791
+ class Presenter
3792
+ attr_accessor :status_message
3793
+
3794
+ def initialize
3795
+ @status_message = "Accordion section 1 is expanded!"
3796
+ end
3797
+ end
3798
+
3799
+ include Glimmer::Web::Component
3800
+
3801
+ before_render do
3802
+ @presenter = Presenter.new
3803
+ @shipping_address = Address.new(
3804
+ full_name: 'Johnny Doe',
3805
+ street: '3922 Park Ave',
3806
+ street2: 'PO BOX 8382',
3807
+ city: 'San Diego',
3808
+ state: 'California',
3809
+ zip_code: '91913',
3810
+ )
3811
+ @billing_address = Address.new(
3812
+ full_name: 'John C Doe',
3813
+ street: '123 Main St',
3814
+ street2: 'Apartment 3C',
3815
+ city: 'San Diego',
3816
+ state: 'California',
3817
+ zip_code: '91911',
3818
+ )
3819
+ @emergency_address = Address.new(
3820
+ full_name: 'Mary Doe',
3821
+ street: '2038 Ipswitch St',
3822
+ street2: 'Suite 300',
3823
+ city: 'San Diego',
3824
+ state: 'California',
3825
+ zip_code: '91912',
3826
+ )
3827
+ end
3828
+
3829
+ markup {
3830
+ div {
3831
+ h1(style: {font_style: :italic}) {
3832
+ inner_html <= [@presenter, :status_message]
3833
+ }
3834
+
3835
+ accordion {
3836
+ # any content nested under component directly is added to its markup_root_slot element if no default_slot is specified
3837
+ accordion_section2(title: 'Shipping Address') {
3838
+ address_form(address: @shipping_address) # automatically inserts content in default_slot :section_content
3839
+ }
3840
+
3841
+ accordion_section2(title: 'Billing Address') {
3842
+ address_form(address: @billing_address) # automatically inserts content in default_slot :section_content
3843
+ }
3844
+
3845
+ accordion_section2(title: 'Emergency Address') {
3846
+ address_form(address: @emergency_address) # automatically inserts content in default_slot :section_content
3847
+ }
3848
+
3849
+ # on_accordion_section_expanded listener matches event :accordion_section_expanded declared in Accordion component
3850
+ on_accordion_section_expanded { |accordion_section_number|
3851
+ @presenter.status_message = "Accordion section #{accordion_section_number} is expanded!"
3852
+ }
3853
+
3854
+ on_accordion_section_collapsed { |accordion_section_number|
3855
+ @presenter.status_message = "Accordion section #{accordion_section_number} is collapsed!"
3856
+ }
3857
+ }
3858
+ }
3859
+ }
3860
+ end
3861
+ end
3862
+
3863
+ Document.ready? do
3864
+ # renders a top-level (root) HelloComponentListenersDefaultSlot component
3865
+ # Note: this is a simpler version of hello_component_slots.rb as it leverages the default slot feature
3866
+ HelloComponentListenersDefaultSlot.render
3867
+ end
3868
+ ```
3869
+
3870
+ Screenshot:
3871
+
3872
+ ![Hello, Component Listeners!](/images/glimmer-dsl-web-samples-hello-hello-component-listeners.gif)
3873
+
3090
3874
  #### Hello, glimmer_component Rails Helper!
3091
3875
 
3092
3876
  You may insert a Glimmer component anywhere into a Rails View using
@@ -3465,9 +4249,9 @@ class StyledButton
3465
4249
  class_name(:pushed) <= [button_model, :pushed]
3466
4250
  class_name(:pulled) <= [button_model, :pushed, on_read: :!]
3467
4251
 
3468
- style(:width) <= [button_model, :width, on_read: :px]
3469
- style(:height) <= [button_model, :height, on_read: :px]
3470
- style(:font_size) <= [button_model, :font_size, on_read: :px]
4252
+ style(:width) <= [button_model, :width]
4253
+ style(:height) <= [button_model, :height]
4254
+ style(:font_size) <= [button_model, :font_size]
3471
4255
  style(:background_color) <= [button_model, :background_color]
3472
4256
  style(:border_color) <= [button_model, :border_color, computed_by: :background_color]
3473
4257