react.rb 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +30 -0
  3. data/Gemfile +2 -0
  4. data/Gemfile.lock +48 -0
  5. data/LICENSE +19 -0
  6. data/README.md +166 -0
  7. data/config.ru +15 -0
  8. data/example/react-tutorial/Gemfile +6 -0
  9. data/example/react-tutorial/Gemfile.lock +45 -0
  10. data/example/react-tutorial/README.md +8 -0
  11. data/example/react-tutorial/_comments.json +6 -0
  12. data/example/react-tutorial/config.ru +55 -0
  13. data/example/react-tutorial/example.rb +104 -0
  14. data/example/react-tutorial/public/base.css +62 -0
  15. data/example/todos/Gemfile +11 -0
  16. data/example/todos/Gemfile.lock +84 -0
  17. data/example/todos/README.md +37 -0
  18. data/example/todos/Rakefile +8 -0
  19. data/example/todos/app/application.rb +22 -0
  20. data/example/todos/app/components/app.react.rb +61 -0
  21. data/example/todos/app/components/footer.react.rb +31 -0
  22. data/example/todos/app/components/todo_item.react.rb +46 -0
  23. data/example/todos/app/components/todo_list.react.rb +25 -0
  24. data/example/todos/app/models/todo.rb +19 -0
  25. data/example/todos/config.ru +14 -0
  26. data/example/todos/index.html.haml +16 -0
  27. data/example/todos/spec/todo_spec.rb +28 -0
  28. data/example/todos/vendor/base.css +410 -0
  29. data/example/todos/vendor/bg.png +0 -0
  30. data/example/todos/vendor/jquery.js +4 -0
  31. data/lib/react.rb +16 -0
  32. data/lib/react/api.rb +95 -0
  33. data/lib/react/callbacks.rb +35 -0
  34. data/lib/react/component.rb +197 -0
  35. data/lib/react/element.rb +63 -0
  36. data/lib/react/event.rb +76 -0
  37. data/lib/react/ext/hash.rb +9 -0
  38. data/lib/react/ext/string.rb +8 -0
  39. data/lib/react/top_level.rb +50 -0
  40. data/lib/react/validator.rb +65 -0
  41. data/lib/react/version.rb +3 -0
  42. data/react.rb.gemspec +24 -0
  43. data/spec/callbacks_spec.rb +107 -0
  44. data/spec/component_spec.rb +556 -0
  45. data/spec/element_spec.rb +60 -0
  46. data/spec/event_spec.rb +22 -0
  47. data/spec/react_spec.rb +168 -0
  48. data/spec/reactjs/index.html.erb +10 -0
  49. data/spec/spec_helper.rb +29 -0
  50. data/spec/validator_spec.rb +79 -0
  51. data/vendor/active_support/core_ext/array/extract_options.rb +29 -0
  52. data/vendor/active_support/core_ext/class/attribute.rb +127 -0
  53. data/vendor/active_support/core_ext/kernel/singleton_class.rb +13 -0
  54. data/vendor/active_support/core_ext/module/remove_method.rb +11 -0
  55. metadata +189 -0
