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 +4 -4
- data/CHANGELOG.md +14 -1
- data/README.md +400 -10
- data/VERSION +1 -1
- data/glimmer-dsl-web.gemspec +6 -5
- data/lib/glimmer/web/component.rb +81 -13
- data/lib/glimmer-dsl-web/samples/hello/hello_component.rb +1 -1
- data/lib/glimmer-dsl-web/samples/hello/hello_component_listeners.rb +390 -0
- data/lib/glimmer-dsl-web/samples/hello/hello_component_slots.rb +7 -7
- data/lib/glimmer-dsl-web/samples/hello/hello_style.rb +3 -3
- data/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/new_todo_form.rb +1 -1
- data/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/todo_input.rb +1 -1
- data/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/todo_list.rb +2 -2
- data/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/todo_list_item.rb +1 -3
- data/lib/glimmer-dsl-web/samples/regular/todo_mvc.rb +2 -2
- metadata +11 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ff9463575ae6b368ccbde387628ed3267109d92ca7239eb893647b24d80fddd7
|
4
|
+
data.tar.gz: 6896f526bef25233083ad18e5fb2a8c668e2c88dfd91fe671de2bc3ad9e6d45e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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)
|
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.
|
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.
|
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
|
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
|
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(:
|
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
|
3467
|
-
style(:height) <= [button_model, :height
|
3468
|
-
style(:font_size) <= [button_model, :font_size
|
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.
|
1
|
+
0.6.1
|
data/glimmer-dsl-web.gemspec
CHANGED
@@ -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
|
+
# 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.
|
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-
|
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
|
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.
|
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?('
|
321
|
-
property = observation_request.sub(/^
|
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?('
|
330
|
-
property = observation_request.sub(/^
|
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?(
|
338
|
-
|
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
|
342
|
-
|
343
|
-
|
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
|
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
|
|
@@ -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
|
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
|
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:
|
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
|
-
|
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
|
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
|
-
|
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
|
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
|
90
|
-
style(:height) <= [button_model, :height
|
91
|
-
style(:font_size) <= [button_model, :font_size
|
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
|
|
@@ -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
|
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
|
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
|
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.
|
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-
|
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.
|
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.
|
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
|
244
|
-
Slots. And, a Ruby CSS DSL is supported with
|
245
|
-
samples are demonstrated in the Rails sample
|
246
|
-
[No Rails] sample app too). You can finally
|
247
|
-
both the frontend and backend with Glimmer DSL
|
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
|