vdom-rb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,5 @@
1
+ # __FILE__ MUST BE SAME NAME AS GEM
2
+ module VDOM
3
+ require_relative "vdom/version"
4
+ end
5
+ Opal.append_path(File.expand_path(File.join("..", "..", "opal"), __FILE__).untaint)
@@ -0,0 +1,3 @@
1
+ module VDOM
2
+ VERSION = "0.1.0"
3
+ end
data/opal/vdom.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'vdom/component'
2
+ require 'vdom/content'
3
+ require 'vdom/table'
4
+ require 'vdom/util'
@@ -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
+