reactive-ruby 0.7.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +30 -0
  3. data/Gemfile +3 -0
  4. data/Gemfile.lock +53 -0
  5. data/LICENSE +19 -0
  6. data/README.md +303 -0
  7. data/config.ru +15 -0
  8. data/example/examples/Gemfile +7 -0
  9. data/example/examples/Gemfile.lock +45 -0
  10. data/example/examples/config.ru +44 -0
  11. data/example/examples/hello.js.rb +43 -0
  12. data/example/react-tutorial/Gemfile +7 -0
  13. data/example/react-tutorial/Gemfile.lock +49 -0
  14. data/example/react-tutorial/README.md +8 -0
  15. data/example/react-tutorial/_comments.json +14 -0
  16. data/example/react-tutorial/config.ru +63 -0
  17. data/example/react-tutorial/example.js.rb +290 -0
  18. data/example/react-tutorial/public/base.css +62 -0
  19. data/example/todos/Gemfile +11 -0
  20. data/example/todos/Gemfile.lock +84 -0
  21. data/example/todos/README.md +37 -0
  22. data/example/todos/Rakefile +8 -0
  23. data/example/todos/app/application.rb +22 -0
  24. data/example/todos/app/components/app.react.rb +61 -0
  25. data/example/todos/app/components/footer.react.rb +31 -0
  26. data/example/todos/app/components/todo_item.react.rb +46 -0
  27. data/example/todos/app/components/todo_list.react.rb +25 -0
  28. data/example/todos/app/models/todo.rb +19 -0
  29. data/example/todos/config.ru +14 -0
  30. data/example/todos/index.html.haml +16 -0
  31. data/example/todos/spec/todo_spec.rb +28 -0
  32. data/example/todos/vendor/base.css +410 -0
  33. data/example/todos/vendor/bg.png +0 -0
  34. data/example/todos/vendor/jquery.js +4 -0
  35. data/lib/rails-helpers/react_component.rb +32 -0
  36. data/lib/reactive-ruby.rb +23 -0
  37. data/lib/reactive-ruby/api.rb +177 -0
  38. data/lib/reactive-ruby/callbacks.rb +35 -0
  39. data/lib/reactive-ruby/component.rb +411 -0
  40. data/lib/reactive-ruby/element.rb +87 -0
  41. data/lib/reactive-ruby/event.rb +76 -0
  42. data/lib/reactive-ruby/ext/hash.rb +9 -0
  43. data/lib/reactive-ruby/ext/string.rb +8 -0
  44. data/lib/reactive-ruby/isomorphic_helpers.rb +223 -0
  45. data/lib/reactive-ruby/observable.rb +33 -0
  46. data/lib/reactive-ruby/rendering_context.rb +91 -0
  47. data/lib/reactive-ruby/serializers.rb +15 -0
  48. data/lib/reactive-ruby/state.rb +90 -0
  49. data/lib/reactive-ruby/top_level.rb +53 -0
  50. data/lib/reactive-ruby/validator.rb +83 -0
  51. data/lib/reactive-ruby/version.rb +3 -0
  52. data/logo1.png +0 -0
  53. data/logo2.png +0 -0
  54. data/logo3.png +0 -0
  55. data/reactive-ruby.gemspec +25 -0
  56. data/spec/callbacks_spec.rb +107 -0
  57. data/spec/component_spec.rb +597 -0
  58. data/spec/element_spec.rb +60 -0
  59. data/spec/event_spec.rb +22 -0
  60. data/spec/react_spec.rb +209 -0
  61. data/spec/reactjs/index.html.erb +11 -0
  62. data/spec/spec_helper.rb +29 -0
  63. data/spec/tutorial/tutorial_spec.rb +37 -0
  64. data/spec/validator_spec.rb +79 -0
  65. data/vendor/active_support/core_ext/array/extract_options.rb +29 -0
  66. data/vendor/active_support/core_ext/class/attribute.rb +127 -0
  67. data/vendor/active_support/core_ext/kernel/singleton_class.rb +13 -0
  68. data/vendor/active_support/core_ext/module/remove_method.rb +11 -0
  69. metadata +205 -0
@@ -0,0 +1,45 @@
1
+ PATH
2
+ remote: ../..
3
+ specs:
4
+ opal-react (0.1.1)
5
+ opal
6
+ opal-activesupport
7
+
8
+ GEM
9
+ remote: http://rubygems.org/
10
+ specs:
11
+ hike (1.2.3)
12
+ opal (0.8.0)
13
+ hike (~> 1.2)
14
+ sourcemap (~> 0.1.0)
15
+ sprockets (~> 3.1)
16
+ tilt (>= 1.4)
17
+ opal-activesupport (0.1.0)
18
+ opal (>= 0.5.0, < 1.0.0)
19
+ opal-jquery (0.4.0)
20
+ opal (>= 0.7.0, < 0.9.0)
21
+ rack (1.6.4)
22
+ rack-protection (1.5.3)
23
+ rack
24
+ react-source (0.13.3)
25
+ sinatra (1.4.6)
26
+ rack (~> 1.4)
27
+ rack-protection (~> 1.4)
28
+ tilt (>= 1.3, < 3)
29
+ sourcemap (0.1.1)
30
+ sprockets (3.2.0)
31
+ rack (~> 1.0)
32
+ tilt (2.0.1)
33
+
34
+ PLATFORMS
35
+ ruby
36
+
37
+ DEPENDENCIES
38
+ opal
39
+ opal-jquery
40
+ opal-react!
41
+ react-source
42
+ sinatra
43
+
44
+ BUNDLED WITH
45
+ 1.10.2
@@ -0,0 +1,44 @@
1
+ # config.ru
2
+ require 'bundler'
3
+ Bundler.require
4
+
5
+ require "react/source"
6
+
7
+ Opal::Processor.source_map_enabled = true
8
+
9
+ opal = Opal::Server.new {|s|
10
+ s.append_path './'
11
+ s.append_path File.dirname(::React::Source.bundled_path_for("react-with-addons.js"))
12
+ s.main = 'example'
13
+ s.debug = true
14
+ }
15
+
16
+ map opal.source_maps.prefix do
17
+ run opal.source_maps
18
+ end rescue nil
19
+
20
+ map '/assets' do
21
+ run opal.sprockets
22
+ end
23
+
24
+ get '/example/:example' do
25
+ example = params[:example]
26
+ <<-HTML
27
+ <!doctype html>
28
+ <html>
29
+ <head>
30
+ <title>Example: #{example}.rb</title>
31
+ <script src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
32
+ <script src="http://cdnjs.cloudflare.com/ajax/libs/showdown/0.3.1/showdown.min.js"></script>
33
+ <script src="/assets/react-with-addons.min.js"></script>
34
+ <script src="/assets/#{example}.js"></script>
35
+ <script>#{Opal::Processor.load_asset_code(opal.sprockets, example+".js")}</script>
36
+ </head>
37
+ <body>
38
+ <div id="content"></div>
39
+ </body>
40
+ </html>
41
+ HTML
42
+ end
43
+
44
+ run Sinatra::Application
@@ -0,0 +1,43 @@
1
+ require 'opal'
2
+ require 'opal-react'
3
+
4
+ class HelloMessage
5
+
6
+ include React::Component # will create a new component named HelloMessage
7
+
8
+ MSG = {great: 'Cool!', bad: 'Cheer up!'}
9
+
10
+ optional_param :mood
11
+ required_param :name
12
+ define_state :foo, "Default greeting"
13
+
14
+ before_mount do
15
+ foo! "#{name}: #{MSG[mood]}" if mood # change the state of foo using foo!, read the state using foo
16
+ end
17
+
18
+ after_mount :log # notice the two forms of callback
19
+
20
+ def log
21
+ puts "mounted!"
22
+ end
23
+
24
+ def render # render method MUST return just one component
25
+ div do # basic dsl syntax component_name(options) { ...children... }
26
+ span { "#{foo} #{name}!" } # all html5 components are defined with lower case text
27
+ end
28
+ end
29
+
30
+ end
31
+
32
+ class App
33
+ include React::Component
34
+
35
+ def render
36
+ HelloMessage name: 'John', mood: :great # new components are accessed via the class name
37
+ end
38
+ end
39
+
40
+ # later we will talk about nicer ways to do this: For now wait till doc is loaded
41
+ # then tell React to create an "App" and render it into the document body.
42
+
43
+ `window.onload = #{lambda {React.render(React.create_element(App), `document.body`)}}`
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'opal-react', :path => '../..'
4
+ gem 'opal-browser'
5
+ gem 'sinatra'
6
+ gem 'opal-jquery'
7
+ gem 'react-source'
@@ -0,0 +1,49 @@
1
+ PATH
2
+ remote: ../..
3
+ specs:
4
+ opal-react (0.2.1)
5
+ opal
6
+ opal-activesupport
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ hike (1.2.3)
12
+ opal (0.8.0)
13
+ hike (~> 1.2)
14
+ sourcemap (~> 0.1.0)
15
+ sprockets (~> 3.1)
16
+ tilt (>= 1.4)
17
+ opal-activesupport (0.1.0)
18
+ opal (>= 0.5.0, < 1.0.0)
19
+ opal-browser (0.1.0.beta1)
20
+ opal (>= 0.5.5)
21
+ paggio
22
+ opal-jquery (0.4.0)
23
+ opal (>= 0.7.0, < 0.9.0)
24
+ paggio (0.2.4)
25
+ rack (1.6.4)
26
+ rack-protection (1.5.3)
27
+ rack
28
+ react-source (0.13.3)
29
+ sinatra (1.4.6)
30
+ rack (~> 1.4)
31
+ rack-protection (~> 1.4)
32
+ tilt (>= 1.3, < 3)
33
+ sourcemap (0.1.1)
34
+ sprockets (3.2.0)
35
+ rack (~> 1.0)
36
+ tilt (2.0.1)
37
+
38
+ PLATFORMS
39
+ ruby
40
+
41
+ DEPENDENCIES
42
+ opal-browser
43
+ opal-jquery
44
+ opal-react!
45
+ react-source
46
+ sinatra
47
+
48
+ BUNDLED WITH
49
+ 1.10.2
@@ -0,0 +1,8 @@
1
+ # React Tutorial
2
+
3
+ This is a rewrite of original comment box example using React.rb from the [React tutorial](http://facebook.github.io/react/docs/tutorial.html).
4
+
5
+ ## To use
6
+
7
+ 1. Make sure you use Bundler, then `bundle exec rackup`
8
+ 2. And visit <http://localhost:9292/>. Try opening multiple tabs!
@@ -0,0 +1,14 @@
1
+ [
2
+ {
3
+ "author": "Pete Hunt",
4
+ "text": "Hey there!"
5
+ },
6
+ {
7
+ "author": "Mitch",
8
+ "text": "i ***am*** saying something!"
9
+ },
10
+ {
11
+ "author": "Jan",
12
+ "text": "well what are you saying *dear*"
13
+ }
14
+ ]
@@ -0,0 +1,63 @@
1
+ # config.ru
2
+ require 'bundler'
3
+ Bundler.require
4
+
5
+ require "react/source"
6
+
7
+ Opal::Processor.source_map_enabled = true
8
+
9
+ opal = Opal::Server.new {|s|
10
+ s.append_path './'
11
+ s.append_path File.dirname(::React::Source.bundled_path_for("react-with-addons.js"))
12
+ s.main = 'example'
13
+ s.debug = true
14
+ }
15
+
16
+ map opal.source_maps.prefix do
17
+ run opal.source_maps
18
+ end rescue nil
19
+
20
+ map '/assets' do
21
+ run opal.sprockets
22
+ end
23
+
24
+ get '/comments.json' do
25
+ comments = JSON.parse(open("./_comments.json").read)
26
+ JSON.generate(comments)
27
+ end
28
+
29
+ get '/comments.js' do
30
+ content_type "application/javascript"
31
+ comments = JSON.parse(open("./_comments.json").read)
32
+ "window.initial_comments = #{JSON.generate(comments)}"
33
+ end
34
+
35
+ post "/comments.json" do
36
+ comments = JSON.parse(open("./_comments.json").read)
37
+ comments << JSON.parse(request.body.read)
38
+ File.write('./_comments.json', JSON.pretty_generate(comments, :indent => ' '))
39
+ JSON.generate(comments)
40
+ end
41
+
42
+ get '/' do
43
+ <<-HTML
44
+ <!doctype html>
45
+ <html>
46
+ <head>
47
+ <title>Hello React</title>
48
+ <link rel="stylesheet" href="base.css" />
49
+ <script src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
50
+ <script src="http://cdnjs.cloudflare.com/ajax/libs/showdown/0.3.1/showdown.min.js"></script>
51
+ <script src="/assets/react-with-addons.js"></script>
52
+ <script src="/assets/example.js"></script>
53
+ <script src="/comments.js"></script>
54
+ <script>#{Opal::Processor.load_asset_code(opal.sprockets, "example.js")}</script>
55
+ </head>
56
+ <body>
57
+ <div id="content"></div>
58
+ </body>
59
+ </html>
60
+ HTML
61
+ end
62
+
63
+ run Sinatra::Application
@@ -0,0 +1,290 @@
1
+ require 'opal'
2
+ require 'browser' # gives us wrappers on javascript methods such as setTimer and setInterval
3
+ require 'opal-jquery' # gives us a nice wrapper on jQuery which we will use mainly for HTTP calls
4
+ require "json" # json conversions
5
+ require 'opal-react' # and the whole reason we are gathered here today!
6
+
7
+ Document.ready? do # Document.ready? is a opal-jquery method. The block will run when doc is loaded
8
+
9
+ # render an instance of the CommentBox component at the '#content' element.
10
+ # url and poll_interval are the initial params for this comment box
11
+
12
+ React.render(
13
+ React.create_element(
14
+ CommentBox, url: "comments.json", poll_interval: 2),
15
+ Element['#content']
16
+ )
17
+ end
18
+
19
+ class CommentBox
20
+
21
+ # A react component is simply a class that has a "render" method.
22
+
23
+ # But including React::Component mixin provides a nice dsl, and many other features
24
+
25
+ include React::Component
26
+
27
+ # Components can have parameters that are passed in when the component is first "mounted"
28
+ # and then updated as the application state changes. In this case url, and poll_interval will
29
+ # never change since this is the top level component.
30
+
31
+ required_param :url
32
+ required_param :poll_interval
33
+
34
+ # Components also may have internal state variables, which are like instance variables,
35
+ # with one added feature: Changing state causes a rerender to occur.
36
+
37
+ # The "comments" state is being initialized by parsing the javascript object at window.initial_comments
38
+ # This is not a react feature, but was just set up in the HTML header (see config.ru for how this was done).
39
+
40
+ define_state comments: JSON.from_object(`window.initial_comments`)
41
+
42
+ # The following call backs are made during the component lifecycle:
43
+
44
+ # before_mount before component is first rendered
45
+ # after_mount after component is first rendered, after DOM is loaded. ONLY CALLED ON CLIENT
46
+ # before_receive_props when component is being about to be rerendered by an outside state change. CANCELLABLE
47
+ # before_update just before a rerender, and not cancellable.
48
+ # after_update after DOM has been updated.
49
+ # before_unmount before component instance will be removed. Use this to kill low level handlers etc.
50
+
51
+ # just to show off how these callbacks work we have separated setting up a repeating fetch into three pieces.
52
+
53
+ # before mounting we will initialize a polling loop, but we don't want to start it yet.
54
+
55
+ before_mount do
56
+ @fetcher = every(poll_interval) do # we use the opal browser utility to call the server every poll_interval seconds
57
+ HTTP.get(url) do |response| # notice that params poll_interval, and url are accessed as instance methods
58
+ if response.ok?
59
+ comments! JSON.parse(response.body) # comments!(value) updates the state and notifies react of the state change
60
+ else
61
+ puts "failed with status #{response.status_code}"
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ # once we have things up and displayed lets start polling for updates
68
+
69
+ after_mount do
70
+ puts "start me up!"
71
+ @fetcher.start
72
+ end
73
+
74
+ # finally our component should be a good citizen and stop the polling when its unmounted
75
+
76
+ before_unmount do
77
+ @fetcher.stop
78
+ end
79
+
80
+ # components can have their own methods like any other class
81
+ # in this case we receive a new comment and send it the server
82
+
83
+ def send_comment_to_server(comment)
84
+ HTTP.post(url, payload: comment) do |response|
85
+ puts "failed with status #{response.status_code}" unless response.ok?
86
+ end
87
+ comment
88
+ end
89
+
90
+ # every component must implement a render method. The method must generate a single
91
+ # react virtual DOM element. React compares the output of each render and determines
92
+ # the minimum actual DOM update needed.
93
+
94
+ # A very common mistake is to try generate two or more elements (or none at all.) Either case will
95
+ # throw an error. Just remember that there is already a DOM node waiting for the output of the render
96
+ # hence the need for exactly one element per render.
97
+
98
+ def render
99
+
100
+ # the dsl syntax is simply a method call, with params hash, followed by a block
101
+ # the built in dsl methods correspond to the standard HTML5 tags such as div, h1, table, tr, td, span etc.
102
+ #return div.comment { h1 {"hello"} }
103
+ div class: "commentBox" do # just like <div class="commentBox">
104
+
105
+ h1 { "Comments" } # yep just like <h1>Comments</h1>
106
+
107
+ # Custom components use their class name, as the tag. Notice that the comments state is passed to
108
+ # to the CommentList component. This is the normal React paradigm: Data flows towards the leaf nodes.
109
+
110
+ CommentList comments: comments
111
+
112
+ # Sometimes its necessary for data to move upwards, and react provides several ways to do this.
113
+
114
+ # In this case we need to know when a new comment is submitted. So we pass a callback proc.
115
+
116
+ # The callback takes the new comment and sends it to the server and then pushes it onto the comments list.
117
+ # Again the comments! method is used to signal that the state is changing. The use of the "bang" pseudo
118
+ # operator is important as the value of comments has NOT changed (its still tha same array), but its
119
+ # internal state has.
120
+
121
+ CommentForm submit_comment: lambda { |comment| comments! << send_comment_to_server(comment)}
122
+
123
+ end
124
+ end
125
+
126
+ end
127
+
128
+ # Our second component!
129
+
130
+ class CommentList
131
+
132
+ include React::Component
133
+
134
+ # As we saw above a CommentList component takes a comments parameter
135
+ # Here we introduce optional parameter type checking. The syntax [Hash] means "Array of Hashes"
136
+ # In our case each comment is a hash with an author and text key.
137
+
138
+ # Failure to match the type puts a warning on the console not an error,
139
+ # and only in development mode not production.
140
+
141
+ required_param :comments, type: Array
142
+
143
+ # This is a good place to think more about the component lifecycle. The first time
144
+ # CommentList is mounted, comments will be the initial array of author, text hashes.
145
+ # As new comments are added the component will receive new params. However the component
146
+ # does NOT reinitialize its state. If changes in state are needed as result of incoming param changes
147
+ # the before_receive_props call back can be used.
148
+
149
+ def render
150
+
151
+ # Lets render some comments - all we need to do is iterate over the comments array using the usual
152
+ # ruby "each" method.
153
+
154
+ # This is a good place to clarify how the DSL works. Notice that we use comments.each NOT comments.collect
155
+ # When a tag method (such as div, or Comment) is called its "output" is internally pushed into a render buffer.
156
+ # This simplifies the DSL by separating the control flow from the output, but can sometimes be a bit confusing.
157
+
158
+ div.commentList.and_another_class.and_another do # you can also include the class haml style (tx to @dancinglightning!)
159
+ comments.each do |comment|
160
+ # By now we are getting used to the react paradigm: Stuff comes in, is processed, and then
161
+ # passed to next lower level. In this case we pass along each author-text pair to the Comment component.
162
+ Comment author: comment[:author], text: comment[:text], hash: comment
163
+ end
164
+ end
165
+ end
166
+
167
+ end
168
+
169
+ # Notice that the above CommentList component had no state. Each time its parameters change, it simply re-renders.
170
+ # CommentForm does have internal state as we will see...
171
+
172
+ class CommentForm
173
+
174
+ include React::Component
175
+
176
+ # While declaring the type of a param is optional its handy not only for debug, but also to let React create
177
+ # appropriate helpers based on the type. In this case we are passing in a Proc, and so React will treat the
178
+ # "submit_comment" param specially. Instead of submit_comment returning its value (as the previous params have done)
179
+ # it will call the associated Proc, thus allow CommentForm to communicate state changes back to the parent.
180
+
181
+ required_param :submit_comment, type: Proc
182
+
183
+ # We are going to have 2 state variable. One for each field in the comment. As the user types,
184
+ # these state variables will be updating causing a rerender of the CommentForm (but no other components.)
185
+
186
+ define_state :author, :text
187
+
188
+ def render
189
+ div do
190
+ div do
191
+
192
+ "Author: ".span # Note the shorthand for span { "Author" }. You can do this with br, span, th, td, and para (for p) tags
193
+
194
+ # Now we are going to generate an input tag. Notice how the author state variable is provided. Referencing
195
+ # author is what will cause us to re-render and update the input as the value of author changes.
196
+ # React will optimize the updates so parts that are not changing will not be effected.
197
+
198
+ input.author_name(type: :text, value: author, placeholder: "Your name", style: {width: "30%"}).
199
+ # and we attach an on_change handler to the input. As the input changes we simply update author.
200
+ on(:change) { |e| author! e.target.value }
201
+
202
+ end
203
+
204
+ div do
205
+ # lets have some fun with the text. Same deal as the author except we will use a text area...
206
+ div(style: {float: :left, width: "50%"}) do
207
+ textarea(value: text, placeholder: "Say something...", style: {width: "90%"}, rows: 30).
208
+ on(:change) { |e| text! e.target.value }
209
+ end
210
+ # and lets use Showdown to allow for markdown, and display the mark down to the left of input
211
+ # we will define Showdown later, and it will be our first reusable component, as we will use it twice.
212
+ div(style: {float: :left, width: "50%"}) do
213
+ Showdown markup: text
214
+ end
215
+ end
216
+
217
+ # Finally lets give the use a button to submit changes. Why not? We have come this far!
218
+ # Notice how the submit_comment proc param allows us to be ignorant of how the update is made.
219
+
220
+ # Notice that (author! "") updates author, but returns the current value.
221
+ # This is usually the desired behavior in React as we are typically interested in state changes,
222
+ # and before/after values, not simply doing a chained update of multiple variables.
223
+
224
+ button { "Post" }.on(:click) { submit_comment :author => (author! ""), :text => (text! "") }
225
+
226
+ end
227
+ end
228
+ end
229
+
230
+ # Wow only two more components left! This one is a breeze. We just take the author, and text and display
231
+ # them. We already know how to use our Showdown component to display the markdown so we can just reuse that.
232
+
233
+ class Comment
234
+
235
+ include React::Component
236
+
237
+ required_param :author
238
+ required_param :text
239
+ required_param :hash, type: Hash
240
+
241
+ def render
242
+ div.comment do
243
+ h2.comment_author { author } # NOTE: single underscores in haml style class names are converted to dashes
244
+ # so comment_author becomes comment-author, but comment__author would be comment_author
245
+ # this is handy for boot strap names like col-md-push-9 which can be written as col_md_push_9
246
+ Showdown markup: text
247
+ end
248
+ end
249
+
250
+ end
251
+
252
+ # Last but not least here is our ShowDown Component
253
+
254
+ class Showdown
255
+
256
+ include React::Component
257
+
258
+ required_param :markup
259
+
260
+ def render
261
+
262
+ # we will use some Opal lowlevel stuff to interface to the javascript Showdown class
263
+ # we only need to build the converter once, and then reuse it so we will use a plain old
264
+ # instance variable to keep track of it.
265
+
266
+ @converter ||= Native(`new Showdown.converter()`)
267
+
268
+ # then we will take our markup param, and convert it to html
269
+
270
+ raw_markup = @converter.makeHtml(markup) if markup
271
+
272
+ # React.js takes a very dim view of passing raw html so its purposefully made
273
+ # difficult so you won't do it by accident. After all think of how dangerous what we
274
+ # are doing right here is!
275
+
276
+ # The span tag can be replaced by any tag that could sensibly take a child html element.
277
+ # You could also use div, td, etc.
278
+
279
+ span(dangerously_set_inner_HTML: {__html: raw_markup})
280
+
281
+ end
282
+
283
+ end
284
+
285
+
286
+
287
+
288
+
289
+
290
+