glimmer-dsl-web 0.5.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 702f2aed3db1d4fcb6fbda9e794a756a3cdb44dfa47db714bbd350a5dd5e1d81
4
- data.tar.gz: 5c6b8c276bc246c023cb9419744bceea8021450943cf7e2da2dfb0f86cbb4b3c
3
+ metadata.gz: ff9463575ae6b368ccbde387628ed3267109d92ca7239eb893647b24d80fddd7
4
+ data.tar.gz: 6896f526bef25233083ad18e5fb2a8c668e2c88dfd91fe671de2bc3ad9e6d45e
5
5
  SHA512:
6
- metadata.gz: 5e3dbff73a4ebb082523dbcf76facc65d27a93e69e2873c08f02bd5939b34a1e9292eadd7698963d5eb53cfa164fbf7b2b14a100213c5d0f3d8d5566d6dc08cd
7
- data.tar.gz: dfcc0d083636b8d0943b1ff78215c73651bfe768b970ac9cf81852a9e29ab54bf18f94d49483dc4753154c5497cbeb0f99acfbf91d684eebb97d533be19ccb30
6
+ metadata.gz: 204c573bae64566e947914a8862360eff803bd1be3b9f67a671c5053cf65cb07a880c0f176650db757ed9bb720cd9afeb4a1954eed865c9faa34e8540b21d4d0
7
+ data.tar.gz: ec311e00c136cbdce076b798735c02118f2c3459d8a3af929093325e70b3c5fbec8696868a02046cab9154e7745bb3356a77d83b05876a03517c82293c751693
data/CHANGELOG.md CHANGED
@@ -1,8 +1,21 @@
1
1
  # Change Log
2
2
 
3
+ ## 0.6.1
4
+
5
+ - Hello, Component Listeners! Sample: `require 'glimmer-dsl-web/samples/hello/hello_component_listeners'`
6
+ - Upgrade to glimmer-dsl-css 1.5.2 to fix issue with `%` Number operator and support `rem`, `Q`, `pt`, `pc`, `ch`, `ex`, `vh`, `vw`, `vmin`, `vmax` Number methods
7
+ - Refactor Todo MVC sample
8
+ - Fix issue with components that have no events declared when nesting slot content within them
9
+
10
+ ## 0.6.0
11
+
12
+ - Support Component Custom Event Listeners (declare inside Component with `event :eventname` or `events :event1, :event2, ...` and then listen inside consumer code by adding `on_customeventname do; ...; end` listener inside content block of a consumed Glimmer Web Component)
13
+ - Using latest opal-sprockets 1.0.4 will fix an issue with Opal compilation breaking whenever upgrading Opal gems in a Rails project and trying to restart server and render a webpage locally.
14
+
3
15
  ## 0.5.0
4
16
 
5
- - Support Glimmer Web Component Slots (by adding `slot: :slot_name` to any parent element, like a `div`, inside a `Glimmer::Web::Component` `markup {...}` element)
17
+ - Support Glimmer Web Component Slots (by adding `slot: :slot_name` to any parent element, like a `div`, inside a `Glimmer::Web::Component` `markup {...}` element, and later having a consumer open a `slot_name {...}` block inside the content block of a consumed component)
18
+ - Hello, Component Slots! Sample: `require 'glimmer-dsl-web/samples/hello/hello_component_slots'`
6
19
 
7
20
  ## 0.4.4
8
21
 
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.5.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.1 (Beta)
2
2
  ## Ruby-in-the-Browser Web Frontend Framework
3
3
  ### Finally, Ruby Developer Productivity, Happiness, and Fun in the Frontend!!!
4
4
  [![Gem Version](https://badge.fury.io/rb/glimmer-dsl-web.svg)](http://badge.fury.io/rb/glimmer-dsl-web)
@@ -12,7 +12,7 @@
12
12
 
13
13
  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
14
 
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) and [Slots](#hello-component-slots). 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)!
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](#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
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
18
 
@@ -1344,6 +1344,7 @@ Learn more about the differences between various [Glimmer](https://github.com/An
1344
1344
  - [Hello, Content Data-Binding!](#hello-content-data-binding)
1345
1345
  - [Hello, Component!](#hello-compoent)
1346
1346
  - [Hello, Component Slots!](#hello-component-slots)
1347
+ - [Hello, Component Listeners!](#hello-compoent-listeners)
1347
1348
  - [Hello, glimmer_component Rails Helper!](#hello-glimmer_component-rails-helper)
1348
1349
  - [Hello, Paragraph!](#hello-paragraph)
1349
1350
  - [Hello, Style!](#hello-style)
@@ -1400,7 +1401,7 @@ rails new glimmer_app_server
1400
1401
  Add the following to `Gemfile`:
1401
1402
 
1402
1403
  ```
1403
- gem 'glimmer-dsl-web', '~> 0.5.0'
1404
+ gem 'glimmer-dsl-web', '~> 0.6.1'
1404
1405
  ```
1405
1406
 
1406
1407
  Run:
@@ -1629,7 +1630,7 @@ Disable the `webpacker` gem line in `Gemfile`:
1629
1630
  Add the following to `Gemfile`:
1630
1631
 
1631
1632
  ```ruby
1632
- gem 'glimmer-dsl-web', '~> 0.5.0'
1633
+ gem 'glimmer-dsl-web', '~> 0.6.1'
1633
1634
  ```
1634
1635
 
1635
1636
  Run:
@@ -3059,7 +3060,7 @@ class HelloComponentSlots
3059
3060
  h1('Shipping Address')
3060
3061
  legend('This is the address that is used for shipping your purchase.', style: {margin_bottom: 10})
3061
3062
  }
3062
- address_footer { # contribute elements to the address_header component slot
3063
+ address_footer { # contribute elements to the address_footer component slot
3063
3064
  p(sub("#{strong('Note:')} #{em('Purchase will be returned if the Shipping Address does not accept it in one week.')}"))
3064
3065
  }
3065
3066
  }
@@ -3069,7 +3070,7 @@ class HelloComponentSlots
3069
3070
  h1('Billing Address')
3070
3071
  legend('This is the address that is used for your billing method (e.g. credit card).', style: {margin_bottom: 10})
3071
3072
  }
3072
- address_footer { # contribute elements to the address_header component slot
3073
+ address_footer { # contribute elements to the address_footer component slot
3073
3074
  p(sub("#{strong('Note:')} #{em('Payment will fail if payment method does not match the Billing Address.')}"))
3074
3075
  }
3075
3076
  }
@@ -3087,6 +3088,393 @@ Screenshot:
3087
3088
 
3088
3089
  ![Hello, Component Slots!](/images/glimmer-dsl-web-samples-hello-hello-component-slots.png)
3089
3090
 
3091
+ #### Hello, Component Listeners!
3092
+
3093
+ 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.
3094
+
3095
+ 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.
3096
+
3097
+ [lib/glimmer-dsl-web/samples/hello/hello_component_listeners.rb](/lib/glimmer-dsl-web/samples/hello/hello_component_listeners.rb)
3098
+
3099
+ Glimmer HTML DSL Ruby code in the frontend:
3100
+
3101
+ ```ruby
3102
+ require 'glimmer-dsl-web'
3103
+
3104
+ unless Object.const_defined?(:Address)
3105
+ Address = Struct.new(:full_name, :street, :street2, :city, :state, :zip_code, keyword_init: true) do
3106
+ STATES = {
3107
+ "AK"=>"Alaska",
3108
+ "AL"=>"Alabama",
3109
+ "AR"=>"Arkansas",
3110
+ "AS"=>"American Samoa",
3111
+ "AZ"=>"Arizona",
3112
+ "CA"=>"California",
3113
+ "CO"=>"Colorado",
3114
+ "CT"=>"Connecticut",
3115
+ "DC"=>"District of Columbia",
3116
+ "DE"=>"Delaware",
3117
+ "FL"=>"Florida",
3118
+ "GA"=>"Georgia",
3119
+ "GU"=>"Guam",
3120
+ "HI"=>"Hawaii",
3121
+ "IA"=>"Iowa",
3122
+ "ID"=>"Idaho",
3123
+ "IL"=>"Illinois",
3124
+ "IN"=>"Indiana",
3125
+ "KS"=>"Kansas",
3126
+ "KY"=>"Kentucky",
3127
+ "LA"=>"Louisiana",
3128
+ "MA"=>"Massachusetts",
3129
+ "MD"=>"Maryland",
3130
+ "ME"=>"Maine",
3131
+ "MI"=>"Michigan",
3132
+ "MN"=>"Minnesota",
3133
+ "MO"=>"Missouri",
3134
+ "MS"=>"Mississippi",
3135
+ "MT"=>"Montana",
3136
+ "NC"=>"North Carolina",
3137
+ "ND"=>"North Dakota",
3138
+ "NE"=>"Nebraska",
3139
+ "NH"=>"New Hampshire",
3140
+ "NJ"=>"New Jersey",
3141
+ "NM"=>"New Mexico",
3142
+ "NV"=>"Nevada",
3143
+ "NY"=>"New York",
3144
+ "OH"=>"Ohio",
3145
+ "OK"=>"Oklahoma",
3146
+ "OR"=>"Oregon",
3147
+ "PA"=>"Pennsylvania",
3148
+ "PR"=>"Puerto Rico",
3149
+ "RI"=>"Rhode Island",
3150
+ "SC"=>"South Carolina",
3151
+ "SD"=>"South Dakota",
3152
+ "TN"=>"Tennessee",
3153
+ "TX"=>"Texas",
3154
+ "UT"=>"Utah",
3155
+ "VA"=>"Virginia",
3156
+ "VI"=>"Virgin Islands",
3157
+ "VT"=>"Vermont",
3158
+ "WA"=>"Washington",
3159
+ "WI"=>"Wisconsin",
3160
+ "WV"=>"West Virginia",
3161
+ "WY"=>"Wyoming"
3162
+ }
3163
+
3164
+ def state_code
3165
+ STATES.invert[state]
3166
+ end
3167
+
3168
+ def state_code=(value)
3169
+ self.state = STATES[value]
3170
+ end
3171
+
3172
+ def summary
3173
+ to_h.values.map(&:to_s).reject(&:empty?).join(', ')
3174
+ end
3175
+ end
3176
+ end
3177
+
3178
+ unless Object.const_defined?(:AddressForm)
3179
+ # AddressForm Glimmer Web Component (View component)
3180
+ #
3181
+ # Including Glimmer::Web::Component makes this class a View component and automatically
3182
+ # generates a new Glimmer HTML DSL keyword that matches the lowercase underscored version
3183
+ # of the name of the class. AddressForm generates address_form keyword, which can be used
3184
+ # elsewhere in Glimmer HTML DSL code as done inside HelloComponentListeners below.
3185
+ class AddressForm
3186
+ include Glimmer::Web::Component
3187
+
3188
+ option :address
3189
+
3190
+ markup {
3191
+ div {
3192
+ div(style: {display: :grid, grid_auto_columns: '80px 260px'}) { |address_div|
3193
+ label('Full Name: ', for: 'full-name-field')
3194
+ input(id: 'full-name-field') {
3195
+ value <=> [address, :full_name]
3196
+ }
3197
+
3198
+ label('Street: ', for: 'street-field')
3199
+ input(id: 'street-field') {
3200
+ value <=> [address, :street]
3201
+ }
3202
+
3203
+ label('Street 2: ', for: 'street2-field')
3204
+ textarea(id: 'street2-field') {
3205
+ value <=> [address, :street2]
3206
+ }
3207
+
3208
+ label('City: ', for: 'city-field')
3209
+ input(id: 'city-field') {
3210
+ value <=> [address, :city]
3211
+ }
3212
+
3213
+ label('State: ', for: 'state-field')
3214
+ select(id: 'state-field') {
3215
+ Address::STATES.each do |state_code, state|
3216
+ option(value: state_code) { state }
3217
+ end
3218
+
3219
+ value <=> [address, :state_code]
3220
+ }
3221
+
3222
+ label('Zip Code: ', for: 'zip-code-field')
3223
+ input(id: 'zip-code-field', type: 'number', min: '0', max: '99999') {
3224
+ value <=> [address, :zip_code,
3225
+ on_write: :to_s,
3226
+ ]
3227
+ }
3228
+
3229
+ style {
3230
+ r("#{address_div.selector} *") {
3231
+ margin '5px'
3232
+ }
3233
+ r("#{address_div.selector} input, #{address_div.selector} select") {
3234
+ grid_column '2'
3235
+ }
3236
+ }
3237
+ }
3238
+
3239
+ div(style: {margin: 5}) {
3240
+ inner_text <= [address, :summary,
3241
+ computed_by: address.members + ['state_code'],
3242
+ ]
3243
+ }
3244
+ }
3245
+ }
3246
+ end
3247
+ end
3248
+
3249
+ unless Object.const_defined?(:AccordionSection)
3250
+ class AccordionSection
3251
+ class Presenter
3252
+ attr_accessor :collapsed, :instant_transition
3253
+
3254
+ def toggle_collapsed(instant: false)
3255
+ self.instant_transition = instant
3256
+ self.collapsed = !collapsed
3257
+ end
3258
+
3259
+ def expand(instant: false)
3260
+ self.instant_transition = instant
3261
+ self.collapsed = false
3262
+ end
3263
+
3264
+ def collapse(instant: false)
3265
+ self.instant_transition = instant
3266
+ self.collapsed = true
3267
+ end
3268
+ end
3269
+
3270
+ include Glimmer::Web::Component
3271
+
3272
+ events :expanded, :collapsed
3273
+
3274
+ option :title
3275
+
3276
+ attr_reader :presenter
3277
+
3278
+ before_render do
3279
+ @presenter = Presenter.new
3280
+ end
3281
+
3282
+ markup {
3283
+ section {
3284
+ # Unidirectionally data-bind the class inclusion of 'collapsed' to the @presenter.collapsed boolean attribute,
3285
+ # meaning if @presenter.collapsed changes to true, the CSS class 'collapsed' is included on the element,
3286
+ # and if it changes to false, the CSS class 'collapsed' is removed from the element.
3287
+ class_name(:collapsed) <= [@presenter, :collapsed]
3288
+ class_name(:instant_transition) <= [@presenter, :instant_transition]
3289
+
3290
+ header(title, class: 'accordion-section-title') {
3291
+ onclick do |event|
3292
+ @presenter.toggle_collapsed
3293
+ if @presenter.collapsed
3294
+ notify_listeners(:collapsed)
3295
+ else
3296
+ notify_listeners(:expanded)
3297
+ end
3298
+ end
3299
+ }
3300
+
3301
+ div(slot: :section_content, class: 'accordion-section-content')
3302
+ }
3303
+ }
3304
+
3305
+ style {
3306
+ r('.accordion-section-title') {
3307
+ font_size 2.em
3308
+ font_weight :bold
3309
+ cursor :pointer
3310
+ padding_left 20
3311
+ position :relative
3312
+ margin_block_start 0.33.em
3313
+ margin_block_end 0.33.em
3314
+ }
3315
+
3316
+ r('.accordion-section-title::before') {
3317
+ content '"▼"'
3318
+ position :absolute
3319
+ font_size 0.5.em
3320
+ top 10
3321
+ left 0
3322
+ }
3323
+
3324
+ r('.accordion-section-content') {
3325
+ height 246
3326
+ overflow :hidden
3327
+ transition 'height 0.5s linear'
3328
+ }
3329
+
3330
+ r("#{component_element_selector}.instant_transition .accordion-section-content") {
3331
+ transition 'initial'
3332
+ }
3333
+
3334
+ r("#{component_element_selector}.collapsed .accordion-section-title::before") {
3335
+ content '"►"'
3336
+ }
3337
+
3338
+ r("#{component_element_selector}.collapsed .accordion-section-content") {
3339
+ height 0
3340
+ }
3341
+ }
3342
+ end
3343
+ end
3344
+
3345
+ unless Object.const_defined?(:Accordion)
3346
+ class Accordion
3347
+ include Glimmer::Web::Component
3348
+
3349
+ events :accordion_section_expanded, :accordion_section_collapsed
3350
+
3351
+ markup {
3352
+ # given that no slots are specified, nesting content under the accordion component
3353
+ # in consumer code adds content directly inside the markup root div.
3354
+ div { |accordion|
3355
+ # on render, all accordion sections would have been added by consumers already, so we can
3356
+ # attach listeners to all of them by re-opening their content with `.content { ... }` block
3357
+ on_render do
3358
+ accordion_section_elements = accordion.children
3359
+ accordion_sections = accordion_section_elements.map(&:component)
3360
+ accordion_sections.each_with_index do |accordion_section, index|
3361
+ accordion_section_number = index + 1
3362
+
3363
+ # ensure only the first section is expanded
3364
+ accordion_section.presenter.collapse(instant: true) if accordion_section_number != 1
3365
+
3366
+ accordion_section.content {
3367
+ on_expanded do
3368
+ other_accordion_sections = accordion_sections.reject {|other_accordion_section| other_accordion_section == accordion_section }
3369
+ other_accordion_sections.each { |other_accordion_section| other_accordion_section.presenter.collapse }
3370
+ notify_listeners(:accordion_section_expanded, accordion_section_number)
3371
+ end
3372
+
3373
+ on_collapsed do
3374
+ notify_listeners(:accordion_section_collapsed, accordion_section_number)
3375
+ end
3376
+ }
3377
+ end
3378
+ end
3379
+ }
3380
+ }
3381
+ end
3382
+ end
3383
+
3384
+ unless Object.const_defined?(:HelloComponentListeners)
3385
+ # HelloComponentListeners Glimmer Web Component (View component)
3386
+ #
3387
+ # This View component represents the main page being rendered,
3388
+ # as done by its `render` class method below
3389
+ class HelloComponentListeners
3390
+ class Presenter
3391
+ attr_accessor :status_message
3392
+
3393
+ def initialize
3394
+ @status_message = "Accordion section 1 is expanded!"
3395
+ end
3396
+ end
3397
+
3398
+ include Glimmer::Web::Component
3399
+
3400
+ before_render do
3401
+ @presenter = Presenter.new
3402
+ @shipping_address = Address.new(
3403
+ full_name: 'Johnny Doe',
3404
+ street: '3922 Park Ave',
3405
+ street2: 'PO BOX 8382',
3406
+ city: 'San Diego',
3407
+ state: 'California',
3408
+ zip_code: '91913',
3409
+ )
3410
+ @billing_address = Address.new(
3411
+ full_name: 'John C Doe',
3412
+ street: '123 Main St',
3413
+ street2: 'Apartment 3C',
3414
+ city: 'San Diego',
3415
+ state: 'California',
3416
+ zip_code: '91911',
3417
+ )
3418
+ @emergency_address = Address.new(
3419
+ full_name: 'Mary Doe',
3420
+ street: '2038 Ipswitch St',
3421
+ street2: 'Suite 300',
3422
+ city: 'San Diego',
3423
+ state: 'California',
3424
+ zip_code: '91912',
3425
+ )
3426
+ end
3427
+
3428
+ markup {
3429
+ div {
3430
+ h1(style: {font_style: :italic}) {
3431
+ inner_html <= [@presenter, :status_message]
3432
+ }
3433
+
3434
+ accordion { # any content nested under component directly is added under its markup root div element
3435
+ accordion_section(title: 'Shipping Address') {
3436
+ section_content { # contribute elements to section_content slot declared in AccordionSection component
3437
+ address_form(address: @shipping_address)
3438
+ }
3439
+ }
3440
+
3441
+ accordion_section(title: 'Billing Address') {
3442
+ section_content {
3443
+ address_form(address: @billing_address)
3444
+ }
3445
+ }
3446
+
3447
+ accordion_section(title: 'Emergency Address') {
3448
+ section_content {
3449
+ address_form(address: @emergency_address)
3450
+ }
3451
+ }
3452
+
3453
+ # on_accordion_section_expanded listener matches event :accordion_section_expanded declared in Accordion component
3454
+ on_accordion_section_expanded { |accordion_section_number|
3455
+ @presenter.status_message = "Accordion section #{accordion_section_number} is expanded!"
3456
+ }
3457
+
3458
+ on_accordion_section_collapsed { |accordion_section_number|
3459
+ @presenter.status_message = "Accordion section #{accordion_section_number} is collapsed!"
3460
+ }
3461
+ }
3462
+ }
3463
+ }
3464
+ end
3465
+ end
3466
+
3467
+ Document.ready? do
3468
+ # renders a top-level (root) HelloComponentListeners component
3469
+ HelloComponentListeners.render
3470
+ end
3471
+ ```
3472
+
3473
+ Screenshot:
3474
+
3475
+ ![Hello, Component Listeners!](/images/glimmer-dsl-web-samples-hello-hello-component-listeners.gif)
3476
+
3477
+
3090
3478
  #### Hello, glimmer_component Rails Helper!
3091
3479
 
3092
3480
  You may insert a Glimmer component anywhere into a Rails View using
@@ -3389,7 +3777,9 @@ Screenshot:
3389
3777
 
3390
3778
  Every Glimmer Web Component can have a `style {}` block that contains CSS styles common to all instances of that element. That block is evaluated against the component class as such.
3391
3779
 
3392
- Also, within every element, you can add `style(:property) <= [model, attribute]` element inline style data-binding statements to dynamically alter a CSS style property based on some changes to a model attribute.
3780
+ Also, within every element, you can add `style(:some_property) <= [model, attribute]` element inline-style data-binding statements to dynamically alter a CSS style property based on some changes to a model attribute.
3781
+
3782
+ And, within every element, you can add `class_name(:some_css_class) <= [model, attribute]` element class-inclusion data-binding statements to dynamically alter the inclusion of a CSS class based on some changes to a model attribute.
3393
3783
 
3394
3784
  [lib/glimmer-dsl-web/samples/hello/hello_style.rb](/lib/glimmer-dsl-web/samples/hello/hello_style.rb)
3395
3785
 
@@ -3463,9 +3853,9 @@ class StyledButton
3463
3853
  class_name(:pushed) <= [button_model, :pushed]
3464
3854
  class_name(:pulled) <= [button_model, :pushed, on_read: :!]
3465
3855
 
3466
- style(:width) <= [button_model, :width, on_read: :px]
3467
- style(:height) <= [button_model, :height, on_read: :px]
3468
- style(:font_size) <= [button_model, :font_size, on_read: :px]
3856
+ style(:width) <= [button_model, :width]
3857
+ style(:height) <= [button_model, :height]
3858
+ style(:font_size) <= [button_model, :font_size]
3469
3859
  style(:background_color) <= [button_model, :background_color]
3470
3860
  style(:border_color) <= [button_model, :border_color, computed_by: :background_color]
3471
3861
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.0
1
+ 0.6.1
@@ -2,17 +2,17 @@
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.5.0 ruby lib
5
+ # stub: glimmer-dsl-web 0.6.1 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "glimmer-dsl-web".freeze
9
- s.version = "0.5.0"
9
+ s.version = "0.6.1"
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]
13
13
  s.authors = ["Andy Maleh".freeze]
14
- s.date = "2024-08-03"
15
- s.description = "Glimmer DSL for Web (Ruby in the Browser Web Frontend Framework) enables building Web Frontends using Ruby in the Browser, as per Matz's recommendation in his RubyConf 2022 keynote speech to replace JavaScript with Ruby. 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 and TIMTOWTDI) and the Rails way (Convention over Configuration) in building Isomorphic Ruby on Rails Applications. It provides a Ruby HTML DSL, which uniquely enables writing both structure code and logic code in one language. It supports both Unidirectional (One-Way) Data-Binding (using <=) and Bidirectional (Two-Way) Data-Binding (using <=>). Dynamic rendering (and re-rendering) of HTML content is also supported via Content Data-Binding. Modular design is supported with Glimmer Web Components and Slots. And, a Ruby CSS DSL is supported with the included Glimmer DSL for CSS. Many samples are demonstrated in the Rails sample app (there is a very minimal Standalone [No Rails] sample app too). You can finally live in pure Rubyland on the Web in both the frontend and backend with Glimmer DSL for Web! This gem relies on Opal Ruby.".freeze
14
+ s.date = "2024-09-02"
15
+ s.description = "Glimmer DSL for Web (Ruby in the Browser Web Frontend Framework) enables building Web Frontends using Ruby in the Browser, as per Matz's recommendation in his RubyConf 2022 keynote speech to replace JavaScript with Ruby. 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 and TIMTOWTDI) and the Rails way (Convention over Configuration) in building Isomorphic Ruby on Rails Applications. It provides a Ruby HTML DSL, which uniquely enables writing both structure code and logic code in one language. It supports both Unidirectional (One-Way) Data-Binding (using <=) and Bidirectional (Two-Way) Data-Binding (using <=>). Dynamic rendering (and re-rendering) of HTML content is also supported via Content Data-Binding. Modular design is supported with Glimmer Web Components, Component Slots, and Component Custom Event Listeners. And, a Ruby CSS DSL is supported with the included Glimmer DSL for CSS. Many samples are demonstrated in the Rails sample app (there is a very minimal Standalone [No Rails] sample app too). You can finally live in pure Rubyland on the Web in both the frontend and backend with Glimmer DSL for Web! This gem relies on Opal Ruby.".freeze
16
16
  s.email = "andy.am@gmail.com".freeze
17
17
  s.extra_rdoc_files = [
18
18
  "CHANGELOG.md",
@@ -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_listeners.rb",
36
37
  "lib/glimmer-dsl-web/samples/hello/hello_component_slots.rb",
37
38
  "lib/glimmer-dsl-web/samples/hello/hello_content_data_binding.rb",
38
39
  "lib/glimmer-dsl-web/samples/hello/hello_data_binding.rb",
@@ -102,7 +103,7 @@ Gem::Specification.new do |s|
102
103
 
103
104
  s.add_runtime_dependency(%q<glimmer>.freeze, ["~> 2.8.0"])
104
105
  s.add_runtime_dependency(%q<glimmer-dsl-xml>.freeze, ["~> 1.4.0"])
105
- s.add_runtime_dependency(%q<glimmer-dsl-css>.freeze, ["~> 1.5.1"])
106
+ s.add_runtime_dependency(%q<glimmer-dsl-css>.freeze, ["~> 1.5.2"])
106
107
  s.add_runtime_dependency(%q<opal>.freeze, ["= 1.8.2"])
107
108
  s.add_runtime_dependency(%q<opal-rails>.freeze, ["= 2.0.3"])
108
109
  s.add_runtime_dependency(%q<opal-async>.freeze, ["~> 1.4.1"])
@@ -89,6 +89,21 @@ module Glimmer
89
89
  @after_render = block
90
90
  end
91
91
 
92
+ def event(event_name)
93
+ @events ||= []
94
+ event_name = event_name.to_sym
95
+ @events << event_name unless @events.include?(event_name)
96
+ end
97
+
98
+ def events(*event_names)
99
+ @events ||= []
100
+ if event_names.empty?
101
+ @events
102
+ else
103
+ event_names.each { |event| event(event) }
104
+ end
105
+ end
106
+
92
107
  def keyword
93
108
  self.name.underscore.gsub('::', '__')
94
109
  end
@@ -255,7 +270,7 @@ module Glimmer
255
270
  end
256
271
  # <- end of class methods
257
272
 
258
- attr_reader :markup_root, :parent, :args, :options, :style_block, :component_style, :slot_elements
273
+ attr_reader :markup_root, :parent, :args, :options, :style_block, :component_style, :slot_elements, :events
259
274
  alias parent_proxy parent
260
275
 
261
276
  def initialize(parent, args, options, &content)
@@ -272,6 +287,7 @@ module Glimmer
272
287
  @args = args
273
288
  options ||= {}
274
289
  @options = self.class.options.merge(options)
290
+ @events = self.class.instance_variable_get("@events") || []
275
291
  @content = Util::ProcTracker.new(content) if content
276
292
  # @style_blocks = {} # TODO enable when doing bulk head rendering in the future
277
293
  execute_hooks('before_render')
@@ -317,32 +333,84 @@ module Glimmer
317
333
  def can_handle_observation_request?(observation_request)
318
334
  observation_request = observation_request.to_s
319
335
  result = false
320
- if observation_request.start_with?('on_updated_')
321
- property = observation_request.sub(/^on_updated_/, '')
336
+ if observation_request.start_with?('on_update_') # TODO change to on_someprop_update & document this feature
337
+ property = observation_request.sub(/^on_update_/, '')
322
338
  result = can_add_observer?(property)
339
+ elsif observation_request.start_with?('on_')
340
+ event = observation_request.sub(/^on_/, '')
341
+ result = can_add_observer?(event)
323
342
  end
324
- result || markup_root&.can_handle_observation_request?(observation_request)
343
+ result || @markup_root&.can_handle_observation_request?(observation_request)
325
344
  end
326
345
 
327
346
  def handle_observation_request(observation_request, block)
328
347
  observation_request = observation_request.to_s
329
- if observation_request.start_with?('on_updated_')
330
- property = observation_request.sub(/^on_updated_/, '') # TODO look into eliminating duplication from above
348
+ if observation_request.start_with?('on_update_')
349
+ property = observation_request.sub(/^on_update_/, '') # TODO look into eliminating duplication from above
331
350
  add_observer(DataBinding::Observer.proc(&block), property) if can_add_observer?(property)
351
+ elsif observation_request.start_with?('on_')
352
+ event = observation_request.sub(/^on_/, '') # TODO look into eliminating duplication from above
353
+ add_observer(DataBinding::Observer.proc(&block), event) if can_add_observer?(event)
332
354
  else
333
- markup_root.handle_observation_request(observation_request, block)
355
+ @markup_root.handle_observation_request(observation_request, block)
334
356
  end
335
357
  end
336
358
 
337
- def can_add_observer?(attribute_name)
338
- has_instance_method?(attribute_name) || has_instance_method?("#{attribute_name}?") || @markup_root.can_add_observer?(attribute_name)
359
+ def can_add_observer?(attribute_or_event)
360
+ can_add_attribute_observer?(attribute_or_event) ||
361
+ can_add_custom_event_listener?(attribute_or_event)
339
362
  end
340
363
 
341
- def add_observer(observer, attribute_name)
342
- if has_instance_method?(attribute_name)
343
- super(observer, attribute_name)
364
+ def can_add_attribute_observer?(attribute_name)
365
+ has_instance_method?(attribute_name) || has_instance_method?("#{attribute_name}?")
366
+ end
367
+
368
+ def can_add_custom_event_listener?(event)
369
+ events.include?(event.to_sym)
370
+ end
371
+
372
+ def add_observer(observer, attribute_or_event)
373
+ if can_add_attribute_observer?(attribute_or_event)
374
+ super(observer, attribute_or_event)
375
+ elsif can_add_custom_event_listener?(attribute_or_event)
376
+ add_custom_event_listener(observer, attribute_or_event)
377
+ end
378
+ end
379
+
380
+ def custom_event_listeners_for(event)
381
+ event = event.to_sym
382
+ @custom_event_listeners ||= {}
383
+ @custom_event_listeners[event] ||= []
384
+ end
385
+
386
+ def add_custom_event_listener(observer, event)
387
+ custom_event_listeners_for(event) << observer
388
+ end
389
+
390
+ def remove_observer(observer, attribute_or_event, options = {})
391
+ if can_add_attribute_observer?(attribute_or_event)
392
+ super(observer, attribute_or_event)
393
+ elsif can_add_custom_event_listener?(attribute_or_event)
394
+ remove_custom_event_listener(observer, attribute_or_event)
395
+ end
396
+ end
397
+
398
+ def remove_custom_event_listener(observer, event)
399
+ event = event.to_sym
400
+ custom_event_listeners_for(event).delete(observer) if custom_event_listeners_for(event).include?(observer)
401
+ end
402
+
403
+ def notify_listeners(event, *args)
404
+ if can_add_custom_event_listener?(event)
405
+ notify_custom_event_listeners(event, *args)
344
406
  else
345
- @markup_root.add_observer(observer, attribute_name)
407
+ @markup_root&.notify_listeners(event)
408
+ end
409
+ end
410
+
411
+ def notify_custom_event_listeners(event, *args)
412
+ custom_event_listeners_for(event).each do |listener|
413
+ listener.call(*args)
346
414
  end
347
415
  end
348
416
 
@@ -126,7 +126,7 @@ class AddressForm
126
126
  value <=> [address, :full_name]
127
127
  }
128
128
 
129
- @somelabel = label('Street: ', for: 'street-field')
129
+ label('Street: ', for: 'street-field')
130
130
  input(id: 'street-field') {
131
131
  value <=> [address, :street]
132
132
  }
@@ -0,0 +1,390 @@
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, keyword_init: true) do
26
+ STATES = {
27
+ "AK"=>"Alaska",
28
+ "AL"=>"Alabama",
29
+ "AR"=>"Arkansas",
30
+ "AS"=>"American Samoa",
31
+ "AZ"=>"Arizona",
32
+ "CA"=>"California",
33
+ "CO"=>"Colorado",
34
+ "CT"=>"Connecticut",
35
+ "DC"=>"District of Columbia",
36
+ "DE"=>"Delaware",
37
+ "FL"=>"Florida",
38
+ "GA"=>"Georgia",
39
+ "GU"=>"Guam",
40
+ "HI"=>"Hawaii",
41
+ "IA"=>"Iowa",
42
+ "ID"=>"Idaho",
43
+ "IL"=>"Illinois",
44
+ "IN"=>"Indiana",
45
+ "KS"=>"Kansas",
46
+ "KY"=>"Kentucky",
47
+ "LA"=>"Louisiana",
48
+ "MA"=>"Massachusetts",
49
+ "MD"=>"Maryland",
50
+ "ME"=>"Maine",
51
+ "MI"=>"Michigan",
52
+ "MN"=>"Minnesota",
53
+ "MO"=>"Missouri",
54
+ "MS"=>"Mississippi",
55
+ "MT"=>"Montana",
56
+ "NC"=>"North Carolina",
57
+ "ND"=>"North Dakota",
58
+ "NE"=>"Nebraska",
59
+ "NH"=>"New Hampshire",
60
+ "NJ"=>"New Jersey",
61
+ "NM"=>"New Mexico",
62
+ "NV"=>"Nevada",
63
+ "NY"=>"New York",
64
+ "OH"=>"Ohio",
65
+ "OK"=>"Oklahoma",
66
+ "OR"=>"Oregon",
67
+ "PA"=>"Pennsylvania",
68
+ "PR"=>"Puerto Rico",
69
+ "RI"=>"Rhode Island",
70
+ "SC"=>"South Carolina",
71
+ "SD"=>"South Dakota",
72
+ "TN"=>"Tennessee",
73
+ "TX"=>"Texas",
74
+ "UT"=>"Utah",
75
+ "VA"=>"Virginia",
76
+ "VI"=>"Virgin Islands",
77
+ "VT"=>"Vermont",
78
+ "WA"=>"Washington",
79
+ "WI"=>"Wisconsin",
80
+ "WV"=>"West Virginia",
81
+ "WY"=>"Wyoming"
82
+ }
83
+
84
+ def state_code
85
+ STATES.invert[state]
86
+ end
87
+
88
+ def state_code=(value)
89
+ self.state = STATES[value]
90
+ end
91
+
92
+ def summary
93
+ to_h.values.map(&:to_s).reject(&:empty?).join(', ')
94
+ end
95
+ end
96
+ end
97
+
98
+ unless Object.const_defined?(:AddressForm)
99
+ # AddressForm Glimmer Web Component (View component)
100
+ #
101
+ # Including Glimmer::Web::Component makes this class a View component and automatically
102
+ # generates a new Glimmer HTML DSL keyword that matches the lowercase underscored version
103
+ # of the name of the class. AddressForm generates address_form keyword, which can be used
104
+ # elsewhere in Glimmer HTML DSL code as done inside HelloComponentListeners below.
105
+ class AddressForm
106
+ include Glimmer::Web::Component
107
+
108
+ option :address
109
+
110
+ markup {
111
+ div {
112
+ div(style: {display: :grid, grid_auto_columns: '80px 260px'}) { |address_div|
113
+ label('Full Name: ', for: 'full-name-field')
114
+ input(id: 'full-name-field') {
115
+ value <=> [address, :full_name]
116
+ }
117
+
118
+ label('Street: ', for: 'street-field')
119
+ input(id: 'street-field') {
120
+ value <=> [address, :street]
121
+ }
122
+
123
+ label('Street 2: ', for: 'street2-field')
124
+ textarea(id: 'street2-field') {
125
+ value <=> [address, :street2]
126
+ }
127
+
128
+ label('City: ', for: 'city-field')
129
+ input(id: 'city-field') {
130
+ value <=> [address, :city]
131
+ }
132
+
133
+ label('State: ', for: 'state-field')
134
+ select(id: 'state-field') {
135
+ Address::STATES.each do |state_code, state|
136
+ option(value: state_code) { state }
137
+ end
138
+
139
+ value <=> [address, :state_code]
140
+ }
141
+
142
+ label('Zip Code: ', for: 'zip-code-field')
143
+ input(id: 'zip-code-field', type: 'number', min: '0', max: '99999') {
144
+ value <=> [address, :zip_code,
145
+ on_write: :to_s,
146
+ ]
147
+ }
148
+
149
+ style {
150
+ r("#{address_div.selector} *") {
151
+ margin '5px'
152
+ }
153
+ r("#{address_div.selector} input, #{address_div.selector} select") {
154
+ grid_column '2'
155
+ }
156
+ }
157
+ }
158
+
159
+ div(style: {margin: 5}) {
160
+ inner_text <= [address, :summary,
161
+ computed_by: address.members + ['state_code'],
162
+ ]
163
+ }
164
+ }
165
+ }
166
+ end
167
+ end
168
+
169
+ unless Object.const_defined?(:AccordionSection)
170
+ class AccordionSection
171
+ class Presenter
172
+ attr_accessor :collapsed, :instant_transition
173
+
174
+ def toggle_collapsed(instant: false)
175
+ self.instant_transition = instant
176
+ self.collapsed = !collapsed
177
+ end
178
+
179
+ def expand(instant: false)
180
+ self.instant_transition = instant
181
+ self.collapsed = false
182
+ end
183
+
184
+ def collapse(instant: false)
185
+ self.instant_transition = instant
186
+ self.collapsed = true
187
+ end
188
+ end
189
+
190
+ include Glimmer::Web::Component
191
+
192
+ events :expanded, :collapsed
193
+
194
+ option :title
195
+
196
+ attr_reader :presenter
197
+
198
+ before_render do
199
+ @presenter = Presenter.new
200
+ end
201
+
202
+ markup {
203
+ section {
204
+ # Unidirectionally data-bind the class inclusion of 'collapsed' to the @presenter.collapsed boolean attribute,
205
+ # meaning if @presenter.collapsed changes to true, the CSS class 'collapsed' is included on the element,
206
+ # and if it changes to false, the CSS class 'collapsed' is removed from the element.
207
+ class_name(:collapsed) <= [@presenter, :collapsed]
208
+ class_name(:instant_transition) <= [@presenter, :instant_transition]
209
+
210
+ header(title, class: 'accordion-section-title') {
211
+ onclick do |event|
212
+ @presenter.toggle_collapsed
213
+ if @presenter.collapsed
214
+ notify_listeners(:collapsed)
215
+ else
216
+ notify_listeners(:expanded)
217
+ end
218
+ end
219
+ }
220
+
221
+ div(slot: :section_content, class: 'accordion-section-content')
222
+ }
223
+ }
224
+
225
+ style {
226
+ r('.accordion-section-title') {
227
+ font_size 2.em
228
+ font_weight :bold
229
+ cursor :pointer
230
+ padding_left 20
231
+ position :relative
232
+ margin_block_start 0.33.em
233
+ margin_block_end 0.33.em
234
+ }
235
+
236
+ r('.accordion-section-title::before') {
237
+ content '"▼"'
238
+ position :absolute
239
+ font_size 0.5.em
240
+ top 10
241
+ left 0
242
+ }
243
+
244
+ r('.accordion-section-content') {
245
+ height 246
246
+ overflow :hidden
247
+ transition 'height 0.5s linear'
248
+ }
249
+
250
+ r("#{component_element_selector}.instant_transition .accordion-section-content") {
251
+ transition 'initial'
252
+ }
253
+
254
+ r("#{component_element_selector}.collapsed .accordion-section-title::before") {
255
+ content '"►"'
256
+ }
257
+
258
+ r("#{component_element_selector}.collapsed .accordion-section-content") {
259
+ height 0
260
+ }
261
+ }
262
+ end
263
+ end
264
+
265
+ unless Object.const_defined?(:Accordion)
266
+ class Accordion
267
+ include Glimmer::Web::Component
268
+
269
+ events :accordion_section_expanded, :accordion_section_collapsed
270
+
271
+ markup {
272
+ # given that no slots are specified, nesting content under the accordion component
273
+ # in consumer code adds content directly inside the markup root div.
274
+ div { |accordion|
275
+ # on render, all accordion sections would have been added by consumers already, so we can
276
+ # attach listeners to all of them by re-opening their content with `.content { ... }` block
277
+ on_render do
278
+ accordion_section_elements = accordion.children
279
+ accordion_sections = accordion_section_elements.map(&:component)
280
+ accordion_sections.each_with_index do |accordion_section, index|
281
+ accordion_section_number = index + 1
282
+
283
+ # ensure only the first section is expanded
284
+ accordion_section.presenter.collapse(instant: true) if accordion_section_number != 1
285
+
286
+ accordion_section.content {
287
+ on_expanded do
288
+ other_accordion_sections = accordion_sections.reject {|other_accordion_section| other_accordion_section == accordion_section }
289
+ other_accordion_sections.each { |other_accordion_section| other_accordion_section.presenter.collapse }
290
+ notify_listeners(:accordion_section_expanded, accordion_section_number)
291
+ end
292
+
293
+ on_collapsed do
294
+ notify_listeners(:accordion_section_collapsed, accordion_section_number)
295
+ end
296
+ }
297
+ end
298
+ end
299
+ }
300
+ }
301
+ end
302
+ end
303
+
304
+ unless Object.const_defined?(:HelloComponentListeners)
305
+ # HelloComponentListeners Glimmer Web Component (View component)
306
+ #
307
+ # This View component represents the main page being rendered,
308
+ # as done by its `render` class method below
309
+ class HelloComponentListeners
310
+ class Presenter
311
+ attr_accessor :status_message
312
+
313
+ def initialize
314
+ @status_message = "Accordion section 1 is expanded!"
315
+ end
316
+ end
317
+
318
+ include Glimmer::Web::Component
319
+
320
+ before_render do
321
+ @presenter = Presenter.new
322
+ @shipping_address = Address.new(
323
+ full_name: 'Johnny Doe',
324
+ street: '3922 Park Ave',
325
+ street2: 'PO BOX 8382',
326
+ city: 'San Diego',
327
+ state: 'California',
328
+ zip_code: '91913',
329
+ )
330
+ @billing_address = Address.new(
331
+ full_name: 'John C Doe',
332
+ street: '123 Main St',
333
+ street2: 'Apartment 3C',
334
+ city: 'San Diego',
335
+ state: 'California',
336
+ zip_code: '91911',
337
+ )
338
+ @emergency_address = Address.new(
339
+ full_name: 'Mary Doe',
340
+ street: '2038 Ipswitch St',
341
+ street2: 'Suite 300',
342
+ city: 'San Diego',
343
+ state: 'California',
344
+ zip_code: '91912',
345
+ )
346
+ end
347
+
348
+ markup {
349
+ div {
350
+ h1(style: {font_style: :italic}) {
351
+ inner_html <= [@presenter, :status_message]
352
+ }
353
+
354
+ accordion { # any content nested under component directly is added under its markup root div element
355
+ accordion_section(title: 'Shipping Address') {
356
+ section_content { # contribute elements to section_content slot declared in AccordionSection component
357
+ address_form(address: @shipping_address)
358
+ }
359
+ }
360
+
361
+ accordion_section(title: 'Billing Address') {
362
+ section_content {
363
+ address_form(address: @billing_address)
364
+ }
365
+ }
366
+
367
+ accordion_section(title: 'Emergency Address') {
368
+ section_content {
369
+ address_form(address: @emergency_address)
370
+ }
371
+ }
372
+
373
+ # on_accordion_section_expanded listener matches event :accordion_section_expanded declared in Accordion component
374
+ on_accordion_section_expanded { |accordion_section_number|
375
+ @presenter.status_message = "Accordion section #{accordion_section_number} is expanded!"
376
+ }
377
+
378
+ on_accordion_section_collapsed { |accordion_section_number|
379
+ @presenter.status_message = "Accordion section #{accordion_section_number} is collapsed!"
380
+ }
381
+ }
382
+ }
383
+ }
384
+ end
385
+ end
386
+
387
+ Document.ready? do
388
+ # renders a top-level (root) HelloComponentListeners component
389
+ HelloComponentListeners.render
390
+ end
@@ -97,9 +97,9 @@ end
97
97
  #
98
98
  # Including Glimmer::Web::Component makes this class a View component and automatically
99
99
  # generates a new Glimmer HTML DSL keyword that matches the lowercase underscored version
100
- # of the name of the class. AddressForm generates address_form keyword, which can be used
100
+ # of the name of the class. AddressForm generates address_form_with_slots keyword, which can be used
101
101
  # elsewhere in Glimmer HTML DSL code as done inside HelloComponentSlots below.
102
- class AddressForm
102
+ class AddressFormWithSlots
103
103
  include Glimmer::Web::Component
104
104
 
105
105
  option :address
@@ -149,7 +149,7 @@ class AddressForm
149
149
  }
150
150
  }
151
151
 
152
- div(style: 'margin: 5px') {
152
+ div(style: {margin: 5}) {
153
153
  inner_text <= [address, :summary,
154
154
  computed_by: address.members + ['state_code'],
155
155
  ]
@@ -199,22 +199,22 @@ class HelloComponentSlots
199
199
 
200
200
  markup {
201
201
  div {
202
- address_form(address: @shipping_address) {
202
+ address_form_with_slots(address: @shipping_address) {
203
203
  address_header { # contribute elements to the address_header component slot
204
204
  h1('Shipping Address')
205
205
  legend('This is the address that is used for shipping your purchase.', style: {margin_bottom: 10})
206
206
  }
207
- address_footer { # contribute elements to the address_header component slot
207
+ address_footer { # contribute elements to the address_footer component slot
208
208
  p(sub("#{strong('Note:')} #{em('Purchase will be returned if the Shipping Address does not accept it in one week.')}"))
209
209
  }
210
210
  }
211
211
 
212
- address_form(address: @billing_address) {
212
+ address_form_with_slots(address: @billing_address) {
213
213
  address_header { # contribute elements to the address_header component slot
214
214
  h1('Billing Address')
215
215
  legend('This is the address that is used for your billing method (e.g. credit card).', style: {margin_bottom: 10})
216
216
  }
217
- address_footer { # contribute elements to the address_header component slot
217
+ address_footer { # contribute elements to the address_footer component slot
218
218
  p(sub("#{strong('Note:')} #{em('Payment will fail if payment method does not match the Billing Address.')}"))
219
219
  }
220
220
  }
@@ -86,9 +86,9 @@ class StyledButton
86
86
  class_name(:pushed) <= [button_model, :pushed]
87
87
  class_name(:pulled) <= [button_model, :pushed, on_read: :!]
88
88
 
89
- style(:width) <= [button_model, :width, on_read: :px]
90
- style(:height) <= [button_model, :height, on_read: :px]
91
- style(:font_size) <= [button_model, :font_size, on_read: :px]
89
+ style(:width) <= [button_model, :width]
90
+ style(:height) <= [button_model, :height]
91
+ style(:font_size) <= [button_model, :font_size]
92
92
  style(:background_color) <= [button_model, :background_color]
93
93
  style(:border_color) <= [button_model, :border_color, computed_by: :background_color]
94
94
 
@@ -24,7 +24,7 @@ class NewTodoForm
24
24
  _moz_text_rendering :optimizeLegibility
25
25
  text_rendering :optimizeLegibility
26
26
  top -140
27
- width '100%'
27
+ width 100.%
28
28
  }
29
29
  }
30
30
  end
@@ -7,7 +7,7 @@ class TodoInput
7
7
  r(component_element_selector) {
8
8
  position :relative
9
9
  margin 0
10
- width '100%'
10
+ width 100.%
11
11
  font_size 24
12
12
  font_family :inherit
13
13
  font_weight :inherit
@@ -46,11 +46,11 @@ class TodoList
46
46
 
47
47
  r('.toggle-all') {
48
48
  border :none
49
- bottom '100%'
49
+ bottom 100.%
50
50
  height 1
51
51
  opacity 0
52
52
  position :absolute
53
- right '100%'
53
+ right 100.%
54
54
  width 1
55
55
  }
56
56
 
@@ -14,8 +14,6 @@ class TodoListItem
14
14
  end
15
15
 
16
16
  markup {
17
-
18
-
19
17
  li {
20
18
  # Data-bind inclusion of `completed` in `li` `class` attribute unidirectionally to `todo` `completed` attribute,
21
19
  # meaning inclusion/exclusion of `completed` class happens automatically when `todo.completed` boolean value changes.
@@ -149,7 +147,7 @@ class TodoListItem
149
147
  r('.todo-list li .destroy:after') {
150
148
  content '"×"'
151
149
  display :block
152
- height '100%'
150
+ height 100.%
153
151
  line_height '1.1'
154
152
  }
155
153
 
@@ -19,7 +19,7 @@ class TodoMvc
19
19
  end
20
20
 
21
21
  markup {
22
- div(class: 'todomvc') {
22
+ div {
23
23
  section(class: 'todoapp') {
24
24
  new_todo_form(presenter: @presenter)
25
25
 
@@ -50,7 +50,7 @@ class TodoMvc
50
50
  border 0
51
51
  color :inherit
52
52
  font_family :inherit
53
- font_size '100%'
53
+ font_size 100.%
54
54
  font_weight :inherit
55
55
  vertical_align :baseline
56
56
  }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: glimmer-dsl-web
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Maleh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-08-03 00:00:00.000000000 Z
11
+ date: 2024-09-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: glimmer
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 1.5.1
47
+ version: 1.5.2
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 1.5.1
54
+ version: 1.5.2
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: opal
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -240,12 +240,12 @@ description: Glimmer DSL for Web (Ruby in the Browser Web Frontend Framework) en
240
240
  both structure code and logic code in one language. It supports both Unidirectional
241
241
  (One-Way) Data-Binding (using <=) and Bidirectional (Two-Way) Data-Binding (using
242
242
  <=>). Dynamic rendering (and re-rendering) of HTML content is also supported via
243
- Content Data-Binding. Modular design is supported with Glimmer Web Components and
244
- Slots. And, a Ruby CSS DSL is supported with the included Glimmer DSL for CSS. Many
245
- samples are demonstrated in the Rails sample app (there is a very minimal Standalone
246
- [No Rails] sample app too). You can finally live in pure Rubyland on the Web in
247
- both the frontend and backend with Glimmer DSL for Web! This gem relies on Opal
248
- Ruby.
243
+ Content Data-Binding. Modular design is supported with Glimmer Web Components, Component
244
+ Slots, and Component Custom Event Listeners. And, a Ruby CSS DSL is supported with
245
+ the included Glimmer DSL for CSS. Many samples are demonstrated in the Rails sample
246
+ app (there is a very minimal Standalone [No Rails] sample app too). You can finally
247
+ live in pure Rubyland on the Web in both the frontend and backend with Glimmer DSL
248
+ for Web! This gem relies on Opal Ruby.
249
249
  email: andy.am@gmail.com
250
250
  executables: []
251
251
  extensions: []
@@ -267,6 +267,7 @@ files:
267
267
  - lib/glimmer-dsl-web/ext/kernel.rb
268
268
  - lib/glimmer-dsl-web/samples/hello/hello_button.rb
269
269
  - lib/glimmer-dsl-web/samples/hello/hello_component.rb
270
+ - lib/glimmer-dsl-web/samples/hello/hello_component_listeners.rb
270
271
  - lib/glimmer-dsl-web/samples/hello/hello_component_slots.rb
271
272
  - lib/glimmer-dsl-web/samples/hello/hello_content_data_binding.rb
272
273
  - lib/glimmer-dsl-web/samples/hello/hello_data_binding.rb