glimmer-dsl-web 0.2.7 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +116 -11
- data/VERSION +1 -1
- data/glimmer-dsl-web.gemspec +3 -3
- data/lib/glimmer/dsl/web/general_element_expression.rb +2 -4
- data/lib/glimmer/dsl/web/style_expression.rb +2 -4
- data/lib/glimmer/web/element_proxy.rb +210 -106
- data/lib/glimmer-dsl-web/samples/hello/hello_content_data_binding.rb +2 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6147bc2c609c3b5d7871c0d7f16b84f57c5b10c83c22a8ab63fb7e7120a69e65
|
4
|
+
data.tar.gz: e469cc54afb68870470d1bb5ab4a265000d87901873119e01c68f7586ebd883e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dadb81d22299adef771b31cc9152881a61f2cde0a93bd4a46223669d688fd78bae4fe33311875baa2cf6ee30f6f53d534ba2d4af2b5d57a49e3e902b00605dad
|
7
|
+
data.tar.gz: a3426ea5baa49c13d0091bd1e3410f43ac168b1c3f04dbff5b7c7eb4c584b61bf9463b9ad59e89d3e7827abc6a48ad5c468219762a9b58332b46c8980a74397d
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## 0.3.0
|
4
|
+
|
5
|
+
- Optimize performance (~170%-226% faster) by building GUI with a bulk_render call that assembles html as a string from all nested elements and mounts all HTML at once (instead of making many small DOM mount calls). The trade-off is not being able to interact with elements until rendering of the complete hierarchy is complete, which is acceptable because interactions do not happen till after everything is rendered anyways. Can be disabled by passing `bulk_render: false` option to the top-level element of a frontend app.
|
6
|
+
- Fix issue with not being able to add content to a custom control by opening a block that should add content inside its markup root element
|
7
|
+
|
8
|
+
## 0.2.8
|
9
|
+
|
10
|
+
- Support Content Data-Binding to multiple model attributes via `computed_by` option (e.g. `content(@game, :scale, computed_by: [:width, :height])` or `content(@game, computed_by: [:scale, :width, :height])` will re-render content on changes to `:scale`, `:width`, or `:height`)
|
11
|
+
|
3
12
|
## 0.2.7
|
4
13
|
|
5
14
|
- Unidirectional Data-Binding of element `style` property
|
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.3.0 (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)
|
@@ -10,7 +10,7 @@
|
|
10
10
|
|
11
11
|
[![Todo MVC](/images/glimmer-dsl-web-samples-regular-todo-mvc.gif)](/lib/glimmer-dsl-web/samples/regular/todo_mvc.rb)
|
12
12
|
|
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, companies can cut their hiring budget in half by having Backend Ruby Software Engineers do Frontend Development
|
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
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), 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, 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
|
|
@@ -379,7 +379,7 @@ Screenshot:
|
|
379
379
|
|
380
380
|
If you need to regenerate HTML element content dynamically, you can use Content Data-Binding to effortlessly
|
381
381
|
rebuild HTML elements based on changes in a Model attribute that provides the source data.
|
382
|
-
In this example, we generate multiple address forms based on the number of addresses the user has.
|
382
|
+
In this example, we generate multiple address forms based on the number of addresses the user has using `content(@user, :address_count)` (you can add a `computed_by: array_of_attributes` option if you want to re-render content based on changes to multiple attributes like `content(@user, computed_by: [:address_count, :street_count])`, which fires on changes to `address_count` or `street_count`) .
|
383
383
|
|
384
384
|
[lib/glimmer-dsl-web/samples/hello/hello_content_data_binding.rb](/lib/glimmer-dsl-web/samples/hello/hello_content_data_binding.rb)
|
385
385
|
|
@@ -468,7 +468,7 @@ Document.ready? do
|
|
468
468
|
div {
|
469
469
|
# Content Data-Binding is used to dynamically (re)generate content of div
|
470
470
|
# based on changes to @user.addresses, replacing older content on every change
|
471
|
-
content(@user, :
|
471
|
+
content(@user, :address_count) do
|
472
472
|
@user.addresses.each do |address|
|
473
473
|
div {
|
474
474
|
div(style: 'display: grid; grid-auto-columns: 80px 280px;') { |address_div|
|
@@ -1152,6 +1152,8 @@ Screenshot:
|
|
1152
1152
|
|
1153
1153
|
**Todo MVC**
|
1154
1154
|
|
1155
|
+
[Todo MVC Ruby Edition Is the One Todo MVC To Rule Them All!!!](https://andymaleh.blogspot.com/2024/06/todo-mvc-in-ruby-is-one-todo-mvc-to.html)
|
1156
|
+
|
1155
1157
|
```ruby
|
1156
1158
|
require 'glimmer-dsl-web'
|
1157
1159
|
|
@@ -1280,6 +1282,7 @@ Learn more about the differences between various [Glimmer](https://github.com/An
|
|
1280
1282
|
- [Hello, Paragraph!](#hello-paragraph)
|
1281
1283
|
- [Hello, Input (Date/Time)!](#hello-input-datetime)
|
1282
1284
|
- [Button Counter](#button-counter)
|
1285
|
+
- [Todo MVC](#todo-mvc)
|
1283
1286
|
- [Design Principles](#design-principles)
|
1284
1287
|
- [Supporting Libraries](#supporting-libraries)
|
1285
1288
|
- [Glimmer Process](#glimmer-process)
|
@@ -1326,7 +1329,7 @@ rails new glimmer_app_server
|
|
1326
1329
|
Add the following to `Gemfile`:
|
1327
1330
|
|
1328
1331
|
```
|
1329
|
-
gem 'glimmer-dsl-web', '~> 0.
|
1332
|
+
gem 'glimmer-dsl-web', '~> 0.3.0'
|
1330
1333
|
```
|
1331
1334
|
|
1332
1335
|
Run:
|
@@ -1553,7 +1556,7 @@ Disable the `webpacker` gem line in `Gemfile`:
|
|
1553
1556
|
Add the following to `Gemfile`:
|
1554
1557
|
|
1555
1558
|
```ruby
|
1556
|
-
gem 'glimmer-dsl-web', '~> 0.
|
1559
|
+
gem 'glimmer-dsl-web', '~> 0.3.0'
|
1557
1560
|
```
|
1558
1561
|
|
1559
1562
|
Run:
|
@@ -2411,9 +2414,9 @@ Screenshot:
|
|
2411
2414
|
|
2412
2415
|
#### Hello, Content Data-Binding!
|
2413
2416
|
|
2414
|
-
If you need to regenerate HTML element content dynamically, you can use Content Data-Binding to effortlessly
|
2415
|
-
rebuild HTML elements based on changes in a Model attribute that provides the source data.
|
2416
|
-
In this example, we generate multiple address forms based on the number of addresses the user has.
|
2417
|
+
If you need to regenerate (re-render) HTML element content dynamically, you can use Content Data-Binding to effortlessly
|
2418
|
+
rebuild (rerender) HTML elements based on changes in a Model attribute that provides the source data.
|
2419
|
+
In this example, we generate multiple address forms based on the number of addresses the user has using `content(@user, :address_count)` (you can add a `computed_by: array_of_attributes` option if you want to re-render content based on changes to multiple attributes like `content(@user, computed_by: [:address_count, :street_count])`, which fires on changes to `address_count` or `street_count`) .
|
2417
2420
|
|
2418
2421
|
[lib/glimmer-dsl-web/samples/hello/hello_content_data_binding.rb](/lib/glimmer-dsl-web/samples/hello/hello_content_data_binding.rb)
|
2419
2422
|
|
@@ -2501,8 +2504,8 @@ Document.ready? do
|
|
2501
2504
|
|
2502
2505
|
div {
|
2503
2506
|
# Content Data-Binding is used to dynamically (re)generate content of div
|
2504
|
-
# based on changes to @user.
|
2505
|
-
content(@user, :
|
2507
|
+
# based on changes to @user.address_count, replacing older content on every change
|
2508
|
+
content(@user, :address_count) do
|
2506
2509
|
@user.addresses.each do |address|
|
2507
2510
|
div {
|
2508
2511
|
div(style: 'display: grid; grid-auto-columns: 80px 280px;') { |address_div|
|
@@ -3253,6 +3256,108 @@ Screenshot:
|
|
3253
3256
|
|
3254
3257
|
![Button Counter](/images/glimmer-dsl-web-samples-regular-button-counter.gif)
|
3255
3258
|
|
3259
|
+
#### Todo MVC
|
3260
|
+
|
3261
|
+
[Todo MVC Ruby Edition Is the One Todo MVC To Rule Them All!!!](https://andymaleh.blogspot.com/2024/06/todo-mvc-in-ruby-is-one-todo-mvc-to.html)
|
3262
|
+
|
3263
|
+
[lib/glimmer-dsl-web/samples/regular/todo_mvc.rb](/lib/glimmer-dsl-web/samples/regular/todo_mvc.rb)
|
3264
|
+
|
3265
|
+
```ruby
|
3266
|
+
require 'glimmer-dsl-web'
|
3267
|
+
|
3268
|
+
require_relative 'todo_mvc/presenters/todo_presenter'
|
3269
|
+
|
3270
|
+
require_relative 'todo_mvc/views/new_todo_form'
|
3271
|
+
require_relative 'todo_mvc/views/todo_list'
|
3272
|
+
require_relative 'todo_mvc/views/todo_filters'
|
3273
|
+
require_relative 'todo_mvc/views/todo_mvc_footer'
|
3274
|
+
|
3275
|
+
class TodoMvc
|
3276
|
+
include Glimmer::Web::Component
|
3277
|
+
|
3278
|
+
before_render do
|
3279
|
+
@presenter = TodoPresenter.new
|
3280
|
+
end
|
3281
|
+
|
3282
|
+
after_render do
|
3283
|
+
@presenter.setup_filter_routes
|
3284
|
+
end
|
3285
|
+
|
3286
|
+
markup {
|
3287
|
+
div(class: 'todomvc') {
|
3288
|
+
section(class: 'todoapp') {
|
3289
|
+
new_todo_form(presenter: @presenter)
|
3290
|
+
|
3291
|
+
todo_list(presenter: @presenter)
|
3292
|
+
|
3293
|
+
todo_filters(presenter: @presenter)
|
3294
|
+
|
3295
|
+
style {
|
3296
|
+
todo_mvc_styles
|
3297
|
+
}
|
3298
|
+
}
|
3299
|
+
|
3300
|
+
todo_mvc_footer
|
3301
|
+
|
3302
|
+
on_remove do
|
3303
|
+
@presenter.unsetup_filter_routes
|
3304
|
+
end
|
3305
|
+
}
|
3306
|
+
}
|
3307
|
+
|
3308
|
+
def todo_mvc_styles
|
3309
|
+
rule('body, button, html') {
|
3310
|
+
margin '0'
|
3311
|
+
padding '0'
|
3312
|
+
}
|
3313
|
+
|
3314
|
+
rule('button') {
|
3315
|
+
_webkit_font_smoothing 'antialiased'
|
3316
|
+
_webkit_appearance 'none'
|
3317
|
+
appearance 'none'
|
3318
|
+
background 'none'
|
3319
|
+
border '0'
|
3320
|
+
color 'inherit'
|
3321
|
+
font_family 'inherit'
|
3322
|
+
font_size '100%'
|
3323
|
+
font_weight 'inherit'
|
3324
|
+
vertical_align 'baseline'
|
3325
|
+
}
|
3326
|
+
|
3327
|
+
rule('.todoapp') {
|
3328
|
+
background '#fff'
|
3329
|
+
margin '130px 0 40px 0'
|
3330
|
+
position 'relative'
|
3331
|
+
box_shadow '0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1)'
|
3332
|
+
}
|
3333
|
+
|
3334
|
+
media('screen and (-webkit-min-device-pixel-ratio:0)') {
|
3335
|
+
rule('body') {
|
3336
|
+
font "14px 'Helvetica Neue', Helvetica, Arial, sans-serif"
|
3337
|
+
line_height '1.4em'
|
3338
|
+
background '#f5f5f5'
|
3339
|
+
color '#111111'
|
3340
|
+
min_width '230px'
|
3341
|
+
max_width '550px'
|
3342
|
+
margin '0 auto'
|
3343
|
+
_webkit_font_smoothing 'antialiased'
|
3344
|
+
font_weight '300'
|
3345
|
+
}
|
3346
|
+
}
|
3347
|
+
end
|
3348
|
+
end
|
3349
|
+
|
3350
|
+
Document.ready? do
|
3351
|
+
TodoMvc.render
|
3352
|
+
end
|
3353
|
+
```
|
3354
|
+
|
3355
|
+
![Todo MVC](/images/glimmer-dsl-web-samples-regular-todo-mvc.gif)
|
3356
|
+
|
3357
|
+
The rest of the files are found at:
|
3358
|
+
|
3359
|
+
[lib/glimmer-dsl-web/samples/regular/todo_mvc](/lib/glimmer-dsl-web/samples/regular/todo_mvc)
|
3360
|
+
|
3256
3361
|
## Design Principles
|
3257
3362
|
|
3258
3363
|
- The Ruby Way (including TIMTOWTDI: There Is More Than One Way To Do It)
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.3.0
|
data/glimmer-dsl-web.gemspec
CHANGED
@@ -2,16 +2,16 @@
|
|
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.3.0 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.3.0"
|
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-06-
|
14
|
+
s.date = "2024-06-25"
|
15
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, 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 = [
|
@@ -12,11 +12,9 @@ module Glimmer
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def add_content(parent, keyword, *args, &block)
|
15
|
-
if parent.rendered? || parent.skip_content_on_render_blocks?
|
15
|
+
if parent.bulk_render? || parent.rendered? || parent.skip_content_on_render_blocks?
|
16
16
|
return_value = super(parent, keyword, *args, &block)
|
17
|
-
if return_value.is_a?(String)
|
18
|
-
parent.add_text_content(return_value)
|
19
|
-
end
|
17
|
+
parent.add_text_content(return_value, on_empty: true) if return_value.is_a?(String)
|
20
18
|
parent.post_add_content
|
21
19
|
return_value
|
22
20
|
else
|
@@ -14,12 +14,10 @@ module Glimmer
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def add_content(parent, keyword, *args, &block)
|
17
|
-
if parent.rendered? || parent.skip_content_on_render_blocks?
|
17
|
+
if parent.bulk_render? || parent.rendered? || parent.skip_content_on_render_blocks?
|
18
18
|
return_value = css(&block).to_s
|
19
19
|
return_value = super(parent, keyword, *args, &block) if return_value.to_s.empty?
|
20
|
-
if return_value.is_a?(String)
|
21
|
-
parent.add_text_content(return_value)
|
22
|
-
end
|
20
|
+
parent.add_text_content(return_value, on_empty: true) if return_value.is_a?(String)
|
23
21
|
parent.post_add_content
|
24
22
|
return_value
|
25
23
|
else
|
@@ -83,6 +83,11 @@ module Glimmer
|
|
83
83
|
"<#{element}#{attributes}>#{content}</#{element}>"
|
84
84
|
end
|
85
85
|
end
|
86
|
+
|
87
|
+
def unrendered_dom_element(keyword)
|
88
|
+
@unrendered_dom_elements ||= {}
|
89
|
+
@unrendered_dom_elements[keyword] ||= Element["<#{keyword} />"]
|
90
|
+
end
|
86
91
|
end
|
87
92
|
|
88
93
|
include Glimmer
|
@@ -118,22 +123,28 @@ module Glimmer
|
|
118
123
|
REGEX_FORMAT_DATE = /^\d{4}-\d{2}-\d{2}$/
|
119
124
|
REGEX_FORMAT_TIME = /^\d{2}:\d{2}$/
|
120
125
|
|
121
|
-
attr_reader :keyword, :parent, :args, :options, :children, :enabled, :foreground, :background, :removed?, :rendered
|
126
|
+
attr_reader :keyword, :parent, :parent_component, :args, :options, :children, :enabled, :foreground, :background, :removed?, :rendered
|
122
127
|
alias rendered? rendered
|
123
128
|
|
124
129
|
def initialize(keyword, parent, args, block)
|
125
130
|
@keyword = keyword
|
126
|
-
@parent = parent
|
131
|
+
@parent = parent.is_a?(Glimmer::Web::Component) ? parent.markup_root : parent
|
132
|
+
@parent_component = parent if parent.is_a?(Glimmer::Web::Component)
|
127
133
|
@options = args.last.is_a?(Hash) ? args.last.symbolize_keys : {}
|
128
134
|
if parent.nil?
|
129
135
|
options[:parent] ||= Component.interpretation_stack.last&.options&.[](:parent)
|
130
136
|
options[:render] ||= Component.interpretation_stack.last&.options&.[](:render)
|
137
|
+
options[:bulk_render] ||= Component.interpretation_stack.last&.options&.[](:bulk_render)
|
131
138
|
end
|
132
139
|
@args = args
|
133
140
|
@block = block
|
134
141
|
@children = []
|
135
142
|
@parent&.post_initialize_child(self)
|
136
|
-
render if !@rendered && render_after_create?
|
143
|
+
render if !bulk_render? && !@rendered && render_after_create?
|
144
|
+
end
|
145
|
+
|
146
|
+
def bulk_render?
|
147
|
+
options[:bulk_render] != false && (@parent.nil? || @parent.bulk_render?)
|
137
148
|
end
|
138
149
|
|
139
150
|
def render_after_create?
|
@@ -143,7 +154,7 @@ module Glimmer
|
|
143
154
|
# Executes for the parent of a child that just got added
|
144
155
|
def post_initialize_child(child)
|
145
156
|
@children << child
|
146
|
-
child.render if !render_after_create?
|
157
|
+
child.render if !bulk_render? && !render_after_create?
|
147
158
|
end
|
148
159
|
|
149
160
|
# Executes for the parent of a child that just got removed
|
@@ -153,21 +164,22 @@ module Glimmer
|
|
153
164
|
|
154
165
|
# Executes at the closing of a parent widget curly braces after all children/properties have been added/set
|
155
166
|
def post_add_content
|
156
|
-
|
157
|
-
# No Op
|
167
|
+
render if bulk_render? && @parent.nil?
|
158
168
|
end
|
159
169
|
|
160
170
|
def css_classes
|
161
|
-
dom_element.attr('class').to_s.split
|
171
|
+
dom_element.attr('class').to_s.split if rendered?
|
162
172
|
end
|
163
173
|
|
164
174
|
def remove
|
165
|
-
@children.dup.each do |child|
|
166
|
-
child.remove
|
167
|
-
end
|
168
175
|
on_remove_listeners = listeners_for('on_remove').dup
|
169
|
-
|
170
|
-
|
176
|
+
if rendered?
|
177
|
+
@children.dup.each do |child|
|
178
|
+
child.remove
|
179
|
+
end
|
180
|
+
remove_all_listeners
|
181
|
+
dom_element.remove
|
182
|
+
end
|
171
183
|
parent&.post_remove_child(self)
|
172
184
|
@removed = true
|
173
185
|
on_remove_listeners.each do |listener|
|
@@ -222,20 +234,32 @@ module Glimmer
|
|
222
234
|
end
|
223
235
|
|
224
236
|
def enabled=(value)
|
225
|
-
|
226
|
-
|
237
|
+
if rendered?
|
238
|
+
@enabled = value
|
239
|
+
dom_element.prop('disabled', !@enabled)
|
240
|
+
else
|
241
|
+
enqueue_post_render_method_call('enabled=', value)
|
242
|
+
end
|
227
243
|
end
|
228
244
|
|
229
245
|
def foreground=(value)
|
230
|
-
|
231
|
-
|
232
|
-
|
246
|
+
if rendered?
|
247
|
+
value = ColorProxy.new(value) if value.is_a?(String)
|
248
|
+
@foreground = value
|
249
|
+
dom_element.css('color', foreground.to_css) unless foreground.nil?
|
250
|
+
else
|
251
|
+
enqueue_post_render_method_call('foreground=', value)
|
252
|
+
end
|
233
253
|
end
|
234
254
|
|
235
255
|
def background=(value)
|
236
|
-
|
237
|
-
|
238
|
-
|
256
|
+
if rendered?
|
257
|
+
value = ColorProxy.new(value) if value.is_a?(String)
|
258
|
+
@background = value
|
259
|
+
dom_element.css('background-color', background.to_css) unless background.nil?
|
260
|
+
else
|
261
|
+
enqueue_post_render_method_call('background=', value)
|
262
|
+
end
|
239
263
|
end
|
240
264
|
|
241
265
|
def parent_selector
|
@@ -243,8 +267,8 @@ module Glimmer
|
|
243
267
|
end
|
244
268
|
|
245
269
|
def parent_dom_element
|
246
|
-
if
|
247
|
-
|
270
|
+
if parent
|
271
|
+
parent.dom_element
|
248
272
|
else
|
249
273
|
options[:parent] ||= 'body'
|
250
274
|
the_element = Document.find(options[:parent])
|
@@ -272,26 +296,12 @@ module Glimmer
|
|
272
296
|
else
|
273
297
|
reattach(old_element)
|
274
298
|
end
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
children.each do |child|
|
282
|
-
child.render
|
283
|
-
end
|
284
|
-
end
|
285
|
-
@rendered = true
|
286
|
-
unless skip_content_on_render_blocks?
|
287
|
-
content_on_render_blocks.each do |content_block|
|
288
|
-
content(&content_block)
|
289
|
-
end
|
290
|
-
end
|
291
|
-
# TODO replace following line with a method call like (`notify_listeners('on_render')`)
|
292
|
-
listeners_for('on_render').each do |listener|
|
293
|
-
listener.original_event_listener.call(EventProxy.new(listener: listener))
|
294
|
-
end
|
299
|
+
mark_rendered
|
300
|
+
invoke_post_render_method_calls if bulk_render?
|
301
|
+
handle_observation_requests
|
302
|
+
children.each(&:render) if !bulk_render? && !render_after_create?
|
303
|
+
add_contents_for_render_blocks
|
304
|
+
notify_on_render_listeners
|
295
305
|
end
|
296
306
|
alias rerender render
|
297
307
|
|
@@ -303,8 +313,17 @@ module Glimmer
|
|
303
313
|
old_element.replace_with(@dom)
|
304
314
|
end
|
305
315
|
|
306
|
-
def
|
307
|
-
|
316
|
+
def mark_rendered
|
317
|
+
@rendered = true
|
318
|
+
children.each(&:mark_rendered) if bulk_render?
|
319
|
+
end
|
320
|
+
|
321
|
+
def add_text_content(text, on_empty: false)
|
322
|
+
if rendered?
|
323
|
+
dom_element.append(text.to_s) if !on_empty || dom_element.text.to_s.empty?
|
324
|
+
else
|
325
|
+
enqueue_post_render_method_call('add_text_content', text, on_empty:)
|
326
|
+
end
|
308
327
|
end
|
309
328
|
|
310
329
|
def content_on_render_blocks
|
@@ -332,11 +351,16 @@ module Glimmer
|
|
332
351
|
# TODO auto-convert known glimmer attributes like parent to data attributes like data-parent
|
333
352
|
# TODO check if we need to avoid rendering content block if no content is available
|
334
353
|
@dom ||= begin
|
335
|
-
content = args.first
|
354
|
+
content = args.first.is_a?(String) ? args.first : ''
|
355
|
+
content += children_dom_content if bulk_render?
|
336
356
|
ElementProxy.render_html(keyword, html_options, content)
|
337
357
|
end
|
338
358
|
end
|
339
359
|
|
360
|
+
def children_dom_content
|
361
|
+
children.map(&:dom).join
|
362
|
+
end
|
363
|
+
|
340
364
|
def html_options
|
341
365
|
body_class = ([name, element_id] + css_classes.to_a).join(' ')
|
342
366
|
html_options = options.dup
|
@@ -351,8 +375,12 @@ module Glimmer
|
|
351
375
|
html_options
|
352
376
|
end
|
353
377
|
|
354
|
-
def content(&block)
|
355
|
-
|
378
|
+
def content(bulk_render: false, &block)
|
379
|
+
original_bulk_render = options[:bulk_render]
|
380
|
+
options[:bulk_render] = bulk_render if rendered?
|
381
|
+
return_value = Glimmer::DSL::Engine.add_content(self, Glimmer::DSL::Web::ElementExpression.new, keyword, &block)
|
382
|
+
options[:bulk_render] = original_bulk_render if rendered?
|
383
|
+
return_value
|
356
384
|
end
|
357
385
|
|
358
386
|
# Subclasses must override with their own mappings
|
@@ -372,13 +400,21 @@ module Glimmer
|
|
372
400
|
end
|
373
401
|
|
374
402
|
def class_name=(value)
|
375
|
-
|
376
|
-
|
377
|
-
|
403
|
+
if rendered?
|
404
|
+
value = value.is_a?(Array) ? value.join(' ') : value.to_s
|
405
|
+
new_class_name = "#{name} #{element_id} #{value}"
|
406
|
+
dom_element.prop('className', new_class_name)
|
407
|
+
else
|
408
|
+
enqueue_post_render_method_call('class_name=', value)
|
409
|
+
end
|
378
410
|
end
|
379
411
|
|
380
412
|
def add_css_class(css_class)
|
381
|
-
|
413
|
+
if rendered?
|
414
|
+
dom_element.add_class(css_class)
|
415
|
+
else
|
416
|
+
enqueue_post_render_method_call('class_name=', value)
|
417
|
+
end
|
382
418
|
end
|
383
419
|
|
384
420
|
def add_css_classes(css_classes_to_add)
|
@@ -386,7 +422,11 @@ module Glimmer
|
|
386
422
|
end
|
387
423
|
|
388
424
|
def remove_css_class(css_class)
|
389
|
-
|
425
|
+
if rendered?
|
426
|
+
dom_element.remove_class(css_class)
|
427
|
+
else
|
428
|
+
enqueue_post_render_method_call('class_name=', value)
|
429
|
+
end
|
390
430
|
end
|
391
431
|
|
392
432
|
def remove_css_classes(css_classes_to_remove)
|
@@ -397,38 +437,17 @@ module Glimmer
|
|
397
437
|
css_classes.each {|css_class| remove_css_class(css_class)}
|
398
438
|
end
|
399
439
|
|
400
|
-
def has_style?(symbol)
|
401
|
-
@args.include?(symbol) # not a very solid implementation. Bring SWT constants eventually
|
402
|
-
end
|
403
|
-
|
404
440
|
def dom_element
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
def style_element
|
412
|
-
style_element_id = "#{id}-style"
|
413
|
-
style_element_selector = "style##{style_element_id}"
|
414
|
-
element = dom_element.find(style_element_selector)
|
415
|
-
if element.empty?
|
416
|
-
new_element = Element.new(:style)
|
417
|
-
new_element.attr('id', style_element_id)
|
418
|
-
new_element.attr('class', "#{name.gsub('_', '-')}-instance-style widget-instance-style")
|
419
|
-
dom_element.prepend(new_element)
|
420
|
-
element = dom_element.find(style_element_selector)
|
441
|
+
if rendered?
|
442
|
+
# TODO consider making this pick an element in relation to its parent, allowing unhooked dom elements to be built if needed (unhooked to the visible page dom)
|
443
|
+
Document.find(selector)
|
444
|
+
else
|
445
|
+
# Using a fill-in dom element until self is rendered
|
446
|
+
ElementProxy.unrendered_dom_element(keyword)
|
421
447
|
end
|
422
|
-
element
|
423
|
-
end
|
424
|
-
|
425
|
-
def listener_selector
|
426
|
-
selector
|
427
448
|
end
|
428
449
|
|
429
|
-
|
430
|
-
Document.find(listener_selector)
|
431
|
-
end
|
450
|
+
# TODO consider adding a default #dom method implementation for the common case, automatically relying on #element and other methods to build the dom html
|
432
451
|
|
433
452
|
def observation_requests
|
434
453
|
@observation_requests ||= {}
|
@@ -473,16 +492,20 @@ module Glimmer
|
|
473
492
|
end
|
474
493
|
|
475
494
|
def handle_observation_request(keyword, original_event_listener)
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
495
|
+
if rendered?
|
496
|
+
listener = ListenerProxy.new(
|
497
|
+
element: self,
|
498
|
+
selector: selector,
|
499
|
+
dom_element: dom_element,
|
500
|
+
event_attribute: keyword,
|
501
|
+
original_event_listener: original_event_listener,
|
502
|
+
)
|
503
|
+
listener.register
|
504
|
+
listeners_for(keyword) << listener
|
505
|
+
listener
|
506
|
+
else
|
507
|
+
enqueue_post_render_method_call('handle_observation_request', keyword, original_event_listener)
|
508
|
+
end
|
486
509
|
end
|
487
510
|
|
488
511
|
def remove_event_listener_proxies
|
@@ -492,10 +515,29 @@ module Glimmer
|
|
492
515
|
event_listener_proxies.clear
|
493
516
|
end
|
494
517
|
|
518
|
+
def notify_listeners(event)
|
519
|
+
listeners_for(event).each do |listener|
|
520
|
+
listener.original_event_listener.call(EventProxy.new(listener: listener))
|
521
|
+
end
|
522
|
+
end
|
523
|
+
|
524
|
+
def notify_on_render_listeners
|
525
|
+
notify_listeners('on_render')
|
526
|
+
children.each(&:notify_on_render_listeners) if bulk_render?
|
527
|
+
end
|
528
|
+
|
495
529
|
def data_bindings
|
496
530
|
@data_bindings ||= {}
|
497
531
|
end
|
498
532
|
|
533
|
+
def type
|
534
|
+
if rendered?
|
535
|
+
super
|
536
|
+
else
|
537
|
+
options[:type] || 'text'
|
538
|
+
end
|
539
|
+
end
|
540
|
+
|
499
541
|
def data_bind(property, model_binding)
|
500
542
|
element_binding_translator = value_converters_for_input_type(type)[:model_to_view]
|
501
543
|
element_binding_parameters = [self, property, element_binding_translator]
|
@@ -521,13 +563,24 @@ module Glimmer
|
|
521
563
|
# Data-binds the generation of nested content to a model/property (in binding args)
|
522
564
|
# consider providing an option to avoid initial rendering without any changes happening
|
523
565
|
def bind_content(*binding_args, &content_block)
|
524
|
-
# TODO in the future, consider optimizing code by diffing content if that makes sense
|
525
566
|
content_binding_work = proc do |*values|
|
567
|
+
# TODO in the future, consider optimizing code by diffing content if that makes sense (e.g. using opal-virtual-dom)
|
568
|
+
# To do so, we must avoid generating new content with new unique IDs/Classes and only append the new IDs classes after mounting
|
569
|
+
# TODO consider optimizing remove performance by doing clear instead and removing listeners separately
|
526
570
|
children.dup.each { |child| child.remove }
|
527
|
-
content(&content_block)
|
571
|
+
content(bulk_render: true, &content_block)
|
572
|
+
if bulk_render? && rendered?
|
573
|
+
self.inner_html = children_dom_content
|
574
|
+
children.each(&:mark_rendered)
|
575
|
+
children.each(&:invoke_post_render_method_calls)
|
576
|
+
children.each(&:handle_observation_requests)
|
577
|
+
children.each(&:add_contents_for_render_blocks)
|
578
|
+
children.each(&:notify_on_render_listeners)
|
579
|
+
end
|
528
580
|
end
|
581
|
+
model_binding_observer = Glimmer::DataBinding::ModelBinding.new(*binding_args)
|
529
582
|
content_binding_observer = Glimmer::DataBinding::Observer.proc(&content_binding_work)
|
530
|
-
content_binding_observer.observe(
|
583
|
+
content_binding_observer.observe(model_binding_observer)
|
531
584
|
content_binding_work.call # TODO inspect if we need to pass args here (from observed attributes) [but it's simpler not to pass anything at first]
|
532
585
|
end
|
533
586
|
|
@@ -551,35 +604,86 @@ module Glimmer
|
|
551
604
|
if method_name.to_s.start_with?('on_')
|
552
605
|
handle_observation_request(method_name, block)
|
553
606
|
elsif dom_element.respond_to?(method_name)
|
554
|
-
|
607
|
+
if rendered?
|
608
|
+
dom_element.send(method_name, *args, &block)
|
609
|
+
else
|
610
|
+
enqueue_post_render_method_call(method_name, *args, &block)
|
611
|
+
end
|
555
612
|
elsif !dom_element.prop(property_name).nil? && !dom_element.prop(property_name).is_a?(Proc)
|
556
|
-
if
|
557
|
-
|
613
|
+
if rendered?
|
614
|
+
if method_name.end_with?('=')
|
615
|
+
dom_element.prop(property_name, *args)
|
616
|
+
else
|
617
|
+
dom_element.prop(property_name)
|
618
|
+
end
|
558
619
|
else
|
559
|
-
|
620
|
+
enqueue_post_render_method_call(method_name, *args, &block)
|
560
621
|
end
|
561
622
|
elsif !dom_element.prop(unnormalized_property_name).nil? && !dom_element.prop(unnormalized_property_name).is_a?(Proc)
|
562
|
-
if
|
563
|
-
|
623
|
+
if rendered?
|
624
|
+
if method_name.end_with?('=')
|
625
|
+
dom_element.prop(unnormalized_property_name, *args)
|
626
|
+
else
|
627
|
+
dom_element.prop(unnormalized_property_name)
|
628
|
+
end
|
564
629
|
else
|
565
|
-
|
630
|
+
enqueue_post_render_method_call(method_name, *args, &block)
|
566
631
|
end
|
567
632
|
elsif dom_element && dom_element.length > 0
|
568
|
-
|
569
|
-
|
570
|
-
Native.call(dom_element, '0').method_missing(method_name.to_s.camelcase, *js_args)
|
571
|
-
rescue Exception => e
|
633
|
+
if rendered?
|
634
|
+
js_args = block.nil? ? args : (args + [block])
|
572
635
|
begin
|
573
|
-
Native.call(dom_element, '0').method_missing(method_name.to_s, *js_args)
|
636
|
+
Native.call(dom_element, '0').method_missing(method_name.to_s.camelcase, *js_args)
|
574
637
|
rescue Exception => e
|
575
|
-
|
638
|
+
begin
|
639
|
+
Native.call(dom_element, '0').method_missing(method_name.to_s, *js_args)
|
640
|
+
rescue Exception => e
|
641
|
+
super(method_name, *args, &block)
|
642
|
+
end
|
576
643
|
end
|
644
|
+
else
|
645
|
+
enqueue_post_render_method_call(method_name, *args, &block)
|
577
646
|
end
|
578
647
|
else
|
579
648
|
super(method_name, *args, &block)
|
580
649
|
end
|
581
650
|
end
|
582
651
|
|
652
|
+
def post_render_method_calls
|
653
|
+
@post_render_method_calls ||= []
|
654
|
+
end
|
655
|
+
|
656
|
+
def enqueue_post_render_method_call(method_name, *args, &block)
|
657
|
+
post_render_method_calls << [method_name, args, block]
|
658
|
+
nil
|
659
|
+
end
|
660
|
+
|
661
|
+
def invoke_post_render_method_calls
|
662
|
+
return unless rendered?
|
663
|
+
post_render_method_calls.each do |method_name, args, block|
|
664
|
+
send(method_name, *args, &block)
|
665
|
+
end
|
666
|
+
children.each(&:invoke_post_render_method_calls) if bulk_render?
|
667
|
+
end
|
668
|
+
|
669
|
+
def handle_observation_requests
|
670
|
+
observation_requests&.each do |keyword, event_listener_set|
|
671
|
+
event_listener_set.each do |event_listener|
|
672
|
+
handle_observation_request(keyword, event_listener)
|
673
|
+
end
|
674
|
+
end
|
675
|
+
children.each(&:handle_observation_requests) if bulk_render?
|
676
|
+
end
|
677
|
+
|
678
|
+
def add_contents_for_render_blocks
|
679
|
+
unless skip_content_on_render_blocks?
|
680
|
+
content_on_render_blocks.each do |content_block|
|
681
|
+
content(&content_block)
|
682
|
+
end
|
683
|
+
end
|
684
|
+
children.each(&:add_contents_for_render_blocks) if bulk_render?
|
685
|
+
end
|
686
|
+
|
583
687
|
def property_name_for(method_name)
|
584
688
|
attribute_name = method_name.end_with?('=') ? method_name.to_s[0...-1] : method_name.to_s
|
585
689
|
PROPERTY_ALIASES[attribute_name] || attribute_name.camelcase
|
@@ -100,8 +100,8 @@ Document.ready? do
|
|
100
100
|
|
101
101
|
div {
|
102
102
|
# Content Data-Binding is used to dynamically (re)generate content of div
|
103
|
-
# based on changes to @user.
|
104
|
-
content(@user, :
|
103
|
+
# based on changes to @user.address_count, replacing older content on every change
|
104
|
+
content(@user, :address_count) do
|
105
105
|
@user.addresses.each do |address|
|
106
106
|
div {
|
107
107
|
div(style: 'display: grid; grid-auto-columns: 80px 280px;') { |address_div|
|
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.3.0
|
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-06-
|
11
|
+
date: 2024-06-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: glimmer
|