react.rb 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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