glimmer-dsl-web 0.3.1 → 0.4.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.
@@ -123,13 +123,18 @@ module Glimmer
123
123
  REGEX_FORMAT_DATE = /^\d{4}-\d{2}-\d{2}$/
124
124
  REGEX_FORMAT_TIME = /^\d{2}:\d{2}$/
125
125
 
126
- attr_reader :keyword, :parent, :parent_component, :args, :options, :children, :enabled, :foreground, :background, :removed?, :rendered
126
+ attr_reader :keyword, :parent, :parent_component, :component, :args, :options, :children, :enabled, :foreground, :background, :removed, :rendered
127
127
  alias rendered? rendered
128
+ alias removed? removed
128
129
 
129
130
  def initialize(keyword, parent, args, block)
130
131
  @keyword = keyword
131
132
  @parent = parent.is_a?(Glimmer::Web::Component) ? parent.markup_root : parent
132
133
  @parent_component = parent if parent.is_a?(Glimmer::Web::Component)
134
+ if Component.interpretation_stack.last&.markup_root.nil?
135
+ @component = Component.interpretation_stack.last
136
+ @component&.instance_variable_set("@markup_root", self)
137
+ end
133
138
  @options = args.last.is_a?(Hash) ? args.last.symbolize_keys : {}
134
139
  if parent.nil?
135
140
  options[:parent] ||= Component.interpretation_stack.last&.options&.[](:parent)
@@ -164,7 +169,7 @@ module Glimmer
164
169
 
165
170
  # Executes at the closing of a parent widget curly braces after all children/properties have been added/set
166
171
  def post_add_content
167
- render if bulk_render? && @parent.nil?
172
+ render if bulk_render? && @parent.nil? && !rendered?
168
173
  end
169
174
 
170
175
  def css_classes
@@ -172,6 +177,7 @@ module Glimmer
172
177
  end
173
178
 
174
179
  def remove
180
+ return if @removed
175
181
  on_remove_listeners = listeners_for('on_remove').dup
176
182
  if rendered?
177
183
  @children.dup.each do |child|
@@ -181,6 +187,10 @@ module Glimmer
181
187
  dom_element.remove
182
188
  end
183
189
  parent&.post_remove_child(self)
190
+ if component
191
+ Glimmer::Web::Component.remove_component(component)
192
+ component.remove_style_block
193
+ end
184
194
  @removed = true
185
195
  on_remove_listeners.each do |listener|
186
196
  listener.original_event_listener.call(EventProxy.new(listener: listener))
@@ -362,7 +372,12 @@ module Glimmer
362
372
  end
363
373
 
364
374
  def html_options
365
- body_class = ([name, element_id] + css_classes.to_a).join(' ')
375
+ framework_css_classes = [name, element_id]
376
+ if component
377
+ framework_css_classes.prepend(component.class.component_element_class)
378
+ framework_css_classes.prepend(component.class.component_shortcut_element_class) if component.class.component_shortcut_element_class != component.class.component_element_class
379
+ end
380
+ body_class = (framework_css_classes + css_classes.to_a).join(' ')
366
381
  html_options = options.dup
367
382
  GLIMMER_ATTRIBUTES.each do |attribute|
368
383
  next unless html_options.include?(attribute)
@@ -413,7 +428,7 @@ module Glimmer
413
428
  if rendered?
414
429
  dom_element.add_class(css_class)
415
430
  else
416
- enqueue_post_render_method_call('class_name=', value)
431
+ enqueue_post_render_method_call('add_css_class', css_class)
417
432
  end
418
433
  end
419
434
 
@@ -425,7 +440,7 @@ module Glimmer
425
440
  if rendered?
426
441
  dom_element.remove_class(css_class)
427
442
  else
428
- enqueue_post_render_method_call('class_name=', value)
443
+ enqueue_post_render_method_call('remove_css_class', css_class)
429
444
  end
430
445
  end
431
446
 
@@ -539,8 +554,8 @@ module Glimmer
539
554
  end
540
555
 
541
556
  def data_bind(property, model_binding)
542
- element_binding_translator = value_converters_for_input_type(type)[:model_to_view]
543
- element_binding_parameters = [self, property, element_binding_translator]
557
+ element_binding_read_translator = value_converters_for_input_type(type)&.[](:model_to_view)
558
+ element_binding_parameters = [self, property, element_binding_read_translator]
544
559
  element_binding = DataBinding::ElementBinding.new(*element_binding_parameters)
545
560
  #TODO make this options observer dependent and all similar observers in element specific data binding handlers
546
561
  element_binding.observe(model_binding)
