glimmer-dsl-web 0.0.8 → 0.0.10

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.10 (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,287 @@ 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
+ You may also insert a Glimmer component anywhere into a Rails application View using
483
+ `glimmer_component(component_path, *args)` Rails helper. Add `include GlimmerHelper` to `ApplicationHelper`
484
+ or another Rails helper, and use `<%= glimmer_component("path/to/component", *args) %>` in Views.
485
+ Below, we define an `AddressForm` component that generates a `address_form` keyword, and then we
486
+ reuse it twice inside an `AddressPage` component displaying a Shipping Address and a Billing Address.
476
487
 
477
- Glimmer GUI code demonstrating MVC + Glimmer Web Components (Views) + Data-Binding:
488
+ Glimmer GUI code:
478
489
 
479
490
  ```ruby
480
491
  require 'glimmer-dsl-web'
481
492
 
482
- class Counter
483
- attr_accessor :count
484
-
485
- def initialize
486
- self.count = 0
493
+ Address = Struct.new(:full_name, :street, :street2, :city, :state, :zip_code, keyword_init: true) do
494
+ STATES = {
495
+ "AK"=>"Alaska",
496
+ "AL"=>"Alabama",
497
+ "AR"=>"Arkansas",
498
+ "AS"=>"American Samoa",
499
+ "AZ"=>"Arizona",
500
+ "CA"=>"California",
501
+ "CO"=>"Colorado",
502
+ "CT"=>"Connecticut",
503
+ "DC"=>"District of Columbia",
504
+ "DE"=>"Delaware",
505
+ "FL"=>"Florida",
506
+ "GA"=>"Georgia",
507
+ "GU"=>"Guam",
508
+ "HI"=>"Hawaii",
509
+ "IA"=>"Iowa",
510
+ "ID"=>"Idaho",
511
+ "IL"=>"Illinois",
512
+ "IN"=>"Indiana",
513
+ "KS"=>"Kansas",
514
+ "KY"=>"Kentucky",
515
+ "LA"=>"Louisiana",
516
+ "MA"=>"Massachusetts",
517
+ "MD"=>"Maryland",
518
+ "ME"=>"Maine",
519
+ "MI"=>"Michigan",
520
+ "MN"=>"Minnesota",
521
+ "MO"=>"Missouri",
522
+ "MS"=>"Mississippi",
523
+ "MT"=>"Montana",
524
+ "NC"=>"North Carolina",
525
+ "ND"=>"North Dakota",
526
+ "NE"=>"Nebraska",
527
+ "NH"=>"New Hampshire",
528
+ "NJ"=>"New Jersey",
529
+ "NM"=>"New Mexico",
530
+ "NV"=>"Nevada",
531
+ "NY"=>"New York",
532
+ "OH"=>"Ohio",
533
+ "OK"=>"Oklahoma",
534
+ "OR"=>"Oregon",
535
+ "PA"=>"Pennsylvania",
536
+ "PR"=>"Puerto Rico",
537
+ "RI"=>"Rhode Island",
538
+ "SC"=>"South Carolina",
539
+ "SD"=>"South Dakota",
540
+ "TN"=>"Tennessee",
541
+ "TX"=>"Texas",
542
+ "UT"=>"Utah",
543
+ "VA"=>"Virginia",
544
+ "VI"=>"Virgin Islands",
545
+ "VT"=>"Vermont",
546
+ "WA"=>"Washington",
547
+ "WI"=>"Wisconsin",
548
+ "WV"=>"West Virginia",
549
+ "WY"=>"Wyoming"
550
+ }
551
+
552
+ def state_code
553
+ STATES.invert[state]
554
+ end
555
+
556
+ def state_code=(value)
557
+ self.state = STATES[value]
487
558
  end
488
559
 
489
- def increment!
490
- self.count += 1
560
+ def summary
561
+ to_h.values.map(&:to_s).reject(&:empty?).join(', ')
491
562
  end
492
563
  end
493
564
 
494
- class HelloButton
565
+ # AddressForm Glimmer Web Component (View component)
566
+ #
567
+ # Including Glimmer::Web::Component makes this class a View component and automatically
568
+ # generates a new Glimmer GUI DSL keyword that matches the lowercase underscored version
569
+ # of the name of the class. AddressForm generates address_form keyword, which can be used
570
+ # elsewhere in Glimmer GUI DSL code as done inside AddressPage below.
571
+ class AddressForm
495
572
  include Glimmer::Web::Component
496
573
 
497
- before_render do
498
- @counter = Counter.new
499
- end
574
+ option :address
575
+
576
+ # Optionally, you can execute code before rendering markup.
577
+ # This is useful for pre-setup of variables (e.g. Models) that you would use in the markup.
578
+ #
579
+ # before_render do
580
+ # end
500
581
 
582
+ # Optionally, you can execute code after rendering markup.
583
+ # This is useful for post-setup of extra Model listeners that would interact with the
584
+ # markup elements and expect them to be rendered already.
585
+ #
586
+ # after_render do
587
+ # end
588
+
589
+ # markup block provides the content of the
501
590
  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} " }]
591
+ div {
592
+ div(style: 'display: grid; grid-auto-columns: 80px 260px;') { |address_div|
593
+ label('Full Name: ', for: 'full-name-field')
594
+ input(id: 'full-name-field') {
595
+ value <=> [address, :full_name]
596
+ }
511
597
 
512
- onclick {
513
- @counter.increment!
598
+ @somelabel = label('Street: ', for: 'street-field')
599
+ input(id: 'street-field') {
600
+ value <=> [address, :street]
601
+ }
602
+
603
+ label('Street 2: ', for: 'street2-field')
604
+ textarea(id: 'street2-field') {
605
+ value <=> [address, :street2]
606
+ }
607
+
608
+ label('City: ', for: 'city-field')
609
+ input(id: 'city-field') {
610
+ value <=> [address, :city]
611
+ }
612
+
613
+ label('State: ', for: 'state-field')
614
+ select(id: 'state-field') {
615
+ Address::STATES.each do |state_code, state|
616
+ option(value: state_code) { state }
617
+ end
618
+
619
+ value <=> [address, :state_code]
620
+ }
621
+
622
+ label('Zip Code: ', for: 'zip-code-field')
623
+ input(id: 'zip-code-field', type: 'number', min: '0', max: '99999') {
624
+ value <=> [address, :zip_code,
625
+ on_write: :to_s,
626
+ ]
627
+ }
628
+
629
+ style {
630
+ <<~CSS
631
+ #{address_div.selector} * {
632
+ margin: 5px;
633
+ }
634
+ #{address_div.selector} input, #{address_div.selector} select {
635
+ grid-column: 2;
636
+ }
637
+ CSS
514
638
  }
515
639
  }
640
+
641
+ div(style: 'margin: 5px') {
642
+ inner_text <= [address, :summary,
643
+ computed_by: address.members + ['state_code'],
644
+ ]
645
+ }
516
646
  }
517
647
  }
518
648
  end
519
649
 
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:
650
+ # AddressPage Glimmer Web Component (View component)
651
+ #
652
+ # This View component represents the main page being rendered,
653
+ # as done by its `render` class method below
654
+ class AddressPage
655
+ include Glimmer::Web::Component
656
+
657
+ before_render do
658
+ @shipping_address = Address.new(
659
+ full_name: 'Johnny Doe',
660
+ street: '3922 Park Ave',
661
+ street2: 'PO BOX 8382',
662
+ city: 'San Diego',
663
+ state: 'California',
664
+ zip_code: '91913',
665
+ )
666
+ @billing_address = Address.new(
667
+ full_name: 'John C Doe',
668
+ street: '123 Main St',
669
+ street2: 'Apartment 3C',
670
+ city: 'San Diego',
671
+ state: 'California',
672
+ zip_code: '91911',
673
+ )
674
+ end
675
+
676
+ markup {
677
+ div {
678
+ h1('Shipping Address')
679
+
680
+ address_form(address: @shipping_address)
681
+
682
+ h1('Billing Address')
683
+
684
+ address_form(address: @billing_address)
685
+ }
686
+ }
687
+ end
544
688
 
545
- ```html
546
- <div id="application">
547
- <button>
548
- Click To Increment: 7
549
- </button>
550
- </div>
689
+ Document.ready? do
690
+ # renders a top-level (root) AddressPage component
691
+ AddressPage.render
692
+ end
551
693
  ```
552
694
 
695
+ Screenshot:
553
696
 
697
+ ![Hello, Component!](/images/glimmer-dsl-web-samples-hello-hello-component.png)
554
698
 
555
699
  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
700
 
@@ -575,6 +719,7 @@ Learn more about the differences between various [Glimmer](https://github.com/An
575
719
  - [Hello, Form!](#hello-form)
576
720
  - [Hello, Data-Binding!](#hello-data-binding)
577
721
  - [Hello, Content Data-Binding!](#hello-content-data-binding)
722
+ - [Hello, Component!](#hello-content-data-binding)
578
723
  - [Hello, Input (Date/Time)!](#hello-input-datetime)
579
724
  - [Button Counter](#button-counter)
580
725
  - [Glimmer Process](#glimmer-process)
@@ -621,7 +766,7 @@ rails new glimmer_app_server
621
766
  Add the following to `Gemfile`:
622
767
 
623
768
  ```
624
- gem 'glimmer-dsl-web', '~> 0.0.8'
769
+ gem 'glimmer-dsl-web', '~> 0.0.10'
625
770
  ```
626
771
 
627
772
  Run:
@@ -630,15 +775,25 @@ Run:
630
775
  bundle
631
776
  ```
632
777
 
778
+ (run `rm -rf tmp/cache` from inside your Rails app if you upgrade your `glimmer-dsl-web` gem version from an older one to clear Opal-Rails's cache)
779
+
633
780
  Follow [opal-rails](https://github.com/opal/opal-rails) instructions, basically running:
634
781
 
635
782
  ```
636
783
  bin/rails g opal:install
637
784
  ```
638
785
 
639
- Edit `config/initializers/assets.rb` and add the following at the bottom:
640
- ```
786
+ To enable the `glimmer-dsl-web` library in the frontend, edit `config/initializers/assets.rb` and add the following at the bottom:
787
+
788
+ ```ruby
641
789
  Opal.use_gem 'glimmer-dsl-web'
790
+ Opal.append_path Rails.root.join('app', 'assets', 'opal')
791
+ ```
792
+
793
+ 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:
794
+
795
+ ```ruby
796
+ config.assets.debug = true
642
797
  ```
643
798
 
644
799
  Run:
@@ -660,17 +815,22 @@ mount Glimmer::Engine => "/glimmer" # add on top
660
815
  root to: 'welcomes#index'
661
816
  ```
662
817
 
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
818
  Clear the file `app/views/welcomes/index.html.erb` completely from all content.
670
819
 
671
820
  Delete `app/javascript/application.js`
672
821
 
673
- Edit and replace `app/assets/javascript/application.js.rb` content with code below (optionally including a require statement for one of the [samples](#samples) below):
822
+ Rename `app/assets/javascript` directory to `app/assets/opal`.
823
+
824
+ Add the following lines to `app/assets/config/manifest.js` (and delete their `javascript` equivalents):
825
+
826
+ ```js
827
+ //= link_tree ../../opal .js
828
+ //= link_directory ../opal .js
829
+ ```
830
+
831
+ Rename `app/assets/opal/application.js.rb` to `app/assets/opal/application.rb`.
832
+
833
+ Edit and replace `app/assets/opal/application.rb` content with code below (optionally including a require statement for one of the [samples](#samples) below inside a `Document.ready? do; end` block):
674
834
 
675
835
  ```ruby
676
836
  require 'glimmer-dsl-web' # brings opal and other dependencies automatically
@@ -680,15 +840,6 @@ require 'glimmer-dsl-web' # brings opal and other dependencies automatically
680
840
 
681
841
  Example to confirm setup is working:
682
842
 
683
- Initial HTML Markup:
684
-
685
- ```html
686
- ...
687
- <div id="app-container">
688
- </div>
689
- ...
690
- ```
691
-
692
843
  Glimmer GUI code:
693
844
 
694
845
  ```ruby
@@ -710,13 +861,11 @@ That produces:
710
861
 
711
862
  ```html
712
863
  ...
713
- <div id="app-container">
714
- <div data-parent="#app-container" class="element element-1">
864
+ <div data-parent="body" class="element element-1">
715
865
  <label class="greeting element element-2">
716
866
  Hello, World!
717
867
  </label>
718
868
  </div>
719
- </div>
720
869
  ...
721
870
  ```
722
871
 
@@ -731,6 +880,22 @@ You should see:
731
880
 
732
881
  ![setup is working](/images/glimmer-dsl-web-setup-example-working.png)
733
882
 
883
+ You may also insert a Glimmer component anywhere into a Rails application View using `glimmer_component(component_path, *args)` Rails helper. Add `include GlimmerHelper` to `ApplicationHelper` or another Rails helper, and use `<%= glimmer_component("path/to/component", *args) %>` in Views.
884
+
885
+ To use `glimmer_component`, edit `app/helpers/application_helper.rb` in your Rails application, add `require 'glimmer/helpers/glimmer_helper'` on top and `include GlimmerHelper` inside `module`.
886
+
887
+ `app/helpers/application_helper.rb` should look like this after the change:
888
+
889
+ ```ruby
890
+ require 'glimmer/helpers/glimmer_helper'
891
+
892
+ module ApplicationHelper
893
+ # ...
894
+ include GlimmerHelper
895
+ # ...
896
+ end
897
+ ```
898
+
734
899
  If you run into any issues in setup, refer to the [Sample Glimmer DSL for Web Rails 7 App](https://github.com/AndyObtiva/sample-glimmer-dsl-web-rails7-app) project (in case I forgot to include some setup steps by mistake).
735
900
 
736
901
  Otherwise, if you still cannot setup successfully (even with the help of the sample project, or if the sample project stops working), please do not hesitate to report an [Issue request](https://github.com/AndyObtiva/glimmer-dsl-web/issues) or fix and submit a [Pull Request](https://github.com/AndyObtiva/glimmer-dsl-web/pulls).
@@ -760,7 +925,7 @@ Disable the `webpacker` gem line in `Gemfile`:
760
925
  Add the following to `Gemfile`:
761
926
 
762
927
  ```ruby
763
- gem 'glimmer-dsl-web', '~> 0.0.8'
928
+ gem 'glimmer-dsl-web', '~> 0.0.10'
764
929
  ```
765
930
 
766
931
  Run:
@@ -769,15 +934,25 @@ Run:
769
934
  bundle
770
935
  ```
771
936
 
937
+ (run `rm -rf tmp/cache` from inside your Rails app if you upgrade your `glimmer-dsl-web` gem version from an older one to clear Opal-Rails's cache)
938
+
772
939
  Follow [opal-rails](https://github.com/opal/opal-rails) instructions, basically running:
773
940
 
774
941
  ```
775
942
  bin/rails g opal:install
776
943
  ```
777
944
 
778
- Edit `config/initializers/assets.rb` and add the following at the bottom:
779
- ```
945
+ To enable the `glimmer-dsl-web` library in the frontend, edit `config/initializers/assets.rb` and add the following at the bottom:
946
+
947
+ ```ruby
780
948
  Opal.use_gem 'glimmer-dsl-web'
949
+ Opal.append_path Rails.root.join('app', 'assets', 'opal')
950
+ ```
951
+
952
+ 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:
953
+
954
+ ```ruby
955
+ config.assets.debug = true
781
956
  ```
782
957
 
783
958
  Run:
@@ -798,11 +973,6 @@ Add the following to `config/routes.rb` inside the `Rails.application.routes.dra
798
973
  mount Glimmer::Engine => "/glimmer" # add on top
799
974
  root to: 'welcomes#index'
800
975
  ```
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
976
  ```
807
977
 
808
978
  Also, delete the following line:
@@ -813,7 +983,18 @@ Also, delete the following line:
813
983
 
814
984
  Clear the file `app/views/welcomes/index.html.erb` completely from all content.
815
985
 
816
- Edit and replace `app/assets/javascript/application.js.rb` content with code below (optionally including a require statement for one of the [samples](#samples) below):
986
+ Rename `app/assets/javascript` directory to `app/assets/opal`.
987
+
988
+ Add the following lines to `app/assets/config/manifest.js` (and delete their `javascript` equivalents):
989
+
990
+ ```js
991
+ //= link_tree ../../opal .js
992
+ //= link_directory ../opal .js
993
+ ```
994
+
995
+ Rename `app/assets/opal/application.js.rb` to `app/assets/opal/application.rb`.
996
+
997
+ Edit and replace `app/assets/opal/application.rb` content with code below (optionally including a require statement for one of the [samples](#samples) below inside a `Document.ready? do; end` block):
817
998
 
818
999
  ```ruby
819
1000
  require 'glimmer-dsl-web' # brings opal and other dependencies automatically
@@ -874,6 +1055,22 @@ You should see:
874
1055
 
875
1056
  ![setup is working](/images/glimmer-dsl-web-setup-example-working.png)
876
1057
 
1058
+ You may also insert a Glimmer component anywhere into a Rails application View using `glimmer_component(component_path, *args)` Rails helper. Add `include GlimmerHelper` to `ApplicationHelper` or another Rails helper, and use `<%= glimmer_component("path/to/component", *args) %>` in Views.
1059
+
1060
+ To use `glimmer_component`, edit `app/helpers/application_helper.rb` in your Rails application, add `require 'glimmer/helpers/glimmer_helper'` on top and `include GlimmerHelper` inside `module`.
1061
+
1062
+ `app/helpers/application_helper.rb` should look like this after the change:
1063
+
1064
+ ```ruby
1065
+ require 'glimmer/helpers/glimmer_helper'
1066
+
1067
+ module ApplicationHelper
1068
+ # ...
1069
+ include GlimmerHelper
1070
+ # ...
1071
+ end
1072
+ ```
1073
+
877
1074
  **NOT RELEASED OR SUPPORTED YET**
878
1075
 
879
1076
  If you run into any issues in setup, refer to the [Sample Glimmer DSL for Web Rails 6 App](https://github.com/AndyObtiva/sample-glimmer-dsl-web-rails6-app) project (in case I forgot to include some setup steps by mistake).
@@ -886,7 +1083,7 @@ Glimmer DSL for Web offers a GUI DSL for building HTML Web User Interfaces decla
886
1083
 
887
1084
  1- **Keywords (HTML Elements)**
888
1085
 
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`.
1086
+ 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
1087
 
891
1088
  Under the hood, HTML element DSL keywords are invoked as Ruby methods.
892
1089
 
@@ -1022,6 +1219,8 @@ That produces the following under `<body></body>`:
1022
1219
 
1023
1220
  #### Hello, Button!
1024
1221
 
1222
+ 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`.
1223
+
1025
1224
  Glimmer GUI code:
1026
1225
 
1027
1226
  ```ruby
@@ -1054,6 +1253,8 @@ Screenshot:
1054
1253
 
1055
1254
  #### Hello, Form!
1056
1255
 
1256
+ [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.
1257
+
1057
1258
  Glimmer GUI code:
1058
1259
 
1059
1260
  ```ruby
@@ -1207,6 +1408,8 @@ Screenshot:
1207
1408
 
1208
1409
  #### Hello, Data-Binding!
1209
1410
 
1411
+ [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.
1412
+
1210
1413
  Glimmer GUI code:
1211
1414
 
1212
1415
  ```ruby
@@ -1378,6 +1581,10 @@ Screenshot:
1378
1581
 
1379
1582
  #### Hello, Content Data-Binding!
1380
1583
 
1584
+ If you need to regenerate HTML element content dynamically, you can use Content Data-Binding to effortlessly
1585
+ rebuild HTML elements based on changes in a Model attribute that provides the source data.
1586
+ In this example, we generate multiple address forms based on the number of addresses the user has.
1587
+
1381
1588
  Glimmer GUI code:
1382
1589
 
1383
1590
  ```ruby
@@ -1449,59 +1656,287 @@ end
1449
1656
 
1450
1657
  @user = User.new
1451
1658
 
1452
- div {
1659
+ include Glimmer
1660
+
1661
+ Document.ready? do
1453
1662
  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]
1663
+ div {
1664
+ label('Number of addresses: ', for: 'address-count-field')
1665
+ input(id: 'address-count-field', type: 'number', min: 1, max: 3) {
1666
+ value <=> [@user, :address_count]
1667
+ }
1668
+ }
1669
+
1670
+ div {
1671
+ # Content Data-Binding is used to dynamically (re)generate content of div
1672
+ # based on changes to @user.addresses, replacing older content on every change
1673
+ content(@user, :addresses) do
1674
+ @user.addresses.each do |address|
1675
+ div {
1676
+ div(style: 'display: grid; grid-auto-columns: 80px 280px;') { |address_div|
1677
+ [:name, :street, :city, :state, :zip].each do |attribute|
1678
+ label(attribute.to_s.capitalize, for: "#{attribute}-field")
1679
+ input(id: "#{attribute}-field", type: 'text') {
1680
+ value <=> [address, attribute]
1681
+ }
1682
+ end
1683
+
1684
+ div(style: 'grid-column: 1 / span 2;') {
1685
+ inner_text <= [address, :text]
1686
+ }
1687
+
1688
+ style {
1689
+ <<~CSS
1690
+ #{address_div.selector} {
1691
+ margin: 10px 0;
1692
+ }
1693
+ #{address_div.selector} * {
1694
+ margin: 5px;
1695
+ }
1696
+ #{address_div.selector} label {
1697
+ grid-column: 1;
1698
+ }
1699
+ #{address_div.selector} input, #{address_div.selector} select {
1700
+ grid-column: 2;
1701
+ }
1702
+ CSS
1703
+ }
1704
+ }
1705
+ }
1706
+ end
1707
+ end
1457
1708
  }
1709
+ }.render
1710
+ end
1711
+ ```
1712
+
1713
+ Screenshot:
1714
+
1715
+ ![Hello, Content Data-Binding!](/images/glimmer-dsl-web-samples-hello-hello-content-data-binding.gif)
1716
+
1717
+ #### Hello, Component!
1718
+
1719
+ You can define Glimmer web components (View components) to reuse visual concepts to your heart's content,
1720
+ by simply defining a class with `include Glimmer::Web::Component` and encasing the reusable markup inside
1721
+ a `markup {...}` block. Glimmer web components automatically extend the Glimmer GUI DSL with new keywords
1722
+ that match the underscored versions of the component class names (e.g. a `OrderSummary` class yields
1723
+ the `order_summary` keyword for reusing that component within the Glimmer GUI DSL).
1724
+ You may also insert a Glimmer component anywhere into a Rails application View using
1725
+ `glimmer_component(component_path, *args)` Rails helper. Add `include GlimmerHelper` to `ApplicationHelper`
1726
+ or another Rails helper, and use `<%= glimmer_component("path/to/component", *args) %>` in Views.
1727
+ Below, we define an `AddressForm` component that generates a `address_form` keyword, and then we
1728
+ reuse it twice inside an `AddressPage` component displaying a Shipping Address and a Billing Address.
1729
+
1730
+ Glimmer GUI code:
1731
+
1732
+ ```ruby
1733
+ require 'glimmer-dsl-web'
1734
+
1735
+ Address = Struct.new(:full_name, :street, :street2, :city, :state, :zip_code, keyword_init: true) do
1736
+ STATES = {
1737
+ "AK"=>"Alaska",
1738
+ "AL"=>"Alabama",
1739
+ "AR"=>"Arkansas",
1740
+ "AS"=>"American Samoa",
1741
+ "AZ"=>"Arizona",
1742
+ "CA"=>"California",
1743
+ "CO"=>"Colorado",
1744
+ "CT"=>"Connecticut",
1745
+ "DC"=>"District of Columbia",
1746
+ "DE"=>"Delaware",
1747
+ "FL"=>"Florida",
1748
+ "GA"=>"Georgia",
1749
+ "GU"=>"Guam",
1750
+ "HI"=>"Hawaii",
1751
+ "IA"=>"Iowa",
1752
+ "ID"=>"Idaho",
1753
+ "IL"=>"Illinois",
1754
+ "IN"=>"Indiana",
1755
+ "KS"=>"Kansas",
1756
+ "KY"=>"Kentucky",
1757
+ "LA"=>"Louisiana",
1758
+ "MA"=>"Massachusetts",
1759
+ "MD"=>"Maryland",
1760
+ "ME"=>"Maine",
1761
+ "MI"=>"Michigan",
1762
+ "MN"=>"Minnesota",
1763
+ "MO"=>"Missouri",
1764
+ "MS"=>"Mississippi",
1765
+ "MT"=>"Montana",
1766
+ "NC"=>"North Carolina",
1767
+ "ND"=>"North Dakota",
1768
+ "NE"=>"Nebraska",
1769
+ "NH"=>"New Hampshire",
1770
+ "NJ"=>"New Jersey",
1771
+ "NM"=>"New Mexico",
1772
+ "NV"=>"Nevada",
1773
+ "NY"=>"New York",
1774
+ "OH"=>"Ohio",
1775
+ "OK"=>"Oklahoma",
1776
+ "OR"=>"Oregon",
1777
+ "PA"=>"Pennsylvania",
1778
+ "PR"=>"Puerto Rico",
1779
+ "RI"=>"Rhode Island",
1780
+ "SC"=>"South Carolina",
1781
+ "SD"=>"South Dakota",
1782
+ "TN"=>"Tennessee",
1783
+ "TX"=>"Texas",
1784
+ "UT"=>"Utah",
1785
+ "VA"=>"Virginia",
1786
+ "VI"=>"Virgin Islands",
1787
+ "VT"=>"Vermont",
1788
+ "WA"=>"Washington",
1789
+ "WI"=>"Wisconsin",
1790
+ "WV"=>"West Virginia",
1791
+ "WY"=>"Wyoming"
1458
1792
  }
1459
1793
 
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]
1794
+ def state_code
1795
+ STATES.invert[state]
1796
+ end
1797
+
1798
+ def state_code=(value)
1799
+ self.state = STATES[value]
1800
+ end
1801
+
1802
+ def summary
1803
+ to_h.values.map(&:to_s).reject(&:empty?).join(', ')
1804
+ end
1805
+ end
1806
+
1807
+ # AddressForm Glimmer Web Component (View component)
1808
+ #
1809
+ # Including Glimmer::Web::Component makes this class a View component and automatically
1810
+ # generates a new Glimmer GUI DSL keyword that matches the lowercase underscored version
1811
+ # of the name of the class. AddressForm generates address_form keyword, which can be used
1812
+ # elsewhere in Glimmer GUI DSL code as done inside AddressPage below.
1813
+ class AddressForm
1814
+ include Glimmer::Web::Component
1815
+
1816
+ option :address
1817
+
1818
+ # Optionally, you can execute code before rendering markup.
1819
+ # This is useful for pre-setup of variables (e.g. Models) that you would use in the markup.
1820
+ #
1821
+ # before_render do
1822
+ # end
1823
+
1824
+ # Optionally, you can execute code after rendering markup.
1825
+ # This is useful for post-setup of extra Model listeners that would interact with the
1826
+ # markup elements and expect them to be rendered already.
1827
+ #
1828
+ # after_render do
1829
+ # end
1830
+
1831
+ # markup block provides the content of the
1832
+ markup {
1833
+ div {
1834
+ div(style: 'display: grid; grid-auto-columns: 80px 260px;') { |address_div|
1835
+ label('Full Name: ', for: 'full-name-field')
1836
+ input(id: 'full-name-field') {
1837
+ value <=> [address, :full_name]
1838
+ }
1839
+
1840
+ @somelabel = label('Street: ', for: 'street-field')
1841
+ input(id: 'street-field') {
1842
+ value <=> [address, :street]
1843
+ }
1844
+
1845
+ label('Street 2: ', for: 'street2-field')
1846
+ textarea(id: 'street2-field') {
1847
+ value <=> [address, :street2]
1848
+ }
1849
+
1850
+ label('City: ', for: 'city-field')
1851
+ input(id: 'city-field') {
1852
+ value <=> [address, :city]
1853
+ }
1854
+
1855
+ label('State: ', for: 'state-field')
1856
+ select(id: 'state-field') {
1857
+ Address::STATES.each do |state_code, state|
1858
+ option(value: state_code) { state }
1859
+ end
1860
+
1861
+ value <=> [address, :state_code]
1862
+ }
1863
+
1864
+ label('Zip Code: ', for: 'zip-code-field')
1865
+ input(id: 'zip-code-field', type: 'number', min: '0', max: '99999') {
1866
+ value <=> [address, :zip_code,
1867
+ on_write: :to_s,
1868
+ ]
1869
+ }
1870
+
1871
+ style {
1872
+ <<~CSS
1873
+ #{address_div.selector} * {
1874
+ margin: 5px;
1476
1875
  }
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
1876
+ #{address_div.selector} input, #{address_div.selector} select {
1877
+ grid-column: 2;
1493
1878
  }
1494
- }
1879
+ CSS
1495
1880
  }
1496
- end
1497
- end
1881
+ }
1882
+
1883
+ div(style: 'margin: 5px') {
1884
+ inner_text <= [address, :summary,
1885
+ computed_by: address.members + ['state_code'],
1886
+ ]
1887
+ }
1888
+ }
1889
+ }
1890
+ end
1891
+
1892
+ # AddressPage Glimmer Web Component (View component)
1893
+ #
1894
+ # This View component represents the main page being rendered,
1895
+ # as done by its `render` class method below
1896
+ class AddressPage
1897
+ include Glimmer::Web::Component
1898
+
1899
+ before_render do
1900
+ @shipping_address = Address.new(
1901
+ full_name: 'Johnny Doe',
1902
+ street: '3922 Park Ave',
1903
+ street2: 'PO BOX 8382',
1904
+ city: 'San Diego',
1905
+ state: 'California',
1906
+ zip_code: '91913',
1907
+ )
1908
+ @billing_address = Address.new(
1909
+ full_name: 'John C Doe',
1910
+ street: '123 Main St',
1911
+ street2: 'Apartment 3C',
1912
+ city: 'San Diego',
1913
+ state: 'California',
1914
+ zip_code: '91911',
1915
+ )
1916
+ end
1917
+
1918
+ markup {
1919
+ div {
1920
+ h1('Shipping Address')
1921
+
1922
+ address_form(address: @shipping_address)
1923
+
1924
+ h1('Billing Address')
1925
+
1926
+ address_form(address: @billing_address)
1927
+ }
1498
1928
  }
1499
- }.render
1929
+ end
1930
+
1931
+ Document.ready? do
1932
+ # renders a top-level (root) AddressPage component
1933
+ AddressPage.render
1934
+ end
1500
1935
  ```
1501
1936
 
1502
1937
  Screenshot:
1503
1938
 
1504
- ![Hello, Content Data-Binding!](/images/glimmer-dsl-web-samples-hello-hello-content-data-binding.gif)
1939
+ ![Hello, Component!](/images/glimmer-dsl-web-samples-hello-hello-component.png)
1505
1940
 
1506
1941
  #### Hello, Input (Date/Time)!
1507
1942
 
@@ -1657,7 +2092,7 @@ class HelloButton
1657
2092
  }
1658
2093
  end
1659
2094
 
1660
- HelloButton.render('#app-container')
2095
+ HelloButton.render
1661
2096
  ```
1662
2097
 
1663
2098
  That produces: