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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 75e7b37a6cf240d81c9a1e9849b3c5ee131e687562082f4cab743b93773acd1a
4
- data.tar.gz: 449aa6cb6effc7d7b2be71a93e74a3b5478a3b4c35152a3261b551de02018001
3
+ metadata.gz: 2f64cd1a4155afc8688d37c70266a28288ae80473332c9dc2bb0dbf01d81b590
4
+ data.tar.gz: 51ff57301add3f920f5a0425bb4817659bdcddf77882591313f76693906a6116
5
5
  SHA512:
6
- metadata.gz: bcb77d7c98262ea80a1acde5a329009eb50e9abd34fb05a9d9cbb15260429b012374c961c86ce8988d46ad2ac6cefb0092e6d77455c01d035bfc8433586560f3
7
- data.tar.gz: 55fd5286dc976ec1f00535c29b5b240186e96938edae060708f1004b2b16aaee3f286f94ee020c9a9306a14e7fc25de0912a05656c7c83832f478ca111cc7e89
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.9.2 (Beta)
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
  [![Gem Version](https://badge.fury.io/rb/glimmer-dsl-web.svg)](http://badge.fury.io/rb/glimmer-dsl-web)
6
6
  [![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)
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.9.2
1
+ 0.10.0
@@ -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.9.2 ruby lib
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.2".freeze
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
- else
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
- else
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
- else
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
- else
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
- else
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
- class TimePresenter
4
- attr_accessor :date_time, :week_string
5
-
6
- def initialize
7
- @date_time = Time.now
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
- def week_string
11
- return nil if @date_time.nil?
12
- year = @date_time.year
13
- week = ((@date_time.yday / 7).to_i + 1).to_s.rjust(2, '0')
14
- "#{year}-W#{week}"
15
- end
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
- def date_time_string
18
- @date_time&.strftime('%Y-%m-%dT%H:%M')
19
- end
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
- def date_time_string=(value)
22
- if value.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/)
23
- date_time_parts = value.split('T')
24
- date_parts = date_time_parts.first.split('-')
25
- time_parts = date_time_parts.last.split(':')
26
- self.date_time = Time.new(*date_parts, *time_parts)
27
- elsif value.strip.empty?
28
- self.date_time = nil
29
- end
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
- div {
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
- class NumberHolder
4
- attr_accessor :number
5
-
6
- def initialize
7
- self.number = 50
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
- class HelloObserver
12
- include Glimmer::Web::Component
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
- # Observe Model attribute @number_holder.number for changes and update View elements.
23
- # Observer is automatically cleaned up when `remove` method is called on rendered
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
- # Bidirectional Data-Binding does the same thing automatically as per alternative sample: Hello, Observer (Data-Binding)!
31
- end
32
-
33
- markup {
34
- div {
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
- @number_input = input(type: 'number', min: 0, max: 100) {
37
- # oninput listener (observer) updates Model attribute @number_holder.number
38
- oninput do
39
- @number_holder.number = @number_input.value.to_i
40
- end
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
- div {
44
- @range_input = input(type: 'range', min: 0, max: 100) {
45
- # oninput listener (observer) updates Model attribute @number_holder.number
46
- oninput do
47
- @number_holder.number = @range_input.value.to_i
48
- end
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
- class NumberHolder
4
- attr_accessor :number
5
-
6
- def initialize
7
- self.number = 50
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
- class HelloObserverDataBinding
12
- include Glimmer::Web::Component
13
-
14
- before_render do
15
- @number_holder = NumberHolder.new
16
- end
17
-
18
- markup {
19
- div {
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
- input(type: 'number', min: 0, max: 100) {
22
- value <=> [@number_holder, :number]
23
+ div {
24
+ input(type: 'number', min: 0, max: 100) {
25
+ value <=> [@number_holder, :number]
26
+ }
23
27
  }
24
- }
25
- div {
26
- input(type: 'range', min: 0, max: 100) {
27
- value <=> [@number_holder, :number]
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, :deleted, keyword_init: true) do
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, :created_todo
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 { # evaluated against instance as a smart convention
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 { # evaluated against class as a smart convention (common to all instances)
16
+ style {
20
17
  todo_input_styles
21
18
 
22
- r(component_element_selector) { # NewTodoInput has component_element_class as 'new-todo-input'
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
- @todo_ul = ul {
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.todos.each do |todo|
34
- todo_list_item(presenter:, todo:)
35
- end
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.9.2
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