@@ -552,7 +567,8 @@ module Glimmer
552
567
  if listener_keyword
553
568
  data_binding_read_listener = lambda do |event|
554
569
  view_property_value = send(property)
555
- converted_view_property_value = value_converters_for_input_type(type)[:view_to_model].call(view_property_value, model_binding.evaluate_property)
570
+ element_binding_write_translator = value_converters_for_input_type(type)&.[](:view_to_model)
571
+ converted_view_property_value = element_binding_write_translator&.call(view_property_value, model_binding.evaluate_property) || view_property_value
556
572
  model_binding.call(converted_view_property_value)
557
573
  end
558
574
  handle_observation_request(listener_keyword, data_binding_read_listener)
@@ -722,7 +738,7 @@ module Glimmer
722
738
  end
723
739
 
724
740
  def value_converters_for_input_type(input_type)
725
- input_value_converters[input_type] || {model_to_view: ->(value, old_value) {value}, view_to_model: ->(value, old_value) {value}}
741
+ input_value_converters[input_type]
726
742
  end
727
743
 
728
744
  def input_value_converters
@@ -29,7 +29,7 @@ class NumberHolder
29
29
  end
30
30
  end
31
31
 
32
- class HelloObserver
32
+ class HelloObserverDataBinding
33
33
  include Glimmer::Web::Component
34
34
 
35
35
  before_render do
@@ -53,5 +53,5 @@ class HelloObserver
53
53
  end
54
54
 
55
55
  Document.ready? do
56
- HelloObserver.render
56
+ HelloObserverDataBinding.render
57
57
  end