@@ -0,0 +1,60 @@
1
+ require "spec_helper"
2
+
3
+ describe React::Element do
4
+ it "should bridge `type` of native React.Element attributes" do
5
+ element = React.create_element('div')
6
+ expect(element.element_type).to eq("div")
7
+ end
8
+
9
+ async "should be renderable" do
10
+ element = React.create_element('span')
11
+ div = `document.createElement("div")`
12
+ React.render(element, div) do
13
+ run_async {
14
+ expect(`div.children[0].tagName`).to eq("SPAN")
15
+ }
16
+ end
17
+ end
18
+
19
+ describe "Event subscription" do
20
+ it "should be subscribable through `on(:event_name)` method" do
21
+ expect { |b|
22
+ element = React.create_element("div").on(:click, &b)
23
+ instance = renderElementToDocument(element)
24
+ simulateEvent(:click, instance)
25
+ }.to yield_with_args(React::Event)
26
+
27
+ expect { |b|
28
+ element = React.create_element("div").on(:key_down, &b)
29
+ instance = renderElementToDocument(element)
30
+ simulateEvent(:keyDown, instance, {key: "Enter"})
31
+ }.to yield_control
32
+
33
+ expect { |b|
34
+ element = React.create_element("form").on(:submit, &b)
35
+ instance = renderElementToDocument(element)
36
+ simulateEvent(:submit, instance, {})
37
+ }.to yield_control
38
+ end
39
+
40
+ it "should return self for `on` method" do
41
+ element = React.create_element("div")
42
+ expect(element.on(:click){}).to eq(element)
43
+ end
44
+ end
45
+
46
+ describe "Children" do
47
+ it "should return a Enumerable" do
48
+ ele = React.create_element('div') { [React.create_element('a'), React.create_element('li')] }
49
+ nodes = ele.children.map {|ele| ele.element_type }
50
+ expect(nodes).to eq(["a", "li"])
51
+ end
52
+
53
+ it "should return a Enumerator when not providing a block" do
54
+ ele = React.create_element('div') { [React.create_element('a'), React.create_element('li')] }
55
+ nodes = ele.children.each
56
+ expect(nodes).to be_a(Enumerator)
57
+ expect(nodes.size).to eq(2)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,22 @@
1
+ require "spec_helper"
2
+
3
+ describe React::Event do
4
+ it "should bridge attributes of native SyntheticEvent (see http://facebook.github.io/react/docs/events.html#syntheticevent)" do
5
+ element = React.create_element('div').on(:click) do |event|
6
+ expect(event.bubbles).to eq(`#{event.to_n}.bubbles`)
7
+ expect(event.cancelable).to eq(`#{event.to_n}.cancelable`)
8
+ expect(event.current_target).to eq(`#{event.to_n}.currentTarget`)
9
+ expect(event.default_prevented).to eq(`#{event.to_n}.defaultPrevented`)
10
+ expect(event.event_phase).to eq(`#{event.to_n}.eventPhase`)
11
+ expect(event.is_trusted?).to eq(`#{event.to_n}.isTrusted`)
12
+ expect(event.native_event).to eq(`#{event.to_n}.nativeEvent`)
13
+ expect(event.target).to eq(`#{event.to_n}.target`)
14
+ expect(event.timestamp).to eq(`#{event.to_n}.timeStamp`)
15
+ expect(event.event_type).to eq(`#{event.to_n}.type`)
16
+ expect(event).to respond_to(:prevent_default)
17
+ expect(event).to respond_to(:stop_propagation)
18
+ end
19
+ instance = renderElementToDocument(element)
20
+ simulateEvent(:click, instance)
21
+ end
22
+ end
@@ -0,0 +1,168 @@
1
+ require "spec_helper"
2
+
3
+ describe React do
4
+ describe "is_valid_element" do
5
+ it "should return true if passed a valid element" do
6
+ element = React::Element.new(`React.createElement('div')`)
7
+ expect(React.is_valid_element(element)).to eq(true)
8
+ end
9
+
10
+ it "should return false is passed a non React element" do
11
+ element = React::Element.new(`{}`)
12
+ expect(React.is_valid_element(element)).to eq(false)
13
+ end
14
+ end
15
+
16
+ describe "create_element" do
17
+ it "should create a valid element with only tag" do
18
+ element = React.create_element('div')
19
+ expect(React.is_valid_element(element)).to eq(true)
20
+ end
21
+
22
+ context "with block" do
23
+ it "should create a valid element with text as only child when block yield String" do
24
+ element = React.create_element('div') { "lorem ipsum" }
25
+ expect(React.is_valid_element(element)).to eq(true)
26
+ expect(element.props.children).to eq("lorem ipsum")
27
+ end
28
+
29
+ it "should create a valid element with children as array when block yield Array of element" do
30
+ element = React.create_element('div') do
31
+ [React.create_element('span'), React.create_element('span'), React.create_element('span')]
32
+ end
33
+ expect(React.is_valid_element(element)).to eq(true)
34
+ expect(element.props.children.length).to eq(3)
35
+ end
36
+
37
+ it "should render element with children as array when block yield Array of element" do
38
+ element = React.create_element('div') do
39
+ [React.create_element('span'), React.create_element('span'), React.create_element('span')]
40
+ end
41
+ instance = renderElementToDocument(element)
42
+ expect(instance.getDOMNode.childNodes.length).to eq(3)
43
+ end
44
+ end
45
+ describe "custom element" do
46
+ before do
47
+ stub_const 'Foo', Class.new
48
+ Foo.class_eval do
49
+ def render
50
+ React.create_element("div") { "lorem" }
51
+ end
52
+ end
53
+ end
54
+
55
+ it "should render element with only one children correctly" do
56
+ element = React.create_element(Foo) { React.create_element('span') }
57
+ instance = renderElementToDocument(element)
58
+ expect(instance.props.children).not_to be_a(Array)
59
+ expect(instance.props.children.type).to eq("span")
60
+ end
61
+
62
+ it "should render element with more than one children correctly" do
63
+ element = React.create_element(Foo) { [React.create_element('span'), React.create_element('span')] }
64
+ instance = renderElementToDocument(element)
65
+ expect(instance.props.children).to be_a(Array)
66
+ expect(instance.props.children.length).to eq(2)
67
+ end
68
+
69
+ it "should create a valid element provided class defined `render`" do
70
+ element = React.create_element(Foo)
71
+ expect(React.is_valid_element(element)).to eq(true)
72
+ end
73
+
74
+ it "should allow creating with properties" do
75
+ element = React.create_element(Foo, foo: "bar")
76
+ expect(element.props.foo).to eq("bar")
77
+ end
78
+
79
+ it "should raise error if provided class doesn't defined `render`" do
80
+ expect { React.create_element(Array) }.to raise_error
81
+ end
82
+ end
83
+
84
+ describe "create element with properties" do
85
+ it "should enforce snake-cased property name" do
86
+ element = React.create_element("div", class_name: "foo")
87
+ expect(element.props.className).to eq("foo")
88
+ end
89
+
90
+ it "should allow custom property" do
91
+ element = React.create_element("div", foo: "bar")
92
+ expect(element.props.foo).to eq("bar")
93
+ end
94
+
95
+ it "should not camel-case custom property" do
96
+ element = React.create_element("div", foo_bar: "foo")
97
+ expect(element.props.foo_bar).to eq("foo")
98
+ end
99
+ end
100
+
101
+ describe "class_name helpers (React.addons.classSet)" do
102
+ it "should transform Hash provided to `class_name` props as string" do
103
+ classes = {foo: true, bar: false, lorem: true}
104
+ element = React.create_element("div", class_name: classes)
105
+
106
+ expect(element.props.className).to eq("foo lorem")
107
+ end
108
+
109
+ it "should not alter behavior when passing a string" do
110
+ element = React.create_element("div", class_name: "foo bar")
111
+
112
+ expect(element.props.className).to eq("foo bar")
113
+ end
114
+ end
115
+ end
116
+
117
+
118
+ describe "render" do
119
+ async "should render element to DOM" do
120
+ div = `document.createElement("div")`
121
+ React.render(React.create_element('span') { "lorem" }, div) do
122
+ run_async {
123
+ expect(`div.children[0].tagName`).to eq("SPAN")
124
+ expect(`div.textContent`).to eq("lorem")
125
+ }
126
+ end
127
+ end
128
+
129
+ it "should work without providing a block" do
130
+ div = `document.createElement("div")`
131
+ React.render(React.create_element('span') { "lorem" }, div)
132
+ end
133
+
134
+ it "should return a React::Component::API compatible object" do
135
+ div = `document.createElement("div")`
136
+ component = React.render(React.create_element('span') { "lorem" }, div)
137
+ React::Component::API.public_instance_methods(true).each do |method_name|
138
+ expect(component).to respond_to(method_name)
139
+ end
140
+ end
141
+
142
+ pending "should return nil to prevent abstraction leakage" do
143
+ div = `document.createElement("div")`
144
+ expect {
145
+ React.render(React.create_element('span') { "lorem" }, div)
146
+ }.to be_nil
147
+ end
148
+ end
149
+
150
+ describe "render_to_string" do
151
+ it "should render a React.Element to string" do
152
+ ele = React.create_element('span') { "lorem" }
153
+ expect(React.render_to_string(ele)).to be_kind_of(String)
154
+ end
155
+ end
156
+
157
+ describe "unmount_component_at_node" do
158
+ async "should unmount component at node" do
159
+ div = `document.createElement("div")`
160
+ React.render(React.create_element('span') { "lorem" }, div ) do
161
+ run_async {
162
+ expect(React.unmount_component_at_node(div)).to eq(true)
163
+ }
164
+ end
165
+ end
166
+ end
167
+
168
+ end
@@ -0,0 +1,10 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ </head>
5
+ <body>
6
+ <%= javascript_include_tag 'react-with-addons' %>
7
+ <%= javascript_include_tag @server.main %>
8
+ <div id="placeholder" style="display: none"></div>
9
+ </body>
10
+ </html>
@@ -0,0 +1,29 @@
1
+ require 'react'
2
+
3
+ module ReactTestHelpers
4
+ `var ReactTestUtils = React.addons.TestUtils`
5
+
6
+ def renderToDocument(type, options = {})
7
+ element = React.create_element(type, options)
8
+ return renderElementToDocument(element)
9
+ end
10
+
11
+ def renderElementToDocument(element)
12
+ instance = Native(`ReactTestUtils.renderIntoDocument(#{element.to_n})`)
13
+ instance.class.include(React::Component::API)
14
+ return instance
15
+ end
16
+
17
+ def simulateEvent(event, element, params = {})
18
+ simulator = Native(`ReactTestUtils.Simulate`)
19
+ simulator[event.to_s].call(`#{element.to_n}.getDOMNode()`, params)
20
+ end
21
+
22
+ def isElementOfType(element, type)
23
+ `React.addons.TestUtils.isElementOfType(#{element.to_n}, #{type.cached_component_class})`
24
+ end
25
+ end
26
+
27
+ RSpec.configure do |config|
28
+ config.include ReactTestHelpers
29
+ end
@@ -0,0 +1,79 @@
1
+ require "spec_helper"
2
+
3
+ describe React::Validator do
4
+ describe "validate" do
5
+ describe "Presence validation" do
6
+ it "should check if required props provided" do
7
+ validator = React::Validator.build do
8
+ requires :foo
9
+ requires :bar
10
+ end
11
+
12
+ expect(validator.validate({})).to eq(["Required prop `foo` was not specified", "Required prop `bar` was not specified"])
13
+ expect(validator.validate({foo: 1, bar: 3})).to eq([])
14
+ end
15
+
16
+ it "should check if passed non specified prop" do
17
+ validator = React::Validator.build do
18
+ optional :foo
19
+ end
20
+
21
+ expect(validator.validate({bar: 10})).to eq(["Provided prop `bar` not specified in spec"])
22
+ expect(validator.validate({foo: 10})).to eq([])
23
+ end
24
+ end
25
+
26
+ describe "Type validation" do
27
+ it "should check if passed value with wrong type" do
28
+ validator = React::Validator.build do
29
+ requires :foo, type: String
30
+ end
31
+
32
+ expect(validator.validate({foo: 10})).to eq(["Provided prop `foo` was not the specified type `String`"])
33
+ expect(validator.validate({foo: "10"})).to eq([])
34
+ end
35
+
36
+ it "should check if passed value with wrong custom type" do
37
+ stub_const 'Bar', Class.new
38
+ validator = React::Validator.build do
39
+ requires :foo, type: Bar
40
+ end
41
+
42
+ expect(validator.validate({foo: 10})).to eq(["Provided prop `foo` was not the specified type `Bar`"])
43
+ expect(validator.validate({foo: Bar.new})).to eq([])
44
+ end
45
+
46
+ it "should support Array[Class] validation" do
47
+ validator = React::Validator.build do
48
+ requires :foo, type: Array[Hash]
49
+ end
50
+
51
+ expect(validator.validate({foo: [1,'2',3]})).to eq(["Provided prop `foo` was not an Array of the specified type `Hash`"])
52
+ expect(validator.validate({foo: [{},{},{}]})).to eq([])
53
+ end
54
+ end
55
+
56
+ describe "Limited values" do
57
+ it "should check if passed value is not one of the specified values" do
58
+ validator = React::Validator.build do
59
+ requires :foo, values: [4,5,6]
60
+ end
61
+
62
+ expect(validator.validate({foo: 3})).to eq(["Value `3` for prop `foo` is not an allowed value"])
63
+ expect(validator.validate({foo: 4})).to eq([])
64
+ end
65
+ end
66
+ end
67
+
68
+ describe "default_props" do
69
+ it "should return specified default values" do
70
+ validator = React::Validator.build do
71
+ requires :foo, default: 10
72
+ requires :bar
73
+ optional :lorem, default: 20
74
+ end
75
+
76
+ expect(validator.default_props).to eq({foo: 10, lorem: 20})
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,29 @@
1
+ class Hash
2
+ # By default, only instances of Hash itself are extractable.
3
+ # Subclasses of Hash may implement this method and return
4
+ # true to declare themselves as extractable. If a Hash
5
+ # is extractable, Array#extract_options! pops it from
6
+ # the Array when it is the last element of the Array.
7
+ def extractable_options?
8
+ instance_of?(Hash)
9
+ end
10
+ end
11
+
12
+ class Array
13
+ # Extracts options from a set of arguments. Removes and returns the last
14
+ # element in the array if it's a hash, otherwise returns a blank hash.
15
+ #
16
+ # def options(*args)
17
+ # args.extract_options!
18
+ # end
19
+ #
20
+ # options(1, 2) # => {}
21
+ # options(1, 2, :a => :b) # => {:a=>:b}
22
+ def extract_options!
23
+ if last.is_a?(Hash) && last.extractable_options?
24
+ pop
25
+ else
26
+ {}
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,127 @@
1
+ require 'active_support/core_ext/kernel/singleton_class'
2
+ require 'active_support/core_ext/module/remove_method'
3
+ require 'active_support/core_ext/array/extract_options'
4
+
5
+ class Class
6
+ # Declare a class-level attribute whose value is inheritable by subclasses.
7
+ # Subclasses can change their own value and it will not impact parent class.
8
+ #
9
+ # class Base
10
+ # class_attribute :setting
11
+ # end
12
+ #
13
+ # class Subclass < Base
14
+ # end
15
+ #
16
+ # Base.setting = true
17
+ # Subclass.setting # => true
18
+ # Subclass.setting = false
19
+ # Subclass.setting # => false
20
+ # Base.setting # => true
21
+ #
22
+ # In the above case as long as Subclass does not assign a value to setting
23
+ # by performing <tt>Subclass.setting = _something_ </tt>, <tt>Subclass.setting</tt>
24
+ # would read value assigned to parent class. Once Subclass assigns a value then
25
+ # the value assigned by Subclass would be returned.
26
+ #
27
+ # This matches normal Ruby method inheritance: think of writing an attribute
28
+ # on a subclass as overriding the reader method. However, you need to be aware
29
+ # when using +class_attribute+ with mutable structures as +Array+ or +Hash+.
30
+ # In such cases, you don't want to do changes in places but use setters:
31
+ #
32
+ # Base.setting = []
33
+ # Base.setting # => []
34
+ # Subclass.setting # => []
35
+ #
36
+ # # Appending in child changes both parent and child because it is the same object:
37
+ # Subclass.setting << :foo
38
+ # Base.setting # => [:foo]
39
+ # Subclass.setting # => [:foo]
40
+ #
41
+ # # Use setters to not propagate changes:
42
+ # Base.setting = []
43
+ # Subclass.setting += [:foo]
44
+ # Base.setting # => []
45
+ # Subclass.setting # => [:foo]
46
+ #
47
+ # For convenience, an instance predicate method is defined as well.
48
+ # To skip it, pass <tt>instance_predicate: false</tt>.
49
+ #
50
+ # Subclass.setting? # => false
51
+ #
52
+ # Instances may overwrite the class value in the same way:
53
+ #
54
+ # Base.setting = true
55
+ # object = Base.new
56
+ # object.setting # => true
57
+ # object.setting = false
58
+ # object.setting # => false
59
+ # Base.setting # => true
60
+ #
61
+ # To opt out of the instance reader method, pass <tt>instance_reader: false</tt>.
62
+ #
63
+ # object.setting # => NoMethodError
64
+ # object.setting? # => NoMethodError
65
+ #
66
+ # To opt out of the instance writer method, pass <tt>instance_writer: false</tt>.
67
+ #
68
+ # object.setting = false # => NoMethodError
69
+ #
70
+ # To opt out of both instance methods, pass <tt>instance_accessor: false</tt>.
71
+ def class_attribute(*attrs)
72
+ options = attrs.extract_options!
73
+ instance_reader = options.fetch(:instance_accessor, true) && options.fetch(:instance_reader, true)
74
+ instance_writer = options.fetch(:instance_accessor, true) && options.fetch(:instance_writer, true)
75
+ instance_predicate = options.fetch(:instance_predicate, true)
76
+
77
+ attrs.each do |name|
78
+ define_singleton_method(name) { nil }
79
+ define_singleton_method("#{name}?") { !!public_send(name) } if instance_predicate
80
+
81
+ ivar = "@#{name}"
82
+
83
+ define_singleton_method("#{name}=") do |val|
84
+ singleton_class.class_eval do
85
+ remove_possible_method(name)
86
+ define_method(name) { val }
87
+ end
88
+
89
+ if singleton_class?
90
+ class_eval do
91
+ remove_possible_method(name)
92
+ define_method(name) do
93
+ if instance_variable_defined? ivar
94
+ instance_variable_get ivar
95
+ else
96
+ singleton_class.send name
97
+ end
98
+ end
99
+ end
100
+ end
101
+ val
102
+ end
103
+
104
+ if instance_reader
105
+ remove_possible_method name
106
+ define_method(name) do
107
+ if instance_variable_defined?(ivar)
108
+ instance_variable_get ivar
109
+ else
110
+ self.class.public_send name
111
+ end
112
+ end
113
+ define_method("#{name}?") { !!public_send(name) } if instance_predicate
114
+ end
115
+
116
+ attr_writer name if instance_writer
117
+ end
118
+ end
119
+
120
+ private
121
+
122
+ unless respond_to?(:singleton_class?)
123
+ def singleton_class?
124
+ ancestors.first != self
125
+ end
126
+ end
127
+ end