glimmer-dsl-web 0.0.7 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,9 +1,9 @@
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.9 (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](#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). And, modular design is supported with [Glimmer Web Components](#hello-component). 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
7
 
8
8
  **Hello, World! Sample**
9
9
 
@@ -337,87 +337,361 @@ Screenshot:
337
337
 
338
338
  ![Hello, Data-Binding!](/images/glimmer-dsl-web-samples-hello-hello-data-binding.gif)
339
339
 
340
- **Button Counter Sample**
340
+ **Hello, Content Data-Binding!**
341
341
 
342
- **UPCOMING (NOT RELEASED OR SUPPORTED YET)**
342
+ If you need to regenerate HTML element content dynamically, you can use Content Data-Binding to effortlessly
343
+ rebuild HTML elements based on changes in a Model attribute that provides the source data.
344
+ In this example, we generate multiple address forms based on the number of addresses the user has.
343
345
 
344
- Glimmer GUI code demonstrating MVC + Glimmer Web Components (Views) + Data-Binding:
346
+ Glimmer GUI code:
345
347
 
346
348
  ```ruby
347
349
  require 'glimmer-dsl-web'
348
350
 
349
- class Counter
350
- attr_accessor :count
351
+ class Address
352
+ attr_accessor :text
353
+ attr_reader :name, :street, :city, :state, :zip
354
+
355
+ def name=(value)
356
+ @name = value
357
+ update_text
358
+ end
359
+
360
+ def street=(value)
361
+ @street = value
362
+ update_text
363
+ end
364
+
365
+ def city=(value)
366
+ @city = value
367
+ update_text
368
+ end
369
+
370
+ def state=(value)
371
+ @state = value
372
+ update_text
373
+ end
374
+
375
+ def zip=(value)
376
+ @zip = value
377
+ update_text
378
+ end
379
+
380
+ private
381
+
382
+ def update_text
383
+ self.text = [name, street, city, state, zip].compact.reject(&:empty?).join(', ')
384
+ end
385
+ end
351
386
 
387
+ class User
388
+ attr_accessor :addresses
389
+ attr_reader :address_count
390
+
352
391
  def initialize
353
- self.count = 0
392
+ @address_count = 1
393
+ @addresses = []
394
+ update_addresses
395
+ end
396
+
397
+ def address_count=(value)
398
+ value = [[1, value.to_i].max, 3].min
399
+ @address_count = value
400
+ update_addresses
401
+ end
402
+
403
+ private
404
+
405
+ def update_addresses
406
+ address_count_change = address_count - addresses.size
407
+ if address_count_change > 0
408
+ address_count_change.times { addresses << Address.new }
409
+ else
410
+ address_count_change.abs.times { addresses.pop }
411
+ end
354
412
  end
413
+ end
355
414
 
356
- def increment!
357
- self.count += 1
415
+ @user = User.new
416
+
417
+ include Glimmer
418
+
419
+ Document.ready? do
420
+ div {
421
+ div {
422
+ label('Number of addresses: ', for: 'address-count-field')
423
+ input(id: 'address-count-field', type: 'number', min: 1, max: 3) {
424
+ value <=> [@user, :address_count]
425
+ }
426
+ }
427
+
428
+ div {
429
+ # Content Data-Binding is used to dynamically (re)generate content of div
430
+ # based on changes to @user.addresses, replacing older content on every change
431
+ content(@user, :addresses) do
432
+ @user.addresses.each do |address|
433
+ div {
434
+ div(style: 'display: grid; grid-auto-columns: 80px 280px;') { |address_div|
435
+ [:name, :street, :city, :state, :zip].each do |attribute|
436
+ label(attribute.to_s.capitalize, for: "#{attribute}-field")
437
+ input(id: "#{attribute}-field", type: 'text') {
438
+ value <=> [address, attribute]
439
+ }
440
+ end
441
+
442
+ div(style: 'grid-column: 1 / span 2;') {
443
+ inner_text <= [address, :text]
444
+ }
445
+
446
+ style {
447
+ <<~CSS
448
+ #{address_div.selector} {
449
+ margin: 10px 0;
450
+ }
451
+ #{address_div.selector} * {
452
+ margin: 5px;
453
+ }
454
+ #{address_div.selector} label {
455
+ grid-column: 1;
456
+ }
457
+ #{address_div.selector} input, #{address_div.selector} select {
458
+ grid-column: 2;
459
+ }
460
+ CSS
461
+ }
462
+ }
463
+ }
464
+ end
465
+ end
466
+ }
467
+ }.render
468
+ end
469
+ ```
470
+
471
+ Screenshot:
472
+
473
+ ![Hello, Content Data-Binding!](/images/glimmer-dsl-web-samples-hello-hello-content-data-binding.gif)
474
+
475
+ **Hello, Component!**
476
+
477
+ You can define Glimmer web components (View components) to reuse visual concepts to your heart's content,
478
+ by simply defining a class with `include Glimmer::Web::Component` and encasing the reusable markup inside
479
+ a `markup {...}` block. Glimmer web components automatically extend the Glimmer GUI DSL with new keywords
480
+ that match the underscored versions of the component class names (e.g. a `OrderSummary` class yields
481
+ the `order_summary` keyword for reusing that component within the Glimmer GUI DSL).
482
+ Below, we define an `AddressForm` component that generates a `address_form` keyword, and then we
483
+ reuse it twice inside an `AddressPage` component displaying a Shipping Address and a Billing Address.
484
+
485
+ Glimmer GUI code:
486
+
487
+ ```ruby
488
+ require 'glimmer-dsl-web'
489
+
490
+ Address = Struct.new(:full_name, :street, :street2, :city, :state, :zip_code, keyword_init: true) do
491
+ STATES = {
492
+ "AK"=>"Alaska",
493
+ "AL"=>"Alabama",
494
+ "AR"=>"Arkansas",
495
+ "AS"=>"American Samoa",
496
+ "AZ"=>"Arizona",
497
+ "CA"=>"California",
498
+ "CO"=>"Colorado",
499
+ "CT"=>"Connecticut",
500
+ "DC"=>"District of Columbia",
501
+ "DE"=>"Delaware",
502
+ "FL"=>"Florida",
503
+ "GA"=>"Georgia",
504
+ "GU"=>"Guam",
505
+ "HI"=>"Hawaii",
506
+ "IA"=>"Iowa",
507
+ "ID"=>"Idaho",
508
+ "IL"=>"Illinois",
509
+ "IN"=>"Indiana",
510
+ "KS"=>"Kansas",
511
+ "KY"=>"Kentucky",
512
+ "LA"=>"Louisiana",
513
+ "MA"=>"Massachusetts",
514
+ "MD"=>"Maryland",
515
+ "ME"=>"Maine",
516
+ "MI"=>"Michigan",
517
+ "MN"=>"Minnesota",
518
+ "MO"=>"Missouri",
519
+ "MS"=>"Mississippi",
520
+ "MT"=>"Montana",
521
+ "NC"=>"North Carolina",
522
+ "ND"=>"North Dakota",
523
+ "NE"=>"Nebraska",
524
+ "NH"=>"New Hampshire",
525
+ "NJ"=>"New Jersey",
526
+ "NM"=>"New Mexico",
527
+ "NV"=>"Nevada",
528
+ "NY"=>"New York",
529
+ "OH"=>"Ohio",
530
+ "OK"=>"Oklahoma",
531
+ "OR"=>"Oregon",
532
+ "PA"=>"Pennsylvania",
533
+ "PR"=>"Puerto Rico",
534
+ "RI"=>"Rhode Island",
535
+ "SC"=>"South Carolina",
536
+ "SD"=>"South Dakota",
537
+ "TN"=>"Tennessee",
538
+ "TX"=>"Texas",
539
+ "UT"=>"Utah",
540
+ "VA"=>"Virginia",
541
+ "VI"=>"Virgin Islands",
542
+ "VT"=>"Vermont",
543
+ "WA"=>"Washington",
544
+ "WI"=>"Wisconsin",
545
+ "WV"=>"West Virginia",
546
+ "WY"=>"Wyoming"
547
+ }
548
+
549
+ def state_code
550
+ STATES.invert[state]
551
+ end
552
+
553
+ def state_code=(value)
554
+ self.state = STATES[value]
555
+ end
556
+
557
+ def summary
558
+ to_h.values.map(&:to_s).reject(&:empty?).join(', ')
358
559
  end
359
560
  end
360
561
 
361
- class HelloButton
562
+ # AddressForm Glimmer Web Component (View component)
563
+ #
564
+ # Including Glimmer::Web::Component makes this class a View component and automatically
565
+ # generates a new Glimmer GUI DSL keyword that matches the lowercase underscored version
566
+ # of the name of the class. AddressForm generates address_form keyword, which can be used
567
+ # elsewhere in Glimmer GUI DSL code as done inside AddressPage below.
568
+ class AddressForm
362
569
  include Glimmer::Web::Component
363
570
 
364
- before_render do
365
- @counter = Counter.new
366
- end
571
+ option :address
572
+
573
+ # Optionally, you can execute code before rendering markup.
574
+ # This is useful for pre-setup of variables (e.g. Models) that you would use in the markup.
575
+ #
576
+ # before_render do
577
+ # end
367
578
 
579
+ # Optionally, you can execute code after rendering markup.
580
+ # This is useful for post-setup of extra Model listeners that would interact with the
581
+ # markup elements and expect them to be rendered already.
582
+ #
583
+ # after_render do
584
+ # end
585
+
586
+ # markup block provides the content of the
368
587
  markup {
369
- # This will hook into element #app-container and then build HTML inside it using Ruby DSL code
370
- div(parent: parent_selector) {
371
- text 'Button Counter'
372
-
373
- button {
374
- # Unidirectional Data-Binding indicating that on every change to @counter.count, the value
375
- # is read and converted to "Click To Increment: #{value} ", and then automatically
376
- # copied to button innerText (content) to display to the user
377
- inner_text <= [@counter, :count, on_read: ->(value) { "Click To Increment: #{value} " }]
588
+ div {
589
+ div(style: 'display: grid; grid-auto-columns: 80px 260px;') { |address_div|
590
+ label('Full Name: ', for: 'full-name-field')
591
+ input(id: 'full-name-field') {
592
+ value <=> [address, :full_name]
593
+ }
378
594
 
379
- onclick {
380
- @counter.increment!
595
+ @somelabel = label('Street: ', for: 'street-field')
596
+ input(id: 'street-field') {
597
+ value <=> [address, :street]
598
+ }
599
+
600
+ label('Street 2: ', for: 'street2-field')
601
+ textarea(id: 'street2-field') {
602
+ value <=> [address, :street2]
603
+ }
604
+
605
+ label('City: ', for: 'city-field')
606
+ input(id: 'city-field') {
607
+ value <=> [address, :city]
608
+ }
609
+
610
+ label('State: ', for: 'state-field')
611
+ select(id: 'state-field') {
612
+ Address::STATES.each do |state_code, state|
613
+ option(value: state_code) { state }
614
+ end
615
+
616
+ value <=> [address, :state_code]
617
+ }
618
+
619
+ label('Zip Code: ', for: 'zip-code-field')
620
+ input(id: 'zip-code-field', type: 'number', min: '0', max: '99999') {
621
+ value <=> [address, :zip_code,
622
+ on_write: :to_s,
623
+ ]
381
624
  }
625
+
626
+ style {
627
+ <<~CSS
628
+ #{address_div.selector} * {
629
+ margin: 5px;
630
+ }
631
+ #{address_div.selector} input, #{address_div.selector} select {
632
+ grid-column: 2;
633
+ }
634
+ CSS
635
+ }
636
+ }
637
+
638
+ div(style: 'margin: 5px') {
639
+ inner_text <= [address, :summary,
640
+ computed_by: address.members + ['state_code'],
641
+ ]
382
642
  }
383
643
  }
384
644
  }
385
645
  end
386
646
 
387
- HelloButton.render('#app-container')
388
- ```
389
-
390
- That produces:
391
-
392
- ```html
393
- <div id="application">
394
- <button>
395
- Click To Increment: 0
396
- </button>
397
- </div>
398
- ```
399
-
400
- When clicked:
401
-
402
- ```html
403
- <div id="application">
404
- <button>
405
- Click To Increment: 1
406
- </button>
407
- </div>
408
- ```
409
-
410
- When clicked 7 times:
647
+ # AddressPage Glimmer Web Component (View component)
648
+ #
649
+ # This View component represents the main page being rendered,
650
+ # as done by its `render` class method below
651
+ class AddressPage
652
+ include Glimmer::Web::Component
653
+
654
+ before_render do
655
+ @shipping_address = Address.new(
656
+ full_name: 'Johnny Doe',
657
+ street: '3922 Park Ave',
658
+ street2: 'PO BOX 8382',
659
+ city: 'San Diego',
660
+ state: 'California',
661
+ zip_code: '91913',
662
+ )
663
+ @billing_address = Address.new(
664
+ full_name: 'John C Doe',
665
+ street: '123 Main St',
666
+ street2: 'Apartment 3C',
667
+ city: 'San Diego',
668
+ state: 'California',
669
+ zip_code: '91911',
670
+ )
671
+ end
672
+
673
+ markup {
674
+ div {
675
+ h1('Shipping Address')
676
+
677
+ address_form(address: @shipping_address)
678
+
679
+ h1('Billing Address')
680
+
681
+ address_form(address: @billing_address)
682
+ }
683
+ }
684
+ end
411
685
 
412
- ```html
413
- <div id="application">
414
- <button>
415
- Click To Increment: 7
416
- </button>
417
- </div>
686
+ Document.ready? do
687
+ # renders a top-level (root) AddressPage component
688
+ AddressPage.render
689
+ end
418
690
  ```
419
691
 
692
+ Screenshot:
420
693
 
694
+ ![Hello, Component!](/images/glimmer-dsl-web-samples-hello-hello-component.png)
421
695
 
422
696
  NOTE: Glimmer DSL for Web is an Early Alpha project. If you want it developed faster, please [open an issue report](https://github.com/AndyObtiva/glimmer-dsl-web/issues/new). I have completed some GitHub project features much faster before due to [issue reports](https://github.com/AndyObtiva/glimmer-dsl-web/issues) and [pull requests](https://github.com/AndyObtiva/glimmer-dsl-web/pulls). Please help make better by contributing, adopting for small or low risk projects, and providing feedback. It is still an early alpha, so the more feedback and issues you report the better.
423
697
 
@@ -441,6 +715,8 @@ Learn more about the differences between various [Glimmer](https://github.com/An
441
715
  - [Hello, Button!](#hello-button)
442
716
  - [Hello, Form!](#hello-form)
443
717
  - [Hello, Data-Binding!](#hello-data-binding)
718
+ - [Hello, Content Data-Binding!](#hello-content-data-binding)
719
+ - [Hello, Component!](#hello-content-data-binding)
444
720
  - [Hello, Input (Date/Time)!](#hello-input-datetime)
445
721
  - [Button Counter](#button-counter)
446
722
  - [Glimmer Process](#glimmer-process)
@@ -487,13 +763,7 @@ rails new glimmer_app_server
487
763
  Add the following to `Gemfile`:
488
764
 
489
765
  ```
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
766
+ gem 'glimmer-dsl-web', '~> 0.0.9'
497
767
  ```
498
768
 
499
769
  Run:
@@ -508,11 +778,18 @@ Follow [opal-rails](https://github.com/opal/opal-rails) instructions, basically
508
778
  bin/rails g opal:install
509
779
  ```
510
780
 
511
- Edit `config/initializers/assets.rb` and add the following at the bottom:
512
- ```
781
+ To enable the `glimmer-dsl-web` library in the frontend, edit `config/initializers/assets.rb` and add the following at the bottom:
782
+
783
+ ```ruby
513
784
  Opal.use_gem 'glimmer-dsl-web'
514
785
  ```
515
786
 
787
+ To enable Opal Browser Debugging in Ruby with the [Source Maps](https://opalrb.com/docs/guides/v1.4.1/source_maps.html) feature, edit `config/initializers/opal.rb` and add the following inside the `Rails.application.configure do; end` block at the bottom of it:
788
+
789
+ ```ruby
790
+ config.assets.debug = true
791
+ ```
792
+
516
793
  Run:
517
794
 
518
795
  ```
@@ -532,12 +809,6 @@ mount Glimmer::Engine => "/glimmer" # add on top
532
809
  root to: 'welcomes#index'
533
810
  ```
534
811
 
535
- Edit `app/views/layouts/application.html.erb` and add the following below other `stylesheet_link_tag` declarations:
536
-
537
- ```erb
538
- <%= stylesheet_link_tag 'glimmer/glimmer', media: 'all', 'data-turbolinks-track': 'reload' %>
539
- ```
540
-
541
812
  Clear the file `app/views/welcomes/index.html.erb` completely from all content.
542
813
 
543
814
  Delete `app/javascript/application.js`
@@ -632,13 +903,7 @@ Disable the `webpacker` gem line in `Gemfile`:
632
903
  Add the following to `Gemfile`:
633
904
 
634
905
  ```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
906
+ gem 'glimmer-dsl-web', '~> 0.0.9'
642
907
  ```
643
908
 
644
909
  Run:
@@ -653,11 +918,18 @@ Follow [opal-rails](https://github.com/opal/opal-rails) instructions, basically
653
918
  bin/rails g opal:install
654
919
  ```
655
920
 
656
- Edit `config/initializers/assets.rb` and add the following at the bottom:
657
- ```
921
+ To enable the `glimmer-dsl-web` library in the frontend, edit `config/initializers/assets.rb` and add the following at the bottom:
922
+
923
+ ```ruby
658
924
  Opal.use_gem 'glimmer-dsl-web'
659
925
  ```
660
926
 
927
+ To enable Opal Browser Debugging in Ruby with the [Source Maps](https://opalrb.com/docs/guides/v1.4.1/source_maps.html) feature, edit `config/initializers/opal.rb` and add the following inside the `Rails.application.configure do; end` block at the bottom of it:
928
+
929
+ ```ruby
930
+ config.assets.debug = true
931
+ ```
932
+
661
933
  Run:
662
934
 
663
935
  ```
@@ -676,11 +948,6 @@ Add the following to `config/routes.rb` inside the `Rails.application.routes.dra
676
948
  mount Glimmer::Engine => "/glimmer" # add on top
677
949
  root to: 'welcomes#index'
678
950
  ```
679
-
680
- Edit `app/views/layouts/application.html.erb` and add the following below other `stylesheet_link_tag` declarations:
681
-
682
- ```erb
683
- <%= stylesheet_link_tag 'glimmer/glimmer', media: 'all', 'data-turbolinks-track': 'reload' %>
684
951
  ```
685
952
 
686
953
  Also, delete the following line:
@@ -764,7 +1031,7 @@ Glimmer DSL for Web offers a GUI DSL for building HTML Web User Interfaces decla
764
1031
 
765
1032
  1- **Keywords (HTML Elements)**
766
1033
 
767
- You can declare any HTML element by simply using the lowercase underscored version of its name (Ruby convention for method names) like `div`, `span`, `form`, `input`, `button`, `table`, `tr`, `th`, and `td`.
1034
+ You can declare any HTML element by simply using the lowercase version of its name (Ruby convention for method names) like `div`, `span`, `form`, `input`, `button`, `table`, `tr`, `th`, and `td`.
768
1035
 
769
1036
  Under the hood, HTML element DSL keywords are invoked as Ruby methods.
770
1037
 
@@ -900,6 +1167,8 @@ That produces the following under `<body></body>`:
900
1167
 
901
1168
  #### Hello, Button!
902
1169
 
1170
+ Event listeners can be setup on any element using the same event names used in HTML (e.g. `onclick`) while passing in a standard Ruby block to handle behavior. `$$` gives access to `window` to invoke functions like `alert`.
1171
+
903
1172
  Glimmer GUI code:
904
1173
 
905
1174
  ```ruby
@@ -932,6 +1201,8 @@ Screenshot:
932
1201
 
933
1202
  #### Hello, Form!
934
1203
 
1204
+ [Glimmer DSL for Web](https://rubygems.org/gems/glimmer-dsl-web) gives access to all Web Browser built-in features like HTML form validations, input focus, events, and element functions from a very terse and productive Ruby GUI DSL.
1205
+
935
1206
  Glimmer GUI code:
936
1207
 
937
1208
  ```ruby
@@ -1085,6 +1356,8 @@ Screenshot:
1085
1356
 
1086
1357
  #### Hello, Data-Binding!
1087
1358
 
1359
+ [Glimmer DSL for Web](https://rubygems.org/gems/glimmer-dsl-web) intuitively supports both Unidirectional (One-Way) Data-Binding via the `<=` operator and Bidirectional (Two-Way) Data-Binding via the `<=>` operator, incredibly simplifying how to sync View properties with Model attributes with the simplest code to reason about.
1360
+
1088
1361
  Glimmer GUI code:
1089
1362
 
1090
1363
  ```ruby
@@ -1254,6 +1527,362 @@ Screenshot:
1254
1527
 
1255
1528
  ![Hello, Data-Binding!](/images/glimmer-dsl-web-samples-hello-hello-data-binding.gif)
1256
1529
 
1530
+ #### Hello, Content Data-Binding!
1531
+
1532
+ If you need to regenerate HTML element content dynamically, you can use Content Data-Binding to effortlessly
1533
+ rebuild HTML elements based on changes in a Model attribute that provides the source data.
1534
+ In this example, we generate multiple address forms based on the number of addresses the user has.
1535
+
1536
+ Glimmer GUI code:
1537
+
1538
+ ```ruby
1539
+ require 'glimmer-dsl-web'
1540
+
1541
+ class Address
1542
+ attr_accessor :text
1543
+ attr_reader :name, :street, :city, :state, :zip
1544
+
1545
+ def name=(value)
1546
+ @name = value
1547
+ update_text
1548
+ end
1549
+
1550
+ def street=(value)
1551
+ @street = value
1552
+ update_text
1553
+ end
1554
+
1555
+ def city=(value)
1556
+ @city = value
1557
+ update_text
1558
+ end
1559
+
1560
+ def state=(value)
1561
+ @state = value
1562
+ update_text
1563
+ end
1564
+
1565
+ def zip=(value)
1566
+ @zip = value
1567
+ update_text
1568
+ end
1569
+
1570
+ private
1571
+
1572
+ def update_text
1573
+ self.text = [name, street, city, state, zip].compact.reject(&:empty?).join(', ')
1574
+ end
1575
+ end
1576
+
1577
+ class User
1578
+ attr_accessor :addresses
1579
+ attr_reader :address_count
1580
+
1581
+ def initialize
1582
+ @address_count = 1
1583
+ @addresses = []
1584
+ update_addresses
1585
+ end
1586
+
1587
+ def address_count=(value)
1588
+ value = [[1, value.to_i].max, 3].min
1589
+ @address_count = value
1590
+ update_addresses
1591
+ end
1592
+
1593
+ private
1594
+
1595
+ def update_addresses
1596
+ address_count_change = address_count - addresses.size
1597
+ if address_count_change > 0
1598
+ address_count_change.times { addresses << Address.new }
1599
+ else
1600
+ address_count_change.abs.times { addresses.pop }
1601
+ end
1602
+ end
1603
+ end
1604
+
1605
+ @user = User.new
1606
+
1607
+ include Glimmer
1608
+
1609
+ Document.ready? do
1610
+ div {
1611
+ div {
1612
+ label('Number of addresses: ', for: 'address-count-field')
1613
+ input(id: 'address-count-field', type: 'number', min: 1, max: 3) {
1614
+ value <=> [@user, :address_count]
1615
+ }
1616
+ }
1617
+
1618
+ div {
1619
+ # Content Data-Binding is used to dynamically (re)generate content of div
1620
+ # based on changes to @user.addresses, replacing older content on every change
1621
+ content(@user, :addresses) do
1622
+ @user.addresses.each do |address|
1623
+ div {
1624
+ div(style: 'display: grid; grid-auto-columns: 80px 280px;') { |address_div|
1625
+ [:name, :street, :city, :state, :zip].each do |attribute|
1626
+ label(attribute.to_s.capitalize, for: "#{attribute}-field")
1627
+ input(id: "#{attribute}-field", type: 'text') {
1628
+ value <=> [address, attribute]
1629
+ }
1630
+ end
1631
+
1632
+ div(style: 'grid-column: 1 / span 2;') {
1633
+ inner_text <= [address, :text]
1634
+ }
1635
+
1636
+ style {
1637
+ <<~CSS
1638
+ #{address_div.selector} {
1639
+ margin: 10px 0;
1640
+ }
1641
+ #{address_div.selector} * {
1642
+ margin: 5px;
1643
+ }
1644
+ #{address_div.selector} label {
1645
+ grid-column: 1;
1646
+ }
1647
+ #{address_div.selector} input, #{address_div.selector} select {
1648
+ grid-column: 2;
1649
+ }
1650
+ CSS
1651
+ }
1652
+ }
1653
+ }
1654
+ end
1655
+ end
1656
+ }
1657
+ }.render
1658
+ end
1659
+ ```
1660
+
1661
+ Screenshot:
1662
+
1663
+ ![Hello, Content Data-Binding!](/images/glimmer-dsl-web-samples-hello-hello-content-data-binding.gif)
1664
+
1665
+ #### Hello, Component!
1666
+
1667
+ You can define Glimmer web components (View components) to reuse visual concepts to your heart's content,
1668
+ by simply defining a class with `include Glimmer::Web::Component` and encasing the reusable markup inside
1669
+ a `markup {...}` block. Glimmer web components automatically extend the Glimmer GUI DSL with new keywords
1670
+ that match the underscored versions of the component class names (e.g. a `OrderSummary` class yields
1671
+ the `order_summary` keyword for reusing that component within the Glimmer GUI DSL).
1672
+ Below, we define an `AddressForm` component that generates a `address_form` keyword, and then we
1673
+ reuse it twice inside an `AddressPage` component displaying a Shipping Address and a Billing Address.
1674
+
1675
+ Glimmer GUI code:
1676
+
1677
+ ```ruby
1678
+ require 'glimmer-dsl-web'
1679
+
1680
+ Address = Struct.new(:full_name, :street, :street2, :city, :state, :zip_code, keyword_init: true) do
1681
+ STATES = {
1682
+ "AK"=>"Alaska",
1683
+ "AL"=>"Alabama",
1684
+ "AR"=>"Arkansas",
1685
+ "AS"=>"American Samoa",
1686
+ "AZ"=>"Arizona",
1687
+ "CA"=>"California",
1688
+ "CO"=>"Colorado",
1689
+ "CT"=>"Connecticut",
1690
+ "DC"=>"District of Columbia",
1691
+ "DE"=>"Delaware",
1692
+ "FL"=>"Florida",
1693
+ "GA"=>"Georgia",
1694
+ "GU"=>"Guam",
1695
+ "HI"=>"Hawaii",
1696
+ "IA"=>"Iowa",
1697
+ "ID"=>"Idaho",
1698
+ "IL"=>"Illinois",
1699
+ "IN"=>"Indiana",
1700
+ "KS"=>"Kansas",
1701
+ "KY"=>"Kentucky",
1702
+ "LA"=>"Louisiana",
1703
+ "MA"=>"Massachusetts",
1704
+ "MD"=>"Maryland",
1705
+ "ME"=>"Maine",
1706
+ "MI"=>"Michigan",
1707
+ "MN"=>"Minnesota",
1708
+ "MO"=>"Missouri",
1709
+ "MS"=>"Mississippi",
1710
+ "MT"=>"Montana",
1711
+ "NC"=>"North Carolina",
1712
+ "ND"=>"North Dakota",
1713
+ "NE"=>"Nebraska",
1714
+ "NH"=>"New Hampshire",
1715
+ "NJ"=>"New Jersey",
1716
+ "NM"=>"New Mexico",
1717
+ "NV"=>"Nevada",
1718
+ "NY"=>"New York",
1719
+ "OH"=>"Ohio",
1720
+ "OK"=>"Oklahoma",
1721
+ "OR"=>"Oregon",
1722
+ "PA"=>"Pennsylvania",
1723
+ "PR"=>"Puerto Rico",
1724
+ "RI"=>"Rhode Island",
1725
+ "SC"=>"South Carolina",
1726
+ "SD"=>"South Dakota",
1727
+ "TN"=>"Tennessee",
1728
+ "TX"=>"Texas",
1729
+ "UT"=>"Utah",
1730
+ "VA"=>"Virginia",
1731
+ "VI"=>"Virgin Islands",
1732
+ "VT"=>"Vermont",
1733
+ "WA"=>"Washington",
1734
+ "WI"=>"Wisconsin",
1735
+ "WV"=>"West Virginia",
1736
+ "WY"=>"Wyoming"
1737
+ }
1738
+
1739
+ def state_code
1740
+ STATES.invert[state]
1741
+ end
1742
+
1743
+ def state_code=(value)
1744
+ self.state = STATES[value]
1745
+ end
1746
+
1747
+ def summary
1748
+ to_h.values.map(&:to_s).reject(&:empty?).join(', ')
1749
+ end
1750
+ end
1751
+
1752
+ # AddressForm Glimmer Web Component (View component)
1753
+ #
1754
+ # Including Glimmer::Web::Component makes this class a View component and automatically
1755
+ # generates a new Glimmer GUI DSL keyword that matches the lowercase underscored version
1756
+ # of the name of the class. AddressForm generates address_form keyword, which can be used
1757
+ # elsewhere in Glimmer GUI DSL code as done inside AddressPage below.
1758
+ class AddressForm
1759
+ include Glimmer::Web::Component
1760
+
1761
+ option :address
1762
+
1763
+ # Optionally, you can execute code before rendering markup.
1764
+ # This is useful for pre-setup of variables (e.g. Models) that you would use in the markup.
1765
+ #
1766
+ # before_render do
1767
+ # end
1768
+
1769
+ # Optionally, you can execute code after rendering markup.
1770
+ # This is useful for post-setup of extra Model listeners that would interact with the
1771
+ # markup elements and expect them to be rendered already.
1772
+ #
1773
+ # after_render do
1774
+ # end
1775
+
1776
+ # markup block provides the content of the
1777
+ markup {
1778
+ div {
1779
+ div(style: 'display: grid; grid-auto-columns: 80px 260px;') { |address_div|
1780
+ label('Full Name: ', for: 'full-name-field')
1781
+ input(id: 'full-name-field') {
1782
+ value <=> [address, :full_name]
1783
+ }
1784
+
1785
+ @somelabel = label('Street: ', for: 'street-field')
1786
+ input(id: 'street-field') {
1787
+ value <=> [address, :street]
1788
+ }
1789
+
1790
+ label('Street 2: ', for: 'street2-field')
1791
+ textarea(id: 'street2-field') {
1792
+ value <=> [address, :street2]
1793
+ }
1794
+
1795
+ label('City: ', for: 'city-field')
1796
+ input(id: 'city-field') {
1797
+ value <=> [address, :city]
1798
+ }
1799
+
1800
+ label('State: ', for: 'state-field')
1801
+ select(id: 'state-field') {
1802
+ Address::STATES.each do |state_code, state|
1803
+ option(value: state_code) { state }
1804
+ end
1805
+
1806
+ value <=> [address, :state_code]
1807
+ }
1808
+
1809
+ label('Zip Code: ', for: 'zip-code-field')
1810
+ input(id: 'zip-code-field', type: 'number', min: '0', max: '99999') {
1811
+ value <=> [address, :zip_code,
1812
+ on_write: :to_s,
1813
+ ]
1814
+ }
1815
+
1816
+ style {
1817
+ <<~CSS
1818
+ #{address_div.selector} * {
1819
+ margin: 5px;
1820
+ }
1821
+ #{address_div.selector} input, #{address_div.selector} select {
1822
+ grid-column: 2;
1823
+ }
1824
+ CSS
1825
+ }
1826
+ }
1827
+
1828
+ div(style: 'margin: 5px') {
1829
+ inner_text <= [address, :summary,
1830
+ computed_by: address.members + ['state_code'],
1831
+ ]
1832
+ }
1833
+ }
1834
+ }
1835
+ end
1836
+
1837
+ # AddressPage Glimmer Web Component (View component)
1838
+ #
1839
+ # This View component represents the main page being rendered,
1840
+ # as done by its `render` class method below
1841
+ class AddressPage
1842
+ include Glimmer::Web::Component
1843
+
1844
+ before_render do
1845
+ @shipping_address = Address.new(
1846
+ full_name: 'Johnny Doe',
1847
+ street: '3922 Park Ave',
1848
+ street2: 'PO BOX 8382',
1849
+ city: 'San Diego',
1850
+ state: 'California',
1851
+ zip_code: '91913',
1852
+ )
1853
+ @billing_address = Address.new(
1854
+ full_name: 'John C Doe',
1855
+ street: '123 Main St',
1856
+ street2: 'Apartment 3C',
1857
+ city: 'San Diego',
1858
+ state: 'California',
1859
+ zip_code: '91911',
1860
+ )
1861
+ end
1862
+
1863
+ markup {
1864
+ div {
1865
+ h1('Shipping Address')
1866
+
1867
+ address_form(address: @shipping_address)
1868
+
1869
+ h1('Billing Address')
1870
+
1871
+ address_form(address: @billing_address)
1872
+ }
1873
+ }
1874
+ end
1875
+
1876
+ Document.ready? do
1877
+ # renders a top-level (root) AddressPage component
1878
+ AddressPage.render
1879
+ end
1880
+ ```
1881
+
1882
+ Screenshot:
1883
+
1884
+ ![Hello, Component!](/images/glimmer-dsl-web-samples-hello-hello-component.png)
1885
+
1257
1886
  #### Hello, Input (Date/Time)!
1258
1887
 
1259
1888
  Glimmer GUI code:
@@ -1408,7 +2037,7 @@ class HelloButton
1408
2037
  }
1409
2038
  end
1410
2039
 
1411
- HelloButton.render('#app-container')
2040
+ HelloButton.render
1412
2041
  ```
1413
2042
 
1414
2043
  That produces: