glimmer-dsl-web 0.0.8 → 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,11 +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.8 (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, 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)
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)!
9
7
 
10
8
  **Hello, World! Sample**
11
9
 
@@ -416,141 +414,284 @@ end
416
414
 
417
415
  @user = User.new
418
416
 
419
- div {
417
+ include Glimmer
418
+
419
+ Document.ready? do
420
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]
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
+ }
424
426
  }
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;
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]
458
439
  }
459
- CSS
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
+ }
460
462
  }
461
463
  }
462
- }
464
+ end
463
465
  end
464
- end
465
- }
466
- }.render
466
+ }
467
+ }.render
468
+ end
467
469
  ```
468
470
 
469
471
  Screenshot:
470
472
 
471
473
  ![Hello, Content Data-Binding!](/images/glimmer-dsl-web-samples-hello-hello-content-data-binding.gif)
472
474
 
473
- **Button Counter Sample**
475
+ **Hello, Component!**
474
476
 
475
- **UPCOMING (NOT RELEASED OR SUPPORTED YET)**
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.
476
484
 
477
- Glimmer GUI code demonstrating MVC + Glimmer Web Components (Views) + Data-Binding:
485
+ Glimmer GUI code:
478
486
 
479
487
  ```ruby
480
488
  require 'glimmer-dsl-web'
481
489
 
482
- class Counter
483
- attr_accessor :count
484
-
485
- def initialize
486
- self.count = 0
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]
487
555
  end
488
556
 
489
- def increment!
490
- self.count += 1
557
+ def summary
558
+ to_h.values.map(&:to_s).reject(&:empty?).join(', ')
491
559
  end
492
560
  end
493
561
 
494
- 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
495
569
  include Glimmer::Web::Component
496
570
 
497
- before_render do
498
- @counter = Counter.new
499
- 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
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
500
585
 
586
+ # markup block provides the content of the
501
587
  markup {
502
- # This will hook into element #app-container and then build HTML inside it using Ruby DSL code
503
- div(parent: parent_selector) {
504
- text 'Button Counter'
505
-
506
- button {
507
- # Unidirectional Data-Binding indicating that on every change to @counter.count, the value
508
- # is read and converted to "Click To Increment: #{value} ", and then automatically
509
- # copied to button innerText (content) to display to the user
510
- 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
+ }
511
594
 
512
- onclick {
513
- @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
+ ]
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
514
635
  }
515
636
  }
637
+
638
+ div(style: 'margin: 5px') {
639
+ inner_text <= [address, :summary,
640
+ computed_by: address.members + ['state_code'],
641
+ ]
642
+ }
516
643
  }
517
644
  }
518
645
  end
519
646
 
520
- HelloButton.render('#app-container')
521
- ```
522
-
523
- That produces:
524
-
525
- ```html
526
- <div id="application">
527
- <button>
528
- Click To Increment: 0
529
- </button>
530
- </div>
531
- ```
532
-
533
- When clicked:
534
-
535
- ```html
536
- <div id="application">
537
- <button>
538
- Click To Increment: 1
539
- </button>
540
- </div>
541
- ```
542
-
543
- 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
544
685
 
545
- ```html
546
- <div id="application">
547
- <button>
548
- Click To Increment: 7
549
- </button>
550
- </div>
686
+ Document.ready? do
687
+ # renders a top-level (root) AddressPage component
688
+ AddressPage.render
689
+ end
551
690
  ```
552
691
 
692
+ Screenshot:
553
693
 
694
+ ![Hello, Component!](/images/glimmer-dsl-web-samples-hello-hello-component.png)
554
695
 
555
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.
556
697
 
@@ -575,6 +716,7 @@ Learn more about the differences between various [Glimmer](https://github.com/An
575
716
  - [Hello, Form!](#hello-form)
576
717
  - [Hello, Data-Binding!](#hello-data-binding)
577
718
  - [Hello, Content Data-Binding!](#hello-content-data-binding)
719
+ - [Hello, Component!](#hello-content-data-binding)
578
720
  - [Hello, Input (Date/Time)!](#hello-input-datetime)
579
721
  - [Button Counter](#button-counter)
580
722
  - [Glimmer Process](#glimmer-process)
@@ -621,7 +763,7 @@ rails new glimmer_app_server
621
763
  Add the following to `Gemfile`:
622
764
 
623
765
  ```
624
- gem 'glimmer-dsl-web', '~> 0.0.8'
766
+ gem 'glimmer-dsl-web', '~> 0.0.9'
625
767
  ```
626
768
 
627
769
  Run:
@@ -636,11 +778,18 @@ Follow [opal-rails](https://github.com/opal/opal-rails) instructions, basically
636
778
  bin/rails g opal:install
637
779
  ```
638
780
 
639
- Edit `config/initializers/assets.rb` and add the following at the bottom:
640
- ```
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
641
784
  Opal.use_gem 'glimmer-dsl-web'
642
785
  ```
643
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
+
644
793
  Run:
645
794
 
646
795
  ```
@@ -660,12 +809,6 @@ mount Glimmer::Engine => "/glimmer" # add on top
660
809
  root to: 'welcomes#index'
661
810
  ```
662
811
 
663
- Edit `app/views/layouts/application.html.erb` and add the following below other `stylesheet_link_tag` declarations:
664
-
665
- ```erb
666
- <%= stylesheet_link_tag 'glimmer/glimmer', media: 'all', 'data-turbolinks-track': 'reload' %>
667
- ```
668
-
669
812
  Clear the file `app/views/welcomes/index.html.erb` completely from all content.
670
813
 
671
814
  Delete `app/javascript/application.js`
@@ -760,7 +903,7 @@ Disable the `webpacker` gem line in `Gemfile`:
760
903
  Add the following to `Gemfile`:
761
904
 
762
905
  ```ruby
763
- gem 'glimmer-dsl-web', '~> 0.0.8'
906
+ gem 'glimmer-dsl-web', '~> 0.0.9'
764
907
  ```
765
908
 
766
909
  Run:
@@ -775,11 +918,18 @@ Follow [opal-rails](https://github.com/opal/opal-rails) instructions, basically
775
918
  bin/rails g opal:install
776
919
  ```
777
920
 
778
- Edit `config/initializers/assets.rb` and add the following at the bottom:
779
- ```
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
780
924
  Opal.use_gem 'glimmer-dsl-web'
781
925
  ```
782
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
+
783
933
  Run:
784
934
 
785
935
  ```
@@ -798,11 +948,6 @@ Add the following to `config/routes.rb` inside the `Rails.application.routes.dra
798
948
  mount Glimmer::Engine => "/glimmer" # add on top
799
949
  root to: 'welcomes#index'
800
950
  ```
801
-
802
- Edit `app/views/layouts/application.html.erb` and add the following below other `stylesheet_link_tag` declarations:
803
-
804
- ```erb
805
- <%= stylesheet_link_tag 'glimmer/glimmer', media: 'all', 'data-turbolinks-track': 'reload' %>
806
951
  ```
807
952
 
808
953
  Also, delete the following line:
@@ -886,7 +1031,7 @@ Glimmer DSL for Web offers a GUI DSL for building HTML Web User Interfaces decla
886
1031
 
887
1032
  1- **Keywords (HTML Elements)**
888
1033
 
889
- 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`.
890
1035
 
891
1036
  Under the hood, HTML element DSL keywords are invoked as Ruby methods.
892
1037
 
@@ -1022,6 +1167,8 @@ That produces the following under `<body></body>`:
1022
1167
 
1023
1168
  #### Hello, Button!
1024
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
+
1025
1172
  Glimmer GUI code:
1026
1173
 
1027
1174
  ```ruby
@@ -1054,6 +1201,8 @@ Screenshot:
1054
1201
 
1055
1202
  #### Hello, Form!
1056
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
+
1057
1206
  Glimmer GUI code:
1058
1207
 
1059
1208
  ```ruby
@@ -1207,6 +1356,8 @@ Screenshot:
1207
1356
 
1208
1357
  #### Hello, Data-Binding!
1209
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
+
1210
1361
  Glimmer GUI code:
1211
1362
 
1212
1363
  ```ruby
@@ -1378,6 +1529,10 @@ Screenshot:
1378
1529
 
1379
1530
  #### Hello, Content Data-Binding!
1380
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
+
1381
1536
  Glimmer GUI code:
1382
1537
 
1383
1538
  ```ruby
@@ -1449,59 +1604,284 @@ end
1449
1604
 
1450
1605
  @user = User.new
1451
1606
 
1452
- div {
1607
+ include Glimmer
1608
+
1609
+ Document.ready? do
1453
1610
  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]
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
+ }
1457
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"
1458
1737
  }
1459
1738
 
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]
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;
1476
1820
  }
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
1821
+ #{address_div.selector} input, #{address_div.selector} select {
1822
+ grid-column: 2;
1493
1823
  }
1494
- }
1824
+ CSS
1495
1825
  }
1496
- end
1497
- end
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
+ }
1498
1873
  }
1499
- }.render
1874
+ end
1875
+
1876
+ Document.ready? do
1877
+ # renders a top-level (root) AddressPage component
1878
+ AddressPage.render
1879
+ end
1500
1880
  ```
1501
1881
 
1502
1882
  Screenshot:
1503
1883
 
1504
- ![Hello, Content Data-Binding!](/images/glimmer-dsl-web-samples-hello-hello-content-data-binding.gif)
1884
+ ![Hello, Component!](/images/glimmer-dsl-web-samples-hello-hello-component.png)
1505
1885
 
1506
1886
  #### Hello, Input (Date/Time)!
1507
1887
 
@@ -1657,7 +2037,7 @@ class HelloButton
1657
2037
  }
1658
2038
  end
1659
2039
 
1660
- HelloButton.render('#app-container')
2040
+ HelloButton.render
1661
2041
  ```
1662
2042
 
1663
2043
  That produces: