glimmer-dsl-web 0.9.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +2 -4
- data/VERSION +1 -1
- data/glimmer-dsl-web.gemspec +3 -2
- data/lib/glimmer/web/element_proxy.rb +60 -7
- data/lib/glimmer-dsl-web/samples/hello/hello_input_date_time.rb +115 -74
- data/lib/glimmer-dsl-web/samples/hello/hello_input_date_time_data_binding.rb +97 -0
- data/lib/glimmer-dsl-web/samples/hello/hello_observer.rb +44 -40
- data/lib/glimmer-dsl-web/samples/hello/hello_observer_data_binding.rb +25 -21
- data/lib/glimmer-dsl-web/samples/regular/todo_mvc/models/todo.rb +1 -2
- data/lib/glimmer-dsl-web/samples/regular/todo_mvc/presenters/todo_presenter.rb +1 -3
- data/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/new_todo_input.rb +3 -6
- data/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/todo_list.rb +6 -14
- data/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/todo_list_item.rb +0 -22
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2f64cd1a4155afc8688d37c70266a28288ae80473332c9dc2bb0dbf01d81b590
|
|
4
|
+
data.tar.gz: 51ff57301add3f920f5a0425bb4817659bdcddf77882591313f76693906a6116
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 90d5df27de10596eccf5c052863b7dbf8552400c1e4f7d31870c2ba7f8b762faa082ef0566555cd920d7b5bb20640e85462cfb70373f6d7f77ee4605a8cf8dbe
|
|
7
|
+
data.tar.gz: 309cd017bc28c4934fa72d1b9ba13e3f692740b147f40834bf6cc8740bf20a4136eca39388f58cfffc2c8efbe930c8424dd1122b68ae011181cad9a3ab161b76
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## 0.10.0
|
|
4
|
+
|
|
5
|
+
- Ensure auto-formatting date/time/datetime/week/month values from date/time/datetime objects even without data-binding
|
|
6
|
+
- Update Hello, Input Date/Time! sample to work without data-binding (as data-binding will happen in a new sample)
|
|
7
|
+
- Hello, Input Date/Time Data-Binding! sample: `require 'glimmer-dsl-web/samples/hello/hello_input_date_time_data_binding.rb'`
|
|
8
|
+
|
|
3
9
|
## 0.9.2
|
|
4
10
|
|
|
5
11
|
- Relax opal dependency to '>= 1.8.2', '< 2.0.0' (to enable use of Glimmer DSL for Web with newer versions of Opal)
|
data/README.md
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
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.
|
|
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.10.0 (Beta)
|
|
2
2
|
## Ruby-in-the-Browser Web Frontend Framework
|
|
3
3
|
### The "Rails" of Frontend Frameworks!!! ([Fukuoka Award Winning](https://andymaleh.blogspot.com/2025/01/glimmer-dsl-for-web-wins-in-fukuoka.html))
|
|
4
4
|
#### Finally, Ruby Developer Productivity, Happiness, and Fun in the Frontend!!!
|
|
5
5
|
[](http://badge.fury.io/rb/glimmer-dsl-web)
|
|
6
6
|
[](https://gitter.im/AndyObtiva/glimmer?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
|
7
7
|
|
|
8
|
-
**UPCOMING MAY 30, 2026 TALK: "Frontend Ruby on Rails with Glimmer DSL for Web" at [RubyConf Austria 2026](https://rubyconf.at/), in Vienna, Austria**
|
|
9
|
-
|
|
10
8
|
**(Based on Original [Glimmer](https://github.com/AndyObtiva/glimmer) Library Handling World’s Ruby GUI Needs Since 2007. Beware of Imitators!)**
|
|
11
9
|
|
|
12
10
|
**([Fukuoka Prefecture Future IT Initiative 2025 Money Forward Award Winner](https://andymaleh.blogspot.com/2025/01/glimmer-dsl-for-web-wins-in-fukuoka.html)) [(Award Announcement)](https://digitalfukuoka.jp/news/info/528/)**
|
|
13
11
|
|
|
14
12
|
(**Workshops: [wroclove.rb 2026 Ruby conference workshop exercises: "Building Rails SPAs in Frontend Ruby with Glimmer DSL for Web"](https://github.com/AndyObtiva/glimmer_commerce)**)
|
|
15
13
|
|
|
16
|
-
**(Talk Videos: [Intro to Ruby in the Browser](https://youtu.be/4AdcfbI6A4c?si=MmxOrkhIXTDHQoYi) / [Frontend Ruby with Glimmer DSL for Web \[Montreal.rb\]](https://youtu.be/rIZ-ILUv9ME?si=raygUXVPd_7ypWuE) / [Frontend Ruby with Glimmer DSL for Web \[/dev/mtl 2024\]](https://www.youtube.com/watch?v=J2VIY9DMJo4)) / [Frontend Ruby with Glimmer DSL for Web at Ruby on Rio 2025-06-06 Talk](https://www.youtube.com/watch?v=LY6ulYICuzE)**
|
|
14
|
+
**(Talk Videos: [Intro to Ruby in the Browser](https://youtu.be/4AdcfbI6A4c?si=MmxOrkhIXTDHQoYi) / [Frontend Ruby with Glimmer DSL for Web \[Montreal.rb\]](https://youtu.be/rIZ-ILUv9ME?si=raygUXVPd_7ypWuE) / [Frontend Ruby with Glimmer DSL for Web \[/dev/mtl 2024\]](https://www.youtube.com/watch?v=J2VIY9DMJo4)) / [Frontend Ruby with Glimmer DSL for Web at Ruby on Rio 2025-06-06 Talk](https://www.youtube.com/watch?v=LY6ulYICuzE) / [Frontend Ruby on Rails with Glimmer DSL for Web at RubyConf Austria 2026](https://www.youtube.com/watch?v=qTukq5AcSK0))**
|
|
17
15
|
|
|
18
16
|
**(Ruby Rogues Podcast: [Building Better Ruby Apps: Glimmer Component Slots and More](https://topenddevs.com/podcasts/ruby-rogues/episodes/building-better-ruby-apps-glimmer-s-component-slots-and-more-ruby-653))**
|
|
19
17
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.10.0
|
data/glimmer-dsl-web.gemspec
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
|
3
3
|
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
|
4
4
|
# -*- encoding: utf-8 -*-
|
|
5
|
-
# stub: glimmer-dsl-web 0.
|
|
5
|
+
# stub: glimmer-dsl-web 0.10.0 ruby lib
|
|
6
6
|
|
|
7
7
|
Gem::Specification.new do |s|
|
|
8
8
|
s.name = "glimmer-dsl-web".freeze
|
|
9
|
-
s.version = "0.
|
|
9
|
+
s.version = "0.10.0".freeze
|
|
10
10
|
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
|
12
12
|
s.require_paths = ["lib".freeze]
|
|
@@ -48,6 +48,7 @@ Gem::Specification.new do |s|
|
|
|
48
48
|
"lib/glimmer-dsl-web/samples/hello/hello_form_mvp/views/contact_table.rb",
|
|
49
49
|
"lib/glimmer-dsl-web/samples/hello/hello_glimmer_component_helper/address_form.rb",
|
|
50
50
|
"lib/glimmer-dsl-web/samples/hello/hello_input_date_time.rb",
|
|
51
|
+
"lib/glimmer-dsl-web/samples/hello/hello_input_date_time_data_binding.rb",
|
|
51
52
|
"lib/glimmer-dsl-web/samples/hello/hello_modal.rb",
|
|
52
53
|
"lib/glimmer-dsl-web/samples/hello/hello_observer.rb",
|
|
53
54
|
"lib/glimmer-dsl-web/samples/hello/hello_observer_data_binding.rb",
|
|
@@ -713,6 +713,51 @@ module Glimmer
|
|
|
713
713
|
end
|
|
714
714
|
end
|
|
715
715
|
|
|
716
|
+
def value(raw: false)
|
|
717
|
+
if rendered?
|
|
718
|
+
view_to_model_converter = value_converters_for_input_type(type)&.[](:view_to_model)
|
|
719
|
+
view_value = super()
|
|
720
|
+
if !raw && view_to_model_converter
|
|
721
|
+
view_to_model_converter.call(view_value, nil)
|
|
722
|
+
else
|
|
723
|
+
view_value
|
|
724
|
+
end
|
|
725
|
+
else # this is the value passed by the programmer as per its format in the model (date/time or string)
|
|
726
|
+
model_to_view_converter = value_converters_for_input_type(type)&.[](:model_to_view)
|
|
727
|
+
if !raw && model_to_view_converter
|
|
728
|
+
model_to_view_converter.call(options[:value], nil)
|
|
729
|
+
else
|
|
730
|
+
options[:value]
|
|
731
|
+
end
|
|
732
|
+
end
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
def value=(new_value)
|
|
736
|
+
model_to_view_converter = value_converters_for_input_type(type)&.[](:model_to_view)
|
|
737
|
+
if rendered?
|
|
738
|
+
if model_to_view_converter
|
|
739
|
+
super(model_to_view_converter.call(new_value, nil))
|
|
740
|
+
else
|
|
741
|
+
super(new_value)
|
|
742
|
+
end
|
|
743
|
+
else
|
|
744
|
+
the_value = if model_to_view_converter
|
|
745
|
+
model_to_view_converter.call(super(new_value), nil)
|
|
746
|
+
else
|
|
747
|
+
new_value
|
|
748
|
+
end
|
|
749
|
+
if keyword == 'textarea'
|
|
750
|
+
if args.first.is_a?(String)
|
|
751
|
+
args[0] = the_value
|
|
752
|
+
else
|
|
753
|
+
args.prepend(the_value)
|
|
754
|
+
end
|
|
755
|
+
else
|
|
756
|
+
options[:value] = the_value
|
|
757
|
+
end
|
|
758
|
+
end
|
|
759
|
+
end
|
|
760
|
+
|
|
716
761
|
def data_bind(property, model_binding)
|
|
717
762
|
element_binding_read_translator = value_converters_for_input_type(type)&.[](:model_to_view)
|
|
718
763
|
element_binding = DataBinding::ElementBinding.new(self, property, translator: element_binding_read_translator)
|
|
@@ -936,13 +981,13 @@ module Glimmer
|
|
|
936
981
|
'number' => {
|
|
937
982
|
model_to_view: -> (value, old_value) { value.to_s },
|
|
938
983
|
view_to_model: -> (value, old_value) {
|
|
939
|
-
value.include?('.') ? value.to_f : value.to_i
|
|
984
|
+
value.to_s.include?('.') ? value.to_f : value.to_i
|
|
940
985
|
},
|
|
941
986
|
},
|
|
942
987
|
'range' => {
|
|
943
988
|
model_to_view: -> (value, old_value) { value.to_s },
|
|
944
989
|
view_to_model: -> (value, old_value) {
|
|
945
|
-
value.include?('.') ? value.to_f : value.to_i
|
|
990
|
+
value.to_s.include?('.') ? value.to_f : value.to_i
|
|
946
991
|
},
|
|
947
992
|
},
|
|
948
993
|
'datetime-local' => {
|
|
@@ -960,7 +1005,7 @@ module Glimmer
|
|
|
960
1005
|
view_to_model: -> (value, old_value) {
|
|
961
1006
|
if value.to_s.empty?
|
|
962
1007
|
nil
|
|
963
|
-
|
|
1008
|
+
elsif value.is_a?(String)
|
|
964
1009
|
date = Native(`new Date(Date.parse(#{value}))`)
|
|
965
1010
|
year = Native.call(date, 'getFullYear')
|
|
966
1011
|
month = Native.call(date, 'getMonth') + 1
|
|
@@ -968,6 +1013,8 @@ module Glimmer
|
|
|
968
1013
|
hour = Native.call(date, 'getHours')
|
|
969
1014
|
minute = Native.call(date, 'getMinutes')
|
|
970
1015
|
Time.new(year, month, day, hour, minute)
|
|
1016
|
+
else
|
|
1017
|
+
value
|
|
971
1018
|
end
|
|
972
1019
|
},
|
|
973
1020
|
},
|
|
@@ -986,13 +1033,15 @@ module Glimmer
|
|
|
986
1033
|
view_to_model: -> (value, old_value) {
|
|
987
1034
|
if value.to_s.empty?
|
|
988
1035
|
nil
|
|
989
|
-
|
|
1036
|
+
elsif value.is_a?(String)
|
|
990
1037
|
year, month, day = value.split('-')
|
|
991
1038
|
if !old_value.to_s.empty?
|
|
992
1039
|
Time.new(year, month, day, old_value.hour, old_value.min)
|
|
993
1040
|
else
|
|
994
1041
|
Time.new(year, month, day)
|
|
995
1042
|
end
|
|
1043
|
+
else
|
|
1044
|
+
value
|
|
996
1045
|
end
|
|
997
1046
|
},
|
|
998
1047
|
},
|
|
@@ -1011,14 +1060,16 @@ module Glimmer
|
|
|
1011
1060
|
view_to_model: -> (value, old_value) {
|
|
1012
1061
|
if value.to_s.empty?
|
|
1013
1062
|
nil
|
|
1014
|
-
|
|
1063
|
+
elsif value.is_a?(String)
|
|
1015
1064
|
hour, minute = value.split(':')
|
|
1016
1065
|
if !old_value.to_s.empty?
|
|
1017
1066
|
Time.new(old_value.year, old_value.month, old_value.day, hour, minute)
|
|
1018
|
-
|
|
1067
|
+
elsif value.is_a?(String)
|
|
1019
1068
|
now = Time.now
|
|
1020
1069
|
Time.new(now.year, now.month, now.day, hour, minute)
|
|
1021
1070
|
end
|
|
1071
|
+
else
|
|
1072
|
+
value
|
|
1022
1073
|
end
|
|
1023
1074
|
},
|
|
1024
1075
|
},
|
|
@@ -1037,13 +1088,15 @@ module Glimmer
|
|
|
1037
1088
|
view_to_model: -> (value, old_value) {
|
|
1038
1089
|
if value.to_s.empty?
|
|
1039
1090
|
nil
|
|
1040
|
-
|
|
1091
|
+
elsif value.is_a?(String)
|
|
1041
1092
|
year, month = value.split('-')
|
|
1042
1093
|
if !old_value.to_s.empty?
|
|
1043
1094
|
Time.new(year, month, old_value.day, old_value.hour, old_value.min)
|
|
1044
1095
|
else
|
|
1045
1096
|
Time.new(year, month)
|
|
1046
1097
|
end
|
|
1098
|
+
else
|
|
1099
|
+
value
|
|
1047
1100
|
end
|
|
1048
1101
|
},
|
|
1049
1102
|
},
|
|
@@ -1,85 +1,126 @@
|
|
|
1
1
|
require 'glimmer-dsl-web'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
unless Object.const_defined?(:TimePresenter) # this is only needed in sample selector app due to file reloading, but not in real apps.
|
|
4
|
+
class TimePresenter
|
|
5
|
+
attr_accessor :date_time, :week_string
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@date_time = Time.now
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def week_string
|
|
12
|
+
return nil if @date_time.nil?
|
|
13
|
+
year = @date_time.year
|
|
14
|
+
week = ((@date_time.yday / 7).to_i + 1).to_s.rjust(2, '0')
|
|
15
|
+
"#{year}-W#{week}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def date_time_string
|
|
19
|
+
@date_time&.strftime('%Y-%m-%dT%H:%M')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def date_time_string=(value)
|
|
23
|
+
if value.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/)
|
|
24
|
+
date_time_parts = value.split('T')
|
|
25
|
+
date_parts = date_time_parts.first.split('-')
|
|
26
|
+
time_parts = date_time_parts.last.split(':')
|
|
27
|
+
self.date_time = Time.new(*date_parts, *time_parts)
|
|
28
|
+
elsif value.strip.empty?
|
|
29
|
+
self.date_time = nil
|
|
30
|
+
end
|
|
31
|
+
end
|
|
8
32
|
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
unless Object.const_defined?(:HelloInputDateTime) # this is only needed in sample selector app due to file reloading, but not in real apps.
|
|
36
|
+
class HelloInputDateTime
|
|
37
|
+
include Glimmer::Web::Component
|
|
38
|
+
|
|
39
|
+
before_render do
|
|
40
|
+
@time_presenter = TimePresenter.new
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
after_render do
|
|
44
|
+
observe(@time_presenter, :date_time) do |new_date_time|
|
|
45
|
+
@date_time_field.value = new_date_time
|
|
46
|
+
@date_field.value = new_date_time
|
|
47
|
+
@time_field.value = new_date_time
|
|
48
|
+
@month_field.value = new_date_time
|
|
49
|
+
@week_field.value = @time_presenter.week_string
|
|
50
|
+
@time_string_field.value = @time_presenter.date_time_string
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
markup {
|
|
55
|
+
div {
|
|
56
|
+
div(style: 'display: grid; grid-auto-columns: 130px 260px;') { |container_div|
|
|
57
|
+
label('Date Time: ', for: 'date-time-field')
|
|
58
|
+
@date_time_field = input(id: 'date-time-field', type: 'datetime-local') {
|
|
59
|
+
value @time_presenter.date_time
|
|
60
|
+
|
|
61
|
+
oninput do |event|
|
|
62
|
+
@time_presenter.date_time = @date_time_field.value
|
|
63
|
+
end
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
label('Date: ', for: 'date-field')
|
|
67
|
+
@date_field = input(id: 'date-field', type: 'date') {
|
|
68
|
+
value @time_presenter.date_time
|
|
69
|
+
|
|
70
|
+
oninput do |event|
|
|
71
|
+
@time_presenter.date_time = @date_field.value
|
|
72
|
+
end
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
label('Time: ', for: 'time-field')
|
|
76
|
+
@time_field = input(id: 'time-field', type: 'time') {
|
|
77
|
+
value @time_presenter.date_time
|
|
78
|
+
|
|
79
|
+
oninput do |event|
|
|
80
|
+
@time_presenter.date_time = @time_field.value
|
|
81
|
+
end
|
|
82
|
+
}
|
|
9
83
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
84
|
+
label('Month: ', for: 'month-field')
|
|
85
|
+
@month_field = input(id: 'month-field', type: 'month') {
|
|
86
|
+
value @time_presenter.date_time
|
|
87
|
+
|
|
88
|
+
oninput do |event|
|
|
89
|
+
@time_presenter.date_time = @month_field.value
|
|
90
|
+
end
|
|
91
|
+
}
|
|
16
92
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
93
|
+
label('Week: ', for: 'week-field')
|
|
94
|
+
@week_field = input(id: 'week-field', type: 'week', disabled: true) {
|
|
95
|
+
value @time_presenter.week_string
|
|
96
|
+
}
|
|
20
97
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
98
|
+
label('Time String: ', for: 'time-string-field')
|
|
99
|
+
@time_string_field = input(id: 'time-string-field', type: 'text') {
|
|
100
|
+
value @time_presenter.date_time_string
|
|
101
|
+
|
|
102
|
+
oninput do |event|
|
|
103
|
+
@time_presenter.date_time_string = @time_string_field.value
|
|
104
|
+
end
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
style {
|
|
108
|
+
r("#{container_div.selector} *") {
|
|
109
|
+
margin '5px'
|
|
110
|
+
}
|
|
111
|
+
r("#{container_div.selector} label") {
|
|
112
|
+
grid_column '1'
|
|
113
|
+
}
|
|
114
|
+
r("#{container_div.selector} input") {
|
|
115
|
+
grid_column '2'
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
30
121
|
end
|
|
31
122
|
end
|
|
32
123
|
|
|
33
|
-
@time_presenter = TimePresenter.new
|
|
34
|
-
|
|
35
|
-
include Glimmer
|
|
36
|
-
|
|
37
124
|
Document.ready? do
|
|
38
|
-
|
|
39
|
-
div(style: 'display: grid; grid-auto-columns: 130px 260px;') { |container_div|
|
|
40
|
-
label('Date Time: ', for: 'date-time-field')
|
|
41
|
-
input(id: 'date-time-field', type: 'datetime-local') {
|
|
42
|
-
# Bidirectional Data-Binding with <=> ensures input.value and @time_presenter.date_time
|
|
43
|
-
# automatically stay in sync when either side changes
|
|
44
|
-
value <=> [@time_presenter, :date_time]
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
label('Date: ', for: 'date-field')
|
|
48
|
-
input(id: 'date-field', type: 'date') {
|
|
49
|
-
value <=> [@time_presenter, :date_time]
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
label('Time: ', for: 'time-field')
|
|
53
|
-
input(id: 'time-field', type: 'time') {
|
|
54
|
-
value <=> [@time_presenter, :date_time]
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
label('Month: ', for: 'month-field')
|
|
58
|
-
input(id: 'month-field', type: 'month') {
|
|
59
|
-
value <=> [@time_presenter, :date_time]
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
label('Week: ', for: 'week-field')
|
|
63
|
-
input(id: 'week-field', type: 'week', disabled: true) {
|
|
64
|
-
value <= [@time_presenter, :week_string, computed_by: :date_time]
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
label('Time String: ', for: 'time-string-field')
|
|
68
|
-
input(id: 'time-string-field', type: 'text') {
|
|
69
|
-
value <=> [@time_presenter, :date_time_string, computed_by: :date_time]
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
style {
|
|
73
|
-
r("#{container_div.selector} *") {
|
|
74
|
-
margin '5px'
|
|
75
|
-
}
|
|
76
|
-
r("#{container_div.selector} label") {
|
|
77
|
-
grid_column '1'
|
|
78
|
-
}
|
|
79
|
-
r("#{container_div.selector} input") {
|
|
80
|
-
grid_column '2'
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
125
|
+
HelloInputDateTime.render
|
|
85
126
|
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
require 'glimmer-dsl-web'
|
|
2
|
+
|
|
3
|
+
unless Object.const_defined?(:TimePresenter) # this is only needed in sample selector app due to file reloading, but not in real apps.
|
|
4
|
+
class TimePresenter
|
|
5
|
+
attr_accessor :date_time, :week_string
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@date_time = Time.now
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def week_string
|
|
12
|
+
return nil if @date_time.nil?
|
|
13
|
+
year = @date_time.year
|
|
14
|
+
week = ((@date_time.yday / 7).to_i + 1).to_s.rjust(2, '0')
|
|
15
|
+
"#{year}-W#{week}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def date_time_string
|
|
19
|
+
@date_time&.strftime('%Y-%m-%dT%H:%M')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def date_time_string=(value)
|
|
23
|
+
if value.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/)
|
|
24
|
+
date_time_parts = value.split('T')
|
|
25
|
+
date_parts = date_time_parts.first.split('-')
|
|
26
|
+
time_parts = date_time_parts.last.split(':')
|
|
27
|
+
self.date_time = Time.new(*date_parts, *time_parts)
|
|
28
|
+
elsif value.strip.empty?
|
|
29
|
+
self.date_time = nil
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
unless Object.const_defined?(:HelloInputDateTimeDataBinding) # this is only needed in sample selector app due to file reloading, but not in real apps.
|
|
36
|
+
class HelloInputDateTimeDataBinding
|
|
37
|
+
include Glimmer::Web::Component
|
|
38
|
+
|
|
39
|
+
before_render do
|
|
40
|
+
@time_presenter = TimePresenter.new
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
markup {
|
|
44
|
+
div {
|
|
45
|
+
div(style: 'display: grid; grid-auto-columns: 130px 260px;') { |container_div|
|
|
46
|
+
label('Date Time: ', for: 'date-time-field')
|
|
47
|
+
input(id: 'date-time-field', type: 'datetime-local') {
|
|
48
|
+
# Bidirectional Data-Binding with <=> ensures input.value and @time_presenter.date_time
|
|
49
|
+
# automatically stay in sync when either side changes
|
|
50
|
+
value <=> [@time_presenter, :date_time]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
label('Date: ', for: 'date-field')
|
|
54
|
+
input(id: 'date-field', type: 'date') {
|
|
55
|
+
value <=> [@time_presenter, :date_time]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
label('Time: ', for: 'time-field')
|
|
59
|
+
input(id: 'time-field', type: 'time') {
|
|
60
|
+
value <=> [@time_presenter, :date_time]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
label('Month: ', for: 'month-field')
|
|
64
|
+
input(id: 'month-field', type: 'month') {
|
|
65
|
+
value <=> [@time_presenter, :date_time]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
label('Week: ', for: 'week-field')
|
|
69
|
+
input(id: 'week-field', type: 'week', disabled: true) {
|
|
70
|
+
value <= [@time_presenter, :week_string, computed_by: :date_time]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
label('Time String: ', for: 'time-string-field')
|
|
74
|
+
input(id: 'time-string-field', type: 'text') {
|
|
75
|
+
value <=> [@time_presenter, :date_time_string, computed_by: :date_time]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
style {
|
|
79
|
+
r("#{container_div.selector} *") {
|
|
80
|
+
margin '5px'
|
|
81
|
+
}
|
|
82
|
+
r("#{container_div.selector} label") {
|
|
83
|
+
grid_column '1'
|
|
84
|
+
}
|
|
85
|
+
r("#{container_div.selector} input") {
|
|
86
|
+
grid_column '2'
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
Document.ready? do
|
|
96
|
+
HelloInputDateTimeDataBinding.render
|
|
97
|
+
end
|
|
@@ -1,55 +1,59 @@
|
|
|
1
1
|
require 'glimmer-dsl-web'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
unless Object.const_defined?(:NumberHolder) # this is only needed in sample selector app due to file reloading, but not in real apps.
|
|
4
|
+
class NumberHolder
|
|
5
|
+
attr_accessor :number
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
self.number = 50
|
|
9
|
+
end
|
|
8
10
|
end
|
|
9
11
|
end
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
before_render do
|
|
15
|
-
@number_holder = NumberHolder.new
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
after_render do
|
|
19
|
-
@number_input.value = @number_holder.number
|
|
20
|
-
@range_input.value = @number_holder.number
|
|
13
|
+
unless Object.const_defined?(:HelloObserver) # this is only needed in sample selector app due to file reloading, but not in real apps.
|
|
14
|
+
class HelloObserver
|
|
15
|
+
include Glimmer::Web::Component
|
|
21
16
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
# HelloObserver web component or its top-level markup element (div)
|
|
25
|
-
observe(@number_holder, :number) do
|
|
26
|
-
number_string = @number_holder.number.to_s
|
|
27
|
-
@number_input.value = number_string unless @number_input.value == number_string
|
|
28
|
-
@range_input.value = number_string unless @range_input.value == number_string
|
|
17
|
+
before_render do
|
|
18
|
+
@number_holder = NumberHolder.new
|
|
29
19
|
end
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
20
|
+
|
|
21
|
+
after_render do
|
|
22
|
+
@number_input.value = @number_holder.number
|
|
23
|
+
@range_input.value = @number_holder.number
|
|
24
|
+
|
|
25
|
+
# Observe Model attribute @number_holder.number for changes and update View elements.
|
|
26
|
+
# Observer is automatically cleaned up when `remove` method is called on rendered
|
|
27
|
+
# HelloObserver web component or its top-level markup element (div)
|
|
28
|
+
observe(@number_holder, :number) do
|
|
29
|
+
number_string = @number_holder.number.to_s
|
|
30
|
+
@number_input.value = number_string unless @number_input.value == number_string
|
|
31
|
+
@range_input.value = number_string unless @range_input.value == number_string
|
|
32
|
+
end
|
|
33
|
+
# Bidirectional Data-Binding does the same thing automatically as per alternative sample: Hello, Observer (Data-Binding)!
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
markup {
|
|
35
37
|
div {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
div {
|
|
39
|
+
@number_input = input(type: 'number', min: 0, max: 100) {
|
|
40
|
+
# oninput listener (observer) updates Model attribute @number_holder.number
|
|
41
|
+
oninput do
|
|
42
|
+
@number_holder.number = @number_input.value.to_i
|
|
43
|
+
end
|
|
44
|
+
}
|
|
41
45
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
div {
|
|
47
|
+
@range_input = input(type: 'range', min: 0, max: 100) {
|
|
48
|
+
# oninput listener (observer) updates Model attribute @number_holder.number
|
|
49
|
+
oninput do
|
|
50
|
+
@number_holder.number = @range_input.value.to_i
|
|
51
|
+
end
|
|
52
|
+
}
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
|
-
|
|
56
|
+
end
|
|
53
57
|
end
|
|
54
58
|
|
|
55
59
|
Document.ready? do
|
|
@@ -1,34 +1,38 @@
|
|
|
1
1
|
require 'glimmer-dsl-web'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
unless Object.const_defined?(:NumberHolder) # this is only needed in the sample app, not in real apps
|
|
4
|
+
class NumberHolder
|
|
5
|
+
attr_accessor :number
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
self.number = 50
|
|
9
|
+
end
|
|
8
10
|
end
|
|
9
11
|
end
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
unless Object.const_defined?(:HelloObserverDataBinding) # this is only needed in the sample app, not in real apps
|
|
14
|
+
class HelloObserverDataBinding
|
|
15
|
+
include Glimmer::Web::Component
|
|
16
|
+
|
|
17
|
+
before_render do
|
|
18
|
+
@number_holder = NumberHolder.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
markup {
|
|
20
22
|
div {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
div {
|
|
24
|
+
input(type: 'number', min: 0, max: 100) {
|
|
25
|
+
value <=> [@number_holder, :number]
|
|
26
|
+
}
|
|
23
27
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
div {
|
|
29
|
+
input(type: 'range', min: 0, max: 100) {
|
|
30
|
+
value <=> [@number_holder, :number]
|
|
31
|
+
}
|
|
28
32
|
}
|
|
29
33
|
}
|
|
30
34
|
}
|
|
31
|
-
|
|
35
|
+
end
|
|
32
36
|
end
|
|
33
37
|
|
|
34
38
|
Document.ready? do
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
Todo = Struct.new(:task, :completed, :editing,
|
|
1
|
+
Todo = Struct.new(:task, :completed, :editing, keyword_init: true) do
|
|
2
2
|
alias completed? completed
|
|
3
3
|
alias editing? editing
|
|
4
|
-
alias deleted? deleted
|
|
5
4
|
|
|
6
5
|
def active = !completed
|
|
7
6
|
alias active? active
|
|
@@ -6,7 +6,7 @@ class TodoPresenter
|
|
|
6
6
|
FILTERS = [:all, :active, :completed]
|
|
7
7
|
FILTER_ROUTE_REGEXP = /\#\/([^\/]*)$/
|
|
8
8
|
|
|
9
|
-
attr_accessor :can_clear_completed, :active_todo_count
|
|
9
|
+
attr_accessor :can_clear_completed, :active_todo_count
|
|
10
10
|
attr_reader :todos, :new_todo, :filter
|
|
11
11
|
|
|
12
12
|
def initialize
|
|
@@ -27,7 +27,6 @@ class TodoPresenter
|
|
|
27
27
|
todos.append(todo)
|
|
28
28
|
observe_todo_completion_to_update_todo_stats(todo)
|
|
29
29
|
new_todo.task = ''
|
|
30
|
-
self.created_todo = todo # notifies View observer indirectly to add created todo to todo list
|
|
31
30
|
end
|
|
32
31
|
|
|
33
32
|
def filter=(filter)
|
|
@@ -87,7 +86,6 @@ class TodoPresenter
|
|
|
87
86
|
todos.delete(todo)
|
|
88
87
|
observer_registration = observers_for_todo_stats.delete(todo.object_id)
|
|
89
88
|
observer_registration&.deregister
|
|
90
|
-
todo.deleted = true # notifies View observer indirectly to delete todo
|
|
91
89
|
end
|
|
92
90
|
|
|
93
91
|
def observers_for_todo_stats
|
|
@@ -3,11 +3,8 @@ require_relative 'todo_input'
|
|
|
3
3
|
class NewTodoInput < TodoInput
|
|
4
4
|
option :presenter
|
|
5
5
|
|
|
6
|
-
markup {
|
|
6
|
+
markup {
|
|
7
7
|
input(placeholder: "What needs to be done?", autofocus: "") {
|
|
8
|
-
# Data-bind `input` `value` property bidirectionally to `presenter.new_todo` `task` attribute
|
|
9
|
-
# meaning make any changes to the new todo task automatically update the input value
|
|
10
|
-
# and any changes to the input value by the user automatically update the new todo task value
|
|
11
8
|
value <=> [presenter.new_todo, :task]
|
|
12
9
|
|
|
13
10
|
onkeyup do |event|
|
|
@@ -16,10 +13,10 @@ class NewTodoInput < TodoInput
|
|
|
16
13
|
}
|
|
17
14
|
}
|
|
18
15
|
|
|
19
|
-
style {
|
|
16
|
+
style {
|
|
20
17
|
todo_input_styles
|
|
21
18
|
|
|
22
|
-
r(component_element_selector) {
|
|
19
|
+
r(component_element_selector) {
|
|
23
20
|
padding '16px 16px 16px 60px'
|
|
24
21
|
height 65
|
|
25
22
|
border :none
|
|
@@ -5,14 +5,6 @@ class TodoList
|
|
|
5
5
|
|
|
6
6
|
option :presenter
|
|
7
7
|
|
|
8
|
-
after_render do
|
|
9
|
-
observe(presenter, :created_todo) do |todo|
|
|
10
|
-
@todo_ul.content { # re-open todo ul content to add created todo
|
|
11
|
-
todo_list_item(presenter:, todo:)
|
|
12
|
-
}
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
|
|
16
8
|
markup {
|
|
17
9
|
main(class: 'main') {
|
|
18
10
|
div(class: 'toggle-all-container') {
|
|
@@ -25,14 +17,14 @@ class TodoList
|
|
|
25
17
|
}
|
|
26
18
|
}
|
|
27
19
|
|
|
28
|
-
|
|
29
|
-
# class name is data-bound unidirectionally to the presenter filter attribute,
|
|
30
|
-
# meaning it would automatically get set to its value whenever presenter.filter changes
|
|
20
|
+
ul {
|
|
31
21
|
class_name <= [presenter, :filter]
|
|
32
22
|
|
|
33
|
-
presenter
|
|
34
|
-
|
|
35
|
-
|
|
23
|
+
content(presenter, :todos) {
|
|
24
|
+
presenter.todos.each do |todo|
|
|
25
|
+
todo_list_item(presenter:, todo:)
|
|
26
|
+
end
|
|
27
|
+
}
|
|
36
28
|
}
|
|
37
29
|
}
|
|
38
30
|
}
|
|
@@ -6,43 +6,21 @@ class TodoListItem
|
|
|
6
6
|
option :presenter
|
|
7
7
|
option :todo
|
|
8
8
|
|
|
9
|
-
after_render do
|
|
10
|
-
# after rendering markup, observe todo deleted attribute and remove component when deleted
|
|
11
|
-
observe(todo, :deleted) do |deleted|
|
|
12
|
-
self.remove if deleted
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
|
|
16
9
|
markup {
|
|
17
10
|
li {
|
|
18
|
-
# Data-bind inclusion of `completed` in `li` `class` attribute unidirectionally to `todo` `completed` attribute,
|
|
19
|
-
# meaning inclusion/exclusion of `completed` class happens automatically when `todo.completed` boolean value changes.
|
|
20
11
|
class_name(:completed) <= [todo, :completed]
|
|
21
|
-
|
|
22
|
-
# Data-bind inclusion of `active` in `li` `class` attribute unidirectionally to `todo` `completed` attribute, negated,
|
|
23
|
-
# meaning inclusion/exclusion of `active` class happens automatically when `todo.completed` negated boolean value changes.
|
|
24
12
|
class_name(:active) <= [todo, :completed, on_read: :!]
|
|
25
|
-
|
|
26
|
-
# Data-bind inclusion of `editing` in `li` `class` attribute unidirectionally to `todo` `editing` attribute,
|
|
27
|
-
# meaning inclusion/exclusion of `editing` class happens automatically when `todo.editing` boolean value changes.
|
|
28
13
|
class_name(:editing) <= [todo, :editing]
|
|
29
14
|
|
|
30
15
|
div(class: 'view') {
|
|
31
16
|
input(class: 'toggle', type: 'checkbox') {
|
|
32
|
-
# Data-bind `input` `checked` property bidirectionally to `todo` `completed` attribute
|
|
33
|
-
# meaning make any changes to the `todo` `completed` attribute value automatically update the `input` `checked` property
|
|
34
|
-
# and any changes to the `input` `checked` property by the user automatically update the `todo` `completed` attribute value.
|
|
35
|
-
# `after_write` hook is invoked after writing a new value to the model attribute (`todo` `completed` attribute)
|
|
36
17
|
checked <=> [todo, :completed]
|
|
37
18
|
}
|
|
38
19
|
|
|
39
20
|
label {
|
|
40
|
-
# Data-bind `label` inner HTML unidirectionally to `todo.task` (`String` value),
|
|
41
|
-
# meaning make changes to `todo` `task` attribute automatically update `label` inner HTML.
|
|
42
21
|
inner_html <= [todo, :task]
|
|
43
22
|
|
|
44
23
|
ondblclick do |event|
|
|
45
|
-
# if the markup root (li) last child is not an input field, re-open content and add an edit input field
|
|
46
24
|
unless markup_root.children.last.keyword == 'input'
|
|
47
25
|
markup_root.content {
|
|
48
26
|
edit_todo_input(presenter:, todo:)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: glimmer-dsl-web
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andy Maleh
|
|
@@ -321,6 +321,7 @@ files:
|
|
|
321
321
|
- lib/glimmer-dsl-web/samples/hello/hello_form_mvp/views/contact_table.rb
|
|
322
322
|
- lib/glimmer-dsl-web/samples/hello/hello_glimmer_component_helper/address_form.rb
|
|
323
323
|
- lib/glimmer-dsl-web/samples/hello/hello_input_date_time.rb
|
|
324
|
+
- lib/glimmer-dsl-web/samples/hello/hello_input_date_time_data_binding.rb
|
|
324
325
|
- lib/glimmer-dsl-web/samples/hello/hello_modal.rb
|
|
325
326
|
- lib/glimmer-dsl-web/samples/hello/hello_observer.rb
|
|
326
327
|
- lib/glimmer-dsl-web/samples/hello/hello_observer_data_binding.rb
|