reactive-ruby 0.7.25 → 0.7.26
Sign up to get free protection for your applications and to get access to all the features.
- 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
|