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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ebc8e783bfbc67d9366e1887368e89bb7a75db102e56ca966268a2091c5a8e90
4
- data.tar.gz: a44da8782e6ca10c5183f4f7377eac39bb2b7af4d7e03f088d885a2fce34b219
3
+ metadata.gz: 6147bc2c609c3b5d7871c0d7f16b84f57c5b10c83c22a8ab63fb7e7120a69e65
4
+ data.tar.gz: e469cc54afb68870470d1bb5ab4a265000d87901873119e01c68f7586ebd883e
5
5
  SHA512:
6
- metadata.gz: 577aea1ffced7cb27937958583c877416921d3f784fd4a2a9589ed3a3b95f39037b1a22f596ab03ed2c336abdab080fce67533b4120e800cb81d3cef144954a8
7
- data.tar.gz: 05726c636474ad543649bb3200a51c2a238e79248f89207698b229a09fc3e3c6cb9b19489142d71c973505098c1809c8d2978c17ab8628ef9c339666f6fae2f5
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.2.7 (Beta)
1
+ # [<img src="https://raw.githubusercontent.com/AndyObtiva/glimmer/master/images/glimmer-logo-hi-res.png" height=85 />](https://github.com/AndyObtiva/glimmer) Glimmer DSL for Web 0.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 with Ruby! [Ruby in the Browser](https://www.youtube.com/watch?v=4AdcfbI6A4c) finally fulfills every 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.
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, :addresses) do
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.2.7'
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.2.7'
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.addresses, replacing older content on every change
2505
- content(@user, :addresses) do
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.2.7
1
+ 0.3.0
@@ -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.2.7 ruby lib
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.2.7"
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-15"
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) && parent.dom_element.text.to_s.empty?
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) && parent.dom_element.text.to_s.empty?
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
- # TODO double check every place we should call this method
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
- remove_all_listeners
170
- dom_element.remove
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
- @enabled = value
226
- dom_element.prop('disabled', !@enabled)
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
- value = ColorProxy.new(value) if value.is_a?(String)
231
- @foreground = value
232
- dom_element.css('color', foreground.to_css) unless foreground.nil?
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
- value = ColorProxy.new(value) if value.is_a?(String)
237
- @background = value
238
- dom_element.css('background-color', background.to_css) unless background.nil?
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 parent_selector
247
- Document.find(parent_selector)
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
- observation_requests&.each do |keyword, event_listener_set|
276
- event_listener_set.each do |event_listener|
277
- handle_observation_request(keyword, event_listener)
278
- end
279
- end
280
- unless render_after_create?
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 add_text_content(text)
307
- dom_element.append(text.to_s)
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 if args.first.is_a?(String)
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
- Glimmer::DSL::Engine.add_content(self, Glimmer::DSL::Web::ElementExpression.new, keyword, &block)
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
- value = value.is_a?(Array) ? value.join(' ') : value.to_s
376
- new_class_name = "#{name} #{element_id} #{value}"
377
- dom_element.prop('className', new_class_name)
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
- dom_element.add_class(css_class)
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
- dom_element.remove_class(css_class)
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
- # 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)
406
- Document.find(selector)
407
- end
408
-
409
- # TODO consider adding a default #dom method implementation for the common case, automatically relying on #element and other methods to build the dom html
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
- def listener_dom_element
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
- listener = ListenerProxy.new(
477
- element: self,
478
- selector: selector,
479
- dom_element: dom_element,
480
- event_attribute: keyword,
481
- original_event_listener: original_event_listener,
482
- )
483
- listener.register
484
- listeners_for(keyword) << listener
485
- listener
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(*binding_args)
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
- dom_element.send(method_name, *args, &block)
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 method_name.end_with?('=')
557
- dom_element.prop(property_name, *args)
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
- dom_element.prop(property_name)
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 method_name.end_with?('=')
563
- dom_element.prop(unnormalized_property_name, *args)
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
- dom_element.prop(unnormalized_property_name)
630
+ enqueue_post_render_method_call(method_name, *args, &block)
566
631
  end
567
632
  elsif dom_element && dom_element.length > 0
568
- js_args = block.nil? ? args : (args + [block])
569
- begin
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
- super(method_name, *args, &block)
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.addresses, replacing older content on every change
104
- content(@user, :addresses) do
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.2.7
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-15 00:00:00.000000000 Z
11
+ date: 2024-06-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: glimmer