glimmer-dsl-web 0.0.7 → 0.0.8

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: 2615511253319f0227b782825764b4b009c0a3d4072b366d7409a3255f653d47
4
- data.tar.gz: 0d09f5a38cd26cd6f8edad82aab884b89178ffa79043e5e7dc88fbd19f2e5958
3
+ metadata.gz: '028be0c05ee74f8e639b3b43adbdd52e7b90e7e5b52a0e7026fbf74e0d017300'
4
+ data.tar.gz: d2b8e3fc639dfa55655f05faf740fd36fcf518bc143eb8f08a874be3e34f72ec
5
5
  SHA512:
6
- metadata.gz: 54ce5174ba09451931424f9c7d30b4abc7aebfd8486ecb6fe0863ece4a4747c908c9445753cc68e535bfb90835d1ad1951fde8f31f9fe2e90f34980754b4ecbf
7
- data.tar.gz: d654378ed6fa4c7d05322c09054a44039d83ef2d4dcecc048963b2dc31c723a68c679072f71da2a1984ba94cd4630a382d363928ca57315ca673ae33dfe62d59
6
+ metadata.gz: e9a0add34e7db5cd7d0816e0006ed0426f9bc739402e4951f672195f6dcce472943b5f5c501213269b0ec84fb636aaea701fe35326639827bd580b94db370a5a
7
+ data.tar.gz: 19ccf1280735e472c6f643a21e105f0b1020fdacc53a66f63588be6f7b8faddb1e8391667094687a204c3d1363ee760c6b752c84866b86323437c17b8fdbe7d6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Change Log
2
2
 
3
+ ## 0.0.8
4
+
5
+ - Validate that element keywords belong to the valid list of HTML element tag names to prevent rendering fake elements the keyword does not match a real HTML element tag
6
+ - Support inner_html/outer_html properties (it seems they did not convert to innerHTML/outerHTML correctly because of the all caps HTML)
7
+ - Support element properties via original JS names (e.g. innerHTML) to prevent logic from breaking if some do not convert correctly from underscored versions
8
+ - Package dependencies properly in the gem to avoid instructing users to add in Rails Gemfile manually
9
+ - Content Data-Binding support to regenerate element content based on changes to an observed model attribute.
10
+ Content Data-Binding Example:
11
+ content(*data_binding_options) { |data_binding_value|
12
+ li {
13
+ data_binding_value
14
+ }
15
+ }
16
+ - Ensuring removing data-binding model listeners when calling `element#remove` in addition to already removing standard HTML event listeners
17
+ - New Hello, Content Data-Binding! Sample: `require 'glimmer-dsl-web/samples/hello/hello_content_data_binding'`
18
+
3
19
  ## 0.0.7
4
20
 
5
21
  - Support input[type=number] value data-binding as a Ruby Numeric object (Integer or Float)
data/README.md CHANGED
@@ -1,9 +1,11 @@
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.0.7 (Early Alpha)
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.0.8 (Early Alpha)
2
2
  ## Ruby in the Browser Web GUI Frontend Library
