glimmer-dsl-web 0.4.1 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -26,7 +26,9 @@ require 'glimmer/dsl/web/listener_expression'
26
26
  require 'glimmer/dsl/web/property_expression'
27
27
  require 'glimmer/dsl/web/a_expression'
28
28
  require 'glimmer/dsl/web/span_expression'
29
- require 'glimmer/dsl/web/style_expression'
29
+ require 'glimmer/dsl/web/style_element_expression'
30
+ require 'glimmer/dsl/web/inline_style_data_binding_expression'
31
+ require 'glimmer/dsl/web/class_name_inclusion_data_binding'
30
32
  require 'glimmer/dsl/web/bind_expression'
31
33
  require 'glimmer/dsl/web/data_binding_expression'
32
34
  require 'glimmer/dsl/web/content_data_binding_expression'
@@ -41,8 +43,10 @@ module Glimmer
41
43
  Web,
42
44
  %w[
43
45
  listener
44
- style
46
+ style_element
45
47
  content_data_binding
48
+ inline_style_data_binding
49
+ class_name_inclusion_data_binding
46
50
  component
47
51
  formatting_element
48
52
  data_binding
@@ -10,7 +10,18 @@ module Glimmer
10
10
  include GeneralElementExpression
11
11
 
12
12
  def can_interpret?(parent, keyword, *args, &block)
13
- Glimmer::Web::ElementProxy.keyword_supported?(keyword)
13
+ Glimmer::Web::ElementProxy.keyword_supported?(keyword) &&
14
+ (
15
+ args.empty? ||
16
+ args.size == 1 && args.first.is_a?(String) ||
17
+ args.size == 1 && args.first.is_a?(Hash) ||
18
+ args.size == 2 && args.first.is_a?(String) && args.last.is_a?(Hash)
19
+ ) &&
20
+ ( # ensure SVG keywords only live under SVG element (unless it's the SVG element itself)
21
+ !Glimmer::Web::ElementProxy.svg_keyword_supported?(keyword) ||
22
+ keyword == 'svg' ||
23
+ parent.find_ancestor(include_self: true) { |ancestor| ancestor.keyword == 'svg' }
24
+ )
14
25
  end
15
26
  end
16
27
  end
@@ -0,0 +1,21 @@
1
+ require 'glimmer/dsl/expression'
2
+
3
+ module Glimmer
4
+ module DSL
5
+ module Web
6
+ class InlineStyleDataBindingExpression < Expression
7
+ def can_interpret?(parent, keyword, *args, &block)
8
+ keyword == 'style' &&
9
+ block.nil? &&
10
+ args.size == 1 &&
11
+ textual?(args.first)
12
+ end
13
+
14
+ def interpret(parent, keyword, *args, &block)
15
+ parent_attribute = "#{keyword}_#{args.first.to_s.underscore}"
16
+ Glimmer::DataBinding::Shine.new(parent, parent_attribute)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -4,7 +4,7 @@ require 'glimmer/dsl/web/general_element_expression'
4
4
  module Glimmer
5
5
  module DSL
6
6
  module Web
7
- class StyleExpression < Expression
7
+ class StyleElementExpression < Expression
8
8
  include GeneralElementExpression
9
9
  include Glimmer
10
10
 
@@ -102,6 +102,11 @@ module Glimmer
102
102
  self.keyword.gsub('_', '-')
103
103
  end
104
104
 
105
+ def component_element_selector
106
+ ".#{component_element_class}"
107
+ end
108
+ alias component_markup_root_selector component_element_selector
109
+
105
110
  def component_shortcut_element_class
106
111
  self.shortcut_keyword.gsub('_', '-')
107
112
  end
@@ -203,7 +208,8 @@ module Glimmer
203
208
  end
204
209
 
205
210
  def remove_component_style(component)
206
- if Glimmer::Web::Component.any_component_style?(component.class)
211
+ # We must not remove the head style element until all components are removed of a component class
212
+ if Glimmer::Web::Component.component_count(component.class) == 0 && Glimmer::Web::Component.any_component_style?(component.class)
207
213
  # TODO in the future, you would need to remove style using a jQuery call if you created head element in bulk
208
214
  Glimmer::Web::Component.component_styles[component.class].remove
209
215
  Glimmer::Web::Component.component_styles.delete(component.class)
@@ -219,7 +225,7 @@ module Glimmer
219
225
  end
220
226
 
221
227
  def component_count(component_class)
222
- component_class_to_components_map[component_class].size
228
+ component_class_to_components_map[component_class]&.size || 0
223
229
  end
224
230
 
225
231
  def components
@@ -31,6 +31,14 @@ module Glimmer
31
31
  ELEMENT_KEYWORDS.include?(keyword.to_s)
32
32
  end
33
33
 
34
+ def html_keyword_supported?(keyword)
35
+ HTML_ELEMENT_KEYWORDS.include?(keyword.to_s)
36
+ end
37
+
38
+ def svg_keyword_supported?(keyword)
39
+ SVG_ELEMENT_KEYWORDS.include?(keyword.to_s)
40
+ end
41
+
34
42
  # NOTE: Avoid using this method until we start supporting ElementProxy subclasses
35
43
  # in which case, we must cache them to avoid the slow performance of element_type
36
44
  # Factory Method that translates a Glimmer DSL keyword into a ElementProxy object
@@ -94,7 +102,7 @@ module Glimmer
94
102
 
95
103
  Event = Struct.new(:widget, keyword_init: true)
96
104
 
97
- ELEMENT_KEYWORDS = [
105
+ HTML_ELEMENT_KEYWORDS = [
98
106
  "a", "abbr", "acronym", "address", "applet", "area", "article", "aside", "audio",
99
107
  "base", "basefont", "bdi", "bdo", "bgsound", "big", "blink", "blockquote", "body",
100
108
  "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "data",
@@ -110,6 +118,19 @@ module Glimmer
110
118
  "template", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt",
111
119
  "u", "ul", "var", "video", "wbr", "xmp",
112
120
  ]
121
+
122
+ SVG_ELEMENT_KEYWORDS = [
123
+ "animate", "animateMotion", "animateTransform", "circle", "clipPath", "defs", "desc", "ellipse",
124
+ "feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix",
125
+ "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feDropShadow", "feFlood", "feFuncA",
126
+ "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode",
127
+ "feMorphology", "feOffset", "fePointLight", "feSpecularLighting", "feSpotLight", "feTile",
128
+ "feTurbulence", "filter", "foreignObject", "g", "image", "line", "linearGradient", "marker",
129
+ "mask", "metadata", "mpath", "path", "pattern", "polygon", "polyline", "radialGradient", "rect",
130
+ "set", "stop", "svg", "switch", "symbol", "text", "textPath", "tspan", "use", "view",
131
+ ].map(&:downcase)
132
+
133
+ ELEMENT_KEYWORDS = HTML_ELEMENT_KEYWORDS + SVG_ELEMENT_KEYWORDS
113
134
 
114
135
  GLIMMER_ATTRIBUTES = [:parent]
115
136
  PROPERTY_ALIASES = {
@@ -122,6 +143,8 @@ module Glimmer
122
143
  REGEX_FORMAT_DATETIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/
123
144
  REGEX_FORMAT_DATE = /^\d{4}-\d{2}-\d{2}$/
124
145
  REGEX_FORMAT_TIME = /^\d{2}:\d{2}$/
146
+ REGEX_STYLE_SUB_PROPERTY = /^(style)_(.*)$/
147
+ REGEX_CLASS_NAME_SUB_PROPERTY = /^(class_name)_(.*)$/
125
148
 
126
149
  attr_reader :keyword, :parent, :parent_component, :component, :args, :options, :children, :enabled, :foreground, :background, :removed, :rendered
127
150
  alias rendered? rendered
@@ -176,6 +199,14 @@ module Glimmer
176
199
  dom_element.attr('class').to_s.split if rendered?
177
200
  end
178
201
 
202
+ def html?
203
+ ElementProxy.html_keyword_supported?(keyword)
204
+ end
205
+
206
+ def svg?
207
+ ElementProxy.svg_keyword_supported?(keyword)
208
+ end
209
+
179
210
  def remove
180
211
  return if @removed
181
212
  on_remove_listeners = listeners_for('on_remove').dup
@@ -233,9 +264,16 @@ module Glimmer
233
264
  end
234
265
  parents_array
235
266
  end
267
+ alias ancestors parents
236
268
 
237
- def dialog_ancestor
238
- parents.detect {|p| p.is_a?(DialogProxy)}
269
+ def find_ancestor(include_self: false, &condition)
270
+ current_element_proxy = self
271
+ return current_element_proxy if include_self && condition.call(current_element_proxy)
272
+ until current_element_proxy.parent.nil?
273
+ current_element_proxy = current_element_proxy.parent
274
+ return current_element_proxy if condition.call(current_element_proxy)
275
+ end
276
+ nil
239
277
  end
240
278
 
241
279
  def print
@@ -272,6 +310,39 @@ module Glimmer
272
310
  end
273
311
  end
274
312
 
313
+ def class_name_included(one_class_name, value = nil)
314
+ if rendered?
315
+ if value.nil?
316
+ class_name.include?(one_class_name)
317
+ else
318
+ if value
319
+ add_css_class(one_class_name)
320
+ else
321
+ remove_css_class(one_class_name)
322
+ end
323
+ end
324
+ else
325
+ enqueue_args = ['class_name_included', one_class_name]
326
+ enqueue_args << value unless value.nil?
327
+ enqueue_post_render_method_call(*enqueue_args)
328
+ end
329
+ end
330
+
331
+ def style_property(property, value = nil)
332
+ if rendered?
333
+ property = property.to_s.gsub('_', '-')
334
+ if value.nil?
335
+ dom_element.css(property)
336
+ else
337
+ dom_element.css(property, value)
338
+ end
339
+ else
340
+ enqueue_args = ['style_property', property]
341
+ enqueue_args << value unless value.nil?
342
+ enqueue_post_render_method_call(*enqueue_args)
343
+ end
344
+ end
345
+
275
346
  def parent_selector
276
347
  @parent&.selector
277
348
  end
@@ -550,13 +621,12 @@ module Glimmer
550
621
 
551
622
  def data_bind(property, model_binding)
552
623
  element_binding_read_translator = value_converters_for_input_type(type)&.[](:model_to_view)
553
- element_binding_parameters = [self, property, element_binding_read_translator]
554
- element_binding = DataBinding::ElementBinding.new(*element_binding_parameters)
624
+ element_binding = DataBinding::ElementBinding.new(self, property, translator: element_binding_read_translator)
555
625
  #TODO make this options observer dependent and all similar observers in element specific data binding handlers
556
626
  element_binding.observe(model_binding)
557
627
  element_binding.call(model_binding.evaluate_property)
558
628
  data_bindings[element_binding] = model_binding
559
- unless model_binding.binding_options[:read_only]
629
+ if !model_binding.binding_options[:read_only]
560
630
  # TODO add guards against nil cases for hash below
561
631
  listener_keyword = data_binding_listener_for_element_and_property(keyword, property)
562
632
  if listener_keyword
@@ -605,14 +675,31 @@ module Glimmer
605
675
  dom_element.respond_to?(method_name, include_private) ||
606
676
  (!dom_element.prop(property_name).nil? && !dom_element.prop(property_name).is_a?(Proc)) ||
607
677
  (!dom_element.prop(unnormalized_property_name).nil? && !dom_element.prop(unnormalized_property_name).is_a?(Proc)) ||
608
- method_name.to_s.start_with?('on_')
678
+ method_name.to_s.start_with?('on_') ||
679
+ method_name.to_s.start_with?('style_') ||
680
+ method_name.to_s.start_with?('class_name_')
609
681
  end
610
682
 
611
683
  def method_missing(method_name, *args, &block)
612
684
  # TODO consider doing more correct checking of availability of properties/methods using native ticks
613
685
  property_name = property_name_for(method_name)
614
686
  unnormalized_property_name = unnormalized_property_name_for(method_name)
615
- if method_name.to_s.start_with?('on_')
687
+ if method_name.to_s.start_with?('class_name_')
688
+ property, sub_property = method_name.to_s.match(REGEX_CLASS_NAME_SUB_PROPERTY).to_a.drop(1)
689
+ if args.empty?
690
+ class_name_included(sub_property)
691
+ else
692
+ class_name_included(sub_property, args.first)
693
+ end
694
+ elsif method_name.to_s.start_with?('style_')
695
+ property, sub_property = method_name.to_s.match(REGEX_STYLE_SUB_PROPERTY).to_a.drop(1)
696
+ sub_property = sub_property.gsub('_', '-')
697
+ if args.empty?
698
+ style_property(sub_property)
699
+ else
700
+ style_property(sub_property, args.first) # TODO in the future, support more sophisticated forms of CSS sub-property values, like [1.px, :solid, :black] for border
701
+ end
702
+ elsif method_name.to_s.start_with?('on_')
616
703
  handle_observation_request(method_name, block)
617
704
  elsif dom_element.respond_to?(method_name)
618
705
  if rendered?
@@ -22,7 +22,6 @@
22
22
  require 'glimmer-dsl-web'
23
23
 
24
24
  class ButtonModel
25
- BUTTON_STYLE_ATTRIBUTES = [:width, :height, :font_size, :background_color]
26
25
  WIDTH_MIN = 160
27
26
  WIDTH_MAX = 960
28
27
  HEIGHT_MIN = 100
@@ -30,7 +29,7 @@ class ButtonModel
30
29
  FONT_SIZE_MIN = 40
31
30
  FONT_SIZE_MAX = 200
32
31
 
33
- attr_accessor :text, :pushed, *BUTTON_STYLE_ATTRIBUTES
32
+ attr_accessor :text, :pushed, :background_color, :width, :height, :font_size
34
33
 
35
34
  def initialize
36
35
  @text = 'Push'
@@ -84,15 +83,14 @@ class StyledButton
84
83
  button {
85
84
  inner_text <= [button_model, :text, computed_by: :pushed]
86
85
 
87
- class_name <= [button_model, :pushed,
88
- on_read: ->(pushed) { pushed ? 'pushed' : 'pulled' }
89
- ]
86
+ class_name(:pushed) <= [button_model, :pushed]
87
+ class_name(:pulled) <= [button_model, :pushed, on_read: :!]
90
88
 
91
- ButtonModel::BUTTON_STYLE_ATTRIBUTES.each do |attribute|
92
- style <= [ button_model, attribute,
93
- on_read: method(:button_style_value) # convert value on read before storing in style
94
- ]
95
- end
89
+ style(:width) <= [button_model, :width, on_read: :px]
90
+ style(:height) <= [button_model, :height, on_read: :px]
91
+ style(:font_size) <= [button_model, :font_size, on_read: :px]
92
+ style(:background_color) <= [button_model, :background_color]
93
+ style(:border_color) <= [button_model, :border_color, computed_by: :background_color]
96
94
 
97
95
  onclick do
98
96
  button_model.push
@@ -100,32 +98,22 @@ class StyledButton
100
98
  }
101
99
  }
102
100
 
103
- style {"
104
- .#{component_element_class} {
105
- font-family: Courrier New, Courrier;
106
- border-radius: 5px;
107
- border-width: 17px;
108
- margin: 5px;
101
+ style {
102
+ r(component_element_selector) {
103
+ font_family 'Courrier New, Courrier'
104
+ border_radius 5
105
+ border_width 17
106
+ margin 5
109
107
  }
110
108
 
111
- .#{component_element_class}.pulled {
112
- border-style: outset;
109
+ r("#{component_element_selector}.pulled") {
110
+ border_style :outset
113
111
  }
114
112
 
115
- .#{component_element_class}.pushed {
116
- border-style: inset;
113
+ r("#{component_element_selector}.pushed") {
114
+ border_style :inset
117
115
  }
118
- "}
119
-
120
- def button_style_value
121
- "
122
- width: #{button_model.width}px;
123
- height: #{button_model.height}px;
124
- font-size: #{button_model.font_size}px;
125
- background-color: #{button_model.background_color};
126
- border-color: #{button_model.border_color};
127
- "
128
- end
116
+ }
129
117
  end
130
118
 
131
119
  class StyledButtonRangeInput
@@ -183,22 +171,22 @@ class HelloStyle
183
171
  }
184
172
  }
185
173
 
186
- style {"
187
- .styled-button-form {
188
- padding: 20px;
189
- display: inline-grid;
190
- grid-template-columns: auto auto;
174
+ style {
175
+ r('.styled-button-form') {
176
+ padding 20
177
+ display 'inline-grid'
178
+ grid_template_columns 'auto auto'
191
179
  }
192
180
 
193
- .styled-button-form label, input {
194
- display: block;
195
- margin: 5px 5px 5px 0;
181
+ r('.styled-button-form label, input') {
182
+ display :block
183
+ margin '5px 5px 5px 0'
196
184
  }
197
185
 
198
- .#{component_element_class} .styled-button {
199
- display: block;
186
+ r("#{component_element_selector} .styled-button") {
187
+ display :block
200
188
  }
201
- "}
189
+ }
202
190
  end
203
191
 
204
192
  Document.ready? do
@@ -0,0 +1,45 @@
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 HelloSvg
25
+ include Glimmer::Web::Component
26
+
27
+ markup {
28
+ div {
29
+ svg(width: '100%', height: '100') {
30
+ circle(cx: '50', cy: '50', r: '50', style: 'fill:blue;') {
31
+ animate(attributename: 'cx', begin: '0s', dur: '8s', from: '50', to: '90%', repeatcount: 'indefinite')
32
+ }
33
+ }
34
+ svg(width: '200', height: '180') {
35
+ rect(x: '30', y: '30', height: '110', width: '110', style: 'stroke:green;fill:red') {
36
+ animatetransform(attributename: 'transform', begin: '0.1s', dur: '10s', type: 'rotate', from: '0 85 85', to: '360 85 85', repeatcount: 'indefinite')
37
+ }
38
+ }
39
+ }
40
+ }
41
+ end
42
+
43
+ Document.ready? do
44
+ HelloSvg.render
45
+ end
@@ -6,11 +6,16 @@ class EditTodoInput < TodoInput
6
6
 
7
7
  markup { # evaluated against instance as a smart default convention
8
8
  input { |edit_input|
9
- style <= [ todo, :editing,
10
- on_read: ->(editing) { editing ? '' : 'display: none;' },
11
- after_read: -> { edit_input.focus if todo.editing? }
12
- ]
9
+ # Data-bind inclusion of `li` `class` `editing` unidirectionally to todo editing attribute,
10
+ # meaning inclusion of editing class is determined by todo editing boolean attribute.
11
+ # `after_read` hook will have `input` grab keyboard focus when editing todo.
12
+ class_name(:editing) <= [ todo, :editing,
13
+ after_read: -> { edit_input.focus if todo.editing? }
14
+ ]
13
15
 
16
+ # Data-bind `input` `value` property bidirectionally to `todo` `task` attribute
17
+ # meaning make any changes to the `todo` `task` attribute value automatically update the `input` `value` property
18
+ # and any changes to the `input` `value` property by the user automatically update the `todo` `task` attribute value.
14
19
  value <=> [todo, :task]
15
20
 
16
21
  onkeyup do |event|
@@ -31,17 +36,21 @@ class EditTodoInput < TodoInput
31
36
  style { # evaluated against class as a smart default convention (common to all instances)
32
37
  todo_input_styles
33
38
 
34
- rule("*:has(> .#{component_element_class})") {
35
- position 'relative'
39
+ r("*:has(> #{component_element_selector})") {
40
+ position :relative
36
41
  }
37
42
 
38
- rule(".#{component_element_class}") {
39
- position 'absolute'
40
- display 'block'
43
+ r(component_element_selector) {
44
+ position :absolute
45
+ display :none
41
46
  width 'calc(100% - 43px)'
42
47
  padding '12px 16px'
43
48
  margin '0 0 0 43px'
44
- top '0'
49
+ top 0
50
+ }
51
+
52
+ r("#{component_element_selector}.editing") {
53
+ display :block
45
54
  }
46
55
  }
47
56
  end
@@ -14,16 +14,16 @@ class NewTodoForm
14
14
  }
15
15
 
16
16
  style {
17
- rule('.header h1') {
17
+ r('.header h1') {
18
18
  color '#b83f45'
19
- font_size '80px'
19
+ font_size 80
20
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'
21
+ position :absolute
22
+ text_align :center
23
+ _webkit_text_rendering :optimizeLegibility
24
+ _moz_text_rendering :optimizeLegibility
25
+ text_rendering :optimizeLegibility
26
+ top -140
27
27
  width '100%'
28
28
  }
29
29
  }
@@ -5,6 +5,9 @@ class NewTodoInput < TodoInput
5
5
 
6
6
  markup { # evaluated against instance as a smart convention
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
8
11
  value <=> [presenter.new_todo, :task]
9
12
 
10
13
  onkeyup do |event|
@@ -16,16 +19,16 @@ class NewTodoInput < TodoInput
16
19
  style { # evaluated against class as a smart convention (common to all instances)
17
20
  todo_input_styles
18
21
 
19
- rule(".#{component_element_class}") { # NewTodoInput has component_element_class as 'new-todo-input'
22
+ r(component_element_selector) { # NewTodoInput has component_element_class as 'new-todo-input'
20
23
  padding '16px 16px 16px 60px'
21
- height '65px'
22
- border 'none'
24
+ height 65
25
+ border :none
23
26
  background 'rgba(0, 0, 0, 0.003)'
24
27
  box_shadow 'inset 0 -2px 1px rgba(0,0,0,0.03)'
25
28
  }
26
29
 
27
- rule(".#{component_element_class}::placeholder") {
28
- font_style 'italic'
30
+ r("#{component_element_selector}::placeholder") {
31
+ font_style :italic
29
32
  font_weight '400'
30
33
  color 'rgba(0, 0, 0, 0.4)'
31
34
  }