hyalite 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +3 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +46 -0
  5. data/Rakefile +7 -0
  6. data/client/hyalite.rb +98 -0
  7. data/client/hyalite/adler32.rb +33 -0
  8. data/client/hyalite/browser_event.rb +201 -0
  9. data/client/hyalite/callback_queue.rb +22 -0
  10. data/client/hyalite/component.rb +77 -0
  11. data/client/hyalite/composite_component.rb +237 -0
  12. data/client/hyalite/dom_component.rb +329 -0
  13. data/client/hyalite/dom_operations.rb +17 -0
  14. data/client/hyalite/dom_property.rb +218 -0
  15. data/client/hyalite/dom_property_operations.rb +117 -0
  16. data/client/hyalite/element.rb +17 -0
  17. data/client/hyalite/event_dispatcher.rb +99 -0
  18. data/client/hyalite/event_plugin/change_event_plugin.rb +83 -0
  19. data/client/hyalite/event_plugin/event_plugin_registry.rb +49 -0
  20. data/client/hyalite/event_plugin/simple_event_plugin.rb +276 -0
  21. data/client/hyalite/input_wrapper.rb +94 -0
  22. data/client/hyalite/instance_handles.rb +78 -0
  23. data/client/hyalite/internal_component.rb +11 -0
  24. data/client/hyalite/linked_value_utils.rb +27 -0
  25. data/client/hyalite/mount.rb +285 -0
  26. data/client/hyalite/multi_children.rb +272 -0
  27. data/client/hyalite/reconcile_transaction.rb +41 -0
  28. data/client/hyalite/reconciler.rb +122 -0
  29. data/client/hyalite/short_hand.rb +23 -0
  30. data/client/hyalite/synthetic_event.rb +18 -0
  31. data/client/hyalite/text_component.rb +42 -0
  32. data/client/hyalite/transaction.rb +47 -0
  33. data/client/hyalite/try.rb +7 -0
  34. data/client/hyalite/update_queue.rb +52 -0
  35. data/client/hyalite/updates.rb +129 -0
  36. data/client/hyalite/utils.rb +19 -0
  37. data/example/Gemfile +5 -0
  38. data/example/Gemfile.lock +46 -0
  39. data/example/app/application.rb +27 -0
  40. data/example/config.ru +12 -0
  41. data/example/index.html.haml +8 -0
  42. data/hyalite.gemspec +27 -0
  43. data/lib/hyalite.rb +6 -0
  44. data/lib/hyalite/main.rb +5 -0
  45. data/lib/hyalite/version.rb +3 -0
  46. metadata +145 -0