3
3
  [![Gem Version](https://badge.fury.io/rb/glimmer-dsl-web.svg)](http://badge.fury.io/rb/glimmer-dsl-web)
4
4
  [![Join the chat at https://gitter.im/AndyObtiva/glimmer](https://badges.gitter.im/AndyObtiva/glimmer.svg)](https://gitter.im/AndyObtiva/glimmer?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
5
5
 
6
- [Glimmer](https://github.com/AndyObtiva/glimmer) DSL for Web enables building Web GUI 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 aims at providing the simplest, most intuitive, and most straight-forward frontend library in existence. The library 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)) while supporting both Unidirectional (One-Way) Data-Binding (using `<=`) and Bidirectional (Two-Way) Data-Binding (using `<=>`). 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)!
6
+ [Glimmer](https://github.com/AndyObtiva/glimmer) DSL for Web enables building Web GUI 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 aims at providing the simplest, most intuitive, most straight-forward, and most productive frontend library in existence. The library 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)) while supporting 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. 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)!
7
+
8
+ (the project plans to add component support very soon, albeit components are already supported by creating your own Ruby classes and having them render part of the GUI hierarchy)
7
9
 
8
10
  **Hello, World! Sample**
9
11
 
@@ -337,6 +339,137 @@ Screenshot:
337
339
 
338
340
  ![Hello, Data-Binding!](/images/glimmer-dsl-web-samples-hello-hello-data-binding.gif)
339
341
 
342
+ **Hello, Content Data-Binding!**
343
+
344
+ If you need to regenerate HTML element content dynamically, you can use Content Data-Binding to effortlessly
345
+ rebuild HTML elements based on changes in a Model attribute that provides the source data.
346
+ In this example, we generate multiple address forms based on the number of addresses the user has.
347
+
348
+ Glimmer GUI code:
349
+
350
+ ```ruby
351
+ require 'glimmer-dsl-web'
352
+
353
+ class Address
354
+ attr_accessor :text
355
+ attr_reader :name, :street, :city, :state, :zip
356
+
357
+ def name=(value)
358
+ @name = value
359
+ update_text
360
+ end
361
+
362
+ def street=(value)
363
+ @street = value
364
+ update_text
365
+ end
366
+
367
+ def city=(value)
368
+ @city = value
369
+ update_text
370
+ end
371
+
372
+ def state=(value)
373
+ @state = value
374
+ update_text
375
+ end
376
+
377
+ def zip=(value)
378
+ @zip = value
379
+ update_text
380
+ end
381
+
382
+ private
383
+
384
+ def update_text
385
+ self.text = [name, street, city, state, zip].compact.reject(&:empty?).join(', ')
386
+ end
387
+ end
388
+
389
+ class User
390
+ attr_accessor :addresses
391
+ attr_reader :address_count
392
+
393
+ def initialize
394
+ @address_count = 1
395
+ @addresses = []
396
+ update_addresses
397
+ end
398
+
399
+ def address_count=(value)
400
+ value = [[1, value.to_i].max, 3].min
401
+ @address_count = value
402
+ update_addresses
403
+ end
404
+
405
+ private
406
+
407
+ def update_addresses
408
+ address_count_change = address_count - addresses.size
409
+ if address_count_change > 0
410
+ address_count_change.times { addresses << Address.new }
411
+ else
412
+ address_count_change.abs.times { addresses.pop }
413
+ end
414
+ end
415
+ end
416
+
417
+ @user = User.new
418
+
419
+ div {
420
+ div {
421
+ label('Number of addresses: ', for: 'address-count-field')
422
+ input(id: 'address-count-field', type: 'number', min: 1, max: 3) {
423
+ value <=> [@user, :address_count]
424
+ }
425
+ }
426
+
427
+ div {
428
+ # Content Data-Binding is used to dynamically (re)generate content of div
429
+ # based on changes to @user.addresses, replacing older content on every change
430
+ content(@user, :addresses) do
431
+ @user.addresses.each do |address|
432
+ div {
433
+ div(style: 'display: grid; grid-auto-columns: 80px 280px;') { |address_div|
434
+ [:name, :street, :city, :state, :zip].each do |attribute|
435
+ label(attribute.to_s.capitalize, for: "#{attribute}-field")
436
+ input(id: "#{attribute}-field", type: 'text') {
437
+ value <=> [address, attribute]
438
+ }
439
+ end
440
+
441
+ div(style: 'grid-column: 1 / span 2;') {
442
+ inner_text <= [address, :text]
443
+ }
444
+
445
+ style {
446
+ <<~CSS
447
+ #{address_div.selector} {
448
+ margin: 10px 0;
449
+ }
450
+ #{address_div.selector} * {
451
+ margin: 5px;
452
+ }
453
+ #{address_div.selector} label {
454
+ grid-column: 1;
455
+ }
456
+ #{address_div.selector} input, #{address_div.selector} select {
457
+ grid-column: 2;
458
+ }
459
+ CSS
460
+ }
461
+ }
462
+ }
463
+ end
464
+ end
465
+ }
466
+ }.render
467
+ ```
468
+
469
+ Screenshot:
470
+
471
+ ![Hello, Content Data-Binding!](/images/glimmer-dsl-web-samples-hello-hello-content-data-binding.gif)
472
+
340
473
  **Button Counter Sample**
341
474
 
342
475
  **UPCOMING (NOT RELEASED OR SUPPORTED YET)**
@@ -441,6 +574,7 @@ Learn more about the differences between various [Glimmer](https://github.com/An
441
574
  - [Hello, Button!](#hello-button)
442
575
  - [Hello, Form!](#hello-form)
443
576
  - [Hello, Data-Binding!](#hello-data-binding)
577
+ - [Hello, Content Data-Binding!](#hello-content-data-binding)
444
578
  - [Hello, Input (Date/Time)!](#hello-input-datetime)
445
579
  - [Button Counter](#button-counter)
446
580
  - [Glimmer Process](#glimmer-process)
@@ -487,13 +621,7 @@ rails new glimmer_app_server
487
621
  Add the following to `Gemfile`:
488
622
 
489
623
  ```
490
- gem 'opal', '1.4.1'
491
- gem 'opal-rails', '2.0.2'
492
- gem 'opal-async', '~> 1.4.0'
493
- gem 'opal-jquery', '~> 0.4.6'
494
- gem 'glimmer-dsl-web', '~> 0.0.7'
495
- gem 'glimmer-dsl-xml', '~> 1.3.1', require: false
496
- gem 'glimmer-dsl-css', '~> 1.2.1', require: false
624
+ gem 'glimmer-dsl-web', '~> 0.0.8'
497
625
  ```
498
626
 
499
627
  Run:
@@ -632,13 +760,7 @@ Disable the `webpacker` gem line in `Gemfile`:
632
760
  Add the following to `Gemfile`:
633
761
 
634
762
  ```ruby
635
- gem 'opal', '1.4.1'
636
- gem 'opal-rails', '2.0.2'
637
- gem 'opal-async', '~> 1.4.0'
638
- gem 'opal-jquery', '~> 0.4.6'
639
- gem 'glimmer-dsl-web', '~> 0.0.7'
640
- gem 'glimmer-dsl-xml', '~> 1.3.1', require: false
641
- gem 'glimmer-dsl-css', '~> 1.2.1', require: false
763
+ gem 'glimmer-dsl-web', '~> 0.0.8'
642
764
  ```
643
765
 
644
766
  Run:
@@ -1254,6 +1376,133 @@ Screenshot:
1254
1376
 
1255
1377
  ![Hello, Data-Binding!](/images/glimmer-dsl-web-samples-hello-hello-data-binding.gif)
1256
1378
 
1379
+ #### Hello, Content Data-Binding!
1380
+
1381
+ Glimmer GUI code:
1382
+
1383
+ ```ruby
1384
+ require 'glimmer-dsl-web'
1385
+
1386
+ class Address
1387
+ attr_accessor :text
1388
+ attr_reader :name, :street, :city, :state, :zip
1389
+
1390
+ def name=(value)
1391
+ @name = value
1392
+ update_text
1393
+ end
1394
+
1395
+ def street=(value)
1396
+ @street = value
1397
+ update_text
1398
+ end
1399
+
1400
+ def city=(value)
1401
+ @city = value
1402
+ update_text
1403
+ end
1404
+
1405
+ def state=(value)
1406
+ @state = value
1407
+ update_text
1408
+ end
1409
+
1410
+ def zip=(value)
1411
+ @zip = value
1412
+ update_text
1413
+ end
1414
+
1415
+ private
1416
+
1417
+ def update_text
1418
+ self.text = [name, street, city, state, zip].compact.reject(&:empty?).join(', ')
1419
+ end
1420
+ end
1421
+
1422
+ class User
1423
+ attr_accessor :addresses
1424
+ attr_reader :address_count
1425
+
1426
+ def initialize
1427
+ @address_count = 1
1428
+ @addresses = []
1429
+ update_addresses
1430
+ end
1431
+
1432
+ def address_count=(value)
1433
+ value = [[1, value.to_i].max, 3].min
1434
+ @address_count = value
1435
+ update_addresses
1436
+ end
1437
+
1438
+ private
1439
+
1440
+ def update_addresses
1441
+ address_count_change = address_count - addresses.size
1442
+ if address_count_change > 0
1443
+ address_count_change.times { addresses << Address.new }
1444
+ else
1445
+ address_count_change.abs.times { addresses.pop }
1446
+ end
1447
+ end
1448
+ end
1449
+
1450
+ @user = User.new
1451
+
1452
+ div {
1453
+ div {
1454
+ label('Number of addresses: ', for: 'address-count-field')
1455
+ input(id: 'address-count-field', type: 'number', min: 1, max: 3) {
1456
+ value <=> [@user, :address_count]
1457
+ }
1458
+ }
1459
+
1460
+ div {
1461
+ # Content Data-Binding is used to dynamically (re)generate content of div
1462
+ # based on changes to @user.addresses, replacing older content on every change
1463
+ content(@user, :addresses) do
1464
+ @user.addresses.each do |address|
1465
+ div {
1466
+ div(style: 'display: grid; grid-auto-columns: 80px 280px;') { |address_div|
1467
+ [:name, :street, :city, :state, :zip].each do |attribute|
1468
+ label(attribute.to_s.capitalize, for: "#{attribute}-field")
1469
+ input(id: "#{attribute}-field", type: 'text') {
1470
+ value <=> [address, attribute]
1471
+ }
1472
+ end
1473
+
1474
+ div(style: 'grid-column: 1 / span 2;') {
1475
+ inner_text <= [address, :text]
1476
+ }
1477
+
1478
+ style {
1479
+ <<~CSS
1480
+ #{address_div.selector} {
1481
+ margin: 10px 0;
1482
+ }
1483
+ #{address_div.selector} * {
1484
+ margin: 5px;
1485
+ }
1486
+ #{address_div.selector} label {
1487
+ grid-column: 1;
1488
+ }
1489
+ #{address_div.selector} input, #{address_div.selector} select {
1490
+ grid-column: 2;
1491
+ }
1492
+ CSS
1493
+ }
1494
+ }
1495
+ }
1496
+ end
1497
+ end
1498
+ }
1499
+ }.render
1500
+ ```
1501
+
1502
+ Screenshot:
1503
+
1504
+ ![Hello, Content Data-Binding!](/images/glimmer-dsl-web-samples-hello-hello-content-data-binding.gif)
1505
+
1257
1506
  #### Hello, Input (Date/Time)!
1258
1507
 
1259
1508
  Glimmer GUI code:
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.7
1
+ 0.0.8
@@ -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.0.7 ruby lib
5
+ # stub: glimmer-dsl-web 0.0.8 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "glimmer-dsl-web".freeze
9
- s.version = "0.0.7".freeze
9
+ s.version = "0.0.8".freeze
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-01-02"
14
+ s.date = "2024-01-03"
15
15
  s.description = "Glimmer DSL for Web (Ruby in the Browser Web GUI Frontend Library) - Enables frontend GUI development with Ruby by adopting a DSL that follows web-like HTML syntax, enabling the transfer of HTML/CSS/JS skills to Ruby frontend development. This library relies on Opal Ruby.".freeze
16
16
  s.email = "andy.am@gmail.com".freeze
17
17
  s.extra_rdoc_files = [
@@ -31,6 +31,7 @@ Gem::Specification.new do |s|
31
31
  "lib/glimmer-dsl-web/ext/date.rb",
32
32
  "lib/glimmer-dsl-web/ext/exception.rb",
33
33
  "lib/glimmer-dsl-web/samples/hello/hello_button.rb",
34
+ "lib/glimmer-dsl-web/samples/hello/hello_content_data_binding.rb",
34
35
  "lib/glimmer-dsl-web/samples/hello/hello_data_binding.rb",
35
36
  "lib/glimmer-dsl-web/samples/hello/hello_form.rb",
36
37
  "lib/glimmer-dsl-web/samples/hello/hello_input_date_time.rb",
@@ -39,6 +40,7 @@ Gem::Specification.new do |s|
39
40
  "lib/glimmer/config/opal_logger.rb",
40
41
  "lib/glimmer/data_binding/element_binding.rb",
41
42
  "lib/glimmer/dsl/web/bind_expression.rb",
43
+ "lib/glimmer/dsl/web/content_data_binding_expression.rb",
42
44
  "lib/glimmer/dsl/web/data_binding_expression.rb",
43
45
  "lib/glimmer/dsl/web/dsl.rb",
44
46
  "lib/glimmer/dsl/web/element_expression.rb",
@@ -61,10 +63,13 @@ Gem::Specification.new do |s|
61
63
 
62
64
  s.specification_version = 4
63
65
 
64
- s.add_runtime_dependency(%q<glimmer>.freeze, ["~> 2.4.1".freeze])
66
+ s.add_runtime_dependency(%q<glimmer>.freeze, ["~> 2.7.6".freeze])
65
67
  s.add_runtime_dependency(%q<glimmer-dsl-xml>.freeze, ["~> 1.3.2".freeze])
66
68
  s.add_runtime_dependency(%q<glimmer-dsl-css>.freeze, ["~> 1.2.2".freeze])
69
+ s.add_runtime_dependency(%q<opal>.freeze, ["= 1.4.1".freeze])
70
+ s.add_runtime_dependency(%q<opal-rails>.freeze, ["= 2.0.2".freeze])
67
71
  s.add_runtime_dependency(%q<opal-async>.freeze, ["~> 1.4.0".freeze])
72
+ s.add_runtime_dependency(%q<opal-jquery>.freeze, ["~> 0.4.6".freeze])
68
73
  s.add_runtime_dependency(%q<to_collection>.freeze, [">= 2.0.1".freeze, "< 3.0.0".freeze])
69
74
  s.add_development_dependency(%q<puts_debuggerer>.freeze, [">= 0".freeze])
70
75
  s.add_development_dependency(%q<rake>.freeze, [">= 10.1.0".freeze, "< 14.0.0".freeze])
@@ -72,7 +77,5 @@ Gem::Specification.new do |s|
72
77
  s.add_development_dependency(%q<jeweler>.freeze, [">= 2.3.9".freeze, "< 3.0.0".freeze])
73
78
  s.add_development_dependency(%q<rdoc>.freeze, [">= 6.2.1".freeze, "< 7.0.0".freeze])
74
79
  s.add_development_dependency(%q<opal-rspec>.freeze, ["~> 0.8.0.alpha2".freeze])
75
- s.add_development_dependency(%q<opal-rails>.freeze, ["~> 1.1.2".freeze])
76
- s.add_development_dependency(%q<opal-jquery>.freeze, ["~> 0.4.4".freeze])
77
80
  end
78
81
 
@@ -0,0 +1,41 @@
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/expression'
23
+
24
+ module Glimmer
25
+ module DSL
26
+ module Web
27
+ class ContentDataBindingExpression < Expression
28
+ def can_interpret?(parent, keyword, *args, &block)
29
+ keyword == 'content' &&
30
+ block_given? &&
31
+ args.size > 0 &&
32
+ parent&.respond_to?(:bind_content)
33
+ end
34
+
35
+ def interpret(parent, keyword, *args, &block)
36
+ parent.bind_content(*args, &block)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,5 +1,4 @@
1
1
  require 'glimmer/dsl/engine'
2
- # Dir[File.expand_path('../*_expression.rb', __FILE__)].each {|f| require f}
3
2
  require 'glimmer/dsl/web/element_expression'
4
3
  require 'glimmer/dsl/web/listener_expression'
5
4
  require 'glimmer/dsl/web/property_expression'
@@ -7,25 +6,19 @@ require 'glimmer/dsl/web/p_expression'
7
6
  require 'glimmer/dsl/web/select_expression'
8
7
  require 'glimmer/dsl/web/bind_expression'
9
8
  require 'glimmer/dsl/web/data_binding_expression'
9
+ require 'glimmer/dsl/web/content_data_binding_expression'
10
10
  require 'glimmer/dsl/web/shine_data_binding_expression'
11
11
 
12
12
  module Glimmer
13
13
  module DSL
14
14
  module Web
15
- # TODO implement all those expressions
16
- # %w[
17
- # listener
18
- # data_binding
19
- # property
20
- # shine_data_binding
21
- # element
22
- # ]
23
15
  Engine.add_dynamic_expressions(
24
16
  Web,
25
17
  %w[
26
18
  listener
27
19
  data_binding
28
20
  property
21
+ content_data_binding
29
22
  shine_data_binding
30
23
  element
31
24
  ]
@@ -1,6 +1,8 @@
1
1
  require 'glimmer/dsl/expression'
2
2
  require 'glimmer/dsl/web/general_element_expression'
3
3
 
4
+ require 'glimmer/web/element_proxy'
5
+
4
6
  module Glimmer
5
7
  module DSL
6
8
  module Web
@@ -8,9 +10,7 @@ module Glimmer
8
10
  include GeneralElementExpression
9
11
 
10
12
  def can_interpret?(parent, keyword, *args, &block)
11
- # TODO automatically pass parent option as element if not passed instead of rejecting elements without a paraent nor root
12
- # TODO raise a proper error if root is an element that is not found (maybe do this in model)
13
- !keyword.to_s.start_with?('on')
13
+ Glimmer::Web::ElementProxy.keyword_supported?(keyword)
14
14
  end
15
15
  end
16
16
  end
@@ -25,6 +25,10 @@ module Glimmer
25
25
  module Web
26
26
  class ElementProxy
27
27
  class << self
28
+ def keyword_supported?(keyword)
29
+ ELEMENT_KEYWORDS.include?(keyword.to_s)
30
+ end
31
+
28
32
  # Factory Method that translates a Glimmer DSL keyword into a ElementProxy object
29
33
  def for(keyword, parent, args, block)
30
34
  element_type(keyword).new(keyword, parent, args, block)
@@ -67,7 +71,28 @@ module Glimmer
67
71
 
68
72
  Event = Struct.new(:widget, keyword_init: true)
69
73
 
74
+ ELEMENT_KEYWORDS = [
75
+ "a", "abbr", "acronym", "address", "applet", "area", "article", "aside", "audio", "b",
76
+ "base", "basefont", "bdi", "bdo", "bgsound", "big", "blink", "blockquote", "body", "br",
77
+ "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "content", "data",
78
+ "datalist", "dd", "decorator", "del", "details", "dfn", "dir", "div", "dl", "dt",
79
+ "element", "em", "embed", "fieldset", "figcaption", "figure", "font", "footer", "form", "frame",
80
+ "frameset", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup",
81
+ "hr", "html", "i", "iframe", "img", "input", "ins", "isindex", "kbd", "keygen",
82
+ "label", "legend", "li", "link", "listing", "main", "map", "mark", "marquee", "menu",
83
+ "menuitem", "meta", "meter", "nav", "nobr", "noframes", "noscript", "object", "ol", "optgroup",
84
+ "option", "output", "p", "param", "plaintext", "pre", "progress", "q", "rp", "rt",
85
+ "ruby", "s", "samp", "script", "section", "select", "shadow", "small", "source", "spacer",
86
+ "span", "strike", "strong", "style", "sub", "summary", "sup", "table", "tbody", "td",
87
+ "template", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt",
88
+ "u", "ul", "var", "video", "wbr", "xmp",
89
+ ]
90
+
70
91
  GLIMMER_ATTRIBUTES = [:parent]
92
+ PROPERTY_ALIASES = {
93
+ 'inner_html' => 'innerHTML',
94
+ 'outer_html' => 'outerHTML',
95
+ }
71
96
  FORMAT_DATETIME = '%Y-%m-%dT%H:%M'
72
97
  FORMAT_DATE = '%Y-%m-%d'
73
98
  FORMAT_TIME = '%H:%M'
@@ -123,6 +148,11 @@ module Glimmer
123
148
  listeners.each do |event, event_listeners|
124
149
  event_listeners.dup.each(&:unregister)
125
150
  end
151
+ listeners.clear
152
+ data_bindings.each do |element_binding, model_binding|
153
+ element_binding.unregister_all_observables
154
+ end
155
+ data_bindings.clear
126
156
  end
127
157
 
128
158
  # Subclasses can override with their own selector
@@ -423,6 +453,10 @@ module Glimmer
423
453
  event_listener_proxies.clear
424
454
  end
425
455
 
456
+ def data_bindings
457
+ @data_bindings ||= {}
458
+ end
459
+
426
460
  def data_bind(property, model_binding)
427
461
  element_binding_translator = value_converters_for_input_type(type)[:model_to_view]
428
462
  element_binding_parameters = [self, property, element_binding_translator]
@@ -430,6 +464,7 @@ module Glimmer
430
464
  element_binding.call(model_binding.evaluate_property)
431
465
  #TODO make this options observer dependent and all similar observers in element specific data binding handlers
432
466
  element_binding.observe(model_binding)
467
+ data_bindings[element_binding] = model_binding
433
468
  unless model_binding.binding_options[:read_only]
434
469
  # TODO add guards against nil cases for hash below
435
470
  listener_keyword = data_binding_listener_for_element_and_property(keyword, property)
@@ -444,19 +479,36 @@ module Glimmer
444
479
  end
445
480
  end
446
481
 
482
+ # Data-binds the generation of nested content to a model/property (in binding args)
483
+ # consider providing an option to avoid initial rendering without any changes happening
484
+ def bind_content(*binding_args, &content_block)
485
+ # TODO in the future, consider optimizing code by diffing content if that makes sense
486
+ content_binding_work = proc do |*values|
487
+ children.dup.each { |child| child.remove }
488
+ content(&content_block)
489
+ end
490
+ content_binding_observer = Glimmer::DataBinding::Observer.proc(&content_binding_work)
491
+ content_binding_observer.observe(*binding_args)
492
+ 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]
493
+ end
494
+
447
495
  def respond_to_missing?(method_name, include_private = false)
448
496
  # TODO consider doing more correct checking of availability of properties/methods using native `` ticks
449
497
  property_name = property_name_for(method_name)
498
+ unnormalized_property_name = unnormalized_property_name_for(method_name)
450
499
  super(method_name, include_private) ||
451
500
  (dom_element && dom_element.length > 0 && Native.call(dom_element, '0').respond_to?(method_name.to_s.camelcase, include_private)) ||
501
+ (dom_element && dom_element.length > 0 && Native.call(dom_element, '0').respond_to?(method_name.to_s, include_private)) ||
452
502
  dom_element.respond_to?(method_name, include_private) ||
453
503
  (!dom_element.prop(property_name).nil? && !dom_element.prop(property_name).is_a?(Proc)) ||
504
+ (!dom_element.prop(unnormalized_property_name).nil? && !dom_element.prop(unnormalized_property_name).is_a?(Proc)) ||
454
505
  method_name.to_s.start_with?('on_')
455
506
  end
456
507
 
457
508
  def method_missing(method_name, *args, &block)
458
509
  # TODO consider doing more correct checking of availability of properties/methods using native `` ticks
459
510
  property_name = property_name_for(method_name)
511
+ unnormalized_property_name = unnormalized_property_name_for(method_name)
460
512
  if method_name.to_s.start_with?('on_')
461
513
  handle_observation_request(method_name, block)
462
514
  elsif dom_element.respond_to?(method_name)
@@ -467,12 +519,22 @@ module Glimmer
467
519
  else
468
520
  dom_element.prop(property_name)
469
521
  end
522
+ elsif !dom_element.prop(unnormalized_property_name).nil? && !dom_element.prop(unnormalized_property_name).is_a?(Proc)
523
+ if method_name.end_with?('=')
524
+ dom_element.prop(unnormalized_property_name, *args)
525
+ else
526
+ dom_element.prop(unnormalized_property_name)
527
+ end
470
528
  elsif dom_element && dom_element.length > 0
529
+ js_args = block.nil? ? args : (args + [block])
471
530
  begin
472
- js_args = block.nil? ? args : (args + [block])
473
531
  Native.call(dom_element, '0').method_missing(method_name.to_s.camelcase, *js_args)
474
532
  rescue Exception => e
475
- super(method_name, *args, &block)
533
+ begin
534
+ Native.call(dom_element, '0').method_missing(method_name.to_s, *js_args)
535
+ rescue Exception => e
536
+ super(method_name, *args, &block)
537
+ end
476
538
  end
477
539
  else
478
540
  super(method_name, *args, &block)
@@ -480,7 +542,12 @@ module Glimmer
480
542
  end
481
543
 
482
544
  def property_name_for(method_name)
483
- method_name.end_with?('=') ? method_name.to_s[0...-1].camelcase : method_name.to_s.camelcase
545
+ attribute_name = method_name.end_with?('=') ? method_name.to_s[0...-1] : method_name.to_s
546
+ PROPERTY_ALIASES[attribute_name] || attribute_name.camelcase
547
+ end
548
+
549
+ def unnormalized_property_name_for(method_name)
550
+ method_name.end_with?('=') ? method_name.to_s[0...-1] : method_name.to_s
484
551
  end
485
552
 
486
553
  def swt_widget
@@ -34,7 +34,6 @@ module Glimmer
34
34
  @selector = selector
35
35
  @listener = listener
36
36
  @js_listener = lambda do |js_event|
37
- # TODO wrap event with a Ruby Event object before passing to listener
38
37
  event = EventProxy.new(js_event: js_event, listener: self)
39
38
  result = listener.call(event)
40
39
  result = true if result.nil?
@@ -0,0 +1,137 @@
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
+ class Address
25
+ attr_accessor :text
26
+ attr_reader :name, :street, :city, :state, :zip
27
+
28
+ def name=(value)
29
+ @name = value
30
+ update_text
31
+ end
32
+
33
+ def street=(value)
34
+ @street = value
35
+ update_text
36
+ end
37
+
38
+ def city=(value)
39
+ @city = value
40
+ update_text
41
+ end
42
+
43
+ def state=(value)
44
+ @state = value
45
+ update_text
46
+ end
47
+
48
+ def zip=(value)
49
+ @zip = value
50
+ update_text
51
+ end
52
+
53
+ private
54
+
55
+ def update_text
56
+ self.text = [name, street, city, state, zip].compact.reject(&:empty?).join(', ')
57
+ end
58
+ end
59
+
60
+ class User
61
+ attr_accessor :addresses
62
+ attr_reader :address_count
63
+
64
+ def initialize
65
+ @address_count = 1
66
+ @addresses = []
67
+ update_addresses
68
+ end
69
+
70
+ def address_count=(value)
71
+ value = [[1, value.to_i].max, 3].min
72
+ @address_count = value
73
+ update_addresses
74
+ end
75
+
76
+ private
77
+
78
+ def update_addresses
79
+ address_count_change = address_count - addresses.size
80
+ if address_count_change > 0
81
+ address_count_change.times { addresses << Address.new }
82
+ else
83
+ address_count_change.abs.times { addresses.pop }
84
+ end
85
+ end
86
+ end
87
+
88
+ @user = User.new
89
+
90
+ div {
91
+ div {
92
+ label('Number of addresses: ', for: 'address-count-field')
93
+ input(id: 'address-count-field', type: 'number', min: 1, max: 3) {
94
+ value <=> [@user, :address_count]
95
+ }
96
+ }
97
+
98
+ div {
99
+ # Content Data-Binding is used to dynamically (re)generate content of div
100
+ # based on changes to @user.addresses, replacing older content on every change
101
+ content(@user, :addresses) do
102
+ @user.addresses.each do |address|
103
+ div {
104
+ div(style: 'display: grid; grid-auto-columns: 80px 280px;') { |address_div|
105
+ [:name, :street, :city, :state, :zip].each do |attribute|
106
+ label(attribute.to_s.capitalize, for: "#{attribute}-field")
107
+ input(id: "#{attribute}-field", type: 'text') {
108
+ value <=> [address, attribute]
109
+ }
110
+ end
111
+
112
+ div(style: 'grid-column: 1 / span 2;') {
113
+ inner_text <= [address, :text]
114
+ }
115
+
116
+ style {
117
+ <<~CSS
118
+ #{address_div.selector} {
119
+ margin: 10px 0;
120
+ }
121
+ #{address_div.selector} * {
122
+ margin: 5px;
123
+ }
124
+ #{address_div.selector} label {
125
+ grid-column: 1;
126
+ }
127
+ #{address_div.selector} input, #{address_div.selector} select {
128
+ grid-column: 2;
129
+ }
130
+ CSS
131
+ }
132
+ }
133
+ }
134
+ end
135
+ end
136
+ }
137
+ }.render
@@ -26,7 +26,11 @@ GLIMMER_DSL_OPAL_LIB = File.join(GLIMMER_DSL_OPAL_ROOT, 'lib')
26
26
 
27
27
  $LOAD_PATH.unshift(GLIMMER_DSL_OPAL_LIB)
28
28
 
29
- if RUBY_ENGINE == 'opal'
29
+ if RUBY_ENGINE != 'opal'
30
+ require 'opal-rails'
31
+ require 'opal-async'
32
+ require 'opal-jquery'
33
+ else
30
34
  # GLIMMER_DSL_OPAL_MISSING = File.join(GLIMMER_DSL_OPAL_ROOT, 'lib', 'glimmer-dsl-opal', 'missing')
31
35
 
32
36
  # $LOAD_PATH.unshift(GLIMMER_DSL_OPAL_MISSING) # missing Ruby classes/methods
@@ -75,7 +79,7 @@ if RUBY_ENGINE == 'opal'
75
79
  require 'glimmer-dsl-xml'
76
80
  require 'glimmer-dsl-css'
77
81
 
78
- Glimmer::Config.loop_max_count = 150 # TODO consider disabling if preferred
82
+ Glimmer::Config.loop_max_count = 50 # TODO consider disabling if preferred
79
83
 
80
84
  original_logger_level = Glimmer::Config.logger.level
81
85
  Glimmer::Config.logger = Glimmer::Config::OpalLogger.new(STDOUT)
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.0.7
4
+ version: 0.0.8
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-01-02 00:00:00.000000000 Z
11
+ date: 2024-01-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: glimmer
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 2.4.1
19
+ version: 2.7.6
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 2.4.1
26
+ version: 2.7.6
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: glimmer-dsl-xml
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +52,34 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: 1.2.2
55
+ - !ruby/object:Gem::Dependency
56
+ name: opal
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '='
60
+ - !ruby/object:Gem::Version
61
+ version: 1.4.1
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '='
67
+ - !ruby/object:Gem::Version
68
+ version: 1.4.1
69
+ - !ruby/object:Gem::Dependency
70
+ name: opal-rails
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '='
74
+ - !ruby/object:Gem::Version
75
+ version: 2.0.2
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '='
81
+ - !ruby/object:Gem::Version
82
+ version: 2.0.2
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: opal-async
57
85
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +94,20 @@ dependencies:
66
94
  - - "~>"
67
95
  - !ruby/object:Gem::Version
68
96
  version: 1.4.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: opal-jquery
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 0.4.6
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 0.4.6
69
111
  - !ruby/object:Gem::Dependency
70
112
  name: to_collection
71
113
  requirement: !ruby/object:Gem::Requirement
@@ -188,34 +230,6 @@ dependencies:
188
230
  - - "~>"
189
231
  - !ruby/object:Gem::Version
190
232
  version: 0.8.0.alpha2
191
- - !ruby/object:Gem::Dependency
192
- name: opal-rails
193
- requirement: !ruby/object:Gem::Requirement
194
- requirements:
195
- - - "~>"
196
- - !ruby/object:Gem::Version
197
- version: 1.1.2
198
- type: :development
199
- prerelease: false
200
- version_requirements: !ruby/object:Gem::Requirement
201
- requirements:
202
- - - "~>"
203
- - !ruby/object:Gem::Version
204
- version: 1.1.2
205
- - !ruby/object:Gem::Dependency
206
- name: opal-jquery
207
- requirement: !ruby/object:Gem::Requirement
208
- requirements:
209
- - - "~>"
210
- - !ruby/object:Gem::Version
211
- version: 0.4.4
212
- type: :development
213
- prerelease: false
214
- version_requirements: !ruby/object:Gem::Requirement
215
- requirements:
216
- - - "~>"
217
- - !ruby/object:Gem::Version
218
- version: 0.4.4
219
233
  description: Glimmer DSL for Web (Ruby in the Browser Web GUI Frontend Library) -
220
234
  Enables frontend GUI development with Ruby by adopting a DSL that follows web-like
221
235
  HTML syntax, enabling the transfer of HTML/CSS/JS skills to Ruby frontend development.
@@ -239,6 +253,7 @@ files:
239
253
  - lib/glimmer-dsl-web/ext/date.rb
240
254
  - lib/glimmer-dsl-web/ext/exception.rb
241
255
  - lib/glimmer-dsl-web/samples/hello/hello_button.rb
256
+ - lib/glimmer-dsl-web/samples/hello/hello_content_data_binding.rb
242
257
  - lib/glimmer-dsl-web/samples/hello/hello_data_binding.rb
243
258
  - lib/glimmer-dsl-web/samples/hello/hello_form.rb
244
259
  - lib/glimmer-dsl-web/samples/hello/hello_input_date_time.rb
@@ -247,6 +262,7 @@ files:
247
262
  - lib/glimmer/config/opal_logger.rb
248
263
  - lib/glimmer/data_binding/element_binding.rb
249
264
  - lib/glimmer/dsl/web/bind_expression.rb
265
+ - lib/glimmer/dsl/web/content_data_binding_expression.rb
250
266
  - lib/glimmer/dsl/web/data_binding_expression.rb
251
267
  - lib/glimmer/dsl/web/dsl.rb
252
268
  - lib/glimmer/dsl/web/element_expression.rb