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

Commit 6ae3163

Browse files
committed
closes #153
1 parent 65cc657 commit 6ae3163

File tree

4 files changed

+194
-30
lines changed

4 files changed

+194
-30
lines changed

lib/react/api.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class API
1616
@@component_classes = {}
1717

1818
def self.import_native_component(opal_class, native_class)
19+
opal_class.instance_variable_set("@native_import", true)
1920
@@component_classes[opal_class] = native_class
2021
end
2122

lib/react/element.rb

Lines changed: 107 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
require 'react/ext/string'
22

33
module React
4+
#
5+
# Wraps the React Native element class
6+
#
7+
# adds the #on method to add event handlers to the element
8+
#
9+
# adds the #render method to place elements in the DOM and
10+
# #delete (alias/deprecated #as_node) method to remove elements from the DOM
11+
#
12+
# handles the haml style class notation so that
13+
# div.bar.blat becomes div(class: "bar blat")
14+
# by using method missing
15+
#
416
class Element
517
include Native
618

@@ -20,58 +32,124 @@ def initialize(native_element, type, properties, block)
2032
@native = native_element
2133
end
2234

23-
def on(event_name)
24-
name = event_name.to_s.event_camelize
25-
props = if React::Event::BUILT_IN_EVENTS.include?("on#{name}")
26-
{"on#{name}" => %x{
27-
function(event){
28-
#{yield React::Event.new(`event`)}
29-
}
30-
}}
31-
else
32-
{"_on#{name}" => %x{
33-
function(){
34-
#{yield *Array(`arguments`)}
35-
}
36-
}}
37-
end
38-
@native = `React.cloneElement(#{self.to_n}, #{props.to_n})`
39-
@properties.merge! props
35+
# Attach event handlers.
36+
37+
def on(*event_names, &block)
38+
event_names.each { |event_name| merge_event_prop!(event_name, &block) }
39+
@native = `React.cloneElement(#{to_n}, #{properties.to_n})`
4040
self
4141
end
4242

43-
def render(props = {}) # for rendering children
43+
# Render element into DOM in the current rendering context.
44+
# Used for elements that are not yet in DOM, i.e. they are provided as children
45+
# or they have been explicitly removed from the rendering context using the delete method.
46+
47+
def render(props = {})
4448
if props.empty?
4549
React::RenderingContext.render(self)
4650
else
4751
React::RenderingContext.render(
4852
Element.new(
49-
`React.cloneElement(#{self.to_n}, #{API.convert_props(props)})`,
50-
type,
51-
properties.merge(props),
52-
block
53+
`React.cloneElement(#{to_n}, #{API.convert_props(props)})`,
54+
type, properties.merge(props), block
5355
)
5456
)
5557
end
5658
end
5759

60+
# Delete (remove) element from rendering context, the element may later be added back in
61+
# using the render method.
62+
63+
def delete
64+
React::RenderingContext.delete(self)
65+
end
66+
67+
# Deprecated version of delete method
68+
69+
def as_node
70+
React::RenderingContext.as_node(self)
71+
end
72+
73+
# Any other method applied to an element will be treated as class name (haml style) thus
74+
# div.foo.bar(id: :fred) is the same as saying div(class: "foo bar", id: :fred)
75+
#
76+
# single underscores become dashes, and double underscores become a single underscore
77+
#
78+
# params may be provide to each class (but typically only to the last for easy reading.)
79+
5880
def method_missing(class_name, args = {}, &new_block)
59-
class_name = class_name.split("__").collect { |s| s.gsub("_", "-") }.join("_")
81+
class_name = class_name.gsub(/__|_/, '__' => '_', '_' => '-')
6082
new_props = properties.dup
61-
new_props["class"] = "#{new_props['class']} #{class_name} #{args.delete("class")} #{args.delete('className')}".split(" ").uniq.join(" ")
83+
new_props[:class] = "\
84+
#{class_name} #{new_props[:class]} #{args.delete(:class)} #{args.delete(:className)}\
85+
".split(' ').uniq.join(' ')
6286
new_props.merge! args
6387
React::RenderingContext.replace(
6488
self,
65-
React::RenderingContext.build { React::RenderingContext.render(type, new_props, &new_block) }
89+
RenderingContext.build { RenderingContext.render(type, new_props, &new_block) }
6690
)
6791
end
6892

69-
def as_node
70-
React::RenderingContext.as_node(self)
93+
private
94+
95+
# built in events, events going to native components, and events going to reactrb
96+
97+
# built in events will have their event param translated to the Event wrapper
98+
# and the name will camelcased and have on prefixed, so :click becomes onClick.
99+
#
100+
# events emitting from native components are assumed to have the same camel case and
101+
# on prefixed.
102+
#
103+
# events emitting from reactrb components will just have on_ prefixed. So
104+
# :play_button_pushed attaches to the :on_play_button_pushed param
105+
#
106+
# in all cases the default name convention can be overriden by wrapping in <...> brackets.
107+
# So on("<MyEvent>") will attach to the "MyEvent" param.
108+
109+
def merge_event_prop!(event_name, &block)
110+
if event_name =~ /^<(.+)>$/
111+
merge_component_event_prop! event_name.gsub(/^<(.+)>$/, '\1'), &block
112+
elsif React::Event::BUILT_IN_EVENTS.include?(name = "on#{event_name.event_camelize}")
113+
merge_built_in_event_prop! name, &block
114+
elsif @type.instance_variable_get('@native_import')
115+
merge_component_event_prop! name, &block
116+
else
117+
merge_deprecated_component_event_prop! event_name, &block
118+
merge_component_event_prop! "on_#{event_name}", &block
119+
end
71120
end
72121

