glimmer-dsl-web 0.4.2 → 0.4.4

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.
@@ -144,6 +144,7 @@ module Glimmer
144
144
  REGEX_FORMAT_DATE = /^\d{4}-\d{2}-\d{2}$/
145
145
  REGEX_FORMAT_TIME = /^\d{2}:\d{2}$/
146
146
  REGEX_STYLE_SUB_PROPERTY = /^(style)_(.*)$/
147
+ REGEX_CLASS_NAME_SUB_PROPERTY = /^(class_name)_(.*)$/
147
148
 
148
149
  attr_reader :keyword, :parent, :parent_component, :component, :args, :options, :children, :enabled, :foreground, :background, :removed, :rendered
149
150
  alias rendered? rendered
@@ -309,6 +310,40 @@ module Glimmer
309
310
  end
310
311
  end
311
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(value = null)
332
+ if rendered?
333
+ if value.nil?
334
+ dom_element.attr('style')
335
+ else
336
+ value = normalize_style(value)
337
+ dom_element.attr('style', value)
338
+ end
339
+ else
340
+ enqueue_args = ['style']
341
+ enqueue_args << value unless value.nil?
342
+ enqueue_post_render_method_call(*enqueue_args)
343
+ end
344
+ end
345
+ alias style= style
346
+
312
347
  def style_property(property, value = nil)
313
348
  if rendered?
314
349
  property = property.to_s.gsub('_', '-')
@@ -432,7 +467,8 @@ module Glimmer
432
467
  html_options["data-#{data_normalized_attribute}"] = html_options.delete(attribute)
433
468
  end
434
469
  html_options[:class] ||= ''
435
- html_options[:class] = "#{html_options[:class]} #{body_class}".strip
470
+ html_options[:class] = "#{normalize_class_name(html_options.delete('class') || html_options.delete(:class))} #{body_class}".strip
471
+ html_options[:style] = normalize_style(html_options.delete('style') || html_options.delete(:style))
436
472
  html_options['data-turbo'] = 'false' if parent.nil?
437
473
  html_options
438
474
  end
@@ -461,15 +497,16 @@ module Glimmer
461
497
  @element_id ||= "element-#{ElementProxy.next_id_number_for(name)}"
462
498
  end
463
499
 
464
- def class_name=(value)
500
+ def class_name=(*values)
465
501
  if rendered?
466
- values = value.is_a?(Array) ? value : [value.to_s]
502
+ values = normalize_class_name(values).split(' ')
467
503
  new_class_name = (base_css_classes + values).uniq.compact.join(' ')
468
504
  dom_element.prop('className', new_class_name)
469
505
  else
470
- enqueue_post_render_method_call('class_name=', value)
506
+ enqueue_post_render_method_call('class_name=', *values)
471
507
  end
472
508
  end
509
+ alias classes= class_name=
473
510
 
474
511
  def add_css_class(css_class)
475
512
  if rendered?
@@ -657,14 +694,22 @@ module Glimmer
657
694
  (!dom_element.prop(property_name).nil? && !dom_element.prop(property_name).is_a?(Proc)) ||
658
695
  (!dom_element.prop(unnormalized_property_name).nil? && !dom_element.prop(unnormalized_property_name).is_a?(Proc)) ||
659
696
  method_name.to_s.start_with?('on_') ||
660
- method_name.to_s.start_with?('style_')
697
+ method_name.to_s.start_with?('style_') ||
698
+ method_name.to_s.start_with?('class_name_')
661
699
  end
662
700
 
663
701
  def method_missing(method_name, *args, &block)
664
702
  # TODO consider doing more correct checking of availability of properties/methods using native ticks
665
703
  property_name = property_name_for(method_name)
666
704
  unnormalized_property_name = unnormalized_property_name_for(method_name)
667
- if method_name.to_s.start_with?('style_')
705
+ if method_name.to_s.start_with?('class_name_')
706
+ property, sub_property = method_name.to_s.match(REGEX_CLASS_NAME_SUB_PROPERTY).to_a.drop(1)
707
+ if args.empty?
708
+ class_name_included(sub_property)
709
+ else
710
+ class_name_included(sub_property, args.first)
711
+ end
712
+ elsif method_name.to_s.start_with?('style_')
668
713
  property, sub_property = method_name.to_s.match(REGEX_STYLE_SUB_PROPERTY).to_a.drop(1)
669
714
  sub_property = sub_property.gsub('_', '-')
670
715
  if args.empty?
@@ -905,6 +950,26 @@ module Glimmer
905
950
  SWT_CURSOR_TO_CSS_CURSOR_MAP[@cursor]
906
951
  end
907
952
 
953
+ def normalize_class_name(class_name_value)
954
+ if class_name_value.is_a?(Array)
955
+ class_name_value.map(&:to_s).join(' ')
956
+ else
957
+ class_name_value.to_s
958
+ end
959
+ end
960
+
961
+ def normalize_style(style_value)
962
+ if style_value.is_a?(Hash)
963
+ style_value.reduce('') do |output, (key, value)|
964
+ key = key.to_s.gsub('_', '-')
965
+ value = value.px if value.is_a?(Numeric)
966
+ output += "#{key}: #{value}; "
967
+ end
968
+ else
969
+ style_value.to_s
970
+ end
971
+ end
972
+
908
973
  end
909
974
  end
910
975
  end
@@ -83,9 +83,8 @@ class StyledButton
83
83
  button {
84
84
  inner_text <= [button_model, :text, computed_by: :pushed]
85
85
 
86
- class_name <= [button_model, :pushed,
87
- on_read: ->(pushed) { pushed ? 'pushed' : 'pulled' }
88
- ]
86
+ class_name(:pushed) <= [button_model, :pushed]
87
+ class_name(:pulled) <= [button_model, :pushed, on_read: :!]
89
88
 
90
89
  style(:width) <= [button_model, :width, on_read: :px]
91
90
  style(:height) <= [button_model, :height, on_read: :px]
@@ -99,22 +98,22 @@ class StyledButton
99
98
  }
100
99
  }
101
100
 
102
- style {"
103
- .#{component_element_class} {
104
- font-family: Courrier New, Courrier;
105
- border-radius: 5px;
106
- border-width: 17px;
107
- 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
108
107
  }
109
108
 
110
- .#{component_element_class}.pulled {
111
- border-style: outset;
109
+ r("#{component_element_selector}.pulled") {
110
+ border_style :outset
112
111
  }
113
112
 
114
- .#{component_element_class}.pushed {
115
- border-style: inset;
113
+ r("#{component_element_selector}.pushed") {
114
+ border_style :inset
116
115
  }
117
- "}
116
+ }
118
117
  end
119
118
 
120
119
  class StyledButtonRangeInput
@@ -172,22 +171,22 @@ class HelloStyle
172
171
  }
173
172
  }
174
173
 
175
- style {"
176
- .styled-button-form {
177
- padding: 20px;
178
- display: inline-grid;
179
- 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'
180
179
  }
181
180
 
182
- .styled-button-form label, input {
183
- display: block;
184
- margin: 5px 5px 5px 0;
181
+ r('.styled-button-form label, input') {
182
+ display :block
183
+ margin '5px 5px 5px 0'
185
184
  }
186
185
 
187
- .#{component_element_class} .styled-button {
188
- display: block;
186
+ r("#{component_element_selector} .styled-button") {
187
+ display :block
189
188
  }
190
- "}
189
+ }
191
190
  end
192
191
 
193
192
  Document.ready? do
@@ -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
  }
@@ -4,16 +4,21 @@ class TodoFilters
4
4
  option :presenter
5
5
 
6
6
  markup {
7
- footer(class: 'todo-filters') {
8
- style <= [ presenter, :todos,
9
- on_read: ->(todos) { todos.empty? ? 'display: none;' : '' }
10
- ]
7
+ footer {
8
+ # Data-bind `footer` `style` `display` unidirectionally to presenter todos,
9
+ # and on read, convert todos based on whether they are empty to 'none' or 'block'
10
+ style(:display) <= [ presenter, :todos,
11
+ on_read: ->(todos) { todos.empty? ? 'none' : 'block' }
12
+ ]
11
13
 
12
14
  span(class: 'todo-count') {
13
15
  span('.strong') {
16
+ # Data-bind `span` `inner_text` unidirectionally to presenter active_todo_count
14
17
  inner_text <= [presenter, :active_todo_count]
15
18
  }
16
19
  span {
20
+ # Data-bind `span` `inner_text` unidirectionally to presenter active_todo_count,
21
+ # and on read, convert active_todo_count to string that follows count number
17
22
  inner_text <= [presenter, :active_todo_count,
18
23
  on_read: -> (active_todo_count) { " item#{'s' if active_todo_count != 1} left" }
19
24
  ]
@@ -24,9 +29,11 @@ class TodoFilters
24
29
  TodoPresenter::FILTERS.each do |filter|
25
30
  li {
26
31
  a(filter.to_s.capitalize, href: "#/#{filter unless filter == :all}") {
27
- class_name <= [ presenter, :filter,
28
- on_read: -> (presenter_filter) { presenter_filter == filter ? 'selected' : '' }
29
- ]
32
+ # Data-bind inclusion of `a` `class` `selected` unidirectionally to presenter filter attribute,
33
+ # and on read of presenter filter, convert to boolean value of whether selected class is included
34
+ class_name(:selected) <= [ presenter, :filter,
35
+ on_read: -> (presenter_filter) { presenter_filter == filter }
36
+ ]
30
37
 
31
38
  onclick do |event|
32
39
  presenter.filter = filter
@@ -37,9 +44,9 @@ class TodoFilters
37
44
  }
38
45
 
39
46
  button('Clear completed', class: 'clear-completed') {
40
- style <= [ presenter, :can_clear_completed,
41
- on_read: -> (can_clear_completed) { can_clear_completed ? '' : 'display: none;' },
42
- ]
47
+ # Data-bind inclusion of `button` `class` `can-clear-completed` unidirectionally to presenter can_clear_completed attribute,
48
+ # meaning inclusion of can-clear-completed class is determined by presenter can_clear_completed boolean attribute.
49
+ class_name('can-clear-completed') <= [presenter, :can_clear_completed]
43
50
 
44
51
  onclick do |event|
45
52
  presenter.clear_completed
@@ -49,76 +56,82 @@ class TodoFilters
49
56
  }
50
57
 
51
58
  style {
52
- rule('.todo-filters') {
59
+ r(component_element_selector) {
53
60
  border_top '1px solid #e6e6e6'
54
- font_size '15px'
55
- height '20px'
61
+ font_size 15
62
+ height 20
56
63
  padding '10px 15px'
57
- text_align 'center'
64
+ text_align :center
65
+ display :none
58
66
  }
59
67
 
60
- rule('.todo-filters:before') {
61
- bottom '0'
68
+ r("#{component_element_selector}:before") {
69
+ bottom 0
62
70
  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
71
  content '""'
64
- height '50px'
65
- left '0'
66
- overflow 'hidden'
67
- position 'absolute'
68
- right '0'
72
+ height 50
73
+ left 0
74
+ overflow :hidden
75
+ position :absolute
76
+ right 0
69
77
  }
70
78
 
71
- rule('.todo-count') {
72
- float 'left'
73
- text_align 'left'
79
+ r('.todo-count') {
80
+ float :left
81
+ text_align :left
74
82
  }
75
83
 
76
- rule('.todo-count .strong') {
84
+ r('.todo-count .strong') {
77
85
  font_weight '300'
78
86
  }
79
87
 
80
- rule('.filters') {
81
- left '0'
82
- list_style 'none'
83
- margin '0'
84
- padding '0'
85
- position 'absolute'
86
- right '0'
88
+ r('.filters') {
89
+ left 0
90
+ list_style :none
91
+ margin 0
92
+ padding 0
93
+ position :absolute
94
+ right 0
87
95
  }
88
96
 
89
- rule('.filters li') {
90
- display 'inline'
97
+ r('.filters li') {
98
+ display :inline
91
99
  }
92
100
 
93
- rule('.filters li a') {
101
+ r('.filters li a') {
94
102
  border '1px solid transparent'
95
- border_radius '3px'
96
- color 'inherit'
97
- margin '3px'
103
+ border_radius 3
104
+ color :inherit
105
+ margin 3
98
106
  padding '3px 7px'
99
- text_decoration 'none'
100
- cursor 'pointer'
107
+ text_decoration :none
108
+ cursor :pointer
101
109
  }
102
110
 
103
- rule('.filters li a.selected') {
111
+ r('.filters li a.selected') {
104
112
  border_color '#ce4646'
105
113
  }
106
114
 
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'
115
+ r('.clear-completed, html .clear-completed:active') {
116
+ cursor :pointer
117
+ float :right
118
+ line_height 19
119
+ position :relative
120
+ text_decoration :none
121
+ display :none
122
+ }
123
+
124
+ r('.clear-completed.can-clear-completed, html .clear-completed.can-clear-completed:active') {
125
+ display :block
113
126
  }
114
127
 
115
128
  media('(max-width: 430px)') {
116
- rule('.todo-filters') {
117
- height '50px'
129
+ r(component_element_selector) {
130
+ height 50
118
131
  }
119
132
 
120
- rule('.filters') {
121
- bottom '10px'
133
+ r('.filters') {
134
+ bottom 10
122
135
  }
123
136
  }
124
137
  }
@@ -4,29 +4,29 @@ class TodoInput
4
4
 
5
5
  class << self
6
6
  def todo_input_styles
7
- rule(".#{component_element_class}") {
8
- position 'relative'
9
- margin '0'
7
+ r(component_element_selector) {
8
+ position :relative
9
+ margin 0
10
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'
11
+ font_size 24
12
+ font_family :inherit
13
+ font_weight :inherit
14
+ line_height 1.4.em
15
+ color :inherit
16
+ padding 6
17
17
  border '1px solid #999'
18
18
  box_shadow 'inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2)'
19
19
  box_sizing 'border-box'
20
- _webkit_font_smoothing 'antialiased'
20
+ _webkit_font_smoothing :antialiased
21
21
  }
22
22
 
23
- rule(".#{component_element_class}::selection") {
24
- background 'red'
23
+ r("#{component_element_selector}::selection") {
24
+ background :red
25
25
  }
26
26
 
27
- rule(".#{component_element_class}:focus") {
27
+ r("#{component_element_selector}:focus") {
28
28
  box_shadow '0 0 2px 2px #cf7d7d'
29
- outline '0'
29
+ outline 0
30
30
  }
31
31
  end
32
32
  end
@@ -15,10 +15,6 @@ class TodoList
15
15
 
16
16
  markup {
17
17
  main(class: 'main') {
18
- style <= [ presenter, :todos,
19
- on_read: ->(todos) { todos.empty? ? 'display: none;' : '' }
20
- ]
21
-
22
18
  div(class: 'toggle-all-container') {
23
19
  input(class: 'toggle-all', type: 'checkbox')
24
20
 
@@ -30,9 +26,9 @@ class TodoList
30
26
  }
31
27
 
32
28
  @todo_ul = ul {
33
- class_name <= [presenter, :filter,
34
- on_read: ->(filter) { "todo-list #{filter}" }
35
- ]
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
31
+ class_name <= [presenter, :filter]
36
32
 
37
33
  presenter.todos.each do |todo|
38
34
  todo_list_item(presenter:, todo:)
@@ -42,61 +38,61 @@ class TodoList
42
38
  }
43
39
 
44
40
  style {
45
- rule('.main') {
41
+ r('.main') {
46
42
  border_top '1px solid #e6e6e6'
47
- position 'relative'
43
+ position :relative
48
44
  z_index '2'
49
45
  }
50
46
 
51
- rule('.toggle-all') {
52
- border 'none'
47
+ r('.toggle-all') {
48
+ border :none
53
49
  bottom '100%'
54
- height '1px'
55
- opacity '0'
56
- position 'absolute'
50
+ height 1
51
+ opacity 0
52
+ position :absolute
57
53
  right '100%'
58
- width '1px'
54
+ width 1
59
55
  }
60
56
 
61
- rule('.toggle-all+label') {
62
- align_items 'center'
63
- display 'flex'
64
- font_size '0'
65
- height '65px'
66
- justify_content 'center'
67
- left '0'
68
- position 'absolute'
69
- top '-65px'
70
- width '45px'
57
+ r('.toggle-all+label') {
58
+ align_items :center
59
+ display :flex
60
+ font_size 0
61
+ height 65
62
+ justify_content :center
63
+ left 0
64
+ position :absolute
65
+ top -65
66
+ width 45
71
67
  }
72
68
 
73
- rule('.toggle-all+label:before') {
69
+ r('.toggle-all+label:before') {
74
70
  color '#949494'
75
71
  content '"❯"'
76
72
  display 'inline-block'
77
- font_size '22px'
73
+ font_size 22
78
74
  padding '10px 27px'
79
75
  _webkit_transform 'rotate(90deg)'
80
76
  transform 'rotate(90deg)'
81
77
  }
82
78
 
83
- rule('.toggle-all:focus+label, .toggle:focus+label') {
79
+ r('.toggle-all:focus+label, .toggle:focus+label') {
84
80
  box_shadow '0 0 2px 2px #cf7d7d'
85
- outline '0'
81
+ outline 0
86
82
  }
87
83
 
88
- rule('.todo-list') {
89
- list_style 'none'
90
- margin '0'
91
- padding '0'
84
+ r('.todo-list ul') {
85
+ list_style :none
86
+ margin 0
87
+ padding 0
92
88
  }
93
89
 
94
- rule('.todo-list.active li.completed') {
95
- display 'none'
90
+ r('.todo-list ul.active li.completed') {
91
+ display :none
96
92
  }
97
93
 
98
- rule('.todo-list.completed li.active') {
99
- display 'none'
94
+ r('.todo-list ul.completed li.active') {
95
+ display :none
100
96
  }
101
97
  }
102
98
  end