reactive-ruby 0.7.25 → 0.7.26
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.
- checksums.yaml +4 -4
- data/.travis.yml +3 -0
- data/Gemfile +3 -1
- data/Gemfile.lock +22 -17
- data/README.md +1 -1
- data/Rakefile +12 -5
- data/example/examples/Gemfile.lock +1 -1
- data/example/rails-tutorial/Gemfile +2 -3
- data/example/rails-tutorial/Gemfile.lock +22 -23
- data/example/sinatra-tutorial/Gemfile.lock +4 -4
- data/lib/generators/reactive_ruby/test_app/templates/views/components/hello_world.rb +11 -0
- data/lib/generators/reactive_ruby/test_app/templates/views/components/todo.rb +14 -0
- data/lib/generators/reactive_ruby/test_app/test_app_generator.rb +6 -2
- data/lib/reactive-ruby.rb +3 -5
- data/lib/reactive-ruby/component_loader.rb +42 -0
- data/lib/reactive-ruby/isomorphic_helpers.rb +44 -71
- data/lib/reactive-ruby/rails.rb +7 -0
- data/lib/reactive-ruby/rails/component_mount.rb +44 -0
- data/lib/reactive-ruby/rails/controller_helper.rb +13 -0
- data/lib/reactive-ruby/rails/railtie.rb +14 -0
- data/lib/reactive-ruby/server_rendering/contextual_renderer.rb +33 -0
- data/lib/reactive-ruby/version.rb +1 -1
- data/reactive-ruby.gemspec +1 -0
- data/spec/reactive-ruby/component_loader_spec.rb +58 -0
- data/spec/reactive-ruby/isomorphic_helpers_spec.rb +137 -0
- data/spec/{react/rails/view_helper_spec.rb → reactive-ruby/rails/component_mount_spec.rb} +15 -1
- data/spec/reactive-ruby/server_rendering/contextual_renderer_spec.rb +33 -0
- data/spec/spec_helper.rb +42 -19
- metadata +32 -5
- data/lib/rails-helpers/react_component.rb +0 -52
@@ -0,0 +1,7 @@
|
|
1
|
+
require 'action_view'
|
2
|
+
require 'react-rails'
|
3
|
+
require 'reactive-ruby/server_rendering/contextual_renderer'
|
4
|
+
require 'reactive-ruby/rails/component_mount'
|
5
|
+
require 'reactive-ruby/rails/railtie'
|
6
|
+
require 'reactive-ruby/rails/controller_helper'
|
7
|
+
require 'reactive-ruby/component_loader'
|
@@ -0,0 +1,44 @@
|
|
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
|
+
options = context_initializer_options(options, name) if options[:prerender]
|
12
|
+
props = serialized_props(props, name, controller)
|
13
|
+
super(top_level_name, props, options, &block) + footers
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def context_initializer_options(options, name)
|
19
|
+
options[:prerender] = {options[:prerender] => true} unless options[:prerender].is_a? Hash
|
20
|
+
existing_context_initializer = options[:prerender][:context_initializer]
|
21
|
+
|
22
|
+
options[:prerender][:context_initializer] = lambda do |ctx|
|
23
|
+
React::IsomorphicHelpers.load_context(ctx, controller, name)
|
24
|
+
existing_context_initializer.call ctx if existing_context_initializer
|
25
|
+
end
|
26
|
+
|
27
|
+
options
|
28
|
+
end
|
29
|
+
|
30
|
+
def serialized_props(props, name, controller)
|
31
|
+
{ render_params: props, component_name: name,
|
32
|
+
controller: controller.class.name.gsub(/Controller$/,"") }.react_serializer
|
33
|
+
end
|
34
|
+
|
35
|
+
def top_level_name
|
36
|
+
'React.TopLevelRailsComponent'
|
37
|
+
end
|
38
|
+
|
39
|
+
def footers
|
40
|
+
React::IsomorphicHelpers.prerender_footers #if options[:prerender]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'action_controller'
|
2
|
+
|
3
|
+
module ReactiveRuby
|
4
|
+
module Rails
|
5
|
+
class ActionController::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[0].is_a? Hash) ? args[0] : {}
|
9
|
+
render inline: "<%= react_component @component_name, @render_params, { prerender: !params[:no_prerender] } %>", layout: 'application'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,14 @@
|
|
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 =
|
8
|
+
ReactiveRuby::ServerRendering::ContextualRenderer
|
9
|
+
app.config.react.view_helper_implementation =
|
10
|
+
ReactiveRuby::Rails::ComponentMount
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module ReactiveRuby
|
2
|
+
module ServerRendering
|
3
|
+
class ContextualRenderer < React::ServerRendering::SprocketsRenderer
|
4
|
+
def initialize(options = {})
|
5
|
+
super(options)
|
6
|
+
ComponentLoader.new(v8_context).load
|
7
|
+
end
|
8
|
+
|
9
|
+
def render(component_name, props, prerender_options)
|
10
|
+
if prerender_options.is_a? Hash
|
11
|
+
if v8_runtime? and prerender_options[:context_initializer]
|
12
|
+
raise React::ServerRendering::PrerenderError.new(component_name, props, "you must use 'therubyracer' with the prerender[:context] option") unless v8_runtime?
|
13
|
+
else
|
14
|
+
prerender_options[:context_initializer].call v8_context
|
15
|
+
prerender_options = prerender_options[:static] ? :static : true
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
super(component_name, props, prerender_options)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def v8_runtime?
|
25
|
+
ExecJS.runtime.name == "(V8)"
|
26
|
+
end
|
27
|
+
|
28
|
+
def v8_context
|
29
|
+
@v8_context ||= @context.instance_variable_get("@v8_context")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/reactive-ruby.gemspec
CHANGED
@@ -25,6 +25,7 @@ Gem::Specification.new do |s|
|
|
25
25
|
s.add_development_dependency 'opal-rails'
|
26
26
|
s.add_development_dependency 'rake'
|
27
27
|
s.add_development_dependency 'rspec-rails'
|
28
|
+
s.add_development_dependency 'timecop'
|
28
29
|
s.add_development_dependency 'opal-rspec'
|
29
30
|
s.add_development_dependency 'sinatra'
|
30
31
|
s.add_development_dependency 'sqlite3' # For Test Rails App
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ReactiveRuby::ComponentLoader do
|
4
|
+
GLOBAL_WRAPPER = <<-JS
|
5
|
+
var global = global || this;
|
6
|
+
var self = self || this;
|
7
|
+
var window = window || this;
|
8
|
+
JS
|
9
|
+
|
10
|
+
let(:js) { ::Rails.application.assets['components'].to_s }
|
11
|
+
let(:context) { ExecJS.compile(GLOBAL_WRAPPER + js) }
|
12
|
+
let(:v8_context) { context.instance_variable_get(:@v8_context) }
|
13
|
+
|
14
|
+
describe '#load' do
|
15
|
+
it 'loads given asset file into context' do
|
16
|
+
loader = described_class.new(v8_context)
|
17
|
+
|
18
|
+
expect {
|
19
|
+
loader.load
|
20
|
+
}.to change { !!v8_context.eval('Opal.React') }.from(false).to(true)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'is truthy upon successful load' do
|
24
|
+
loader = described_class.new(v8_context)
|
25
|
+
expect(loader.load).to be_truthy
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'fails silently returning false' do
|
29
|
+
loader = described_class.new(v8_context)
|
30
|
+
expect(loader.load('foo')).to be_falsey
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe '#load!' do
|
35
|
+
it 'is truthy upon successful load' do
|
36
|
+
loader = described_class.new(v8_context)
|
37
|
+
expect(loader.load!).to be_truthy
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'raises an expection if loading fails' do
|
41
|
+
loader = described_class.new(v8_context)
|
42
|
+
expect { loader.load!('foo') }.to raise_error(/No react\.rb components/)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe '#loaded?' do
|
47
|
+
it 'is truthy if components file is already loaded' do
|
48
|
+
loader = described_class.new(v8_context)
|
49
|
+
loader.load
|
50
|
+
expect(loader).to be_loaded
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'is false if components file is not loaded' do
|
54
|
+
loader = described_class.new(v8_context)
|
55
|
+
expect(loader).to_not be_loaded
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe React::IsomorphicHelpers do
|
4
|
+
describe 'code execution context', :ruby do
|
5
|
+
let(:klass) { Class.send(:include, described_class) }
|
6
|
+
describe 'module class methods' do
|
7
|
+
it { is_expected.to_not be_on_opal_server }
|
8
|
+
it { is_expected.to_not be_on_opal_client }
|
9
|
+
end
|
10
|
+
|
11
|
+
describe 'included class methods' do
|
12
|
+
subject { klass }
|
13
|
+
it { is_expected.to_not be_on_opal_server }
|
14
|
+
it { is_expected.to_not be_on_opal_client }
|
15
|
+
end
|
16
|
+
|
17
|
+
describe 'included instance methods' do
|
18
|
+
subject { klass.new }
|
19
|
+
it { is_expected.to_not be_on_opal_server }
|
20
|
+
it { is_expected.to_not be_on_opal_client }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe 'load_context', :ruby do
|
25
|
+
let(:v8_context) { TestV8Context.new }
|
26
|
+
let(:controller) { double('controller') }
|
27
|
+
let(:name) { double('name') }
|
28
|
+
|
29
|
+
it 'creates a context and sets a controller' do
|
30
|
+
context = described_class.load_context(v8_context, controller, name)
|
31
|
+
expect(context.controller).to eq(controller)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'creates a context and sets a unique_id' do
|
35
|
+
Timecop.freeze do
|
36
|
+
stamp = Time.now.to_i
|
37
|
+
context = described_class.load_context(v8_context, controller, name)
|
38
|
+
expect(context.unique_id).to eq("#{ controller.object_id }-#{ stamp }")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe React::IsomorphicHelpers::Context do
|
44
|
+
class TestV8Context < Hash
|
45
|
+
def eval(args)
|
46
|
+
true
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Need to decouple/dry up this...
|
51
|
+
def test_context(files = nil)
|
52
|
+
js = ReactiveRuby::ServerRendering::ContextualRenderer::CONSOLE_POLYFILL.dup
|
53
|
+
js << Opal::Builder.build('opal').to_s
|
54
|
+
Array(files).each do |filename|
|
55
|
+
js << ::Rails.application.assets[filename].to_s
|
56
|
+
end
|
57
|
+
js = "#{React::ServerRendering::ExecJSRenderer::GLOBAL_WRAPPER}#{js}"
|
58
|
+
ctx = ExecJS.compile(js)
|
59
|
+
ctx = ctx.instance_variable_get("@v8_context")
|
60
|
+
end
|
61
|
+
|
62
|
+
def react_context
|
63
|
+
test_context('components')
|
64
|
+
end
|
65
|
+
|
66
|
+
let(:v8_context) { TestV8Context.new }
|
67
|
+
let(:controller) { double('controller') }
|
68
|
+
let(:name) { double('name') }
|
69
|
+
before do
|
70
|
+
described_class.instance_variable_set :@before_first_mount_blocks, nil
|
71
|
+
end
|
72
|
+
|
73
|
+
describe '#initialize' do
|
74
|
+
it "sets the given V8 context's ServerSideIsomorphicMethods to itself" do
|
75
|
+
context = described_class.new('unique-id', v8_context, controller, name)
|
76
|
+
expect(v8_context['ServerSideIsomorphicMethods']).to eq(context)
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'calls before mount callbacks' do
|
80
|
+
string = instance_double(String)
|
81
|
+
described_class.register_before_first_mount_block do
|
82
|
+
string.inspect
|
83
|
+
end
|
84
|
+
expect(string).to receive(:inspect).once
|
85
|
+
context = described_class.new('unique-id', v8_context, controller, name)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe '#eval' do
|
90
|
+
it 'delegates to given context' do
|
91
|
+
context = described_class.new('unique-id', v8_context, controller, name)
|
92
|
+
js = 'true;'
|
93
|
+
expect(v8_context).to receive(:eval).with(js).once
|
94
|
+
context.eval(js)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe '#send_to_opal' do
|
99
|
+
let(:opal_code) { Opal::Builder.new.build_str(ruby_code, __FILE__) }
|
100
|
+
let(:ruby_code) { %Q[
|
101
|
+
module React::IsomorphicHelpers
|
102
|
+
def self.greet(name)
|
103
|
+
"Hello, #\{name}!"
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.valediction
|
107
|
+
'Goodbye'
|
108
|
+
end
|
109
|
+
end
|
110
|
+
]}
|
111
|
+
|
112
|
+
it 'raises an error when react cannot be loaded' do
|
113
|
+
context = described_class.new('unique-id', v8_context, controller, name)
|
114
|
+
context.instance_variable_set(:@ctx, test_context)
|
115
|
+
expect {
|
116
|
+
context.send_to_opal(:foo)
|
117
|
+
}.to raise_error(/No react.rb components found/)
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'executes method with args inside opal rubyracer context' do
|
121
|
+
ctx = react_context
|
122
|
+
context = described_class.new('unique-id', ctx, controller, name)
|
123
|
+
ctx.eval(opal_code)
|
124
|
+
result = context.send_to_opal(:greet, 'world')
|
125
|
+
expect(result).to eq('Hello, world!')
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'executes the method inside opal rubyracer context' do
|
129
|
+
ctx = react_context
|
130
|
+
context = described_class.new('unique-id', ctx, controller, name)
|
131
|
+
ctx.eval(opal_code)
|
132
|
+
result = context.send_to_opal(:valediction)
|
133
|
+
expect(result).to eq('Goodbye')
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -1,12 +1,26 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
RSpec.describe
|
3
|
+
RSpec.describe ReactiveRuby::Rails::ComponentMount do
|
4
|
+
let(:helper) { described_class.new }
|
5
|
+
|
6
|
+
before do
|
7
|
+
env = double
|
8
|
+
allow(env).to receive(:request).and_return(
|
9
|
+
{ 'controller' => ActionView::TestCase::TestController.new })
|
10
|
+
helper.setup(env)
|
11
|
+
end
|
12
|
+
|
4
13
|
describe '#react_component' do
|
5
14
|
it 'renders a div' do
|
6
15
|
html = helper.react_component('Components::HelloWorld')
|
7
16
|
expect(html).to match(/<div.*><\/div>/)
|
8
17
|
end
|
9
18
|
|
19
|
+
it 'accepts a pre-render option' do
|
20
|
+
html = helper.react_component('Components::HelloWorld', {}, prerender: true)
|
21
|
+
expect(html).to match(/<div.*><span.*>Hello, World!<\/span><\/div>/)
|
22
|
+
end
|
23
|
+
|
10
24
|
it 'sets data-react-class to React.TopLevelRailsComponent' do
|
11
25
|
html = helper.react_component('Components::HelloWorld')
|
12
26
|
top_level_class = 'React.TopLevelRailsComponent'
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ReactiveRuby::ServerRendering::ContextualRenderer do
|
4
|
+
let(:renderer) { described_class.new({}) }
|
5
|
+
let(:init) { Proc.new {} }
|
6
|
+
let(:options) { { context_initializer: init } }
|
7
|
+
|
8
|
+
describe '#render' do
|
9
|
+
it 'pre-renders HTML' do
|
10
|
+
result = renderer.render('Components.Todo',
|
11
|
+
{ todo: 'finish reactive-ruby' },
|
12
|
+
options)
|
13
|
+
expect(result).to match(/<li.*>finish reactive-ruby<\/li>/)
|
14
|
+
expect(result).to match(/data-react-checksum/)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'accepts props as a string' do
|
18
|
+
result = renderer.render('Components.Todo',
|
19
|
+
{ todo: 'finish reactive-ruby' }.to_json,
|
20
|
+
options)
|
21
|
+
expect(result).to match(/<li.*>finish reactive-ruby<\/li>/)
|
22
|
+
expect(result).to match(/data-react-checksum/)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'pre-renders static content' do
|
26
|
+
result = renderer.render('Components.Todo',
|
27
|
+
{ todo: 'finish reactive-ruby' },
|
28
|
+
:static)
|
29
|
+
expect(result).to match(/<li.*>finish reactive-ruby<\/li>/)
|
30
|
+
expect(result).to_not match(/data-react-checksum/)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,29 +1,52 @@
|
|
1
1
|
ENV["RAILS_ENV"] ||= 'test'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
require 'opal'
|
4
|
+
require 'opal-rspec'
|
5
|
+
|
6
|
+
def opal?
|
7
|
+
RUBY_ENGINE == 'opal'
|
8
|
+
end
|
9
|
+
|
10
|
+
def ruby?
|
11
|
+
!opal?
|
12
|
+
end
|
13
|
+
|
14
|
+
if RUBY_ENGINE == 'opal'
|
15
|
+
RSpec.configure do |config|
|
16
|
+
config.filter_run_excluding :ruby
|
17
|
+
end
|
7
18
|
end
|
8
19
|
|
9
|
-
|
20
|
+
if RUBY_ENGINE != 'opal'
|
21
|
+
begin
|
22
|
+
require File.expand_path('../test_app/config/environment', __FILE__)
|
23
|
+
rescue LoadError
|
24
|
+
puts 'Could not load test application. Please ensure you have run `bundle exec rake test_app`'
|
25
|
+
end
|
26
|
+
require 'rspec/rails'
|
27
|
+
require 'timecop'
|
28
|
+
|
29
|
+
Dir["./spec/support/**/*.rb"].sort.each { |f| require f }
|
10
30
|
|
11
|
-
|
31
|
+
RSpec.configure do |config|
|
32
|
+
config.color = true
|
33
|
+
config.fail_fast = ENV['FAIL_FAST'] || false
|
34
|
+
config.fixture_path = File.join(File.expand_path(File.dirname(__FILE__)), "fixtures")
|
35
|
+
config.infer_spec_type_from_file_location!
|
36
|
+
config.mock_with :rspec
|
37
|
+
config.raise_errors_for_deprecations!
|
12
38
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
config.infer_spec_type_from_file_location!
|
18
|
-
config.mock_with :rspec
|
19
|
-
config.raise_errors_for_deprecations!
|
39
|
+
# If you're not using ActiveRecord, or you'd prefer not to run each of your
|
40
|
+
# examples within a transaction, comment the following line or assign false
|
41
|
+
# instead of true.
|
42
|
+
config.use_transactional_fixtures = true
|
20
43
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
config.use_transactional_fixtures = true
|
44
|
+
config.before :each do
|
45
|
+
Rails.cache.clear
|
46
|
+
end
|
25
47
|
|
26
|
-
|
27
|
-
|
48
|
+
config.filter_run_including focus: true
|
49
|
+
config.filter_run_excluding opal: true
|
50
|
+
config.run_all_when_everything_filtered = true
|
28
51
|
end
|
29
52
|
end
|