hyper-component 0.12.3 → 0.99.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +27 -0
  3. data/.gitignore +42 -41
  4. data/.travis.yml +29 -0
  5. data/CHANGELOG.md +143 -0
  6. data/DOCS.md +1515 -0
  7. data/Gemfile +5 -2
  8. data/Gemfile.lock +244 -193
  9. data/LICENSE +5 -7
  10. data/README.md +49 -0
  11. data/Rakefile +40 -0
  12. data/hyper-component.gemspec +41 -31
  13. data/lib/hyper-component.rb +44 -9
  14. data/lib/rails-helpers/top_level_rails_component.rb +79 -0
  15. data/lib/react/api.rb +270 -0
  16. data/lib/react/callbacks.rb +42 -0
  17. data/lib/react/children.rb +38 -0
  18. data/lib/react/component.rb +189 -0
  19. data/lib/react/component/api.rb +70 -0
  20. data/lib/react/component/base.rb +13 -0
  21. data/lib/react/component/class_methods.rb +175 -0
  22. data/lib/react/component/dsl_instance_methods.rb +23 -0
  23. data/lib/react/component/params.rb +6 -0
  24. data/lib/react/component/props_wrapper.rb +90 -0
  25. data/lib/react/component/should_component_update.rb +99 -0
  26. data/lib/react/component/tags.rb +116 -0
  27. data/lib/react/config.rb +5 -0
  28. data/lib/react/element.rb +159 -0
  29. data/lib/react/event.rb +76 -0
  30. data/lib/react/ext/hash.rb +9 -0
  31. data/lib/react/ext/opal-jquery/element.rb +37 -0
  32. data/lib/react/ext/string.rb +8 -0
  33. data/lib/react/native_library.rb +87 -0
  34. data/lib/react/object.rb +15 -0
  35. data/lib/react/react-source-server.rb +3 -0
  36. data/lib/react/react-source.rb +17 -0
  37. data/lib/react/ref_callback.rb +31 -0
  38. data/lib/react/rendering_context.rb +149 -0
  39. data/lib/react/server.rb +19 -0
  40. data/lib/react/state_wrapper.rb +23 -0
  41. data/lib/react/test.rb +16 -0
  42. data/lib/react/test/dsl.rb +17 -0
  43. data/lib/react/test/matchers/render_html_matcher.rb +56 -0
  44. data/lib/react/test/rspec.rb +15 -0
  45. data/lib/react/test/session.rb +37 -0
  46. data/lib/react/test/utils.rb +71 -0
  47. data/lib/react/to_key.rb +26 -0
  48. data/lib/react/top_level.rb +110 -0
  49. data/lib/react/top_level_render.rb +28 -0
  50. data/lib/react/validator.rb +132 -0
  51. data/lib/reactive-ruby/component_loader.rb +43 -0
  52. data/lib/reactive-ruby/isomorphic_helpers.rb +233 -0
  53. data/lib/reactive-ruby/rails.rb +8 -0
  54. data/lib/reactive-ruby/rails/component_mount.rb +48 -0
  55. data/lib/reactive-ruby/rails/controller_helper.rb +14 -0
  56. data/lib/reactive-ruby/rails/railtie.rb +20 -0
  57. data/lib/reactive-ruby/serializers.rb +23 -0
  58. data/lib/reactive-ruby/server_rendering/contextual_renderer.rb +46 -0
  59. data/lib/reactive-ruby/server_rendering/hyper_asset_container.rb +46 -0
  60. data/lib/{hyperloop/component → reactive-ruby}/version.rb +1 -1
  61. data/lib/reactrb/auto-import.rb +27 -0
  62. data/misc/generators/reactive_ruby/test_app/templates/assets/javascripts/components.rb +3 -0
  63. data/misc/generators/reactive_ruby/test_app/templates/assets/javascripts/server_rendering.js +5 -0
  64. data/misc/generators/reactive_ruby/test_app/templates/assets/javascripts/test_application.rb +2 -0
  65. data/misc/generators/reactive_ruby/test_app/templates/boot.rb.erb +6 -0
  66. data/misc/generators/reactive_ruby/test_app/templates/script/rails +5 -0
  67. data/misc/generators/reactive_ruby/test_app/templates/test_application.rb.erb +13 -0
  68. data/misc/generators/reactive_ruby/test_app/templates/views/components/hello_world.rb +11 -0
  69. data/misc/generators/reactive_ruby/test_app/templates/views/components/todo.rb +14 -0
  70. data/misc/generators/reactive_ruby/test_app/templates/views/layouts/test_layout.html.erb +0 -0
  71. data/misc/generators/reactive_ruby/test_app/test_app_generator.rb +121 -0
  72. data/misc/how-component-name-lookup-works.md +145 -0
  73. data/misc/hyperloop-logo-small-pink.png +0 -0
  74. data/misc/logo1.png +0 -0
  75. data/misc/logo2.png +0 -0
  76. data/misc/logo3.png +0 -0
  77. data/path_release_steps.md +9 -0
  78. metadata +260 -37
  79. data/CODE_OF_CONDUCT.md +0 -49
