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.
@@ -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
@@ -1,3 +1,3 @@
1
1
  module React
2
- VERSION = "0.7.25"
2
+ VERSION = "0.7.26"
3
3
  end
@@ -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 React::Rails::ViewHelper, type: :helper do
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
- begin
4
- require File.expand_path('../test_app/config/environment', __FILE__)
5
- rescue LoadError
6
- puts 'Could not load test application. Please ensure you have run `bundle exec rake test_app`'
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
- require 'rspec/rails'
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
- Dir["./spec/support/**/*.rb"].sort.each { |f| require f }
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
- RSpec.configure do |config|
14
- config.color = true
15
- config.fail_fast = ENV['FAIL_FAST'] || false
16
- config.fixture_path = File.join(File.expand_path(File.dirname(__FILE__)), "fixtures")
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
- # If you're not using ActiveRecord, or you'd prefer not to run each of your
22
- # examples within a transaction, comment the following line or assign false
23
- # instead of true.
24
- config.use_transactional_fixtures = true
44
+ config.before :each do
45
+ Rails.cache.clear
46
+ end
25
47
 
26
- config.before :each do
27
- Rails.cache.clear
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