@@ -0,0 +1,178 @@
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
+ class ButtonModel
25
+ WIDTH_MIN = 160
26
+ WIDTH_MAX = 960
27
+ HEIGHT_MIN = 100
28
+ HEIGHT_MAX = 600
29
+ FONT_SIZE_MIN = 40
30
+ FONT_SIZE_MAX = 200
31
+
32
+ attr_accessor :text, :pushed, :width, :height, :font_size
33
+
34
+ def initialize
35
+ @text = 'Push'
36
+ @width = WIDTH_MIN
37
+ @height = HEIGHT_MIN
38
+ @font_size = FONT_SIZE_MIN
39
+ end
40
+
41
+ def push
42
+ self.pushed = !pushed
43
+ end
44
+
45
+ def text
46
+ pushed ? 'Pull' : 'Push'
47
+ end
48
+
49
+ def width=(value)
50
+ @width = value
51
+ self.font_size = @width/4 if @font_size > @width/4
52
+ end
53
+
54
+ def height=(value)
55
+ @height = value
56
+ self.font_size = @height/2.5 if @font_size > @height/2.5
57
+ end
58
+
59
+ def font_size=(value)
60
+ @font_size = value
61
+ self.width = @font_size*4 if @height < @font_size*4
62
+ self.height = @font_size*2.5 if @height < @font_size*2.5
63
+ end
64
+ end
65
+
66
+ class StyledButton
67
+ include Glimmer::Web::Component
68
+
69
+ option :button_model
70
+
71
+ markup {
72
+ button {
73
+ inner_text <= [button_model, :text, computed_by: :pushed]
74
+
75
+ class_name <= [button_model, :pushed,
76
+ on_read: ->(pushed) { pushed ? 'pushed' : 'pulled' }
77
+ ]
78
+
79
+ style <= [ button_model, :width,
80
+ on_read: method(:button_style_value) # convert value on read before storing in style
81
+ ]
82
+
83
+ style <= [ button_model, :height,
84
+ on_read: method(:button_style_value) # convert value on read before storing in style
85
+ ]
86
+
87
+ style <= [ button_model, :font_size,
88
+ on_read: method(:button_style_value) # convert value on read before storing in style
89
+ ]
90
+
91
+ onclick do
92
+ button_model.push
93
+ end
94
+ }
95
+ }
96
+
97
+ style {'
98
+ button {
99
+ font-family: Courrier New, Courrier;
100
+ border-radius: 5px;
101
+ border-width: 17px;
102
+ border-color: #ACC7D5;
103
+ background-color: #ADD8E6;
104
+ margin: 5px;
105
+ }
106
+
107
+ button.pulled {
108
+ border-style: outset;
109
+ }
110
+
111
+ button.pushed {
112
+ border-style: inset;
113
+ }
114
+ '}
115
+
116
+ def button_style_value
117
+ "
118
+ width: #{button_model.width}px;
119
+ height: #{button_model.height}px;
120
+ font-size: #{button_model.font_size}px;
121
+ "
122
+ end
123
+ end
124
+
125
+ class StyledButtonRangeInput
126
+ include Glimmer::Web::Component
127
+
128
+ option :button_model
129
+ option :property
130
+ option :property_min
131
+ option :property_max
132
+
133
+ markup {
134
+ input(type: 'range', min: property_min, max: property_max) {
135
+ value <=> [button_model, property]
136
+ }
137
+ }
138
+ end
139
+
140
+ class HelloStyle
141
+ include Glimmer::Web::Component
142
+
143
+ before_render do
144
+ @button_model = ButtonModel.new
145
+ end
146
+
147
+ markup {
148
+ div(class: 'hello-style') {
149
+ div(class: 'form-row') {
150
+ label('Styled Button Width:', for: 'styled-button-width-input')
151
+ styled_button_range_input(button_model: @button_model, property: :width, property_min: ButtonModel::WIDTH_MIN, property_max: ButtonModel::WIDTH_MAX, id: 'styled-button-width-input')
152
+ }
153
+ div(class: 'form-row') {
154
+ label('Styled Button Height:', for: 'styled-button-height-input')
155
+ styled_button_range_input(button_model: @button_model, property: :height, property_min: ButtonModel::HEIGHT_MIN, property_max: ButtonModel::HEIGHT_MAX, id: 'styled-button-height-input')
156
+ }
157
+ div(class: 'form-row') {
158
+ label('Styled Button Font Size:', for: 'styled-button-font-size-input')
159
+ styled_button_range_input(button_model: @button_model, property: :font_size, property_min: ButtonModel::FONT_SIZE_MIN, property_max: ButtonModel::FONT_SIZE_MAX, id: 'styled-button-font-size-input')
160
+ }
161
+ styled_button(button_model: @button_model)
162
+ }
163
+ }
164
+
165
+ style {'
166
+ .hello-style {
167
+ padding: 20px;
168
+ }
169
+
170
+ .hello-style .form-row {
171
+ margin: 10px 0;
172
+ }
173
+ '}
174
+ end
175
+
176
+ Document.ready? do
177
+ HelloStyle.render
178
+ end
@@ -4,11 +4,11 @@ class EditTodoInput < TodoInput
4
4
  option :presenter
5
5
  option :todo
6
6
 
7
- markup {
8
- input(class: todo_input_class) { |edit_input|
7
+ markup { # evaluated against instance as a smart default convention
8
+ input { |edit_input|
9
9
  style <= [ todo, :editing,
10
10
  on_read: ->(editing) { editing ? '' : 'display: none;' },
11
- after_read: ->(_) { edit_input.focus if todo.editing? }
11
+ after_read: -> { edit_input.focus if todo.editing? }
12
12
  ]
13
13
 
14
14
  value <=> [todo, :task]
@@ -25,25 +25,17 @@ class EditTodoInput < TodoInput
25
25
  onblur do |event|
26
26
  todo.save_editing
27
27
  end
28
-
29
- style {
30
- todo_input_styles
31
- }
32
28
  }
33
29
  }
34
-
35
- def todo_input_class
36
- 'edit-todo'
37
- end
38
-
39
- def todo_input_styles
40
- super
41
30
 
42
- rule("*:has(> .#{todo_input_class})") {
31
+ style { # evaluated against class as a smart default convention (common to all instances)
32
+ todo_input_styles
33
+
34
+ rule("*:has(> .#{component_element_class})") {
43
35
  position 'relative'
44
36
  }
45
37
 
46
- rule(".#{todo_input_class}") {
38
+ rule(".#{component_element_class}") {
47
39
  position 'absolute'
48
40
  display 'block'
49
41
  width 'calc(100% - 43px)'
@@ -51,5 +43,5 @@ class EditTodoInput < TodoInput
51
43
  margin '0 0 0 43px'
52
44
  top '0'
53
45
  }
54
- end
46
+ }
55
47
  end
@@ -10,21 +10,21 @@ class NewTodoForm
10
10
  h1('todos')
11
11
 
12
12
  new_todo_input(presenter: presenter)
13
-
14
- style {
15
- rule('.header h1') {
16
- color '#b83f45'
17
- font_size '80px'
18
- font_weight '200'
19
- position 'absolute'
20
- text_align 'center'
21
- _webkit_text_rendering 'optimizeLegibility'
22
- _moz_text_rendering 'optimizeLegibility'
23
- text_rendering 'optimizeLegibility'
24
- top '-140px'
25
- width '100%'
26
- }
27
- }
13
+ }
14
+ }
15
+
16
+ style {
17
+ rule('.header h1') {
18
+ color '#b83f45'
19
+ font_size '80px'
20
+ font_weight '200'
21
+ position 'absolute'
22
+ text_align 'center'
23
+ _webkit_text_rendering 'optimizeLegibility'
24
+ _moz_text_rendering 'optimizeLegibility'
25
+ text_rendering 'optimizeLegibility'
26
+ top '-140px'
27
+ width '100%'
28
28
  }
29
29
  }
30
30
  end
@@ -3,28 +3,20 @@ require_relative 'todo_input'
3
3
  class NewTodoInput < TodoInput
4
4
  option :presenter
5
5
 
6
- markup {
7
- input(class: todo_input_class, placeholder: "What needs to be done?", autofocus: "") {
6
+ markup { # evaluated against instance as a smart default convention
7
+ input(placeholder: "What needs to be done?", autofocus: "") {
8
8
  value <=> [presenter.new_todo, :task]
9
9
 
10
10
  onkeyup do |event|
11
11
  presenter.create_todo if event.key == 'Enter' || event.keyCode == "\r"
12
12
  end
13
-
14
- style {
15
- todo_input_styles
16
- }
17
13
  }
18
14
  }
19
15
 
20
- def todo_input_class
21
- 'new-todo'
22
- end
23
-
24
- def todo_input_styles
25
- super
16
+ style { # evaluated against class as a smart default convention (common to all instances)
17
+ todo_input_styles
26
18
 
27
- rule(".#{todo_input_class}") {
19
+ rule(".#{component_element_class}") { # built-in component_class.component_element_class (e.g. NewTodoInput has CSS class as new-todo-input)
28
20
  padding '16px 16px 16px 60px'
29
21
  height '65px'
30
22
  border 'none'
@@ -32,10 +24,10 @@ class NewTodoInput < TodoInput
32
24
  box_shadow 'inset 0 -2px 1px rgba(0,0,0,0.03)'
33
25
  }
34
26
 
35
- rule(".#{todo_input_class}::placeholder") {
27
+ rule(".#{component_element_class}::placeholder") { # built-in component_class.component_element_class (e.g. NewTodoInput has CSS class as new-todo-input)
36
28
  font_style 'italic'
37
29
  font_weight '400'
38
30
  color 'rgba(0, 0, 0, 0.4)'
39
31
  }
40
- end
32
+ }
41
33
  end
@@ -45,80 +45,80 @@ class TodoFilters
45
45
  presenter.clear_completed
46
46
  end
47
47
  }
48
+ }
49
+ }
50
+
51
+ style {
52
+ rule('.todo-filters') {
53
+ border_top '1px solid #e6e6e6'
54
+ font_size '15px'
55
+ height '20px'
56
+ padding '10px 15px'
57
+ text_align 'center'
58
+ }
59
+
60
+ rule('.todo-filters:before') {
61
+ bottom '0'
62
+ box_shadow '0 1px 1px rgba(0,0,0,.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0,0,0,.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0,0,0,.2)'
63
+ content '""'
64
+ height '50px'
65
+ left '0'
66
+ overflow 'hidden'
67
+ position 'absolute'
68
+ right '0'
69
+ }
70
+
71
+ rule('.todo-count') {
72
+ float 'left'
73
+ text_align 'left'
74
+ }
75
+
76
+ rule('.todo-count .strong') {
77
+ font_weight '300'
78
+ }
79
+
80
+ rule('.filters') {
81
+ left '0'
82
+ list_style 'none'
83
+ margin '0'
84
+ padding '0'
85
+ position 'absolute'
86
+ right '0'
87
+ }
88
+
89
+ rule('.filters li') {
90
+ display 'inline'
91
+ }
92
+
93
+ rule('.filters li a') {
94
+ border '1px solid transparent'
95
+ border_radius '3px'
96
+ color 'inherit'
97
+ margin '3px'
98
+ padding '3px 7px'
99
+ text_decoration 'none'
100
+ cursor 'pointer'
101
+ }
102
+
103
+ rule('.filters li a.selected') {
104
+ border_color '#ce4646'
105
+ }
106
+
107
+ rule('.clear-completed, html .clear-completed:active') {
108
+ cursor 'pointer'
109
+ float 'right'
110
+ line_height '19px'
111
+ position 'relative'
112
+ text_decoration 'none'
113
+ }
114
+
115
+ media('(max-width: 430px)') {
116
+ rule('.todo-filters') {
117
+ height '50px'
118
+ }
48
119
 
49
- style {
50
- rule('.todo-filters') {
51
- border_top '1px solid #e6e6e6'
52
- font_size '15px'
53
- height '20px'
54
- padding '10px 15px'
55
- text_align 'center'
56
- }
57
-
58
- rule('.todo-filters:before') {
59
- bottom '0'
60
- box_shadow '0 1px 1px rgba(0,0,0,.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0,0,0,.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0,0,0,.2)'
61
- content '""'
62
- height '50px'
63
- left '0'
64
- overflow 'hidden'
65
- position 'absolute'
66
- right '0'
67
- }
68
-
69
- rule('.todo-count') {
70
- float 'left'
71
- text_align 'left'
72
- }
73
-
74
- rule('.todo-count .strong') {
75
- font_weight '300'
76
- }
77
-
78
- rule('.filters') {
79
- left '0'
80
- list_style 'none'
81
- margin '0'
82
- padding '0'
83
- position 'absolute'
84
- right '0'
85
- }
86
-
87
- rule('.filters li') {
88
- display 'inline'
89
- }
90
-
91
- rule('.filters li a') {
92
- border '1px solid transparent'
93
- border_radius '3px'
94
- color 'inherit'
95
- margin '3px'
96
- padding '3px 7px'
97
- text_decoration 'none'
98
- cursor 'pointer'
99
- }
100
-
101
- rule('.filters li a.selected') {
102
- border_color '#ce4646'
103
- }
104
-
105
- rule('.clear-completed, html .clear-completed:active') {
106
- cursor 'pointer'
107
- float 'right'
108
- line_height '19px'
109
- position 'relative'
110
- text_decoration 'none'
111
- }
112
-
113
- media('(max-width: 430px)') {
114
- rule('.todo-filters') {
115
- height '50px'
116
- }
117
-
118
- rule('.filters') {
119
- bottom '10px'
120
- }
121
- }
120
+ rule('.filters') {
121
+ bottom '10px'
122
122
  }
123
123
  }
124
124
  }
@@ -2,29 +2,32 @@
2
2
  class TodoInput
3
3
  include Glimmer::Web::Component
4
4
 
5
- def todo_input_class
6
- 'todo-input'
7
- end
8
-
9
- def todo_input_styles
10
- rule(".#{todo_input_class}") {
11
- position 'relative'
12
- margin '0'
13
- width '100%'
14
- font_size '24px'
15
- font_family 'inherit'
16
- font_weight 'inherit'
17
- line_height '1.4em'
18
- color 'inherit'
19
- padding '6px'
20
- border '1px solid #999'
21
- box_shadow 'inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2)'
22
- box_sizing 'border-box'
23
- _webkit_font_smoothing 'antialiased'
24
- }
25
-
26
- rule(".#{todo_input_class}::selection") {
27
- background 'red'
28
- }
5
+ class << self
6
+ def todo_input_styles
7
+ rule(".#{component_element_class}") {
8
+ position 'relative'
9
+ margin '0'
10
+ width '100%'
11
+ font_size '24px'
12
+ font_family 'inherit'
13
+ font_weight 'inherit'
14
+ line_height '1.4em'
15
+ color 'inherit'
16
+ padding '6px'
17
+ border '1px solid #999'
18
+ box_shadow 'inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2)'
19
+ box_sizing 'border-box'
20
+ _webkit_font_smoothing 'antialiased'
21
+ }
22
+
23
+ rule(".#{component_element_class}::selection") {
24
+ background 'red'
25
+ }
26
+
27
+ rule(".#{component_element_class}:focus") {
28
+ box_shadow '0 0 2px 2px #cf7d7d'
29
+ outline '0'
30
+ }
31
+ end
29
32
  end
30
33
  end
@@ -38,14 +38,10 @@ class TodoList
38
38
  todo_list_item(presenter:, todo:)
39
39
  end
40
40
  }
41
-
42
- style {
43
- todo_list_styles
44
- }
45
41
  }
46
42
  }
47
43
 
48
- def todo_list_styles
44
+ style {
49
45
  rule('.main') {
50
46
  border_top '1px solid #e6e6e6'
51
47
  position 'relative'
@@ -84,7 +80,7 @@ class TodoList
84
80
  transform 'rotate(90deg)'
85
81
  }
86
82
 
87
- rule('.toggle-all:focus+label, .toggle:focus+label, :focus') {
83
+ rule('.toggle-all:focus+label, .toggle:focus+label') {
88
84
  box_shadow '0 0 2px 2px #cf7d7d'
89
85
  outline '0'
90
86
  }
@@ -102,7 +98,5 @@ class TodoList
102
98
  rule('.todo-list.completed li.active') {
103
99
  display 'none'
104
100
  }
105
-
106
- TodoListItem.todo_list_item_styles
107
- end
101
+ }
108
102
  end