@@ -0,0 +1,23 @@
1
+ require "react/children"
2
+
3
+ module React
4
+ module Component
5
+ module DslInstanceMethods
6
+ def children
7
+ Children.new(`#{@native}.props.children`)
8
+ end
9
+
10
+ def params
11
+ @params ||= self.class.props_wrapper.new(self)
12
+ end
13
+
14
+ def props
15
+ Hash.new(`#{@native}.props`)
16
+ end
17
+
18
+ def refs
19
+ Hash.new(`#{@native}.refs`)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,6 @@
1
+ module React
2
+ module Component
3
+ module Params
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,90 @@
1
+ module React
2
+ module Component
3
+
4
+ class PropsWrapper
5
+ attr_reader :component
6
+
7
+ def self.define_param(name, param_type)
8
+ if param_type == Observable
9
+ define_method("#{name}") do
10
+ value_for(name)
11
+ end
12
+ define_method("#{name}!") do |*args|
13
+ current_value = value_for(name)
14
+ if args.count > 0
15
+ props[name].call args[0]
16
+ current_value
17
+ else
18
+ # rescue in case we in middle of render... What happens during a
19
+ # render that causes exception?
20
+ # Where does `dont_update_state` come from?
21
+ props[name].call current_value unless @dont_update_state rescue nil
22
+ props[name]
23
+ end
24
+ end
25
+ elsif param_type == Proc
26
+ define_method("#{name}") do |*args, &block|
27
+ props[name].call(*args, &block) if props[name]
28
+ end
29
+ else
30
+ define_method("#{name}") do
31
+ fetch_from_cache(name) do
32
+ if param_type.respond_to? :_react_param_conversion
33
+ param_type._react_param_conversion props[name], nil
34
+ elsif param_type.is_a?(Array) &&
35
+ param_type[0].respond_to?(:_react_param_conversion)
36
+ props[name].collect do |param|
37
+ param_type[0]._react_param_conversion param, nil
38
+ end
39
+ else
40
+ props[name]
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ def self.define_all_others(name)
48
+ define_method("#{name}") do
49
+ @_all_others_cache ||= yield(props)
50
+ end
51
+ end
52
+
53
+
54
+ def initialize(component)
55
+ @component = component
56
+ end
57
+
58
+ def [](prop)
59
+ props[prop]
60
+ end
61
+
62
+
63
+ def _reset_all_others_cache
64
+ @_all_others_cache = nil
65
+ end
66
+
67
+ private
68
+
69
+ def fetch_from_cache(name)
70
+ last, value = cache[name]
71
+ return value if last.equal?(props[name])
72
+ yield.tap do |value|
73
+ cache[name] = [props[name], value]
74
+ end
75
+ end
76
+
77
+ def cache
78
+ @cache ||= Hash.new { |h, k| h[k] = [] }
79
+ end
80
+
81
+ def props
82
+ component.props
83
+ end
84
+
85
+ def value_for(name)
86
+ self[name].instance_variable_get("@value") if self[name]
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,99 @@
1
+ module React
2
+ module Component
3
+ #
4
+ # React assumes all components should update, unless a component explicitly overrides
5
+ # the shouldComponentUpdate method. Reactrb does an explicit check doing a shallow
6
+ # compare of params, and using a timestamp to determine if state has changed.
7
+
8
+ # If needed components can provide their own #needs_update? method which will be
9
+ # passed the next params and state opal hashes.
10
+
11
+ # Attached to these hashes is a #changed? method that returns whether the hash contains
12
+ # changes as calculated by the base mechanism. This way implementations of #needs_update?
13
+ # can use the base comparison mechanism as needed.
14
+
15
+ # For example
16
+ # def needs_update?(next_params, next_state)
17
+ # # use a special comparison method
18
+ # return false if next_state.changed? || next_params.changed?
19
+ # # do some other special checks
20
+ # end
21
+
22
+ # Note that beginning in 0.9 we will use standard ruby compare on all params further reducing
23
+ # the need for needs_update?
24
+ #
25
+ module ShouldComponentUpdate
26
+ def should_component_update?(next_props, next_state)
27
+ State.set_state_context_to(self, false) do
28
+ # rubocop:disable Style/DoubleNegation # we must return true/false to js land
29
+ if respond_to?(:needs_update?)
30
+ !!call_needs_update(next_props, next_state)
31
+ else
32
+ (props_changed?(next_props) || native_state_changed?(next_state))
33
+ end
34
+ # rubocop:enable Style/DoubleNegation
35
+ end
36
+ end
37
+
38
+ # create opal hashes for next params and state, and attach
39
+ # the changed? method to each hash
40
+
41
+ def call_needs_update(next_params, next_state)
42
+ component = self
43
+ next_params.define_singleton_method(:changed?) do
44
+ component.props_changed?(self)
45
+ end
46
+ next_state.define_singleton_method(:changed?) do
47
+ component.native_state_changed?(next_state)
48
+ end
49
+ needs_update?(next_params, next_state)
50
+ end
51
+
52
+ # Whenever state changes, reactrb updates a timestamp on the state object.
53
+ # We can rapidly check for state changes comparing the incoming state time_stamp
54
+ # with the current time stamp.
55
+
56
+ # we receive a Opal Ruby Hash here, always, so the Hash is either empty or filled
57
+ # Hash is converted to native object
58
+ # if the Hash was empty, the Object has no keys
59
+
60
+ # Different versions of react treat empty state differently, so we first
61
+ # convert anything that looks like an empty state to "false" for consistency.
62
+
63
+ # Then we test if one state is empty and the other is not, then we return false.
64
+ # Then we test if both states are empty we return true.
65
+ # If either state does not have a time stamp then we have to assume a change.
66
+ # Otherwise we check time stamps
67
+
68
+ # rubocop:disable Metrics/MethodLength # for effeciency we want this to be one method
69
+ def native_state_changed?(next_state_hash)
70
+ # next_state = next_state_hash.to_n
71
+ # %x{
72
+ # var current_state = #{@native}.state
73
+ # var normalized_next_state =
74
+ # !next_state || Object.keys(next_state).length === 0 ? false : next_state
75
+ # var normalized_current_state =
76
+ # !current_state || Object.keys(current_state).length === 0 ? false : current_state
77
+ # if (!normalized_current_state != !normalized_next_state) return(true)
78
+ # if (!normalized_current_state && !normalized_next_state) return(false)
79
+ # if (!normalized_current_state['***_state_updated_at-***'] &&
80
+ # !normalized_next_state['***_state_updated_at-***']) return(false)
81
+ # if (!normalized_current_state['***_state_updated_at-***'] ||
82
+ # !normalized_next_state['***_state_updated_at-***']) return(true)
83
+ # return (normalized_current_state['***_state_updated_at-***'] !=
84
+ # normalized_next_state['***_state_updated_at-***'])
85
+ # }
86
+ state_hash = Hash.new(`#{@native}.state`)
87
+ next_state_hash != state_hash
88
+ end
89
+ # rubocop:enable Metrics/MethodLength
90
+
91
+ # Do a shallow compare on the two hashes. Starting in 0.9 we will do a deep compare. ???
92
+
93
+ def props_changed?(next_props)
94
+ props = Hash.new(`#{@native}.props`)
95
+ next_props != props
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,116 @@
1
+ module React
2
+ module Component
3
+ # contains the name of all HTML tags, and the mechanism to register a component
4
+ # class as a new tag
5
+ module Tags
6
+ HTML_TAGS = %w(a abbr address area article aside audio b base bdi bdo big blockquote body br
7
+ button canvas caption cite code col colgroup data datalist dd del details dfn
8
+ dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5
9
+ h6 head header hr html i iframe img input ins kbd keygen label legend li link
10
+ main map mark menu menuitem meta meter nav noscript object ol optgroup option
11
+ output p param picture pre progress q rp rt ruby s samp script section select
12
+ small source span strong style sub summary sup table tbody td textarea tfoot th
13
+ thead time title tr track u ul var video wbr) +
14
+ # The SVG Tags
15
+ %w(circle clipPath defs ellipse g line linearGradient mask path pattern polygon polyline
16
+ radialGradient rect stop svg text tspan)
17
+
18
+ # the present method is retained as a legacy behavior
19
+ def present(component, *params, &children)
20
+ React::RenderingContext.render(component, *params, &children)
21
+ end
22
+
23
+ # define each predefined tag (upcase) as an instance method and a constant
24
+ # deprecated: define each predefined tag (downcase) as the alias of the instance method
25
+
26
+ HTML_TAGS.each do |tag|
27
+
28
+ define_method(tag.upcase) do |*params, &children|
29
+ React::RenderingContext.render(tag, *params, &children)
30
+ end
31
+
32
+ const_set tag.upcase, tag
33
+
34
+ # deprecated: remove
35
+ if tag == 'p'
36
+ define_method(tag) do |*params, &children|
37
+ if children || params.count == 0 || (params.count == 1 && params.first.is_a?(Hash))
38
+ React::RenderingContext.render(tag, *params, &children)
39
+ else
40
+ Kernel.p(*params)
41
+ end
42
+ end
43
+ else
44
+ alias_method tag, tag.upcase
45
+ end
46
+ # end of deprecated code
47
+ end
48
+
49
+ # this is used for haml style (i.e. DIV.foo.bar) class tags which is deprecated
50
+ def self.html_tag_class_for(tag)
51
+ downcased_tag = tag.downcase
52
+ if tag =~ /[A-Z]+/ && HTML_TAGS.include?(downcased_tag)
53
+ Object.const_set tag, React.create_element(downcased_tag)
54
+ end
55
+ end
56
+
57
+ # use method_missing to look up component names in the form of "Foo(..)"
58
+ # where there is no preceeding scope.
59
+
60
+ def method_missing(name, *params, &children)
61
+ component = find_component(name)
62
+ return React::RenderingContext.render(component, *params, &children) if component
63
+ Object.method_missing(name, *params, &children)
64
+ end
65
+
66
+ # install methods with the same name as the component in the parent class/module
67
+ # thus component names in the form Foo::Bar(...) will work
68
+
69
+ class << self
70
+ def included(component)
71
+ name, parent = find_name_and_parent(component)
72
+ tag_names_module = Module.new do
73
+ define_method name do |*params, &children|
74
+ React::RenderingContext.render(component, *params, &children)
75
+ end
76
+ # handle deprecated _as_node style
77
+ define_method "#{name}_as_node" do |*params, &children|
78
+ React::RenderingContext.build_only(component, *params, &children)
79
+ end
80
+ end
81
+ parent.extend(tag_names_module)
82
+ end
83
+
84
+ private
85
+
86
+ def find_name_and_parent(component)
87
+ split_name = component.name && component.name.split('::')
88
+ if split_name && split_name.length > 1
89
+ [split_name.last, split_name.inject([Module]) { |a, e| a + [a.last.const_get(e)] }[-2]]
90
+ end
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ def find_component(name)
97
+ component = lookup_const(name)
98
+ if component && !component.method_defined?(:render)
99
+ raise "#{name} does not appear to be a react component."
100
+ end
101
+ component
102
+ end
103
+
104
+ def lookup_const(name)
105
+ return nil unless name =~ /^[A-Z]/
106
+ #html_tag = React::Component::Tags.html_tag_class(name)
107
+ #return html_tag if html_tag
108
+ scopes = self.class.name.to_s.split('::').inject([Module]) do |nesting, next_const|
109
+ nesting + [nesting.last.const_get(next_const)]
110
+ end.reverse
111
+ scope = scopes.detect { |s| s.const_defined?(name) }
112
+ scope.const_get(name) if scope
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,5 @@
1
+ if RUBY_ENGINE != 'opal'
2
+ module Hyperloop
3
+ define_setting :prerendering, :off
4
+ end
5
+ end
@@ -0,0 +1,159 @@
1
+ require 'react/ext/string'
2
+
3
+ module React
4
+ #
5
+ # Wraps the React Native element class
6
+ #
7
+ # adds the #on method to add event handlers to the element
8
+ #
9
+ # adds the #render method to place elements in the DOM and
10
+ # #delete (alias/deprecated #as_node) method to remove elements from the DOM
11
+ #
12
+ # handles the haml style class notation so that
13
+ # div.bar.blat becomes div(class: "bar blat")
14
+ # by using method missing
15
+ #
16
+ class Element
17
+ include Native
18
+
19
+ alias_native :element_type, :type
20
+ alias_native :props, :props
21
+
22
+ attr_reader :type
23
+ attr_reader :properties
24
+ attr_reader :block
25
+
26
+ attr_accessor :waiting_on_resources
27
+
28
+ def initialize(native_element, type = nil, properties = {}, block = nil)
29
+ @type = type
30
+ @properties = (`typeof #{properties} === 'undefined'` ? nil : properties) || {}
31
+ @block = block
32
+ @native = native_element
33
+ end
34
+
35
+ # Attach event handlers.
36
+
37
+ def on(*event_names, &block)
38
+ event_names.each { |event_name| merge_event_prop!(event_name, &block) }
39
+ @native = `React.cloneElement(#{@native}, #{@properties.shallow_to_n})`
40
+ self
41
+ end
42
+
43
+ # Render element into DOM in the current rendering context.
44
+ # Used for elements that are not yet in DOM, i.e. they are provided as children
45
+ # or they have been explicitly removed from the rendering context using the delete method.
46
+
47
+ def render(props = {}, &new_block)
48
+ if props.empty?
49
+ React::RenderingContext.render(self)
50
+ else
51
+ props = API.convert_props(props)
52
+ React::RenderingContext.render(
53
+ Element.new(`React.cloneElement(#{@native}, #{props.shallow_to_n})`,
54
+ type, @properties.merge(props), block),
55
+ )
56
+ end
57
+ end
58
+
59
+ # Delete (remove) element from rendering context, the element may later be added back in
60
+ # using the render method.
61
+
62
+ def delete
63
+ React::RenderingContext.delete(self)
64
+ end
65
+ # Deprecated version of delete method
66
+ alias as_node delete
67
+
68
+ # Any other method applied to an element will be treated as class name (haml style) thus
69
+ # div.foo.bar(id: :fred) is the same as saying div(class: "foo bar", id: :fred)
70
+ #
71
+ # single underscores become dashes, and double underscores become a single underscore
72
+ #
73
+ # params may be provide to each class (but typically only to the last for easy reading.)
74
+
75
+ def method_missing(class_name, args = {}, &new_block)
76
+ return dup.render.method_missing(class_name, args, &new_block) unless rendered?
77
+ React::RenderingContext.replace(
78
+ self,
79
+ RenderingContext.build do
80
+ RenderingContext.render(type, build_new_properties(class_name, args), &new_block)
81
+ end
82
+ )
83
+ end
84
+
85
+ def rendered?
86
+ React::RenderingContext.rendered? self
87
+ end
88
+
89
+ def self.haml_class_name(class_name)
90
+ class_name.gsub(/__|_/, '__' => '_', '_' => '-')
91
+ end
92
+
93
+ private
94
+
95
+ def build_new_properties(class_name, args)
96
+ class_name = self.class.haml_class_name(class_name)
97
+ new_props = @properties.dup
98
+ new_props[:className] = "\
99
+ #{class_name} #{new_props[:className]} #{args.delete(:class)} #{args.delete(:className)}\
100
+ ".split(' ').uniq.join(' ')
101
+ new_props.merge! args
102
+ end
103
+
104
+ # built in events, events going to native components, and events going to reactrb
105
+
106
+ # built in events will have their event param translated to the Event wrapper
107
+ # and the name will camelcased and have on prefixed, so :click becomes onClick.
108
+ #
109
+ # events emitting from native components are assumed to have the same camel case and
110
+ # on prefixed.
111
+ #
112
+ # events emitting from reactrb components will just have on_ prefixed. So
113
+ # :play_button_pushed attaches to the :on_play_button_pushed param
114
+ #
115
+ # in all cases the default name convention can be overriden by wrapping in <...> brackets.
116
+ # So on("<MyEvent>") will attach to the "MyEvent" param.
117
+
118
+ def merge_event_prop!(event_name, &block)
119
+ if event_name =~ /^<(.+)>$/
120
+ merge_component_event_prop! event_name.gsub(/^<(.+)>$/, '\1'), &block
121
+ elsif React::Event::BUILT_IN_EVENTS.include?(name = "on#{event_name.event_camelize}")
122
+ merge_built_in_event_prop! name, &block
123
+ elsif @type.instance_variable_get('@native_import')
124
+ merge_component_event_prop! name, &block
125
+ else
126
+ merge_component_event_prop! "on_#{event_name}", &block
127
+ end
128
+ end
129
+
130
+ def merge_built_in_event_prop!(prop_name)
131
+ @properties.merge!(
132
+ prop_name => %x{
133
+ function(){
134
+ var react_event = arguments[0];
135
+ var all_args;
136
+ var other_args;
137
+ if (arguments.length > 1) {
138
+ all_args = Array.prototype.slice.call(arguments);
139
+ other_args = all_args.slice(1, arguments.length);
140
+ return #{yield(React::Event.new(`react_event`), *(`other_args`))};
141
+ } else {
142
+ return #{yield(React::Event.new(`react_event`))};
143
+ }
144
+ }
145
+ }
146
+ )
147
+ end
148
+
149
+ def merge_component_event_prop!(prop_name)
150
+ @properties.merge!(
151
+ prop_name => %x{
152
+ function(){
153
+ return #{yield(*Array(`arguments`))}
154
+ }
155
+ }
156
+ )
157
+ end
158
+ end
159
+ end