glimmer-dsl-web 0.0.8 → 0.0.9

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.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.8
1
+ 0.0.9
@@ -2,16 +2,16 @@
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.0.8 ruby lib
5
+ # stub: glimmer-dsl-web 0.0.9 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "glimmer-dsl-web".freeze
9
- s.version = "0.0.8".freeze
9
+ s.version = "0.0.9".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]
13
13
  s.authors = ["Andy Maleh".freeze]
14
- s.date = "2024-01-03"
14
+ s.date = "2024-01-05"
15
15
  s.description = "Glimmer DSL for Web (Ruby in the Browser Web GUI Frontend Library) - Enables frontend GUI development with Ruby by adopting a DSL that follows web-like HTML syntax, enabling the transfer of HTML/CSS/JS skills to Ruby frontend development. This library relies on Opal Ruby.".freeze
16
16
  s.email = "andy.am@gmail.com".freeze
17
17
  s.extra_rdoc_files = [
@@ -31,6 +31,7 @@ Gem::Specification.new do |s|
31
31
  "lib/glimmer-dsl-web/ext/date.rb",
32
32
  "lib/glimmer-dsl-web/ext/exception.rb",
33
33
  "lib/glimmer-dsl-web/samples/hello/hello_button.rb",
34
+ "lib/glimmer-dsl-web/samples/hello/hello_component.rb",
34
35
  "lib/glimmer-dsl-web/samples/hello/hello_content_data_binding.rb",
35
36
  "lib/glimmer-dsl-web/samples/hello/hello_data_binding.rb",
36
37
  "lib/glimmer-dsl-web/samples/hello/hello_form.rb",
@@ -40,6 +41,7 @@ Gem::Specification.new do |s|
40
41
  "lib/glimmer/config/opal_logger.rb",
41
42
  "lib/glimmer/data_binding/element_binding.rb",
42
43
  "lib/glimmer/dsl/web/bind_expression.rb",
44
+ "lib/glimmer/dsl/web/component_expression.rb",
43
45
  "lib/glimmer/dsl/web/content_data_binding_expression.rb",
44
46
  "lib/glimmer/dsl/web/data_binding_expression.rb",
45
47
  "lib/glimmer/dsl/web/dsl.rb",
@@ -52,6 +54,7 @@ Gem::Specification.new do |s|
52
54
  "lib/glimmer/dsl/web/shine_data_binding_expression.rb",
53
55
  "lib/glimmer/util/proc_tracker.rb",
54
56
  "lib/glimmer/web.rb",
57
+ "lib/glimmer/web/component.rb",
55
58
  "lib/glimmer/web/element_proxy.rb",
56
59
  "lib/glimmer/web/event_proxy.rb",
57
60
  "lib/glimmer/web/listener_proxy.rb"
@@ -0,0 +1,30 @@
1
+ require 'glimmer/dsl/parent_expression'
2
+ require 'glimmer/web/component'
3
+
4
+ module Glimmer
5
+ module DSL
6
+ module Web
7
+ class ComponentExpression < Expression
8
+ include ParentExpression
9
+
10
+ def can_interpret?(parent, keyword, *args, &block)
11
+ !!Glimmer::Web::Component.for(keyword)
12
+ end
13
+
14
+ def interpret(parent, keyword, *args, &block)
15
+ custom_widget_class = Glimmer::Web::Component.for(keyword)
16
+ custom_widget_class.new(parent, args, {}, &block)
17
+ end
18
+
19
+ def add_content(parent, keyword, *args, &block)
20
+ # TODO consider avoiding source_location since it does not work in Opal
21
+ if block.source_location && (block.source_location == parent.content&.__getobj__&.source_location)
22
+ parent.content.call(parent) unless parent.content.called?
23
+ else
24
+ super
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -8,6 +8,7 @@ require 'glimmer/dsl/web/bind_expression'
8
8
  require 'glimmer/dsl/web/data_binding_expression'
9
9
  require 'glimmer/dsl/web/content_data_binding_expression'
10
10
  require 'glimmer/dsl/web/shine_data_binding_expression'
11
+ require 'glimmer/dsl/web/component_expression'
11
12
 
12
13
  module Glimmer
13
14
  module DSL
@@ -15,6 +16,7 @@ module Glimmer
15
16
  Engine.add_dynamic_expressions(
16
17
  Web,
17
18
  %w[
19
+ component
18
20
  listener
19
21
  data_binding
20
22
  property
@@ -0,0 +1,317 @@
1
+ # Copyright (c) 2023-2024 Andy Maleh
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ require 'glimmer'
23
+ require 'glimmer/error'
24
+ require 'glimmer/util/proc_tracker'
25
+ require 'glimmer/data_binding/observer'
26
+ require 'glimmer/data_binding/observable_model'
27
+
28
+ module Glimmer
29
+ module Web
30
+ module Component
31
+ include DataBinding::ObservableModel
32
+
33
+ module ClassMethods
34
+ include Glimmer
35
+
36
+ # Allows defining convenience option accessors for an array of option names
37
+ # Example: `options :color1, :color2` defines `#color1` and `#color2`
38
+ # where they return the instance values `options[:color1]` and `options[:color2]`
39
+ # respectively.
40
+ # Can be called multiple times to set more options additively.
41
+ # When passed no arguments, it returns list of all option names captured so far
42
+ def options(*new_options)
43
+ new_options = new_options.compact.map(&:to_s).map(&:to_sym)
44
+ if new_options.empty?
45
+ @options ||= {} # maps options to defaults
46
+ else
47
+ new_options = new_options.reduce({}) {|new_options_hash, new_option| new_options_hash.merge(new_option => nil)}
48
+ @options = options.merge(new_options)
49
+ def_option_attr_accessors(new_options)
50
+ end
51
+ end
52
+
53
+ def option(new_option, default: nil)
54
+ new_option = new_option.to_s.to_sym
55
+ new_options = {new_option => default}
56
+ '@options = options.merge(new_options)'
57
+ @options = options.merge(new_options)
58
+ 'def_option_attr_accessors(new_options)'
59
+ def_option_attr_accessors(new_options)
60
+ end
61
+
62
+ def def_option_attr_accessors(new_options)
63
+ new_options.each do |option, default|
64
+ define_method(option) do
65
+ options[:"#{option}"]
66
+ end
67
+ define_method("#{option}=") do |option_value|
68
+ self.options[:"#{option}"] = option_value
69
+ end
70
+ end
71
+ end
72
+
73
+ def before_render(&block)
74
+ @before_render_blocks ||= []
75
+ @before_render_blocks << block
76
+ end
77
+
78
+ def markup(&block)
79
+ @markup_block = block
80
+ end
81
+
82
+ def after_render(&block)
83
+ @after_render_blocks ||= []
84
+ @after_render_blocks << block
85
+ end
86
+
87
+ def keyword
88
+ self.name.underscore.gsub('::', '__')
89
+ end
90
+
91
+ # Returns shortcut keyword to use for this component (keyword minus namespace)
92
+ def shortcut_keyword
93
+ self.name.underscore.gsub('::', '__').split('__').last
94
+ end
95
+
96
+ def render(*args)
97
+ rendered_component = send(keyword)
98
+ rendered_component.render(*args)
99
+ rendered_component
100
+ end
101
+ end
102
+
103
+ # This module was only created to prevent Glimmer from checking method_missing first
104
+ module GlimmerSupersedable
105
+ def method_missing(method_name, *args, &block)
106
+ Glimmer::DSL::Engine.interpret(method_name, *args, &block)
107
+ rescue
108
+ super(method_name, *args, &block)
109
+ end
110
+ end
111
+
112
+ class << self
113
+ def included(klass)
114
+ if !klass.ancestors.include?(GlimmerSupersedable)
115
+ klass.extend(ClassMethods)
116
+ klass.include(Glimmer)
117
+ klass.prepend(GlimmerSupersedable)
118
+ Glimmer::Web::Component.add_component_namespaces_for(klass)
119
+ end
120
+ end
121
+
122
+ def for(underscored_component_name)
123
+ extracted_namespaces = underscored_component_name.
124
+ to_s.
125
+ split(/__/).map do |namespace|
126
+ namespace.camelcase(:upper)
127
+ end
128
+ Glimmer::Web::Component.component_namespaces.each do |base|
129
+ extracted_namespaces.reduce(base) do |result, namespace|
130
+ if !result.constants.include?(namespace)
131
+ namespace = result.constants.detect {|c| c.to_s.upcase == namespace.to_s.upcase } || namespace
132
+ end
133
+ begin
134
+ constant = result.const_get(namespace)
135
+ return constant if constant&.respond_to?(:ancestors) &&
136
+ (
137
+ constant&.ancestors&.to_a.include?(Glimmer::Web::Component) ||
138
+ # TODO checking GlimmerSupersedable as a hack because when a class is loaded twice (like when loading samples
139
+ # by reloading ruby files), it loses its Glimmer::Web::Component ancestor as a bug in Opal
140
+ # but somehow the prepend module remains
141
+ constant&.ancestors&.to_a.include?(GlimmerSupersedable)
142
+ )
143
+ constant
144
+ rescue => e
145
+ # Glimmer::Config.logger.debug {"#{e.message}\n#{e.backtrace.join("\n")}"}
146
+ result
147
+ end
148
+ end
149
+ end
150
+ raise "#{underscored_component_name} has no Glimmer web component class!"
151
+ rescue => e
152
+ Glimmer::Config.logger.debug {e.message}
153
+ Glimmer::Config.logger.debug {"#{e.message}\n#{e.backtrace.join("\n")}"}
154
+ nil
155
+ end
156
+
157
+ def add_component_namespaces_for(klass)
158
+ Glimmer::Web::Component.namespaces_for_class(klass).drop(1).each do |namespace|
159
+ Glimmer::Web::Component.component_namespaces << namespace
160
+ end
161
+ end
162
+
163
+ def namespaces_for_class(m)
164
+ return [m] if m.name.nil?
165
+ namespace_constants = m.name.split(/::/).map(&:to_sym)
166
+ namespace_constants.reduce([Object]) do |output, namespace_constant|
167
+ output += [output.last.const_get(namespace_constant)]
168
+ end[1..-1].uniq.reverse
169
+ end
170
+
171
+ def component_namespaces
172
+ @component_namespaces ||= reset_component_namespaces
173
+ end
174
+
175
+ def reset_component_namespaces
176
+ @component_namespaces = Set[Object, Glimmer::Web]
177
+ end
178
+ end
179
+ # <- end of class methods
180
+
181
+
182
+ attr_reader :markup_root, :parent, :options
183
+ alias parent_proxy parent
184
+
185
+ def initialize(parent, args, options, &content)
186
+ @parent = parent
187
+ options = args.delete_at(-1) if args.is_a?(Array) && args.last.is_a?(Hash)
188
+ if args.is_a?(Hash)
189
+ options = args
190
+ args = []
191
+ end
192
+ options ||= {}
193
+ @args = args
194
+ options ||= {}
195
+ @options = self.class.options.merge(options)
196
+ @content = Util::ProcTracker.new(content) if content
197
+ execute_hooks('before_render')
198
+ markup_block = self.class.instance_variable_get("@markup_block")
199
+ raise Glimmer::Error, 'Invalid Glimmer web component for having no markup! Please define markup block!' if markup_block.nil?
200
+ @markup_root = instance_exec(&markup_block)
201
+ @parent ||= @markup_root.parent
202
+ raise Glimmer::Error, 'Invalid Glimmer web component for having an empty markup! Please fill markup block!' if @markup_root.nil?
203
+ execute_hooks('after_render')
204
+ end
205
+
206
+ # Subclasses may override to perform post initialization work on an added child
207
+ def post_initialize_child(child)
208
+ # No Op by default
209
+ end
210
+
211
+ def can_handle_observation_request?(observation_request)
212
+ observation_request = observation_request.to_s
213
+ result = false
214
+ if observation_request.start_with?('on_updated_')
215
+ property = observation_request.sub(/^on_updated_/, '')
216
+ result = can_add_observer?(property)
217
+ end
218
+ result || markup_root&.can_handle_observation_request?(observation_request)
219
+ end
220
+
221
+ def handle_observation_request(observation_request, block)
222
+ observation_request = observation_request.to_s
223
+ if observation_request.start_with?('on_updated_')
224
+ property = observation_request.sub(/^on_updated_/, '') # TODO look into eliminating duplication from above
225
+ add_observer(DataBinding::Observer.proc(&block), property) if can_add_observer?(property)
226
+ else
227
+ markup_root.handle_observation_request(observation_request, block)
228
+ end
229
+ end
230
+
231
+ def can_add_observer?(attribute_name)
232
+ has_instance_method?(attribute_name) || has_instance_method?("#{attribute_name}?") || @markup_root.can_add_observer?(attribute_name)
233
+ end
234
+
235
+ def add_observer(observer, attribute_name)
236
+ if has_instance_method?(attribute_name)
237
+ super(observer, attribute_name)
238
+ else
239
+ @markup_root.add_observer(observer, attribute_name)
240
+ end
241
+ end
242
+
243
+ def has_attribute?(attribute_name, *args)
244
+ has_instance_method?(attribute_setter(attribute_name)) ||
245
+ @markup_root.has_attribute?(attribute_name, *args)
246
+ end
247
+
248
+ def set_attribute(attribute_name, *args)
249
+ if has_instance_method?(attribute_setter(attribute_name))
250
+ send(attribute_setter(attribute_name), *args)
251
+ else
252
+ @markup_root.set_attribute(attribute_name, *args)
253
+ end
254
+ end
255
+
256
+ # This method ensures it has an instance method not coming from Glimmer DSL
257
+ def has_instance_method?(method_name)
258
+ respond_to?(method_name) and
259
+ !markup_root&.respond_to?(method_name) and
260
+ !method(method_name)&.source_location&.first&.include?('glimmer/dsl/engine.rb') and
261
+ !method(method_name)&.source_location&.first&.include?('glimmer/web/element_proxy.rb')
262
+ end
263
+
264
+ def get_attribute(attribute_name)
265
+ if has_instance_method?(attribute_name)
266
+ send(attribute_name)
267
+ else
268
+ @markup_root.get_attribute(attribute_name)
269
+ end
270
+ end
271
+
272
+ def attribute_setter(attribute_name)
273
+ "#{attribute_name}="
274
+ end
275
+
276
+ def render(*args)
277
+ # this method is defined to prevent displaying a harmless Glimmer no keyword error as an annoying useless warning
278
+ @markup_root&.render(*args)
279
+ end
280
+
281
+ # Returns content block if used as an attribute reader (no args)
282
+ # Otherwise, if a block is passed, it adds it as content to this Glimmer web component
283
+ def content(&block)
284
+ if block_given?
285
+ Glimmer::DSL::Engine.add_content(self, Glimmer::DSL::Web::ComponentExpression.new, self.class.keyword, &block)
286
+ else
287
+ @content
288
+ end
289
+ end
290
+
291
+ def method_missing(method_name, *args, &block)
292
+ if can_handle_observation_request?(method_name)
293
+ handle_observation_request(method_name, block)
294
+ elsif markup_root.respond_to?(method_name, true)
295
+ markup_root.send(method_name, *args, &block)
296
+ else
297
+ super(method_name, *args, &block)
298
+ end
299
+ end
300
+
301
+ alias local_respond_to? respond_to_missing?
302
+ def respond_to_missing?(method_name, include_private = false)
303
+ super(method_name, include_private) or
304
+ can_handle_observation_request?(method_name) or
305
+ markup_root.respond_to?(method_name, include_private)
306
+ end
307
+
308
+ private
309
+
310
+ def execute_hooks(hook_name)
311
+ self.class.instance_variable_get("@#{hook_name}_blocks")&.each do |hook_block|
312
+ instance_exec(&hook_block)
313
+ end
314
+ end
315
+ end
316
+ end
317
+ end
@@ -223,10 +223,16 @@ module Glimmer
223
223
  end
224
224
  end
225
225
 
226
- def render(custom_parent_dom_element: nil, brand_new: false)
226
+ def render(parent_selector = nil, custom_parent_dom_element: nil, brand_new: false)
227
+ options[:parent] = parent_selector if !parent_selector.to_s.empty?
228
+ if !options[:parent].to_s.empty?
229
+ # ensure element is orphaned as it is becoming a top-level root element
230
+ @parent&.post_remove_child(self)
231
+ @parent = nil
232
+ end
227
233
  the_parent_dom_element = custom_parent_dom_element || parent_dom_element
228
234
  old_element = dom_element
229
- brand_new = @dom.nil? || old_element.empty? || brand_new
235
+ brand_new = @dom.nil? || old_element.empty? || !options[:parent].to_s.empty? || brand_new
230
236
  build_dom(layout: !custom_parent_dom_element) # TODO handle custom parent layout by passing parent instead of parent dom element
231
237
  if brand_new
232
238
  # TODO make a method attach to allow subclasses to override if needed
@@ -0,0 +1,223 @@
1
+ # Copyright (c) 2023-2024 Andy Maleh
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ require 'glimmer-dsl-web'
23
+
24
+ Address = Struct.new(:full_name, :street, :street2, :city, :state, :zip_code, keyword_init: true) do
25
+ STATES = {
26
+ "AK"=>"Alaska",
27
+ "AL"=>"Alabama",
28
+ "AR"=>"Arkansas",
29
+ "AS"=>"American Samoa",
30
+ "AZ"=>"Arizona",
31
+ "CA"=>"California",
32
+ "CO"=>"Colorado",
33
+ "CT"=>"Connecticut",
34
+ "DC"=>"District of Columbia",
35
+ "DE"=>"Delaware",
36
+ "FL"=>"Florida",
37
+ "GA"=>"Georgia",
38
+ "GU"=>"Guam",
39
+ "HI"=>"Hawaii",
40
+ "IA"=>"Iowa",
41
+ "ID"=>"Idaho",
42
+ "IL"=>"Illinois",
43
+ "IN"=>"Indiana",
44
+ "KS"=>"Kansas",
45
+ "KY"=>"Kentucky",
46
+ "LA"=>"Louisiana",
47
+ "MA"=>"Massachusetts",
48
+ "MD"=>"Maryland",
49
+ "ME"=>"Maine",
50
+ "MI"=>"Michigan",
51
+ "MN"=>"Minnesota",
52
+ "MO"=>"Missouri",
53
+ "MS"=>"Mississippi",
54
+ "MT"=>"Montana",
55
+ "NC"=>"North Carolina",
56
+ "ND"=>"North Dakota",
57
+ "NE"=>"Nebraska",
58
+ "NH"=>"New Hampshire",
59
+ "NJ"=>"New Jersey",
60
+ "NM"=>"New Mexico",
61
+ "NV"=>"Nevada",
62
+ "NY"=>"New York",
63
+ "OH"=>"Ohio",
64
+ "OK"=>"Oklahoma",
65
+ "OR"=>"Oregon",
66
+ "PA"=>"Pennsylvania",
67
+ "PR"=>"Puerto Rico",
68
+ "RI"=>"Rhode Island",
69
+ "SC"=>"South Carolina",
70
+ "SD"=>"South Dakota",
71
+ "TN"=>"Tennessee",
72
+ "TX"=>"Texas",
73
+ "UT"=>"Utah",
74
+ "VA"=>"Virginia",
75
+ "VI"=>"Virgin Islands",
76
+ "VT"=>"Vermont",
77
+ "WA"=>"Washington",
78
+ "WI"=>"Wisconsin",
79
+ "WV"=>"West Virginia",
80
+ "WY"=>"Wyoming"
81
+ }
82
+
83
+ def state_code
84
+ STATES.invert[state]
85
+ end
86
+
87
+ def state_code=(value)
88
+ self.state = STATES[value]
89
+ end
90
+
91
+ def summary
92
+ to_h.values.map(&:to_s).reject(&:empty?).join(', ')
93
+ end
94
+ end
95
+
96
+ # AddressForm Glimmer Web Component (View component)
97
+ #
98
+ # Including Glimmer::Web::Component makes this class a View component and automatically
99
+ # generates a new Glimmer GUI DSL keyword that matches the lowercase underscored version
100
+ # of the name of the class. AddressForm generates address_form keyword, which can be used
101
+ # elsewhere in Glimmer GUI DSL code as done inside AddressPage below.
102
+ class AddressForm
103
+ include Glimmer::Web::Component
104
+
105
+ option :address
106
+
107
+ # Optionally, you can execute code before rendering markup.
108
+ # This is useful for pre-setup of variables (e.g. Models) that you would use in the markup.
109
+ #
110
+ # before_render do
111
+ # end
112
+
113
+ # Optionally, you can execute code after rendering markup.
114
+ # This is useful for post-setup of extra Model listeners that would interact with the
115
+ # markup elements and expect them to be rendered already.
116
+ #
117
+ # after_render do
118
+ # end
119
+
120
+ # markup block provides the content of the
121
+ markup {
122
+ div {
123
+ div(style: 'display: grid; grid-auto-columns: 80px 260px;') { |address_div|
124
+ label('Full Name: ', for: 'full-name-field')
125
+ input(id: 'full-name-field') {
126
+ value <=> [address, :full_name]
127
+ }
128
+
129
+ @somelabel = label('Street: ', for: 'street-field')
130
+ input(id: 'street-field') {
131
+ value <=> [address, :street]
132
+ }
133
+
134
+ label('Street 2: ', for: 'street2-field')
135
+ textarea(id: 'street2-field') {
136
+ value <=> [address, :street2]
137
+ }
138
+
139
+ label('City: ', for: 'city-field')
140
+ input(id: 'city-field') {
141
+ value <=> [address, :city]
142
+ }
143
+
144
+ label('State: ', for: 'state-field')
145
+ select(id: 'state-field') {
146
+ Address::STATES.each do |state_code, state|
147
+ option(value: state_code) { state }
148
+ end
149
+
150
+ value <=> [address, :state_code]
151
+ }
152
+
153
+ label('Zip Code: ', for: 'zip-code-field')
154
+ input(id: 'zip-code-field', type: 'number', min: '0', max: '99999') {
155
+ value <=> [address, :zip_code,
156
+ on_write: :to_s,
157
+ ]
158
+ }
159
+
160
+ style {
161
+ <<~CSS
162
+ #{address_div.selector} * {
163
+ margin: 5px;
164
+ }
165
+ #{address_div.selector} input, #{address_div.selector} select {
166
+ grid-column: 2;
167
+ }
168
+ CSS
169
+ }
170
+ }
171
+
172
+ div(style: 'margin: 5px') {
173
+ inner_text <= [address, :summary,
174
+ computed_by: address.members + ['state_code'],
175
+ ]
176
+ }
177
+ }
178
+ }
179
+ end
180
+
181
+ # AddressPage Glimmer Web Component (View component)
182
+ #
183
+ # This View component represents the main page being rendered,
184
+ # as done by its `render` class method below
185
+ class AddressPage
186
+ include Glimmer::Web::Component
187
+
188
+ before_render do
189
+ @shipping_address = Address.new(
190
+ full_name: 'Johnny Doe',
191
+ street: '3922 Park Ave',
192
+ street2: 'PO BOX 8382',
193
+ city: 'San Diego',
194
+ state: 'California',
195
+ zip_code: '91913',
196
+ )
197
+ @billing_address = Address.new(
198
+ full_name: 'John C Doe',
199
+ street: '123 Main St',
200
+ street2: 'Apartment 3C',
201
+ city: 'San Diego',
202
+ state: 'California',
203
+ zip_code: '91911',
204
+ )
205
+ end
206
+
207
+ markup {
208
+ div {
209
+ h1('Shipping Address')
210
+
211
+ address_form(address: @shipping_address)
212
+
213
+ h1('Billing Address')
214
+
215
+ address_form(address: @billing_address)
216
+ }
217
+ }
218
+ end
219
+
220
+ Document.ready? do
221
+ # renders a top-level (root) AddressPage component
222
+ AddressPage.render
223
+ end