Skip to content
This repository was archived by the owner on Oct 19, 2018. It is now read-only.

Commit 4727fbc

Browse files
catmandozetachang
authored andcommitted
WIP
# Conflicts: # .rubocop.yml -> removed # config.ru -> removed # lib/react/component.rb # lib/react/native_library.rb # lib/reactive-ruby/rails/controller_helper.rb -> removed # spec/react/dsl_spec.rb # spec/spec_helper.rb -> removed
1 parent f747191 commit 4727fbc

File tree

9 files changed

+544
-57
lines changed

9 files changed

+544
-57
lines changed

component-name-lookup.md

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
#### Notes on how component names are looked up
2+
3+
Given:
4+
5+
```ruby
6+
7+
class Blat < React::Component::Base
8+
9+
render do
10+
Bar()
11+
Foo::Bar()
12+
end
13+
14+
end
15+
16+
class Bar < React::Component::Base
17+
end
18+
19+
module Foo
20+
21+
class Bar < React::Component::Base
22+
23+
render do
24+
Blat()
25+
Baz()
26+
end
27+
end
28+
29+
class Baz < React::Component::Base
30+
end
31+
32+
end
33+
```
34+
35+
The problem is that method lookup is different than constant lookup. We can prove it by running this code:
36+
37+
```ruby
38+
def try_it(test, &block)
39+
puts "trying #{test}"
40+
result = yield
41+
puts "success#{': '+result.to_s if result}"
42+
rescue Exception => e
43+
puts "failed: #{e}"
44+
ensure
45+
puts "---------------------------------"
46+
end
47+
48+
module Boom
49+
50+
Bar = 12
51+
52+
def self.Bar
53+
puts " Boom::Bar says hi"
54+
end
55+
56+
class Baz
57+
def doit
58+
try_it("Bar()") { Bar() }
59+
try_it("Boom::Bar()") {Boom::Bar()}
60+
try_it("Bar") { Bar }
61+
try_it("Boom::Bar") { Boom::Bar }
62+
end
63+
end
64+
end
65+
66+
67+
68+
Boom::Baz.new.doit
69+
```
70+
71+
which prints:
72+
73+
```text
74+
trying Bar()
75+
failed: Bar: undefined method `Bar' for #<Boom::Baz:0x774>
76+
---------------------------------
77+
trying Boom::Bar()
78+
Boom::Bar says hi
79+
success
80+
---------------------------------
81+
trying Bar
82+
success: 12
83+
---------------------------------
84+
trying Boom::Bar
85+
success: 12
86+
---------------------------------
87+
```
88+
89+
[try-it](http://opalrb.org/try/?code:def%20try_it(test%2C%20%26block)%0A%20%20puts%20%22trying%20%23%7Btest%7D%22%0A%20%20result%20%3D%20yield%0A%20%20puts%20%22success%23%7B%27%3A%20%27%2Bresult.to_s%20if%20result%7D%22%0Arescue%20Exception%20%3D%3E%20e%0A%20%20puts%20%22failed%3A%20%23%7Be%7D%22%0Aensure%0A%20%20puts%20%22---------------------------------%22%0Aend%0A%0Amodule%20Boom%0A%20%20%0A%20%20Bar%20%3D%2012%0A%20%20%0A%20%20def%20self.Bar%0A%20%20%20%20puts%20%22%20%20%20Boom%3A%3ABar%20says%20hi%22%0A%20%20end%0A%0A%20%20class%20Baz%0A%20%20%20%20def%20doit%0A%20%20%20%20%20%20try_it(%22Bar()%22)%20%7B%20Bar()%20%7D%0A%20%20%20%20%20%20try_it(%22Boom%3A%3ABar()%22)%20%7BBoom%3A%3ABar()%7D%0A%20%20%20%20%20%20try_it(%22Bar%22)%20%7B%20Bar%20%7D%0A%20%20%20%20%20%20try_it(%22Boom%3A%3ABar%22)%20%7B%20Boom%3A%3ABar%20%7D%0A%20%20%20%20end%0A%20%20end%0Aend%0A%20%20%0A%0A%0ABoom%3A%3ABaz.new.doit)
90+
91+
92+
What we need to do is:
93+
94+
1. when defining a component class `Foo`, also define in the same scope that Foo is being defined a method `self.Foo` that will accept Foo's params and child block, and render it.
95+
96+
2. As long as a name is qualified with at least one scope (i.e. `ModName::Foo()`) everything will work out, but if we say just `Foo()` then the only way I believe out of this is to handle it via method_missing, and let method_missing do a const_get on the method_name (which will return the class) and then render that component.
97+
98+
#### details
99+
100+
To define `self.Foo` in the same scope level as the class `Foo`, we need code like this:
101+
102+
```ruby
103+
def register_component_dsl_method(component)
104+
split_name = component.name && component.name.split('::')
105+
return unless split_name && split_name.length > 2
106+
component_name = split_name.last
107+
parent = split_name.inject([Module]) { |nesting, next_const| nesting + [nesting.last.const_get(next_const)] }[-2]
108+
class << parent
109+
define_method component_name do |*args, &block|
110+
React::RenderingContext.render(name, *args, &block)
111+
end
112+
define_method "#{component_name}_as_node" do |*args, &block|
113+
React::Component.deprecation_warning("..._as_node is deprecated. Render component and then use the .node method instead")
114+
send(component_name, *args, &block).node
115+
end
116+
end
117+
end
118+
119+
module React
120+
module Component
121+
def self.included(base)
122+
...
123+
register_component_dsl_method(base.name)
124+
end
125+
end
126+
end
127+
```
128+
129+
The component's method_missing function will look like this:
130+
131+
```ruby
132+
def method_missing(name, *args, &block)
133+
if name =~ /_as_node$/
134+
React::Component.deprecation_warning("..._as_node is deprecated. Render component and then use the .node method instead")
135+
method_missing(name.gsub(/_as_node$/,""), *args, &block).node
136+
else
137+
component = const_get name if defined? name
138+
React::RenderingContext.render(nil, component, *args, &block)
139+
end
140+
end
141+
```
142+
143+
### other related issues
144+
145+
The Kernel#p method conflicts with the <p> tag. However the p method can be invoked on any object so we are going to go ahead and use it, and deprecate the para method.

lib/react/api.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ class API
55
@@component_classes = {}
66

77
def self.import_native_component(opal_class, native_class)
8-
@@component_classes[opal_class.to_s] = native_class
8+
@@component_classes[opal_class] = native_class
99
end
1010

1111
def self.create_native_react_class(type)

lib/react/component.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ def self.included(base)
2929
base.extend(ClassMethods)
3030

3131
if base.name
32-
class << base.parent
32+
parent = base.name.split("::").inject([Module]) { |nesting, next_const| nesting + [nesting.last.const_get(next_const)] }[-2]
33+
class << parent
3334
def method_missing(n, *args, &block)
3435
name = n
3536
if name =~ /_as_node$/

lib/react/component/class_methods.rb

+17-5
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,20 @@ def deprecation_warning(message)
2727
end
2828
end
2929

30-
def render(*args, &block)
31-
if args[0].is_a? Hash
32-
define_method :render do
33-
yield
30+
def render(container=nil, params={}, &block)
31+
if container
32+
if block
33+
define_method :render do
34+
send(container, params) { instance_eval &block }
35+
end
36+
else
37+
define_method :render do
38+
send(container, params)
39+
end
3440
end
3541
else
3642
define_method :render do
37-
send *args, &block
43+
instance_eval &block
3844
end
3945
end
4046
end
@@ -174,6 +180,12 @@ def export_component(opts = {})
174180
Native(`window`)[first_name] = add_item_to_tree(Native(`window`)[first_name], [React::API.create_native_react_class(self)] + export_name[1..-1].reverse).to_n
175181
end
176182

183+
def imports(native_component_name)
184+
React::API.import_native_component(native_component_name, self)
185+
render {} # define a dummy render method - will never be called...
186+
self
187+
end
188+
177189
def add_item_to_tree(current_tree, new_item)
178190
if Native(current_tree).class != Native::Object || new_item.length == 1
179191
new_item.inject { |memo, sub_name| { sub_name => memo } }

lib/react/native_library.rb

+71-36
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,88 @@
11
module React
2+
# NativeLibrary handles importing JS libraries
3+
# Importing native components is handled by the
4+
# React::Base.
25
class NativeLibrary
3-
def self.renames_and_exclusions
4-
@renames_and_exclusions ||= {}
5-
end
6+
class << self
7+
def imports(native_name)
8+
@native_prefix = "#{native_name}."
9+
self
10+
end
611

7-
def self.libraries
8-
@libraries ||= []
9-
end
12+
def rename(rename_list)
13+
# rename_list is a hash in the form: native_name => ruby_name, native_name => ruby_name
14+
rename_list.each do |js_name, ruby_name|
15+
native_name = lookup_native_name(js_name)
16+
if lookup_native_name(js_name)
17+
create_wrapper(native_name, ruby_name)
18+
else
19+
raise "class #{name} < React::NativeLibrary could not import #{js_name}. "\
20+
"Native value #{scope_native_name(js_name)} is undefined."
21+
end
22+
end
23+
end
1024

11-
def self.const_missing(name)
12-
if renames_and_exclusions.has_key? name
13-
if native_name = renames_and_exclusions[name]
14-
native_name
25+
def import_const_from_native(klass, const_name)
26+
puts "#{klass} is importing_const_from_native: #{const_name}"
27+
if klass.defined? const_name
28+
klass.const_get const_name
1529
else
16-
super
30+
native_name = lookup_native_name(const_name) ||
31+
lookup_native_name(const_name[0].downcase + const_name[1..-1])
32+
wrapper = create_wrapper(klass, native_name, const_name) if native_name
33+
puts "created #{wrapper} for #{const_name}"
34+
wrapper
1735
end
18-
else
19-
libraries.each do |library|
20-
native_name = "#{library}.#{name}"
21-
native_component = `eval(#{native_name})` rescue nil
22-
React::API.import_native_component(name, native_component) and return name if native_component && `native_component != undefined`
36+
end
37+
38+
def find_and_render_component(container_class, component_name, args, block, &_failure)
39+
puts "#{self} is registering #{method_name}"
40+
component_class = import_const_from_native(container_class, method_name)
41+
if component_class < React::Component::Base
42+
React::RenderingContext.build_or_render(nil, component_class, *args, &block)
43+
else
44+
yield method_name
2345
end
24-
name
2546
end
26-
end
2747

28-
def self.method_missing(n, *args, &block)
29-
name = n
30-
if name =~ /_as_node$/
31-
node_only = true
32-
name = name.gsub(/_as_node$/, "")
48+
def const_missing(const_name)
49+
import_const_from_native(self, const_name) || super
3350
end
34-
unless name = const_get(name)
35-
return super
51+
52+
def method_missing(method_name, *args, &block)
53+
find_and_render_component(self, method_name, args, block) do
54+
raise "could not import a react component named: #{scope_native_name method_name}"
55+
end
3656
end
37-
React::RenderingContext.build_or_render(node_only, name, *args, &block)
38-
rescue
39-
end
4057

41-
def self.imports(library)
42-
libraries << library
43-
end
58+
private
4459

45-
def self.rename(rename_list={})
46-
renames_and_exclusions.merge!(rename_list.invert)
47-
end
60+
def lookup_native_name(js_name)
61+
native_name = scope_native_name(js_name)
62+
`eval(#{native_name}) !== undefined && native_name`
63+
rescue
64+
nil
65+
end
4866

49-
def self.exclude(*exclude_list)
50-
renames_and_exclusions.merge(Hash[exclude_list.map {|k| [k, nil]}])
67+
def scope_native_name(js_name)
68+
"#{@native_prefix}#{js_name}"
69+
end
70+
71+
def create_wrapper(klass, native_name, ruby_name)
72+
if React::API.import_native_component native_name
73+
puts "create wrapper(#{klass.inspect}, #{native_name}, #{ruby_name})"
74+
new_klass = Class.new
75+
klass.const_set ruby_name, new_klass
76+
new_class.class_eval do
77+
include React::Component::Base
78+
imports native_name
79+
end
80+
puts "successfully created #{klass.inspect}.#{ruby_name} wrapper class for #{native_name}"
81+
else
82+
puts "creating wrapper class #{klass}::#{ruby_name} for #{native_name}"
83+
klass.const_set ruby_name, Class.new(React::NativeLibrary).imports(native_name)
84+
end
85+
end
5186
end
5287
end
5388
end

lib/reactive-ruby/rails/component_mount.rb

+3-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ def setup(controller)
1010
def react_component(name, props = {}, options = {}, &block)
1111
options = context_initializer_options(options, name) if options[:prerender]
1212
props = serialized_props(props, name, controller)
13-
super(top_level_name, props, options, &block) + footers
13+
super(top_level_name, props, options, &block).gsub("\n","")
14+
.gsub(/(<script>.*<\/script>)<\/div>$/,'</div>\1').html_safe +
15+
footers
1416
end
1517

1618
private

lib/reactrb/auto-import.rb

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# rubocop:disable Style/FileName
2+
# require 'reactrb/auto-import' to automatically
3+
# import JS libraries and components when they are detected
4+
class Object
5+
class << self
6+
alias _reactrb_original_const_missing const_missing
7+
8+
def const_missing(const_name)
9+
# Opal uses const_missing to initially define things,
10+
# so we always call the original, and respond to the exception
11+
_reactrb_original_const_missing(const_name)
12+
rescue StandardError => e
13+
puts "Object const_missing: #{const_name}"
14+
React::NativeLibrary.import_const_from_native(Object, const_name) || raise(e)
15+
end
16+
17+
def xmethod_missing(method_name, *args, &block)
18+
puts "Object method_missing: #{method_name}"
19+
React::NativeLibrary.register_method(Object, method_name, args, block) do
20+
_reactrb_original_const_missing(method_name, *args, &block)
21+
end
22+
end
23+
end
24+
end

0 commit comments

Comments
 (0)