@@ -0,0 +1,77 @@
1
+ require 'hyalite/short_hand'
2
+
3
+ module Hyalite
4
+ module Component
5
+
6
+ attr_accessor :props, :state, :context, :refs
7
+
8
+ def init_component(props, context, updator)
9
+ @props = props
10
+ @context = context
11
+ @updator = updator
12
+ @state = initial_state
13
+ @refs = nil
14
+ end
15
+
16
+ def initial_state
17
+ {}
18
+ end
19
+
20
+ def context_types
21
+ {}
22
+ end
23
+
24
+ def child_context
25
+ {}
26
+ end
27
+
28
+ def component_did_mount
29
+ end
30
+
31
+ def component_will_mount
32
+ end
33
+
34
+ def component_will_unmount
35
+ end
36
+
37
+ def component_will_update(props, state, context)
38
+ end
39
+
40
+ def component_did_update(props, state, context)
41
+ end
42
+
43
+ def should_component_update(props, state, context)
44
+ true
45
+ end
46
+
47
+ def set_state(states, &block)
48
+ @updator.enqueue_set_state(self, states)
49
+ if block_given?
50
+ @updator.enqueue_callback(self, &block)
51
+ end
52
+ end
53
+
54
+ def render
55
+ end
56
+ end
57
+
58
+ class EmptyComponent
59
+ include Component
60
+
61
+ def self.empty_element
62
+ @instance ||= ElementObject.new(EmptyComponent, nil, nil, nil, nil)
63
+ end
64
+
65
+ def render
66
+ Hyalite.create_element("noscript", nil, nil)
67
+ end
68
+ end
69
+
70
+ class TopLevelWrapper
71
+ include Component
72
+
73
+ def render
74
+ @props
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,237 @@
1
+ require 'hyalite/update_queue'
2
+ require 'hyalite/reconciler'
3
+ require 'hyalite/internal_component'
4
+
5
+ module Hyalite
6
+ class CompositeComponent
7
+ include InternalComponent
8
+
9
+ attr_reader :current_element, :rendered_component
10
+ attr_accessor :top_level_wrapper, :pending_state_queue, :mount_order, :pending_callbacks, :pending_element
11
+
12
+ @next_mount_id = 1
13
+
14
+ def self.next_mount_id
15
+ @next_mount_id += 1
16
+ end
17
+
18
+ def initialize(element)
19
+ @element = element
20
+ @current_element = @element
21
+ end
22
+
23
+ def mount_component(root_id, mount_ready, context)
24
+ @context = context
25
+ @mount_order = CompositeComponent.next_mount_id
26
+ @root_node_id = root_id
27
+
28
+ public_context = mask_context(@context)
29
+ @instance = @current_element.type.new
30
+ @instance.init_component(@current_element.props, public_context, UpdateQueue)
31
+
32
+ Hyalite.instance_map[@instance] = self
33
+
34
+ @pending_state_queue = nil
35
+ @pending_replace_state = false
36
+ @pending_force_update = false
37
+
38
+ @instance.component_will_mount
39
+ if @pending_state_queue
40
+ @instance.state = process_pending_state(@instance.props, @instance.context)
41
+ end
42
+
43
+ @rendered_component = Hyalite.instantiate_component(render_component(@instance))
44
+
45
+ markup = Reconciler.mount_component(
46
+ @rendered_component,
47
+ root_id,
48
+ mount_ready,
49
+ context.merge(@instance.child_context)
50
+ )
51
+
52
+ mount_ready.enqueue { @instance.component_did_mount }
53
+
54
+ markup
55
+ end
56
+
57
+ def unmount_component
58
+
59
+ @instance.component_will_unmount
60
+
61
+ Reconciler.unmount_component(@rendered_component)
62
+ @rendered_component = nil
63
+ @instance = nil
64
+
65
+ @pending_state_queue = nil
66
+ @pending_replace_state = false
67
+ @pending_force_update = false
68
+ @pending_callbacks = nil
69
+ @pending_element = nil
70
+
71
+ @context = nil
72
+ @root_node_id = nil
73
+ @top_level_wrapper = nil
74
+
75
+ Hyalite.instance_map.delete(@instance)
76
+ end
77
+
78
+ def perform_update_if_necessary(mount_ready)
79
+ if @pending_element
80
+ receive_component(@pending_element, mount_ready, @context)
81
+ end
82
+
83
+ if (@pending_state_queue && @pending_state_queue.any?) || @pending_force_update
84
+ update_component(
85
+ mount_ready,
86
+ @current_element,
87
+ @current_element,
88
+ @context,
89
+ @context)
90
+ end
91
+ end
92
+
93
+ def public_instance
94
+ @instance
95
+ end
96
+
97
+ def inspect
98
+ "CompositeComponent: instance: #{@instance.inspect}"
99
+ end
100
+
101
+ def attach_ref(ref, component)
102
+ @instance.refs ||= {}
103
+ @instance.refs[ref] = component.public_instance
104
+ end
105
+
106
+ private
107
+
108
+ def render_component(instance)
109
+ Hyalite.current_owner(self) do
110
+ instance.render
111
+ end
112
+ end
113
+
114
+ def mask_context(context)
115
+ context.select {|k, v| @current_element.type.context_types.has_key? k }
116
+ end
117
+
118
+ def update_component(mount_ready, prev_parent_element, next_parent_element, prev_unmasked_context, next_unmasked_context)
119
+ next_context = (@context == next_unmasked_context ? @instance.context : mask_context(next_unmasked_context))
120
+
121
+ next_props = next_parent_element.props
122
+ next_state = process_pending_state(next_props, next_context)
123
+
124
+ should_update =
125
+ @pending_force_update ||
126
+ @instance.should_component_update(next_props, next_state, next_context)
127
+
128
+ if should_update
129
+ @pending_force_update = false
130
+ perform_component_update(
131
+ next_parent_element,
132
+ next_props,
133
+ next_state,
134
+ next_context,
135
+ mount_ready,
136
+ next_unmasked_context
137
+ )
138
+ else
139
+ @current_element = next_parent_element
140
+ @context = next_unmasked_context
141
+ @instance.props = next_props
142
+ @instance.state = next_state
143
+ @instance.context = next_context
144
+ end
145
+ end
146
+
147
+ def receive_component(next_element, mount_ready, next_context)
148
+ prev_element = @current_element
149
+ prev_context = @context
150
+
151
+ @pending_element = nil
152
+
153
+ update_component(
154
+ mount_ready,
155
+ prev_element,
156
+ next_element,
157
+ prev_context,
158
+ next_context
159
+ )
160
+ end
161
+
162
+ def perform_component_update(next_element, next_props, next_state, next_context, mount_ready, unmasked_context)
163
+ prev_props = @instance.props
164
+ prev_state = @instance.state
165
+ prev_context = @instance.context
166
+
167
+ @instance.component_will_update(next_props, next_state, next_context)
168
+
169
+ @current_element = next_element
170
+ @context = unmasked_context
171
+ @instance.props = next_props
172
+ @instance.state = next_state
173
+ @instance.context = next_context
174
+
175
+ update_rendered_component(mount_ready, unmasked_context)
176
+
177
+ mount_ready.enqueue do
178
+ @instance.component_did_update(prev_props, prev_state, prev_context)
179
+ end
180
+ end
181
+
182
+ def update_rendered_component(mount_ready, context)
183
+ prev_component_instance = @rendered_component
184
+ prev_rendered_element = prev_component_instance.current_element
185
+ next_rendered_element = render_validated_component
186
+ if Reconciler.should_update_component(prev_rendered_element, next_rendered_element)
187
+ Reconciler.receive_component(
188
+ prev_component_instance,
189
+ next_rendered_element,
190
+ mount_ready,
191
+ context
192
+ )
193
+ else
194
+ this_id = @root_node_id
195
+ prev_component_id = prev_component_instance.root_node_id
196
+ Reconciler.unmount_component(prev_component_instance)
197
+
198
+ @rendered_component = Hyalite.instantiate_component(next_rendered_element)
199
+ next_markup = Reconciler.mount_component(
200
+ @rendered_component,
201
+ this_id,
202
+ mount_ready,
203
+ context
204
+ )
205
+ replace_node_with_markup_by_id(prev_component_id, next_markup)
206
+ end
207
+ end
208
+
209
+
210
+ def replace_node_with_markup_by_id(id, markup)
211
+ node = Mount.node(id)
212
+ node.replace(markup)
213
+ end
214
+
215
+ def render_validated_component
216
+ Hyalite.current_owner(self) do
217
+ @instance.render
218
+ end
219
+ end
220
+
221
+ def process_pending_state(props, context)
222
+ replace = @pending_replace_state
223
+ @pending_replace_state = false
224
+
225
+ return @instance.state unless @pending_state_queue
226
+
227
+ queue = @pending_state_queue
228
+ @pending_state_queue = nil
229
+
230
+
231
+ queue.each_with_object(replace ? {} : @instance.state) do |partial, next_state|
232
+ next_state.merge! (partial.is_a?(Proc) ? partial.call(@instance, next_state, props, context) : partial)
233
+ end
234
+ end
235
+
236
+ end
237
+ end
@@ -0,0 +1,329 @@
1
+ require 'hyalite/multi_children'
2
+ require 'hyalite/dom_property_operations'
3
+ require 'hyalite/internal_component'
4
+ require 'hyalite/browser_event'
5
+ require 'hyalite/input_wrapper'
6
+
7
+ module Hyalite
8
+ class DOMComponent
9
+ include MultiChildren
10
+ include InternalComponent
11
+
12
+ attr_reader :root_node_id
13
+
14
+ def initialize(element)
15
+ @element = element
16
+ @tag = @element.type.downcase
17
+ @input_wrapper = InputWrapper.new(self)
18
+ end
19
+
20
+ def current_element
21
+ @element
22
+ end
23
+
24
+ def mount_component(root_id, mount_ready, context)
25
+ return if @tag == "noscript"
26
+ @root_node_id = root_id
27
+
28
+ props = current_element.props
29
+
30
+ case @tag
31
+ # when 'iframe', 'img', 'form', 'video', 'audio'
32
+ # this._wrapperState = {
33
+ # listeners: null,
34
+ # }
35
+ # transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
36
+ # when 'button'
37
+ # props = ReactDOMButton.getNativeProps(this, props, nativeParent);
38
+ when 'input'
39
+ @input_wrapper.mount_wrapper
40
+ props = @input_wrapper.native_props
41
+ # when 'option'
42
+ # ReactDOMOption.mountWrapper(this, props, nativeParent);
43
+ # props = ReactDOMOption.getNativeProps(this, props);
44
+ # when 'select'
45
+ # ReactDOMSelect.mountWrapper(this, props, nativeParent);
46
+ # props = ReactDOMSelect.getNativeProps(this, props);
47
+ # transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
48
+ # when 'textarea'
49
+ # ReactDOMTextarea.mountWrapper(this, props, nativeParent);
50
+ # props = ReactDOMTextarea.getNativeProps(this, props);
51
+ # transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
52
+ end
53
+
54
+ element = create_open_tag_markup_and_put_listeners(mount_ready, @element.props)
55
+ create_content_markup(mount_ready, element, context)
56
+ end
57
+
58
+ def unmount_component
59
+ case @tag
60
+ # when 'iframe', 'img', 'form'
61
+ # listeners = this._wrapperState.listeners;
62
+ # if (listeners) {
63
+ # for (var i = 0; i < listeners.length; i++) {
64
+ # listeners[i].remove();
65
+ # }
66
+ # }
67
+ when 'input'
68
+ @input_wrapper.unmount_wrapper
69
+ end
70
+
71
+ unmount_children
72
+ BrowserEvent.delete_all_listeners(@root_node_id)
73
+ Mount.purge_id(@root_node_id)
74
+ @root_node_id = nil
75
+ # @wrapper_state = null;
76
+ if @node_with_legacy_properties
77
+ node = @node_with_legacy_properties
78
+ node.internal_component = nil
79
+ @node_with_legacy_properties = nil
80
+ end
81
+ end
82
+
83
+ def receive_component(next_element, mount_ready, context)
84
+ prev_element = @element
85
+ @element = next_element
86
+ update_component(mount_ready, prev_element, next_element, context);
87
+ end
88
+
89
+ def update_component(mount_ready, prev_element, next_element, context)
90
+ last_props = prev_element.props
91
+ next_props = @element.props
92
+
93
+ case @tag
94
+ # when 'button':
95
+ # lastProps = ReactDOMButton.getNativeProps(this, lastProps);
96
+ # nextProps = ReactDOMButton.getNativeProps(this, nextProps);
97
+ when 'input'
98
+ @input_wrapper.update_wrapper
99
+ last_props = @input_wrapper.native_props(last_props)
100
+ # nextProps = ReactDOMInput.getNativeProps(this, nextProps);
101
+ # when 'option':
102
+ # lastProps = ReactDOMOption.getNativeProps(this, lastProps);
103
+ # nextProps = ReactDOMOption.getNativeProps(this, nextProps);
104
+ # when 'select':
105
+ # lastProps = ReactDOMSelect.getNativeProps(this, lastProps);
106
+ # nextProps = ReactDOMSelect.getNativeProps(this, nextProps);
107
+ # when 'textarea':
108
+ # ReactDOMTextarea.updateWrapper(this);
109
+ # lastProps = ReactDOMTextarea.getNativeProps(this, lastProps);
110
+ # nextProps = ReactDOMTextarea.getNativeProps(this, nextProps);
111
+ end
112
+
113
+ # assertValidProps(this, nextProps);
114
+ update_dom_properties(last_props, next_props, mount_ready)
115
+ update_dom_children(last_props, next_props, mount_ready, context)
116
+
117
+ # if (!canDefineProperty && this._nodeWithLegacyProperties) {
118
+ # this._nodeWithLegacyProperties.props = nextProps;
119
+ # }
120
+
121
+ if @tag == 'select'
122
+ mount_ready.enqueue { post_update_select_wrapper }
123
+ end
124
+ end
125
+
126
+ def public_instance
127
+ native_node
128
+ end
129
+
130
+ def to_s
131
+ {
132
+ tag: @tag,
133
+ root_node_id: root_node_id,
134
+ rendered_children: @rendered_children,
135
+ }.to_s
136
+ end
137
+
138
+ private
139
+
140
+ def native_node
141
+ @native_node ||= Mount.node(@root_node_id)
142
+ end
143
+
144
+ def update_dom_properties(last_props, next_props, mount_ready)
145
+ style_updates = {}
146
+
147
+ last_props.each do |prop_key, prop_value|
148
+ next if next_props.has_key?(prop_key)
149
+
150
+ if prop_key == :style
151
+ @previous_style_copy.each do |style_name, style|
152
+ style_updates[style_name] = ''
153
+ end
154
+
155
+ @previous_style_copy = nil
156
+ elsif BrowserEvent.include?(prop_key)
157
+ if last_props.has_key? prop_key
158
+ BrowserEvent.delete_listener(root_node_id, prop_key)
159
+ end
160
+ elsif DOMProperty.include?(prop_key) || DOMProperty.is_custom_attribute(prop_key)
161
+ node = Mount.node(root_node_id)
162
+ DOMPropertyOperations.delete_value_for_property(node, prop_key, prop_value)
163
+ end
164
+ end
165
+
166
+ next_props.each do |prop_key, next_prop|
167
+ last_prop = prop_key == :style ? @previous_style_copy : last_props[prop_key]
168
+ next if next_prop == last_prop
169
+
170
+ if prop_key == :style
171
+ if next_prop
172
+ next_prop = @previous_style_copy = next_prop.clone
173
+ else
174
+ @previous_style_copy = nil
175
+ end
176
+
177
+ if last_prop
178
+ last_prop.each do |style_name, style|
179
+ unless next_prop.has_key?(style_name)
180
+ style_updates[style_name] = ''
181
+ end
182
+ end
183
+
184
+ next_prop.each do |style_name, style|
185
+ if next_prop.has_key?(style_name) && last_prop[style_name] != next_prop[style_name]
186
+ style_updates[style_name] = next_prop[style_name]
187
+ end
188
+ end
189
+ else
190
+ style_updates = next_prop
191
+ end
192
+ elsif BrowserEvent.include?(prop_key)
193
+ if next_prop
194
+ enqueue_put_listener(root_node_id, prop_key, next_prop, mount_ready)
195
+ elsif last_prop
196
+ BrowserEvent.delete_listener(root_node_id, prop_key)
197
+ end
198
+ elsif is_custom_component(@tag, next_props)
199
+ node = Mount.node(root_node_id)
200
+ DOMPropertyOperations.set_value_for_attribute(node, prop_key, next_prop);
201
+ elsif DOMProperty.include?(prop_key) || DOMProperty.is_custom_attribute(prop_key)
202
+ node = Mount.node(root_node_id)
203
+ if next_prop
204
+ DOMPropertyOperations.set_value_for_property(node, prop_key, next_prop);
205
+ else
206
+ DOMPropertyOperations.delete_value_for_property(node, prop_key)
207
+ end
208
+ end
209
+ end
210
+
211
+ if style_updates.any?
212
+ node = Mount.node(root_node_id)
213
+ node.style(style_updates)
214
+ end
215
+ rescue => e
216
+ p e
217
+ raise
218
+ end
219
+
220
+ def update_dom_children(last_props, next_props, mount_ready, context)
221
+ last_content = last_props[:children] if is_text_content(last_props[:children])
222
+ next_content = next_props[:children] if is_text_content(next_props[:children])
223
+
224
+ last_html = last_props[:dangerouslySetInnerHTML].try {|_| _['__html'] }
225
+ next_html = next_props[:dangerouslySetInnerHTML].try {|_| _['__html'] }
226
+
227
+ last_children = last_props[:children] unless last_content
228
+ next_children = next_props[:children] unless next_content
229
+
230
+ last_has_content_or_html = !last_content.nil? || !last_html.nil?
231
+ next_has_content_or_html = !next_content.nil? || !next_html.nil?
232
+ if last_children && next_children.nil?
233
+ update_children(nil, mount_ready, context)
234
+ elsif last_has_content_or_html && !next_has_content_or_html
235
+ update_text_content('')
236
+ end
237
+
238
+ if next_content
239
+ unless last_content == next_content
240
+ update_text_content(next_content.to_s)
241
+ end
242
+ elsif next_html
243
+ unless last_html == next_html
244
+ update_markup(next_html.to_s)
245
+ end
246
+ elsif next_children
247
+ update_children(next_children, mount_ready, context)
248
+ end
249
+ end
250
+
251
+ def create_open_tag_markup_and_put_listeners(mount_ready, props)
252
+ element = $document.create_element(@tag)
253
+
254
+ props.each do |prop_key, prop_value|
255
+ next unless prop_value
256
+
257
+ if BrowserEvent.include?(prop_key)
258
+ enqueue_put_listener(@root_node_id, prop_key, prop_value, mount_ready)
259
+ else
260
+ if prop_key == :style
261
+ if prop_value
262
+ prop_value = @previous_style_copy = props[:style].clone
263
+ end
264
+ DOMPropertyOperations.create_markup_for_styles(element, prop_value)
265
+ else
266
+ if is_custom_component(@tag, props)
267
+ DOMPropertyOperations.create_markup_for_custom_attribute(element, prop_key, prop_value)
268
+ else
269
+ DOMPropertyOperations.create_markup_for_property(element, prop_key, prop_value)
270
+ end
271
+ end
272
+ end
273
+ end
274
+
275
+ #return element if mount_ready.render_to_static_markup
276
+
277
+ element[Mount::ID_ATTR_NAME] = @root_node_id
278
+ element
279
+ end
280
+
281
+ def create_content_markup(mount_ready, element, context)
282
+ children = @element.props[:children]
283
+
284
+ inner_html = @element.props[:dangerouslySetInnerHTML]
285
+ if inner_html
286
+ if inner_html[:__html]
287
+ html = inner_html[:__html]
288
+ `element.native.innerHTML = html`
289
+ end
290
+ elsif is_text_content(children)
291
+ element.inner_dom = Browser::DOM::Text.create(@element.props[:children])
292
+ else
293
+ mount_images = mount_children(@element.props[:children], mount_ready, context)
294
+ mount_images.each do |image|
295
+ if image.is_a?(String)
296
+ element.text = image
297
+ else
298
+ image.append_to(element) if image
299
+ end
300
+ end
301
+ end
302
+ element
303
+ end
304
+
305
+ def enqueue_put_listener(id, event_name, listener, mount_ready)
306
+ container = Mount.container_for_id(id)
307
+ if container
308
+ doc = container.node_type == Browser::DOM::Node::ELEMENT_NODE ? container.document : container
309
+ BrowserEvent.listen_to(event_name, doc)
310
+ end
311
+ mount_ready.enqueue do
312
+ BrowserEvent.put_listener(id, event_name, listener)
313
+ end
314
+ end
315
+
316
+ def is_custom_component(tag, props)
317
+ tag.include?('-') || props.has_key?(:is)
318
+ end
319
+
320
+ def is_text_content(children)
321
+ case children
322
+ when String, Numeric
323
+ true
324
+ else
325
+ false
326
+ end
327
+ end
328
+ end
329
+ end