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