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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +293 -56
- data/VERSION +1 -1
- data/glimmer-dsl-web.gemspec +8 -5
- data/lib/glimmer/data_binding/element_binding.rb +35 -13
- data/lib/glimmer/dsl/web/class_name_inclusion_data_binding.rb +21 -0
- data/lib/glimmer/dsl/web/dsl.rb +6 -2
- data/lib/glimmer/dsl/web/element_expression.rb +12 -1
- data/lib/glimmer/dsl/web/inline_style_data_binding_expression.rb +21 -0
- data/lib/glimmer/dsl/web/{style_expression.rb → style_element_expression.rb} +1 -1
- data/lib/glimmer/web/component.rb +8 -2
- data/lib/glimmer/web/element_proxy.rb +95 -8
- data/lib/glimmer-dsl-web/samples/hello/hello_style.rb +30 -42
- data/lib/glimmer-dsl-web/samples/hello/hello_svg.rb +45 -0
- data/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/edit_todo_input.rb +19 -10
- data/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/new_todo_form.rb +8 -8
- data/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/new_todo_input.rb +8 -5
- data/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/todo_filters.rb +64 -51
- data/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/todo_input.rb +14 -14
- data/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/todo_list.rb +33 -37
- data/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/todo_list_item.rb +62 -59
- data/lib/glimmer-dsl-web/samples/regular/todo_mvc/views/todo_mvc_footer.rb +9 -9
- data/lib/glimmer-dsl-web/samples/regular/todo_mvc.rb +20 -20
- metadata +8 -5
data/lib/glimmer/dsl/web/dsl.rb
CHANGED
@@ -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/
|
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
|
-
|
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
|
@@ -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
|
-
|
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]
|
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
|
-
|
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
|
238
|
-
|
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
|
-
|
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
|
-
|
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?('
|
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,
|
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
|
-
|
89
|
-
]
|
86
|
+
class_name(:pushed) <= [button_model, :pushed]
|
87
|
+
class_name(:pulled) <= [button_model, :pushed, on_read: :!]
|
90
88
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
margin
|
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
|
-
|
112
|
-
|
109
|
+
r("#{component_element_selector}.pulled") {
|
110
|
+
border_style :outset
|
113
111
|
}
|
114
112
|
|
115
|
-
|
116
|
-
|
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
|
189
|
-
display
|
190
|
-
|
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:
|
195
|
-
margin
|
181
|
+
r('.styled-button-form label, input') {
|
182
|
+
display :block
|
183
|
+
margin '5px 5px 5px 0'
|
196
184
|
}
|
197
185
|
|
198
|
-
|
199
|
-
display:
|
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
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
35
|
-
position
|
39
|
+
r("*:has(> #{component_element_selector})") {
|
40
|
+
position :relative
|
36
41
|
}
|
37
42
|
|
38
|
-
|
39
|
-
position
|
40
|
-
display
|
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
|
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
|
-
|
17
|
+
r('.header h1') {
|
18
18
|
color '#b83f45'
|
19
|
-
font_size
|
19
|
+
font_size 80
|
20
20
|
font_weight '200'
|
21
|
-
position
|
22
|
-
text_align
|
23
|
-
_webkit_text_rendering
|
24
|
-
_moz_text_rendering
|
25
|
-
text_rendering
|
26
|
-
top
|
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
|
-
|
22
|
+
r(component_element_selector) { # NewTodoInput has component_element_class as 'new-todo-input'
|
20
23
|
padding '16px 16px 16px 60px'
|
21
|
-
height
|
22
|
-
border
|
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
|
-
|
28
|
-
font_style
|
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
|
}
|