hyalite 0.0.1

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.
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