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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +541 -161
- data/VERSION +1 -1
- data/glimmer-dsl-web.gemspec +6 -3
- data/lib/glimmer/dsl/web/component_expression.rb +30 -0
- data/lib/glimmer/dsl/web/dsl.rb +2 -0
- data/lib/glimmer/web/component.rb +317 -0
- data/lib/glimmer/web/element_proxy.rb +8 -2
- data/lib/glimmer-dsl-web/samples/hello/hello_component.rb +223 -0
- data/lib/glimmer-dsl-web/samples/hello/hello_content_data_binding.rb +46 -42
- metadata +5 -2
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.
|
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
|
-
|
417
|
+
include Glimmer
|
418
|
+
|
419
|
+
Document.ready? do
|
420
420
|
div {
|
421
|
-
|
422
|
-
|
423
|
-
|
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
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
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
|
-
|
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
|
-
|
465
|
-
}
|
466
|
-
|
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
|
-
**
|
475
|
+
**Hello, Component!**
|
474
476
|
|
475
|
-
|
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
|
485
|
+
Glimmer GUI code:
|
478
486
|
|
479
487
|
```ruby
|
480
488
|
require 'glimmer-dsl-web'
|
481
489
|
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
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
|
490
|
-
|
557
|
+
def summary
|
558
|
+
to_h.values.map(&:to_s).reject(&:empty?).join(', ')
|
491
559
|
end
|
492
560
|
end
|
493
561
|
|
494
|
-
|
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
|
-
|
498
|
-
|
499
|
-
|
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
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
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
|
-
|
513
|
-
|
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
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
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
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
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.
|
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
|
-
|
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.
|
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
|
-
|
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
|
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
|
-
|
1607
|
+
include Glimmer
|
1608
|
+
|
1609
|
+
Document.ready? do
|
1453
1610
|
div {
|
1454
|
-
|
1455
|
-
|
1456
|
-
|
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
|
-
|
1461
|
-
|
1462
|
-
|
1463
|
-
|
1464
|
-
|
1465
|
-
|
1466
|
-
|
1467
|
-
|
1468
|
-
|
1469
|
-
|
1470
|
-
|
1471
|
-
|
1472
|
-
|
1473
|
-
|
1474
|
-
|
1475
|
-
|
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
|
-
|
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
|
-
|
1497
|
-
|
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
|
-
|
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,
|
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
|
2040
|
+
HelloButton.render
|
1661
2041
|
```
|
1662
2042
|
|
1663
2043
|
That produces:
|