vdom-rb 0.1.0
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.
- checksums.yaml +7 -0
- data/lib/vdom-rb.rb +5 -0
- data/lib/vdom/version.rb +3 -0
- data/opal/vdom.rb +4 -0
- data/opal/vdom/component.rb +151 -0
- data/opal/vdom/content.rb +240 -0
- data/opal/vdom/instance.rb +112 -0
- data/opal/vdom/registry.rb +60 -0
- data/opal/vdom/renderer.rb +164 -0
- data/opal/vdom/table.rb +500 -0
- data/opal/vdom/table_column.rb +81 -0
- data/opal/vdom/util.rb +12 -0
- metadata +84 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 175eed1045ac11f8cf7f8d233172412e44252a7a
|
4
|
+
data.tar.gz: 097298a39a6fa4cbb6f73e231639af21c09f89ab
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f14abe05447c686b6e95e0f24254348d6f12134484bf202208d36d70201b865ca1849ca2b6af6c212fbdda89cf771329c9b82b9a0858d580f71b64f7cd2082f1
|
7
|
+
data.tar.gz: 52c1210dae305dd5a8e12ff67e305276405be2064c85c2047ea85b0f4c3897d857775736bee204135560cef8aa720c1c92111d90c1e9a45db1890898357b5850
|
data/lib/vdom-rb.rb
ADDED
data/lib/vdom/version.rb
ADDED
data/opal/vdom.rb
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
puts "#{__FILE__}[#{__LINE__}] requiring vdom stuff"
|
2
|
+
require 'clearwater'
|
3
|
+
require 'vdom/registry'
|
4
|
+
|
5
|
+
module VDOM
|
6
|
+
|
7
|
+
# Base component class
|
8
|
+
class Component
|
9
|
+
include Clearwater::Component
|
10
|
+
|
11
|
+
attr_reader :dom_id
|
12
|
+
|
13
|
+
def initialize(dom_id: nil)
|
14
|
+
@dom_id = dom_id
|
15
|
+
end
|
16
|
+
|
17
|
+
def render_dom!
|
18
|
+
registry.render(@dom_id)
|
19
|
+
end
|
20
|
+
|
21
|
+
def registry
|
22
|
+
VDOM::Registry.instance
|
23
|
+
end
|
24
|
+
|
25
|
+
end # class Component
|
26
|
+
|
27
|
+
# A DOM element
|
28
|
+
# TODO: call it an element ?
|
29
|
+
class Node < VDOM::Component
|
30
|
+
attr_reader :tag_name, :attributes, :content, :model
|
31
|
+
|
32
|
+
def initialize(tag_name, attributes: nil, content: nil, model: nil, dom_id: nil)
|
33
|
+
super(dom_id: dom_id)
|
34
|
+
@tag_name = tag_name
|
35
|
+
@attributes = attributes
|
36
|
+
@content = content
|
37
|
+
@model = model
|
38
|
+
@is_volatile = Proc === @content ||
|
39
|
+
Proc === @attributes ||
|
40
|
+
Proc === @tag_name
|
41
|
+
@cached_tag = @is_volatile ? nil : render_tag
|
42
|
+
end
|
43
|
+
|
44
|
+
def render
|
45
|
+
result = if @is_volatile
|
46
|
+
render_tag
|
47
|
+
else
|
48
|
+
@cached_tag
|
49
|
+
end
|
50
|
+
result
|
51
|
+
end
|
52
|
+
|
53
|
+
def volatile?
|
54
|
+
@is_volatile
|
55
|
+
end
|
56
|
+
|
57
|
+
def cached?
|
58
|
+
!volatile?
|
59
|
+
end
|
60
|
+
|
61
|
+
def render_tag
|
62
|
+
tag(
|
63
|
+
resolve(@tag_name),
|
64
|
+
resolve(@attributes),
|
65
|
+
resolve(@content)
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_s
|
70
|
+
super + " @tag_name=#{@tag_name}"
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def resolve(thing)
|
76
|
+
Proc === thing ? thing.call : thing
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
# State - a simple but effective way of implying
|
82
|
+
# changed state of contents. Wrap contents in a new
|
83
|
+
# state whenever the content changes. The object
|
84
|
+
# identity then become a proxy for a change
|
85
|
+
# in the contents. Used by StateComponent.
|
86
|
+
class State
|
87
|
+
|
88
|
+
attr_reader :contents
|
89
|
+
|
90
|
+
def initialize(**args)
|
91
|
+
@contents = args
|
92
|
+
end
|
93
|
+
|
94
|
+
def new
|
95
|
+
# self.class.new(**contents)
|
96
|
+
self.class.new(contents) # opal likes it like this ?
|
97
|
+
end
|
98
|
+
|
99
|
+
def contents
|
100
|
+
@contents
|
101
|
+
end
|
102
|
+
|
103
|
+
def [](key)
|
104
|
+
@contents[key]
|
105
|
+
end
|
106
|
+
|
107
|
+
alias_method :get, :[]
|
108
|
+
end
|
109
|
+
|
110
|
+
# A component with cached rendering which
|
111
|
+
# uses State's to determine whether the
|
112
|
+
# component should render. If the previous
|
113
|
+
# instance has a state equal to the
|
114
|
+
# current instance, then no rendering
|
115
|
+
# will be performed. If the state has
|
116
|
+
# changed then the render method will
|
117
|
+
# be called.
|
118
|
+
class StateComponent < VDOM::Component
|
119
|
+
include Clearwater::CachedRender
|
120
|
+
|
121
|
+
attr_reader :state
|
122
|
+
|
123
|
+
def initialize(state, dom_id: nil)
|
124
|
+
super(dom_id: dom_id)
|
125
|
+
@state = state
|
126
|
+
end
|
127
|
+
|
128
|
+
# Placeholder only. Subclasses should implement.
|
129
|
+
# The contents of the state held by the component
|
130
|
+
# can be used to perform the necessary render.
|
131
|
+
def render
|
132
|
+
end
|
133
|
+
|
134
|
+
# Returns true of the receiver's state is
|
135
|
+
# not equal to the previous component's
|
136
|
+
# state.
|
137
|
+
def should_render?(previous)
|
138
|
+
@state != previous.state
|
139
|
+
end
|
140
|
+
|
141
|
+
alias_method :changed?, :should_render?
|
142
|
+
|
143
|
+
# Synonym for !should_render?
|
144
|
+
def unchanged?(previous)
|
145
|
+
!changed?(previous)
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
|
@@ -0,0 +1,240 @@
|
|
1
|
+
require 'vdom/component'
|
2
|
+
|
3
|
+
module VDOM
|
4
|
+
class Content
|
5
|
+
|
6
|
+
# options
|
7
|
+
#
|
8
|
+
# :value # object appropriate to type or proc(context) or proc(value) or proc(context, value)
|
9
|
+
# :type # :container, :media, :string, :integer, :decimal, :bool, :count, :percent, :sign, :date
|
10
|
+
# :css # string with valid css classes or proc(value) or proc(context, value)
|
11
|
+
# :style # { } hash of styles per Clearwater or proc(value) or proc(context, value)
|
12
|
+
# :format # string or proc(value) or proc(context, value) for :decimal, :percent, :date
|
13
|
+
# :comma # boolean for numeric values (default true)
|
14
|
+
# :events # { click: ->{ clicked }, ... } - hash or proc(value) or proc(context, value)
|
15
|
+
# # event names per 'developer.mozilla.org/en-US/docs/Web/Events'
|
16
|
+
|
17
|
+
attr_reader :options, :type
|
18
|
+
|
19
|
+
def initialize(**options)
|
20
|
+
@options = options
|
21
|
+
@type = @options[:type] ||= :container
|
22
|
+
@options[:css] ||= ''
|
23
|
+
@options[:style] ||= {}
|
24
|
+
@options[:events] ||= {}
|
25
|
+
@is_numeric = [:integer, :decimal, :percent, :count].include?(@type)
|
26
|
+
@is_text = !(container? || media?)
|
27
|
+
@options[:comma] = true if @options[:comma].nil?
|
28
|
+
set_format_defaults
|
29
|
+
set_css_defaults
|
30
|
+
set_style_defaults
|
31
|
+
end
|
32
|
+
|
33
|
+
def container?
|
34
|
+
@type == :container
|
35
|
+
end
|
36
|
+
|
37
|
+
def media?
|
38
|
+
@type == :media
|
39
|
+
end
|
40
|
+
|
41
|
+
def text?
|
42
|
+
@is_text
|
43
|
+
end
|
44
|
+
|
45
|
+
def date?
|
46
|
+
@type == :date
|
47
|
+
end
|
48
|
+
|
49
|
+
def numeric?
|
50
|
+
@is_numeric
|
51
|
+
end
|
52
|
+
|
53
|
+
# Context may be anything meaningful to procs.
|
54
|
+
# Returned value will be formatted (if applicable).
|
55
|
+
def node(tag_name, attributes: {}, context: nil)
|
56
|
+
VDOM::Node.new(
|
57
|
+
tag_name,
|
58
|
+
attributes: ->{
|
59
|
+
attributes.merge.({
|
60
|
+
class: css(context: context, value: value),
|
61
|
+
style: style(context: context, value: value),
|
62
|
+
}).merge(
|
63
|
+
events(context: context, value: value)
|
64
|
+
)
|
65
|
+
},
|
66
|
+
content: ->{
|
67
|
+
formatted_value(context: context)
|
68
|
+
},
|
69
|
+
model: self
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Context may be anything meaningful to procs.
|
74
|
+
# Returned value will be formatted (if applicable).
|
75
|
+
def value_and_attributes(context: nil)
|
76
|
+
value = formatted_value(context: context)
|
77
|
+
[
|
78
|
+
value,
|
79
|
+
{
|
80
|
+
class: css(context: context, value: value),
|
81
|
+
style: style(context: context, value: value),
|
82
|
+
}.merge(
|
83
|
+
events(context: context, value: value)
|
84
|
+
)
|
85
|
+
]
|
86
|
+
end
|
87
|
+
|
88
|
+
def sort_value(context: nil)
|
89
|
+
# debug __FILE__, __LINE__, __method__, "context=#{context.class.name}"
|
90
|
+
option(:sort_value, context: context)
|
91
|
+
end
|
92
|
+
|
93
|
+
def value(context: nil)
|
94
|
+
option(:value, context: context)
|
95
|
+
end
|
96
|
+
|
97
|
+
def css(context: nil, value: nil)
|
98
|
+
option(:css, context: context, value: value)
|
99
|
+
end
|
100
|
+
|
101
|
+
def style(context: nil, value: nil)
|
102
|
+
option(:style, context: context, value: value)
|
103
|
+
end
|
104
|
+
|
105
|
+
def events(context: nil, value: nil)
|
106
|
+
result = {}
|
107
|
+
events = option(:events, context: context, value: value)
|
108
|
+
if events && events.size > 0
|
109
|
+
events.each do |event, proc|
|
110
|
+
event = event[0,2] == 'on' ? event : :"on#{event}"
|
111
|
+
result[event] = proc
|
112
|
+
end
|
113
|
+
end
|
114
|
+
result
|
115
|
+
end
|
116
|
+
|
117
|
+
def formatted_value(context: nil)
|
118
|
+
v = value(context: context)
|
119
|
+
if v
|
120
|
+
if text?
|
121
|
+
format = format(context: context, value: v)
|
122
|
+
if numeric?
|
123
|
+
comma_numeric(format % v.to_f)
|
124
|
+
elsif date?
|
125
|
+
parse_date(v).strftime(value)
|
126
|
+
else
|
127
|
+
v.to_s
|
128
|
+
end
|
129
|
+
else
|
130
|
+
v
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def format(context: nil, value: nil)
|
136
|
+
option(:format, context: context, value: value)
|
137
|
+
end
|
138
|
+
|
139
|
+
def option(name, context: nil, value: nil)
|
140
|
+
option = @options[name]
|
141
|
+
resolve(option, context: context, value: value)
|
142
|
+
end
|
143
|
+
|
144
|
+
def resolve(thing, context: nil, value: nil)
|
145
|
+
if Proc === thing
|
146
|
+
if thing.arity == 2
|
147
|
+
thing.call(context, value)
|
148
|
+
elsif thing.arity == 1
|
149
|
+
thing.call(value || context)
|
150
|
+
else
|
151
|
+
thing.call
|
152
|
+
end
|
153
|
+
else
|
154
|
+
thing
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def comma_numeric(s)
|
159
|
+
@options[comma] ? s.comma_numeric : s
|
160
|
+
end
|
161
|
+
|
162
|
+
def parse_date(d)
|
163
|
+
self.class.parse_date(d)
|
164
|
+
end
|
165
|
+
|
166
|
+
def self.parse_date(d)
|
167
|
+
return d if d.is_a?(Date)
|
168
|
+
# opal Date.parse can't handle strings with named months
|
169
|
+
# nor can it parse YYYYMMDD without separators!
|
170
|
+
if RUBY_PLATFORM == 'opal'
|
171
|
+
t = Time.parse(d)
|
172
|
+
Date.new(t.year, t.month, t.day)
|
173
|
+
else
|
174
|
+
Date.parse(d)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
private
|
179
|
+
|
180
|
+
def set_tag_defaults
|
181
|
+
@options[:tag] ||= 'div'
|
182
|
+
end
|
183
|
+
|
184
|
+
def set_style_defaults
|
185
|
+
if text? && options[:style].nil?
|
186
|
+
@options[:style] = case @options[:type]
|
187
|
+
when :integer, :decimal, :count, :percent
|
188
|
+
{ text_align: 'right' }
|
189
|
+
else
|
190
|
+
{ text_align: 'center' }
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def set_css_defaults
|
196
|
+
# ?
|
197
|
+
end
|
198
|
+
|
199
|
+
def set_format_defaults
|
200
|
+
if text? && @options[:format].nil?
|
201
|
+
@options[:format] = case @options[:type]
|
202
|
+
when :decimal
|
203
|
+
->(v) {
|
204
|
+
comma_numeric('%0.2f' % v)
|
205
|
+
}
|
206
|
+
when :integer, :count
|
207
|
+
->(v) {
|
208
|
+
comma_numeric(v.to_i.to_s)
|
209
|
+
}
|
210
|
+
when :percent
|
211
|
+
->(v) {
|
212
|
+
comma_numeric((v * 100).round(0).to_s)
|
213
|
+
}
|
214
|
+
when :sign
|
215
|
+
->(v) {
|
216
|
+
if v == 0
|
217
|
+
'0'
|
218
|
+
else
|
219
|
+
v < 0 ? '-' : '+'
|
220
|
+
end
|
221
|
+
}
|
222
|
+
when :date
|
223
|
+
->(v) {
|
224
|
+
v.strftime('%Y/%m/%d')
|
225
|
+
}
|
226
|
+
when :bool
|
227
|
+
->(v) {
|
228
|
+
v ? 'T' : 'F'
|
229
|
+
}
|
230
|
+
else # :text, ...
|
231
|
+
->(v) {
|
232
|
+
v.to_s
|
233
|
+
}
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'vdom/component'
|
2
|
+
|
3
|
+
module VDOM
|
4
|
+
class Instance
|
5
|
+
|
6
|
+
# Returns a (very probably) unique element id.
|
7
|
+
def self.new_id
|
8
|
+
@@id_chars ||= ('0'..'9').to_a + ('A'..'F').to_a
|
9
|
+
'DOM_' + @@id_chars.shuffle.reduce('') {|r,c| r + c}
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :id
|
13
|
+
attr_reader :root_element
|
14
|
+
attr_reader :root_component
|
15
|
+
|
16
|
+
# Create a VDOM::Instance with given id,
|
17
|
+
# (root) component and (root) DOM element.
|
18
|
+
# Element must be a native element or Bowser::Element.
|
19
|
+
# Component must be a VDOM::Component.
|
20
|
+
def initialize(id: nil, component: nil, element: nil)
|
21
|
+
@id = id || self.class.new_id
|
22
|
+
@root_element = Bowser::Element.new(element)
|
23
|
+
@root_component = component
|
24
|
+
unless VDOM::Component === @root_component
|
25
|
+
raise TypeError, "#{__FILE__}:#{__LINE__}:#{self.class.name}##{__method__}: root component must be a VDOM::Component"
|
26
|
+
end
|
27
|
+
@virtual_dom = Clearwater::VirtualDOM::Document.new(@root_element)
|
28
|
+
Instances.add(self)
|
29
|
+
end
|
30
|
+
|
31
|
+
def ==(other)
|
32
|
+
self.class == other.class && id == other.id
|
33
|
+
id == other.id
|
34
|
+
end
|
35
|
+
|
36
|
+
def eql?(other)
|
37
|
+
self.class == other.class && id == other.id
|
38
|
+
end
|
39
|
+
|
40
|
+
def document
|
41
|
+
@document ||= Bowser.document
|
42
|
+
end
|
43
|
+
|
44
|
+
def window
|
45
|
+
@window ||= Bowser.window
|
46
|
+
end
|
47
|
+
|
48
|
+
# Adapted from Clearwater::Application
|
49
|
+
# Removed optional block argument
|
50
|
+
def render
|
51
|
+
window.animation_frame do
|
52
|
+
perform_render
|
53
|
+
end
|
54
|
+
nil # not sure why
|
55
|
+
end
|
56
|
+
|
57
|
+
# Adapted from Clearwater::Application
|
58
|
+
def perform_render
|
59
|
+
rendered = benchmark('Generated virtual DOM') do
|
60
|
+
_render = @root_component.render
|
61
|
+
# `console.log(#{"#{__FILE__}:#{__LINE__}:#{self.class.name}##{__method__} : @root_component=#{@root_component} _render=#{_render} "})`
|
62
|
+
Clearwater::Component.sanitize_content(_render)
|
63
|
+
end
|
64
|
+
benchmark('Rendered to actual DOM') do
|
65
|
+
@virtual_dom.render(rendered)
|
66
|
+
end
|
67
|
+
nil # not sure why
|
68
|
+
end
|
69
|
+
|
70
|
+
# Adapted from Clearwater::Application - called #debug? there
|
71
|
+
def benchmark?
|
72
|
+
!!@benchmark
|
73
|
+
end
|
74
|
+
|
75
|
+
# Adapted from Clearwater::Application - called #debug! there
|
76
|
+
def benchmark!
|
77
|
+
@benchmark = true
|
78
|
+
end
|
79
|
+
|
80
|
+
# Adapted from Clearwater::Application
|
81
|
+
def benchmark(what)
|
82
|
+
if benchmark?
|
83
|
+
start = `performance.now()`
|
84
|
+
result = yield
|
85
|
+
finish = `performance.now()`
|
86
|
+
debug __FILE__, __LINE__, __method__, "#{what} in #{(finish - start).round(3)}ms"
|
87
|
+
result
|
88
|
+
else
|
89
|
+
yield
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Returns a Bowser::Element (DOM element) with given id or nil.
|
94
|
+
# A Bowser::Element wraps a native JS DOM element.
|
95
|
+
def element(id)
|
96
|
+
self["##{id}"]
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns a Bowser::Element matching query using
|
100
|
+
# Document.querySelector query syntax.
|
101
|
+
# See https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector.
|
102
|
+
def [](query)
|
103
|
+
document[query]
|
104
|
+
end
|
105
|
+
|
106
|
+
def shutdown
|
107
|
+
Instances.delete(self)
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|