glimmer-dsl-web 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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