73-
def delete
74-
React::RenderingContext.delete(self)
122+
def merge_built_in_event_prop!(prop_name)
123+
@properties.merge!(
124+
prop_name => %x{
125+
function(event){
126+
return #{yield(React::Event.new(`event`))}
127+
}
128+
}
129+
)
130+
end
131+
132+
def merge_component_event_prop!(prop_name)
133+
@properties.merge!(
134+
prop_name => %x{
135+
function(){
136+
return #{yield(*Array(`arguments`))}
137+
}
138+
}
139+
)
140+
end
141+
142+
def merge_deprecated_component_event_prop!(event_name)
143+
prop_name = "_on#{event_name.event_camelize}"
144+
fn = %x{function(){#{
145+
React::Component.deprecation_warning(
146+
"In future releases React::Element#on('#{event_name}') will no longer respond "\
147+
"to the '#{prop_name}' emitter.\n"\
148+
"Rename your emitter param to 'on_#{event_name}' or use .on('<#{prop_name}>')"
149+
)}
150+
return #{yield(*Array(`arguments`))}
151+
}}
152+
@properties.merge!(prop_name => fn)
75153
end
76154
end
77155
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# rubocop:disable Style/FileName
2+
# require 'reactrb/new-event-name-convention' to remove missing param declaration "_onXXXX"
3+
if RUBY_ENGINE == 'opal'
4+
# removes generation of the deprecated "_onXXXX" event param syntax
5+
module React
6+
class Element
7+
def merge_deprecated_component_event_prop!(event_name)
8+
end
9+
end
10+
end
11+
end

spec/react/element_spec.rb

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
require "spec_helper"
22

33
if opal?
4+
# require 'reactrb/new-event-name-convention' # this require will get rid of any error messages but
5+
# the on method will no longer attach to the param prefixed with _on
46
describe React::Element do
57
it 'bridges `type` of native React.Element attributes' do
68
element = React.create_element('div')
@@ -17,7 +19,79 @@
1719
end
1820
end
1921

20-
describe 'Event subscription' do
22+
describe 'Component Event Subscription' do
23+
24+
it 'will subscribe to a component event param' do
25+
stub_const 'Foo', Class.new(React::Component::Base)
26+
Foo.class_eval do
27+
param :on_event, type: Proc, default: nil, allow_nil: true
28+
def render
29+
params.on_event
30+
end
31+
end
32+
expect(React.render_to_static_markup(React.create_element(Foo).on(:event) {'works!'})).to eq('<span>works!</span>')
33+
end
34+
35+
it 'will subscribe to multiple component event params' do
36+
stub_const 'Foo', Class.new(React::Component::Base)
37+
Foo.class_eval do
38+
param :on_event1, type: Proc, default: nil, allow_nil: true
39+
param :on_event2, type: Proc, default: nil, allow_nil: true
40+
def render
41+
params.on_event1+params.on_event2
42+
end
43+
end
44+
expect(React.render_to_static_markup(React.create_element(Foo).on(:event1, :event2) {'works!'})).to eq('<span>works!works!</span>')
45+
end
46+
47+
it 'will subscribe to a native components event param' do
48+
%x{
49+
window.NativeComponent = React.createClass({
50+
displayName: "HelloMessage",
51+
render: function render() {
52+
return React.createElement("span", null, this.props.onEvent());
53+
}
54+
})
55+
}
56+
stub_const 'Foo', Class.new(React::Component::Base)
57+
Foo.class_eval do
58+
imports "NativeComponent"
59+
end
60+
expect(React.render_to_static_markup(React.create_element(Foo).on(:event) {'works!'})).to eq('<span>works!</span>')
61+
end
62+
63+
it 'will subscribe to a component event param with a non-default name' do
64+
stub_const 'Foo', Class.new(React::Component::Base)
65+
Foo.class_eval do
66+
param :my_event, type: Proc, default: nil, allow_nil: true
67+
def render
68+
params.my_event
69+
end
70+
end
71+
expect(React.render_to_static_markup(React.create_element(Foo).on("<my_event>") {'works!'})).to eq('<span>works!</span>')
72+
end
73+
74+
it 'will subscribe to a component event param using the deprecated naming convention and generate a message' do
75+
stub_const 'Foo', Class.new(React::Component::Base)
76+
Foo.class_eval do
77+
param :_onEvent, type: Proc, default: nil, allow_nil: true
78+
def render
79+
params._onEvent
80+
end
81+
end
82+
%x{
83+
var log = [];
84+
var org_warn_console = window.console.warn;
85+
var org_error_console = window.console.error;
86+
window.console.warn = window.console.error = function(str){log.push(str)}
87+
}
88+
expect(React.render_to_static_markup(React.create_element(Foo).on(:event) {'works!'})).to eq('<span>works!</span>')
89+
`window.console.warn = org_warn_console; window.console.error = org_error_console;`
90+
expect(`log`).to eq(["Warning: Failed propType: In component `Foo`\nProvided prop `on_event` not specified in spec", "Warning: Deprecated feature used in React::Component. In future releases React::Element#on('event') will no longer respond to the '_onEvent' emitter.\nRename your emitter param to 'on_event' or use .on('<_onEvent>')"])
91+
end
92+
end
93+
94+
describe 'Builtin Event subscription' do
2195
it 'is subscribable through `on(:event_name)` method' do
2296
expect { |b|
2397
element = React.create_element("div").on(:click, &b)

0 commit comments

Comments
 (0)