hyper-component 0.12.3 → 0.99.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.
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,43 @@
1
+ module ReactiveRuby
2
+ class ComponentLoader
3
+ attr_reader :v8_context
4
+ private :v8_context
5
+
6
+ def initialize(v8_context)
7
+ unless v8_context
8
+ raise ArgumentError.new('Could not obtain ExecJS runtime context')
9
+ end
10
+ @v8_context = v8_context
11
+ end
12
+
13
+ def load(file = components)
14
+ return true if loaded?
15
+ !!v8_context.eval(opal(file))
16
+ end
17
+
18
+ def load!(file = components)
19
+ return true if loaded?
20
+ self.load(file)
21
+ ensure
22
+ raise "No HyperReact components found in #{components}" unless loaded?
23
+ end
24
+
25
+ def loaded?
26
+ !!v8_context.eval('Opal.React !== undefined')
27
+ rescue ::ExecJS::Error
28
+ false
29
+ end
30
+
31
+ private
32
+
33
+ def components
34
+ opts = ::Rails.configuration.react.server_renderer_options
35
+ return opts[:files].first.gsub(/.js$/,'') if opts && opts[:files]
36
+ 'components'
37
+ end
38
+
39
+ def opal(file)
40
+ Opal::Sprockets.load_asset(file)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,233 @@
1
+ require "react/config"
2
+
3
+ module React
4
+ module IsomorphicHelpers
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ if RUBY_ENGINE != 'opal'
10
+ def self.load_context(ctx, controller, name = nil)
11
+ @context = Context.new("#{controller.object_id}-#{Time.now.to_i}", ctx, controller, name)
12
+ @context.load_opal_context
13
+ ::Rails.logger.debug "************************** React Server Context Initialized #{name} #{Time.now.to_f} *********************************************"
14
+ @context
15
+ end
16
+ else
17
+ def self.load_context(unique_id = nil, name = nil)
18
+ # can be called on the client to force re-initialization for testing purposes
19
+ if !unique_id || !@context || @context.unique_id != unique_id
20
+ if on_opal_server?
21
+ `console.history = []` rescue nil
22
+ message = "************************ React Prerendering Context Initialized #{name} ***********************"
23
+ else
24
+ message = "************************ React Browser Context Initialized ****************************"
25
+ end
26
+ log(message)
27
+ @context = Context.new(unique_id)
28
+ end
29
+ @context
30
+ end
31
+ end
32
+
33
+ def self.context
34
+ @context
35
+ end
36
+
37
+ def self.log(message, message_type = :info)
38
+ message = [message] unless message.is_a? Array
39
+
40
+ if (message_type == :info || message_type == :warning) && Hyperloop.env.production?
41
+ return
42
+ end
43
+
44
+ if message_type == :info
45
+ if on_opal_server?
46
+ style = 'background: #00FFFF; color: red'
47
+ else
48
+ style = 'background: #222; color: #bada55'
49
+ end
50
+ message = ["%c" + message[0], style]+message[1..-1]
51
+ `console.log.apply(console, message)`
52
+ elsif message_type == :warning
53
+ `console.warn.apply(console, message)`
54
+ else
55
+ `console.error.apply(console, message)`
56
+ end
57
+ end
58
+
59
+ if RUBY_ENGINE != 'opal'
60
+ def self.on_opal_server?
61
+ false
62
+ end
63
+
64
+ def self.on_opal_client?
65
+ false
66
+ end
67
+ else
68
+ def self.on_opal_server?
69
+ `typeof Opal.global.document === 'undefined'`
70
+ end
71
+
72
+ def self.on_opal_client?
73
+ !on_opal_server?
74
+ end
75
+ end
76
+
77
+ def log(*args)
78
+ IsomorphicHelpers.log(*args)
79
+ end
80
+
81
+ def on_opal_server?
82
+ self.class.on_opal_server?
83
+ end
84
+
85
+ def on_opal_client?
86
+ self.class.on_opal_client?
87
+ end
88
+
89
+ def self.prerender_footers(controller = nil)
90
+ footer = Context.prerender_footer_blocks.collect { |block| block.call controller }.join("\n")
91
+ if RUBY_ENGINE != 'opal'
92
+ footer = (footer + @context.send_to_opal(:prerender_footers).to_s) if @context
93
+ footer = footer.html_safe
94
+ end
95
+ footer
96
+ end
97
+
98
+ class Context
99
+ attr_reader :controller
100
+ attr_reader :unique_id
101
+
102
+ def self.define_isomorphic_method(method_name, &block)
103
+ @@ctx_methods ||= {}
104
+ @@ctx_methods[method_name] = block
105
+ end
106
+
107
+ def self.before_first_mount_blocks
108
+ @before_first_mount_blocks ||= []
109
+ end
110
+
111
+ def self.prerender_footer_blocks
112
+ @prerender_footer_blocks ||= []
113
+ end
114
+
115
+ def initialize(unique_id, ctx = nil, controller = nil, cname = nil)
116
+ @unique_id = unique_id
117
+ @cname = cname
118
+ if RUBY_ENGINE != 'opal'
119
+ @controller = controller
120
+ @ctx = ctx
121
+ if defined? @@ctx_methods
122
+ @@ctx_methods.each do |method_name, block|
123
+ @ctx.attach("ServerSideIsomorphicMethod.#{method_name}", proc{|args| block.call(args.to_json)})
124
+ end
125
+ end
126
+ end
127
+ Hyperloop::Application::Boot.run(context: self)
128
+ self.class.before_first_mount_blocks.each { |block| block.call(self) }
129
+ end
130
+
131
+ def load_opal_context
132
+ send_to_opal(:load_context, @unique_id, @cname)
133
+ end
134
+
135
+ def eval(js)
136
+ @ctx.eval(js) if @ctx
137
+ end
138
+
139
+ def send_to_opal(method_name, *args)
140
+ return unless @ctx
141
+ args = [1] if args.length == 0
142
+ ::ReactiveRuby::ComponentLoader.new(@ctx).load!
143
+ method_args = args.collect do |arg|
144
+ quarg = "#{arg}".tr('"', "'")
145
+ "\"#{quarg}\""
146
+ end.join(', ')
147
+ @ctx.eval("Opal.React.$const_get('IsomorphicHelpers').$#{method_name}(#{method_args})")
148
+ end
149
+
150
+ def self.register_before_first_mount_block(&block)
151
+ before_first_mount_blocks << block
152
+ end
153
+
154
+ def self.register_prerender_footer_block(&block)
155
+ prerender_footer_blocks << block
156
+ end
157
+ end
158
+
159
+ class IsomorphicProcCall
160
+
161
+ attr_reader :context
162
+
163
+ def result
164
+ @result.first if @result
165
+ end
166
+
167
+ def initialize(name, block, context, *args)
168
+ @name = name
169
+ @context = context
170
+ block.call(self, *args)
171
+ @result ||= send_to_server(*args)
172
+ end
173
+
174
+ def when_on_client(&block)
175
+ @result = [block.call] if IsomorphicHelpers.on_opal_client?
176
+ end
177
+
178
+ def send_to_server(*args)
179
+ if IsomorphicHelpers.on_opal_server?
180
+ method_string = "ServerSideIsomorphicMethod." + @name + "(" + args.to_json + ")"
181
+ @result = [JSON.parse(`eval(method_string)`)]
182
+ end
183
+ end
184
+
185
+ def when_on_server(&block)
186
+ @result = [block.call.to_json] unless IsomorphicHelpers.on_opal_client? || IsomorphicHelpers.on_opal_server?
187
+ end
188
+ end
189
+
190
+ module ClassMethods
191
+ def on_opal_server?
192
+ IsomorphicHelpers.on_opal_server?
193
+ end
194
+
195
+ def on_opal_client?
196
+ IsomorphicHelpers.on_opal_client?
197
+ end
198
+
199
+ def log(*args)
200
+ IsomorphicHelpers.log(*args)
201
+ end
202
+
203
+ def controller
204
+ IsomorphicHelpers.context.controller
205
+ end
206
+
207
+ def before_first_mount(&block)
208
+ React::IsomorphicHelpers::Context.register_before_first_mount_block(&block)
209
+ end
210
+
211
+ def prerender_footer(&block)
212
+ React::IsomorphicHelpers::Context.register_prerender_footer_block(&block)
213
+ end
214
+
215
+ if RUBY_ENGINE != 'opal'
216
+ def isomorphic_method(name, &block)
217
+ React::IsomorphicHelpers::Context.send(:define_isomorphic_method, name) do |args_as_json|
218
+ React::IsomorphicHelpers::IsomorphicProcCall.new(name, block, self, *JSON.parse(args_as_json)).result
219
+ end
220
+ end
221
+ else
222
+ require 'json'
223
+
224
+ def isomorphic_method(name, &block)
225
+ self.class.send(:define_method, name) do | *args |
226
+ React::IsomorphicHelpers::IsomorphicProcCall.new(name, block, self, *args).result
227
+ end
228
+ end
229
+ end
230
+
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,8 @@
1
+ require 'action_view'
2
+ require 'react-rails'
3
+ require 'reactive-ruby/server_rendering/hyper_asset_container'
4
+ require 'reactive-ruby/server_rendering/contextual_renderer'
5
+ require 'reactive-ruby/rails/component_mount'
6
+ require 'reactive-ruby/rails/railtie'
7
+ require 'reactive-ruby/rails/controller_helper'
8
+ require 'reactive-ruby/component_loader'
@@ -0,0 +1,48 @@
1
+ module ReactiveRuby
2
+ module Rails
3
+ class ComponentMount < React::Rails::ComponentMount
4
+ attr_accessor :controller
5
+
6
+ def setup(controller)
7
+ self.controller = controller
8
+ end
9
+
10
+ def react_component(name, props = {}, options = {}, &block)
11
+ if options[:prerender] || [:on, 'on', true].include?(Hyperloop.prerendering)
12
+ options = context_initializer_options(options, name)
13
+ end
14
+ props = serialized_props(props, name, controller)
15
+ result = super(top_level_name, props, options, &block).gsub("\n","")
16
+ result = result.gsub(/(<script.*<\/script>)<\/div>$/,'</div>\1').html_safe
17
+ result + footers
18
+ end
19
+
20
+ private
21
+
22
+ def context_initializer_options(options, name)
23
+ options[:prerender] = {options[:prerender] => true} unless options[:prerender].is_a? Hash
24
+ existing_context_initializer = options[:prerender][:context_initializer]
25
+
26
+ options[:prerender][:context_initializer] = lambda do |ctx|
27
+ React::IsomorphicHelpers.load_context(ctx, controller, name)
28
+ existing_context_initializer.call(ctx) if existing_context_initializer
29
+ end
30
+
31
+ options
32
+ end
33
+
34
+ def serialized_props(props, name, controller)
35
+ { render_params: props, component_name: name,
36
+ controller: controller.class.name.gsub(/Controller$/,"") }.react_serializer
37
+ end
38
+
39
+ def top_level_name
40
+ 'React.TopLevelRailsComponent'
41
+ end
42
+
43
+ def footers
44
+ React::IsomorphicHelpers.prerender_footers(controller)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,14 @@
1
+ require 'action_controller'
2
+
3
+ module ActionController
4
+ # adds render_component helper to ActionControllers
5
+ class Base
6
+ def render_component(*args)
7
+ @component_name = (args[0].is_a? Hash) || args.empty? ? params[:action].camelize : args.shift
8
+ @render_params = args.shift || {}
9
+ options = args[0] || {}
10
+ render inline: '<%= react_component @component_name, @render_params %>',
11
+ layout: options.key?(:layout) ? options[:layout].to_s : :default
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,20 @@
1
+ module ReactiveRuby
2
+ module Rails
3
+ class Railtie < ::Rails::Railtie
4
+ config.before_configuration do |app|
5
+ app.config.assets.enabled = true
6
+ app.config.assets.paths << ::Rails.root.join('app', 'views').to_s
7
+ app.config.react.server_renderer = ReactiveRuby::ServerRendering::ContextualRenderer
8
+ app.config.react.view_helper_implementation = ReactiveRuby::Rails::ComponentMount
9
+ ReactiveRuby::ServerRendering::ContextualRenderer.asset_container_class = ReactiveRuby::ServerRendering::HyperAssetContainer
10
+ end
11
+ config.after_initialize do
12
+ class ::HyperloopController < ::ApplicationController
13
+ def action_missing(name)
14
+ render_component
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ [FalseClass, Float, Integer, NilClass, String, Symbol, Time, TrueClass].each do |klass|
2
+ klass.send(:define_method, :react_serializer) do
3
+ as_json
4
+ end
5
+ end
6
+
7
+ if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.4.0')
8
+ [Bignum, Fixnum].each do |klass|
9
+ klass.send(:define_method, :react_serializer) do
10
+ as_json
11
+ end
12
+ end
13
+ end
14
+
15
+ BigDecimal.send(:define_method, :react_serializer) { as_json } rescue nil
16
+
17
+ Array.send(:define_method, :react_serializer) do
18
+ self.collect { |e| e.react_serializer }.as_json
19
+ end
20
+
21
+ Hash.send(:define_method, :react_serializer) do
22
+ Hash[*self.collect { |key, value| [key, value.react_serializer] }.flatten(1)].as_json
23
+ end
@@ -0,0 +1,46 @@
1
+ module ReactiveRuby
2
+ module ServerRendering
3
+ def self.context_instance_name
4
+ '@context'
5
+ end
6
+
7
+ def self.context_instance_for(context)
8
+ context.instance_variable_get(context_instance_name)
9
+ end
10
+
11
+ class ContextualRenderer < React::ServerRendering::BundleRenderer
12
+ def initialize(options = {})
13
+ super(options)
14
+ ComponentLoader.new(v8_context).load
15
+ end
16
+
17
+ def before_render(*args)
18
+ # the base class clears the log history... we don't want that as it is taken
19
+ # care of in IsomorphicHelpers.load_context
20
+ end
21
+
22
+ def render(component_name, props, prerender_options)
23
+ if prerender_options.is_a?(Hash)
24
+ if !v8_runtime? && prerender_options[:context_initializer]
25
+ raise React::ServerRendering::PrerenderError.new(component_name, props, "you must use 'mini_racer' with the prerender[:context] option") unless v8_runtime?
26
+ else
27
+ prerender_options[:context_initializer].call v8_context
28
+ prerender_options = prerender_options[:static] ? :static : true
29
+ end
30
+ end
31
+
32
+ super(component_name, props, prerender_options)
33
+ end
34
+
35
+ private
36
+
37
+ def v8_runtime?
38
+ ExecJS.runtime.name == 'mini_racer (V8)'
39
+ end
40
+
41
+ def v8_context
42
+ @v8_context ||= ReactiveRuby::ServerRendering.context_instance_for(@context)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,46 @@
1
+ require 'react/server_rendering/environment_container'
2
+ require 'react/server_rendering/manifest_container'
3
+ require 'react/server_rendering/webpacker_manifest_container'
4
+
5
+ module ReactiveRuby
6
+ module ServerRendering
7
+ class HyperTestAssetContainer
8
+ def find_asset(logical_path)
9
+ ::Rails.cache.read(logical_path)
10
+ end
11
+ end
12
+
13
+ class HyperAssetContainer
14
+ def initialize
15
+ @ass_containers = []
16
+ if assets_precompiled?
17
+ @ass_containers << React::ServerRendering::ManifestContainer.new if React::ServerRendering::ManifestContainer.compatible?
18
+ else
19
+ @ass_containers << React::ServerRendering::EnvironmentContainer.new if ::Rails.application.assets
20
+ end
21
+ if React::ServerRendering::WebpackerManifestContainer.compatible?
22
+ @ass_containers << React::ServerRendering::WebpackerManifestContainer.new
23
+ end
24
+ @ass_containers << HyperTestAssetContainer.new if ::Rails.env.test?
25
+ end
26
+
27
+ def find_asset(logical_path)
28
+ @ass_containers.each do |ass|
29
+ begin
30
+ asset = ass.find_asset(logical_path)
31
+ return asset if asset && asset != ''
32
+ rescue
33
+ next # no asset found, try the next container
34
+ end
35
+ end
36
+ raise "No asset found for #{logical_path}, tried: #{@ass_containers.map { |c| c.class.name }.join(', ')}"
37
+ end
38
+
39
+ private
40
+
41
+ def assets_precompiled?
42
+ !::Rails.application.config.assets.compile
43
+ end
44
+ end
45
+ end
46
+ end