lib/react/element.rb
require 'react/ext/string'
module React
#
# Wraps the React Native element class
#
# adds the #on method to add event handlers to the element
#
# adds the #render method to place elements in the DOM and
# #delete (alias/deprecated #as_node) method to remove elements from the DOM
#
# handles the haml style class notation so that
# div.bar.blat becomes div(class: "bar blat")
# by using method missing
#
class Element
include Native
alias_native :element_type, :type
alias_native :props, :props
attr_reader :type
attr_reader :properties
attr_reader :block
attr_accessor :waiting_on_resources
def initialize(native_element, type = nil, properties = {}, block = nil)
@type = type
@properties = (`typeof #{properties} === 'undefined'` ? nil : properties) || {}
@block = block
@native = native_element
end
# Attach event handlers.
def on(*event_names, &block)
event_names.each { |event_name| merge_event_prop!(event_name, &block) }
@native = `React.cloneElement(#{to_n}, #{properties.shallow_to_n})`
self
end
# Render element into DOM in the current rendering context.
# Used for elements that are not yet in DOM, i.e. they are provided as children
# or they have been explicitly removed from the rendering context using the delete method.
def render(props = {}, &new_block)
if props.empty?
React::RenderingContext.render(self)
else
props = API.convert_props(props)
React::RenderingContext.render(
Element.new(`React.cloneElement(#{to_n}, #{props.shallow_to_n})`,
type, properties.merge(props), block),
)
end
end
# Delete (remove) element from rendering context, the element may later be added back in
# using the render method.
def delete
React::RenderingContext.delete(self)
end
# Deprecated version of delete method
def as_node
React::RenderingContext.as_node(self)
end
# Any other method applied to an element will be treated as class name (haml style) thus
# div.foo.bar(id: :fred) is the same as saying div(class: "foo bar", id: :fred)
#
# single underscores become dashes, and double underscores become a single underscore
#
# params may be provide to each class (but typically only to the last for easy reading.)
def method_missing(class_name, args = {}, &new_block)
return dup.render.method_missing(class_name, args, &new_block) unless rendered?
React::RenderingContext.replace(
self,
RenderingContext.build do
RenderingContext.render(type, build_new_properties(class_name, args), &new_block)
end
)
end
def rendered?
React::RenderingContext.rendered? self
end
def self.haml_class_name(class_name)
class_name.gsub(/__|_/, '__' => '_', '_' => '-')
end
private
def build_new_properties(class_name, args)
class_name = self.class.haml_class_name(class_name)
new_props = properties.dup
new_props[:className] = "\
#{class_name} #{new_props[:className]} #{args.delete(:class)} #{args.delete(:className)}\
".split(' ').uniq.join(' ')
new_props.merge! args
end
# built in events, events going to native components, and events going to reactrb
# built in events will have their event param translated to the Event wrapper
# and the name will camelcased and have on prefixed, so :click becomes onClick.
#
# events emitting from native components are assumed to have the same camel case and
# on prefixed.
#
# events emitting from reactrb components will just have on_ prefixed. So
# :play_button_pushed attaches to the :on_play_button_pushed param
#
# in all cases the default name convention can be overriden by wrapping in <...> brackets.
# So on("<MyEvent>") will attach to the "MyEvent" param.
def merge_event_prop!(event_name, &block)
if event_name =~ /^<(.+)>$/
merge_component_event_prop! event_name.gsub(/^<(.+)>$/, '\1'), &block
elsif React::Event::BUILT_IN_EVENTS.include?(name = "on#{event_name.event_camelize}")
merge_built_in_event_prop! name, &block
elsif @type.instance_variable_get('@native_import')
merge_component_event_prop! name, &block
else
merge_deprecated_component_event_prop! event_name, &block
merge_component_event_prop! "on_#{event_name}", &block
end
end
def merge_built_in_event_prop!(prop_name)
@properties.merge!(
prop_name => %x{
function(event){
return #{yield(React::Event.new(`event`))}
}
}
)
end
def merge_component_event_prop!(prop_name)
@properties.merge!(
prop_name => %x{
function(){
return #{yield(*Array(`arguments`))}
}
}
)
end
def merge_deprecated_component_event_prop!(event_name)
prop_name = "_on#{event_name.event_camelize}"
fn = %x{function(){#{
React::Component.deprecation_warning(
type,
"In future releases React::Element#on('#{event_name}') will no longer respond "\
"to the '#{prop_name}' emitter.\n"\
"Rename your emitter param to 'on_#{event_name}' or use .on('<#{prop_name}>')"
)}
return #{yield(*Array(`arguments`))}
}}
@properties.merge!(prop_name => fn)
end
end
end