glimmer-dsl-web 0.4.2 → 0.4.4

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