diff --git a/Gemfile b/Gemfile index d926697..0d9f3cc 100644 --- a/Gemfile +++ b/Gemfile @@ -1,2 +1,3 @@ source 'https://rubygems.org' -gemspec \ No newline at end of file +gemspec +gem 'opal', :git => "https://github.com/catprintlabs/opal.git" \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 78c7688..2e933ac 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,46 +1,48 @@ +GIT + remote: https://github.com/catprintlabs/opal.git + revision: 3682db6f2b23a584787a68d5338a7ee9cb35565a + specs: + opal (0.7.1) + hike (~> 1.2) + sourcemap (~> 0.1.0) + sprockets (>= 2.2.3, < 4.0.0) + tilt (~> 1.4) + PATH remote: . specs: react.rb (0.0.2) - opal (~> 0.7.0) + opal opal-activesupport GEM remote: https://rubygems.org/ specs: hike (1.2.3) - multi_json (1.10.1) - opal (0.7.1) - hike (~> 1.2) - sourcemap (~> 0.1.0) - sprockets (>= 2.2.3, < 4.0.0) - tilt (~> 1.4) opal-activesupport (0.1.0) opal (>= 0.5.0, < 1.0.0) opal-jquery (0.3.0) opal (~> 0.7.0) - opal-rspec (0.4.1) + opal-rspec (0.4.2) opal (~> 0.7.0) rack (1.6.0) rack-protection (1.5.3) rack - react-source (0.12.2) - sinatra (1.4.5) + react-source (0.13.2) + sinatra (1.4.6) rack (~> 1.4) rack-protection (~> 1.4) - tilt (~> 1.3, >= 1.3.4) + tilt (>= 1.3, < 3) sourcemap (0.1.1) - sprockets (2.12.3) - hike (~> 1.2) - multi_json (~> 1.0) + sprockets (3.0.3) rack (~> 1.0) - tilt (~> 1.1, != 1.3.0) tilt (1.4.1) PLATFORMS ruby DEPENDENCIES + opal! opal-jquery opal-rspec react-source (~> 0.12) diff --git a/README.md b/README.md index 531dd72..a5e6662 100644 --- a/README.md +++ b/README.md @@ -9,89 +9,98 @@ It lets you write reactive UI components, with Ruby's elegance and compiled to r ## Installation +Currently this branch (0.8 in catprint labs) is being used with the following configuration. +It is suggested to begin with this +set of gems which is known to work and then add / remove them as needed. Let us know if you discover anything. + +Currently we are using rails 3.x + ```ruby # Gemfile -gem "react.rb" +# gem 'react-rails', git: "https://github.com/catprintlabs/react-rails.git" # include if you want integration with rails +gem 'opal' + +# gem 'opal', git: "https://github.com/catprintlabs/opal.git" # use this if you are stuck on rails 3.x +# gem 'opal-jquery', git: "https://github.com/catprintlabs/opal-jquery.git" # same as above + +# include if you want integration with rails +# gem 'opal-rails' + +# while not absolutely necessary you will probably want this at least for timers and such +# gem 'opal-browser' + +gem 'opal-react', git: "https://github.com/catprintlabs/react.rb.git", :branch => 'opal-0.8' + +# access active record models from opal! +# gem 'reactive_record', git: "https://github.com/catprintlabs/reactive-record.git" + +# include if you want to use bootstrap +# gem 'react-bootstrap-rails' ``` and in your Opal application, ```ruby -require "opal" +require "opal-react" require "react" React.render(React.create_element('h1'){ "Hello World!" }, `document.body`) ``` -For integration with server (Sinatra, etc), see setup of [TodoMVC](example/todos) or the [official docs](http://opalrb.org/docs/) of Opal. - -## Usage - -### A Simple Component +For a complete example covering most key features, as well as integration with a server (Sinatra, etc), see setup of [Examples](example/tutorial). For additional information on integrating Opal with a server see the [official docs](http://opalrb.org/docs/) of Opal. -A ruby class which define method `render` is a valid component. +## React Overview -```ruby -class HelloMessage - def render - React.create_element("div") { "Hello World!" } - end -end +### Basics -puts React.render_to_static_markup(React.create_element(HelloMessage)) +The biggest problem with react is that its almost too simple. -# => '
Hello World!
' -``` +In react you define components. Components are simply classes that have a "render" method. The render method "draws" a chunk of +HTML. -### More complicated one - -To hook into native ReactComponent life cycle, the native `this` will be passed to the class's initializer. And all corresponding life cycle methods (`componentDidMount`, etc) will be invoked on the instance using the snake-case method name. +Here is a very simple component: ```ruby -class HelloMessage - def initialize(native) - @native = Native(native) - end - def component_will_mount - puts "will mount!" - end +require 'opal' +require 'opal-react' +class Hello def render - React.create_element("div") { "Hello #{@native[:props][:name]}!" } + "hello world" end end - -puts React.render_to_static_markup(React.create_element(HelloMessage, name: 'John')) - -# => will_mount! -# => '
Hello John!
' ``` -### React::Component - -Hey, we are using Ruby, simply include `React::Component` to save your typing and have some handy methods defined. +Include the `React::Component` mixin in a class to turn it into a react component ```ruby +require 'opal' +require 'opal-react' + class HelloMessage - include React::Component + + include React::Component # will create a new component named HelloMessage + MSG = {great: 'Cool!', bad: 'Cheer up!'} - define_state(:foo) { "Default greeting" } + optional_param :mood + required_param :name + define_state :foo, "Default greeting" - before_mount do - self.foo = "#{self.foo}: #{MSG[params[:mood]]}" + before_mount do # you can define life cycle callbacks inline + foo! "#{name}: #{MSG[mood]}" if mood # change the state of foo using foo!, read the state using foo end - after_mount :log + after_mount :log # you can also define life cycle callbacks by reference to a method def log puts "mounted!" end - - def render - div do - span { self.foo + " #{params[:name]}!" } + + def render # render method MUST return just one component + div do # basic dsl syntax component_name(options) { ...children... } + span { "#{foo} #{name}!" } # all html5 components are defined with lower case text end end end @@ -100,17 +109,16 @@ class App include React::Component def render - present HelloMessage, name: 'John', mood: 'great' + HelloMessage name: 'John', mood: :great # new components are accessed via the class name end end -puts React.render_to_static_markup(React.create_element(App)) - -# => '
Default greeting: Cool! John!
' +# later we will talk about nicer ways to do this: For now wait till doc is loaded +# then tell React to create an "App" and render it into the document body. -React.render(React.create_element(App), `document.body`) +`window.onload = #{lambda {React.render(React.create_element(App), `document.body`)}}` -# mounted! +# -> console says: mounted! ``` * Callback of life cycle could be created through helpers `before_mount`, `after_mount`, etc @@ -204,6 +212,47 @@ end # => ... for 5 times then stop ticking after 5 seconds ``` + +### A Simple Component + +A ruby class which define method `render` is a valid component. + +```ruby +class HelloMessage + def render + React.create_element("div") { "Hello World!" } + end +end + +puts React.render_to_static_markup(React.create_element(HelloMessage)) + +# => '
Hello World!
' +``` + +### More complicated one + +To hook into native ReactComponent life cycle, the native `this` will be passed to the class's initializer. And all corresponding life cycle methods (`componentDidMount`, etc) will be invoked on the instance using the snake-case method name. + +```ruby +class HelloMessage + def initialize(native) + @native = Native(native) + end + + def component_will_mount + puts "will mount!" + end + + def render + React.create_element("div") { "Hello #{@native[:props][:name]}!" } + end +end + +puts React.render_to_static_markup(React.create_element(HelloMessage, name: 'John')) + +# => will_mount! +# => '
Hello John!
' +``` ## Example * React Tutorial: see [example/react-tutorial](example/react-tutorial), the original CommentBox example. diff --git a/example/examples/Gemfile b/example/examples/Gemfile new file mode 100644 index 0000000..13b2a66 --- /dev/null +++ b/example/examples/Gemfile @@ -0,0 +1,7 @@ +source 'http://rubygems.org' + +gem 'opal' +gem 'opal-react', :path => '../..' +gem 'sinatra' +#gem 'opal-jquery' +gem 'react-source' \ No newline at end of file diff --git a/example/examples/Gemfile.lock b/example/examples/Gemfile.lock new file mode 100644 index 0000000..32b4b16 --- /dev/null +++ b/example/examples/Gemfile.lock @@ -0,0 +1,45 @@ +PATH + remote: ../.. + specs: + opal-react (0.1.1) + opal + opal-activesupport + +GEM + remote: http://rubygems.org/ + specs: + hike (1.2.3) + opal (0.8.0) + hike (~> 1.2) + sourcemap (~> 0.1.0) + sprockets (~> 3.1) + tilt (>= 1.4) + opal-activesupport (0.1.0) + opal (>= 0.5.0, < 1.0.0) + opal-jquery (0.4.0) + opal (>= 0.7.0, < 0.9.0) + rack (1.6.4) + rack-protection (1.5.3) + rack + react-source (0.13.3) + sinatra (1.4.6) + rack (~> 1.4) + rack-protection (~> 1.4) + tilt (>= 1.3, < 3) + sourcemap (0.1.1) + sprockets (3.2.0) + rack (~> 1.0) + tilt (2.0.1) + +PLATFORMS + ruby + +DEPENDENCIES + opal + opal-jquery + opal-react! + react-source + sinatra + +BUNDLED WITH + 1.10.2 diff --git a/example/examples/config.ru b/example/examples/config.ru new file mode 100644 index 0000000..c7fdbfb --- /dev/null +++ b/example/examples/config.ru @@ -0,0 +1,44 @@ +# config.ru +require 'bundler' +Bundler.require + +require "react/source" + +Opal::Processor.source_map_enabled = true + +opal = Opal::Server.new {|s| + s.append_path './' + s.append_path File.dirname(::React::Source.bundled_path_for("react-with-addons.js")) + s.main = 'example' + s.debug = true +} + +map opal.source_maps.prefix do + run opal.source_maps +end rescue nil + +map '/assets' do + run opal.sprockets +end + +get '/example/:example' do + example = params[:example] + <<-HTML + + + + Example: #{example}.rb + + + + + + + +
+ + + HTML +end + +run Sinatra::Application diff --git a/example/examples/hello.js.rb b/example/examples/hello.js.rb new file mode 100644 index 0000000..16f8c5e --- /dev/null +++ b/example/examples/hello.js.rb @@ -0,0 +1,43 @@ +require 'opal' +require 'opal-react' + +class HelloMessage + + include React::Component # will create a new component named HelloMessage + + MSG = {great: 'Cool!', bad: 'Cheer up!'} + + optional_param :mood + required_param :name + define_state :foo, "Default greeting" + + before_mount do + foo! "#{name}: #{MSG[mood]}" if mood # change the state of foo using foo!, read the state using foo + end + + after_mount :log # notice the two forms of callback + + def log + puts "mounted!" + end + + def render # render method MUST return just one component + div do # basic dsl syntax component_name(options) { ...children... } + span { "#{foo} #{name}!" } # all html5 components are defined with lower case text + end + end + +end + +class App + include React::Component + + def render + HelloMessage name: 'John', mood: :great # new components are accessed via the class name + end +end + +# later we will talk about nicer ways to do this: For now wait till doc is loaded +# then tell React to create an "App" and render it into the document body. + +`window.onload = #{lambda {React.render(React.create_element(App), `document.body`)}}` \ No newline at end of file diff --git a/example/react-tutorial/Gemfile b/example/react-tutorial/Gemfile index 374aa04..e1e9e74 100644 --- a/example/react-tutorial/Gemfile +++ b/example/react-tutorial/Gemfile @@ -1,6 +1,7 @@ source 'https://rubygems.org' -gem 'react.rb', :path => '../..' +gem 'opal-react', :path => '../..' +gem 'opal-browser' gem 'sinatra' gem 'opal-jquery' gem 'react-source' diff --git a/example/react-tutorial/Gemfile.lock b/example/react-tutorial/Gemfile.lock index e95e5ee..58d7a3b 100644 --- a/example/react-tutorial/Gemfile.lock +++ b/example/react-tutorial/Gemfile.lock @@ -1,45 +1,49 @@ PATH remote: ../.. specs: - react.rb (0.0.1) - opal (~> 0.6.0) + opal-react (0.2.1) + opal opal-activesupport GEM remote: https://rubygems.org/ specs: hike (1.2.3) - json (1.8.2) - multi_json (1.10.1) - opal (0.6.3) - source_map - sprockets + opal (0.8.0) + hike (~> 1.2) + sourcemap (~> 0.1.0) + sprockets (~> 3.1) + tilt (>= 1.4) opal-activesupport (0.1.0) opal (>= 0.5.0, < 1.0.0) - opal-jquery (0.2.0) - opal (>= 0.5.0, < 1.0.0) - rack (1.6.0) + opal-browser (0.1.0.beta1) + opal (>= 0.5.5) + paggio + opal-jquery (0.4.0) + opal (>= 0.7.0, < 0.9.0) + paggio (0.2.4) + rack (1.6.4) rack-protection (1.5.3) rack - react-source (0.12.2) - sinatra (1.4.5) + react-source (0.13.3) + sinatra (1.4.6) rack (~> 1.4) rack-protection (~> 1.4) - tilt (~> 1.3, >= 1.3.4) - source_map (3.0.1) - json - sprockets (2.12.3) - hike (~> 1.2) - multi_json (~> 1.0) + tilt (>= 1.3, < 3) + sourcemap (0.1.1) + sprockets (3.2.0) rack (~> 1.0) - tilt (~> 1.1, != 1.3.0) - tilt (1.4.1) + tilt (2.0.1) PLATFORMS ruby DEPENDENCIES + opal-browser opal-jquery + opal-react! react-source - react.rb! sinatra + +BUNDLED WITH + 1.10.2 diff --git a/example/react-tutorial/_comments.json b/example/react-tutorial/_comments.json index 4b07af5..554e6dd 100755 --- a/example/react-tutorial/_comments.json +++ b/example/react-tutorial/_comments.json @@ -2,5 +2,13 @@ { "author": "Pete Hunt", "text": "Hey there!" + }, + { + "author": "Mitch", + "text": "i ***am*** saying something!" + }, + { + "author": "Jan", + "text": "well what are you saying *dear*" } ] \ No newline at end of file diff --git a/example/react-tutorial/config.ru b/example/react-tutorial/config.ru index 0903b8b..d75c476 100644 --- a/example/react-tutorial/config.ru +++ b/example/react-tutorial/config.ru @@ -15,7 +15,7 @@ opal = Opal::Server.new {|s| map opal.source_maps.prefix do run opal.source_maps -end +end rescue nil map '/assets' do run opal.sprockets @@ -26,6 +26,12 @@ get '/comments.json' do JSON.generate(comments) end +get '/comments.js' do + content_type "application/javascript" + comments = JSON.parse(open("./_comments.json").read) + "window.initial_comments = #{JSON.generate(comments)}" +end + post "/comments.json" do comments = JSON.parse(open("./_comments.json").read) comments << JSON.parse(request.body.read) @@ -42,8 +48,10 @@ get '/' do - + + +
diff --git a/example/react-tutorial/example.js.rb b/example/react-tutorial/example.js.rb new file mode 100644 index 0000000..cf01b44 --- /dev/null +++ b/example/react-tutorial/example.js.rb @@ -0,0 +1,290 @@ +require 'opal' +require 'browser' # gives us wrappers on javascript methods such as setTimer and setInterval +require 'opal-jquery' # gives us a nice wrapper on jQuery which we will use mainly for HTTP calls +require "json" # json conversions +require 'opal-react' # and the whole reason we are gathered here today! + +Document.ready? do # Document.ready? is a opal-jquery method. The block will run when doc is loaded + + # render an instance of the CommentBox component at the '#content' element. + # url and poll_interval are the initial params for this comment box + + React.render( + React.create_element( + CommentBox, url: "comments.json", poll_interval: 2), + Element['#content'] + ) +end + +class CommentBox + + # A react component is simply a class that has a "render" method. + + # But including React::Component mixin provides a nice dsl, and many other features + + include React::Component + + # Components can have parameters that are passed in when the component is first "mounted" + # and then updated as the application state changes. In this case url, and poll_interval will + # never change since this is the top level component. + + required_param :url + required_param :poll_interval + + # Components also may have internal state variables, which are like instance variables, + # with one added feature: Changing state causes a rerender to occur. + + # The "comments" state is being initialized by parsing the javascript object at window.initial_comments + # This is not a react feature, but was just set up in the HTML header (see config.ru for how this was done). + + define_state comments: JSON.from_object(`window.initial_comments`) + + # The following call backs are made during the component lifecycle: + + # before_mount before component is first rendered + # after_mount after component is first rendered, after DOM is loaded. ONLY CALLED ON CLIENT + # before_receive_props when component is being about to be rerendered by an outside state change. CANCELLABLE + # before_update just before a rerender, and not cancellable. + # after_update after DOM has been updated. + # before_unmount before component instance will be removed. Use this to kill low level handlers etc. + + # just to show off how these callbacks work we have separated setting up a repeating fetch into three pieces. + + # before mounting we will initialize a polling loop, but we don't want to start it yet. + + before_mount do + @fetcher = every(poll_interval) do # we use the opal browser utility to call the server every poll_interval seconds + HTTP.get(url) do |response| # notice that params poll_interval, and url are accessed as instance methods + if response.ok? + comments! JSON.parse(response.body) # comments!(value) updates the state and notifies react of the state change + else + puts "failed with status #{response.status_code}" + end + end + end + end + + # once we have things up and displayed lets start polling for updates + + after_mount do + puts "start me up!" + @fetcher.start + end + + # finally our component should be a good citizen and stop the polling when its unmounted + + before_unmount do + @fetcher.stop + end + + # components can have their own methods like any other class + # in this case we receive a new comment and send it the server + + def send_comment_to_server(comment) + HTTP.post(url, payload: comment) do |response| + puts "failed with status #{response.status_code}" unless response.ok? + end + comment + end + + # every component must implement a render method. The method must generate a single + # react virtual DOM element. React compares the output of each render and determines + # the minimum actual DOM update needed. + + # A very common mistake is to try generate two or more elements (or none at all.) Either case will + # throw an error. Just remember that there is already a DOM node waiting for the output of the render + # hence the need for exactly one element per render. + + def render + + # the dsl syntax is simply a method call, with params hash, followed by a block + # the built in dsl methods correspond to the standard HTML5 tags such as div, h1, table, tr, td, span etc. + #return div.comment { h1 {"hello"} } + div class: "commentBox" do # just like
+ + h1 { "Comments" } # yep just like

Comments

+ + # Custom components use their class name, as the tag. Notice that the comments state is passed to + # to the CommentList component. This is the normal React paradigm: Data flows towards the leaf nodes. + + CommentList comments: comments + + # Sometimes its necessary for data to move upwards, and react provides several ways to do this. + + # In this case we need to know when a new comment is submitted. So we pass a callback proc. + + # The callback takes the new comment and sends it to the server and then pushes it onto the comments list. + # Again the comments! method is used to signal that the state is changing. The use of the "bang" pseudo + # operator is important as the value of comments has NOT changed (its still tha same array), but its + # internal state has. + + CommentForm submit_comment: lambda { |comment| comments! << send_comment_to_server(comment)} + + end + end + +end + +# Our second component! + +class CommentList + + include React::Component + + # As we saw above a CommentList component takes a comments parameter + # Here we introduce optional parameter type checking. The syntax [Hash] means "Array of Hashes" + # In our case each comment is a hash with an author and text key. + + # Failure to match the type puts a warning on the console not an error, + # and only in development mode not production. + + required_param :comments, type: Array + + # This is a good place to think more about the component lifecycle. The first time + # CommentList is mounted, comments will be the initial array of author, text hashes. + # As new comments are added the component will receive new params. However the component + # does NOT reinitialize its state. If changes in state are needed as result of incoming param changes + # the before_receive_props call back can be used. + + def render + + # Lets render some comments - all we need to do is iterate over the comments array using the usual + # ruby "each" method. + + # This is a good place to clarify how the DSL works. Notice that we use comments.each NOT comments.collect + # When a tag method (such as div, or Comment) is called its "output" is internally pushed into a render buffer. + # This simplifies the DSL by separating the control flow from the output, but can sometimes be a bit confusing. + + div.commentList.and_another_class.and_another do # you can also include the class haml style (tx to @dancinglightning!) + comments.each do |comment| + # By now we are getting used to the react paradigm: Stuff comes in, is processed, and then + # passed to next lower level. In this case we pass along each author-text pair to the Comment component. + Comment author: comment[:author], text: comment[:text], hash: comment + end + end + end + +end + +# Notice that the above CommentList component had no state. Each time its parameters change, it simply re-renders. +# CommentForm does have internal state as we will see... + +class CommentForm + + include React::Component + + # While declaring the type of a param is optional its handy not only for debug, but also to let React create + # appropriate helpers based on the type. In this case we are passing in a Proc, and so React will treat the + # "submit_comment" param specially. Instead of submit_comment returning its value (as the previous params have done) + # it will call the associated Proc, thus allow CommentForm to communicate state changes back to the parent. + + required_param :submit_comment, type: Proc + + # We are going to have 2 state variable. One for each field in the comment. As the user types, + # these state variables will be updating causing a rerender of the CommentForm (but no other components.) + + define_state :author, :text + + def render + div do + div do + + "Author: ".span # Note the shorthand for span { "Author" }. You can do this with br, span, th, td, and para (for p) tags + + # Now we are going to generate an input tag. Notice how the author state variable is provided. Referencing + # author is what will cause us to re-render and update the input as the value of author changes. + # React will optimize the updates so parts that are not changing will not be effected. + + input.author_name(type: :text, value: author, placeholder: "Your name", style: {width: "30%"}). + # and we attach an on_change handler to the input. As the input changes we simply update author. + on(:change) { |e| author! e.target.value } + + end + + div do + # lets have some fun with the text. Same deal as the author except we will use a text area... + div(style: {float: :left, width: "50%"}) do + textarea(value: text, placeholder: "Say something...", style: {width: "90%"}, rows: 30). + on(:change) { |e| text! e.target.value } + end + # and lets use Showdown to allow for markdown, and display the mark down to the left of input + # we will define Showdown later, and it will be our first reusable component, as we will use it twice. + div(style: {float: :left, width: "50%"}) do + Showdown markup: text + end + end + + # Finally lets give the use a button to submit changes. Why not? We have come this far! + # Notice how the submit_comment proc param allows us to be ignorant of how the update is made. + + # Notice that (author! "") updates author, but returns the current value. + # This is usually the desired behavior in React as we are typically interested in state changes, + # and before/after values, not simply doing a chained update of multiple variables. + + button { "Post" }.on(:click) { submit_comment :author => (author! ""), :text => (text! "") } + + end + end +end + +# Wow only two more components left! This one is a breeze. We just take the author, and text and display +# them. We already know how to use our Showdown component to display the markdown so we can just reuse that. + +class Comment + + include React::Component + + required_param :author + required_param :text + required_param :hash, type: Hash + + def render + div.comment do + h2.comment_author { author } # NOTE: single underscores in haml style class names are converted to dashes + # so comment_author becomes comment-author, but comment__author would be comment_author + # this is handy for boot strap names like col-md-push-9 which can be written as col_md_push_9 + Showdown markup: text + end + end + +end + +# Last but not least here is our ShowDown Component + +class Showdown + + include React::Component + + required_param :markup + + def render + + # we will use some Opal lowlevel stuff to interface to the javascript Showdown class + # we only need to build the converter once, and then reuse it so we will use a plain old + # instance variable to keep track of it. + + @converter ||= Native(`new Showdown.converter()`) + + # then we will take our markup param, and convert it to html + + raw_markup = @converter.makeHtml(markup) if markup + + # React.js takes a very dim view of passing raw html so its purposefully made + # difficult so you won't do it by accident. After all think of how dangerous what we + # are doing right here is! + + # The span tag can be replaced by any tag that could sensibly take a child html element. + # You could also use div, td, etc. + + span(dangerously_set_inner_HTML: {__html: raw_markup}) + + end + +end + + + + + + + diff --git a/example/react-tutorial/example.rb b/example/react-tutorial/example.rb deleted file mode 100644 index ebf88e6..0000000 --- a/example/react-tutorial/example.rb +++ /dev/null @@ -1,104 +0,0 @@ -require 'opal' -require 'opal-jquery' -require "json" -require 'react' - -class Window - def self.set_interval(delay, &block) - `window.setInterval(function(){#{block.call}}, #{delay.to_i})` - end -end - -class Comment - include React::Component - - def render - converter = Native(`new Showdown.converter()`) - raw_markup = converter.makeHtml(params[:children].to_s) - div class_name: "comment" do - h2(class_name: "commentAuthor") { params[:author] } - span(dangerously_set_inner_HTML: {__html: raw_markup}.to_n) - end - end -end - -class CommentList - include React::Component - - def render - div class_name: "commentList" do - params[:data].each_with_index.map do |comment, idx| - present(Comment, author: comment["author"], key: idx) { comment["text"] } - end - end - end -end - -class CommentForm - include React::Component - - def render - f = form(class_name: "commentForm") do - input type: "text", placeholder: "Your name", ref: "author" - input type: "text", placeholder: "Say something...", ref: "text" - input type: "submit", value: "Post" - end - - f.on(:submit) do |event| - event.prevent_default - author = self.refs[:author].dom_node.value.strip - text = self.refs[:text].dom_node.value.strip - return if !text || !author - self.emit(:comment_submit, {author: author, text: text}) - self.refs[:author].dom_node.value = "" - self.refs[:text].dom_node.value = "" - end - end -end - -class CommentBox - include React::Component - after_mount :load_comments_from_server, :start_polling - define_state(:data) { [] } - - def load_comments_from_server - HTTP.get(params[:url]) do |response| - if response.ok? - self.data = JSON.parse(response.body) - else - puts "failed with status #{response.status_code}" - end - end - end - - def start_polling - Window.set_interval(params[:poll_interval]) { load_comments_from_server } - end - - def handle_comment_submit(comment) - comments = self.data - comments.push(comment) - self.data = comments - - HTTP.post(params[:url], payload: comment) do |response| - if response.ok? - self.data = JSON.parse(response.body) - else - puts "failed with status #{response.status_code}" - end - end - end - - def render - div class_name: "commentBox" do - h1 { "Comments" } - present CommentList, data: self.data - present(CommentForm).on(:comment_submit) {|c| handle_comment_submit(c) } - end - end -end - - -Document.ready? do - React.render React.create_element(CommentBox, url: "comments.json", poll_interval: 2000), Element.find('#content').get(0) -end diff --git a/lib/opal-react.rb b/lib/opal-react.rb new file mode 100644 index 0000000..d417a08 --- /dev/null +++ b/lib/opal-react.rb @@ -0,0 +1,24 @@ +if RUBY_ENGINE == 'opal' + require "opal-react/top_level" + require "opal-react/component" + require "opal-react/element" + require "opal-react/event" + require "opal-react/version" + require "opal-react/api" + require "opal-react/validator" + require "opal-react/observable" + require "opal-react/rendering_context" + require "opal-react/state" + require "opal-react/while_loading" + require "opal-react/prerender_data_interface" +else + require "opal" + require "opal-react/version" + require "opal-activesupport" + require "rails-helpers/react_component" + require "opal-react/prerender_data_interface" + require "opal-react/serializers" + + Opal.append_path File.expand_path('../', __FILE__).untaint + Opal.append_path File.expand_path('../../vendor', __FILE__).untaint +end diff --git a/lib/opal-react/api.rb b/lib/opal-react/api.rb new file mode 100644 index 0000000..ee0bfa3 --- /dev/null +++ b/lib/opal-react/api.rb @@ -0,0 +1,177 @@ +module React + + class NativeLibrary + + def self.renames_and_exclusions + @renames_and_exclusions ||= {} + end + + def self.libraries + @libraries ||= [] + end + + def self.const_missing(name) + if renames_and_exclusions.has_key? name + if native_name = renames_and_exclusions[name] + native_name + else + super + end + else + libraries.each do |library| + native_name = "#{library}.#{name}" + native_component = `eval(#{native_name})` rescue nil + React::API.import_native_component(name, native_component) and return name if native_component and `native_component != undefined` + end + name + end + end + + def self.method_missing(n, *args, &block) + name = n + if name =~ /_as_node$/ + node_only = true + name = name.gsub(/_as_node$/, "") + end + unless name = const_get(name) + return super + end + if node_only + React::RenderingContext.build { React::RenderingContext.render(name, *args, &block) }.to_n + else + React::RenderingContext.render(name, *args, &block) + end + rescue + end + + def self.imports(library) + libraries << library + end + + def self.rename(rename_list={}) + renames_and_exclusions.merge!(rename_list.invert) + end + + def self.exclude(*exclude_list) + renames_and_exclusions.merge(Hash[exclude_list.map {|k| [k, nil]}]) + end + + end + + class API + + @@component_classes = {} + + def self.import_native_component(opal_class, native_class) + @@component_classes[opal_class.to_s] = native_class + end + + def self.create_native_react_class(type) + raise "Provided class should define `render` method" if !(type.method_defined? :render) + render_fn = (type.method_defined? :_render_wrapper) ? :_render_wrapper : :render + @@component_classes[type.to_s] ||= %x{ + React.createClass({ + propTypes: #{type.respond_to?(:prop_types) ? type.prop_types.to_n : `{}`}, + getDefaultProps: function(){ + return #{type.respond_to?(:default_props) ? type.default_props.to_n : `{}`}; + }, + componentWillMount: function() { + var instance = this._getOpalInstance.apply(this); + return #{`instance`.component_will_mount if type.method_defined? :component_will_mount}; + }, + componentDidMount: function() { + var instance = this._getOpalInstance.apply(this); + return #{`instance`.component_did_mount if type.method_defined? :component_did_mount}; + }, + componentWillReceiveProps: function(next_props) { + var instance = this._getOpalInstance.apply(this); + return #{`instance`.component_will_receive_props(`next_props`) if type.method_defined? :component_will_receive_props}; + }, + shouldComponentUpdate: function(next_props, next_state) { + var instance = this._getOpalInstance.apply(this); + return #{`instance`.should_component_update?(`next_props`, `next_state`) if type.method_defined? :should_component_update?}; + }, + componentWillUpdate: function(next_props, next_state) { + var instance = this._getOpalInstance.apply(this); + return #{`instance`.component_will_update(`next_props`, `next_state`) if type.method_defined? :component_will_update}; + }, + componentDidUpdate: function(prev_props, prev_state) { + var instance = this._getOpalInstance.apply(this); + return #{`instance`.component_did_update(`prev_props`, `prev_state`) if type.method_defined? :component_did_update}; + }, + componentWillUnmount: function() { + var instance = this._getOpalInstance.apply(this); + return #{`instance`.component_will_unmount if type.method_defined? :component_will_unmount}; + }, + _getOpalInstance: function() { + if (this.__opalInstance == undefined) { + var instance = #{type.new(`this`)}; + } else { + var instance = this.__opalInstance; + } + this.__opalInstance = instance; + return instance; + }, + render: function() { + var instance = this._getOpalInstance.apply(this); + return #{`instance`.send(render_fn).to_n}; + } + }) + } + @@component_classes[type.to_s] + end + + def self.create_element(type, properties = {}, &block) + params = [] + + # Component Spec, Normal DOM, String or Native Component + if @@component_classes[type] + params << @@component_classes[type] + elsif type.kind_of?(Class) + params << create_native_react_class(type) + elsif HTML_TAGS.include?(type) + params << type + elsif type.is_a? String + return React::Element.new(type) + else + raise "#{type} not implemented" + end + + # Passed in properties + props = {} + properties.map do |key, value| + if key == "class_name" && value.is_a?(Hash) + props[lower_camelize(key)] = `React.addons.classSet(#{value.to_n})` + elsif key == "class" + props["className"] = value + elsif ["style", "dangerously_set_inner_HTML"].include? key + props[lower_camelize(key)] = value.to_n + else + props[React::ATTRIBUTES.include?(lower_camelize(key)) ? lower_camelize(key) : key] = value + end + end + params << props.shallow_to_n + + # Children Nodes + if block_given? + children = [yield].flatten.each do |ele| + params << ele.to_n + end + end + return React::Element.new(`React.createElement.apply(null, #{params})`, type, properties, block) + end + + def self.clear_component_class_cache + @@component_classes = {} + end + + private + + def self.lower_camelize(snake_cased_word) + words = snake_cased_word.split("_") + result = [words.first] + result.concat(words[1..-1].map {|word| word[0].upcase + word[1..-1] }) + result.join("") + end + end +end diff --git a/lib/react/callbacks.rb b/lib/opal-react/callbacks.rb similarity index 100% rename from lib/react/callbacks.rb rename to lib/opal-react/callbacks.rb diff --git a/lib/opal-react/component.rb b/lib/opal-react/component.rb new file mode 100644 index 0000000..5b10aaa --- /dev/null +++ b/lib/opal-react/component.rb @@ -0,0 +1,407 @@ +require "opal-react/ext/string" +require 'active_support/core_ext/class/attribute' +require 'opal-react/callbacks' +require "opal-react/ext/hash" +require "opal-react/rendering_context" +require "opal-react/observable" +require "opal-react/state" + +require 'native' + +module React + module Component + + def self.included(base) + base.include(API) + base.include(React::Callbacks) + base.class_eval do + class_attribute :initial_state + define_callback :before_mount + define_callback :after_mount + define_callback :before_receive_props + define_callback :before_update + define_callback :after_update + define_callback :before_unmount + + def render + raise "no render defined" + end unless method_defined? :render + + end + base.extend(ClassMethods) + + parent = base.name.split("::").inject([Module]) { |nesting, next_const| nesting + [nesting.last.const_get(next_const)] }[-2] + parent.class_eval do + + def method_missing(n, *args, &block) + name = n + if name =~ /_as_node$/ + node_only = true + name = name.gsub(/_as_node$/, "") + end + unless name = const_get(name) and name.method_defined? :render + return super + end + if node_only + React::RenderingContext.build { React::RenderingContext.render(name, *args, &block) }.to_n + else + React::RenderingContext.render(name, *args, &block) + end + rescue + puts "rescue in method_missing #{n}" + end + + end + end + + def initialize(native_element) + @native = native_element + end + + def params + Hash.new(`#{@native}.props`) + end + + def refs + Hash.new(`#{@native}.refs`) + end + + def state + raise "No native ReactComponent associated" unless @native + Hash.new(`#{@native}.state`) + end + + def update_react_js_state(object, name, value) + set_state({"#{object.class.to_s+'.' unless object == self}name" => value}) rescue nil # in case we are in render + end + + def emit(event_name, *args) + self.params["_on#{event_name.to_s.event_camelize}"].call(*args) + end + + def component_will_mount + @processed_params = {} + React::State.initialize_states(self, initial_state) + React::State.set_state_context_to(self) { self.run_callback(:before_mount) } + rescue Exception => e + self.class.process_exception(e, self) + end + + def component_did_mount + React::State.set_state_context_to(self) do + self.run_callback(:after_mount) + React::State.update_states_to_observe + end + rescue Exception => e + self.class.process_exception(e, self) + end + + def component_will_receive_props(next_props) + # need to rethink how this works in opal-react, or if its actually that useful within the react.rb environment + # for now we are just using it to clear processed_params + React::State.set_state_context_to(self) { self.run_callback(:before_receive_props, Hash.new(next_props)) } + @processed_params = {} + rescue Exception => e + self.class.process_exception(e, self) + end + + def should_component_update?(next_props, next_state) + React::State.set_state_context_to(self) { self.respond_to?(:needs_update?) ? self.needs_update?(Hash.new(next_props), Hash.new(next_state)) : true } + rescue Exception => e + self.class.process_exception(e, self) + end + + def component_will_update(next_props, next_state) + React::State.set_state_context_to(self) { self.run_callback(:before_update, Hash.new(next_props), Hash.new(next_state)) } + rescue Exception => e + self.class.process_exception(e, self) + end + + + def component_did_update(prev_props, prev_state) + React::State.set_state_context_to(self) do + self.run_callback(:after_update, Hash.new(prev_props), Hash.new(prev_state)) + React::State.update_states_to_observe + end + rescue Exception => e + self.class.process_exception(e, self) + end + + def component_will_unmount + React::State.set_state_context_to(self) do + self.run_callback(:before_unmount) + React::State.remove + end + rescue Exception => e + self.class.process_exception(e, self) + end + + def p(*args, &block) + if block || args.count == 0 || (args.count == 1 && args.first.is_a?(Hash)) + _p_tag(*args, &block) + else + Kernel.p(*args) + end + end + + def component?(name) + name_list = name.split("::") + scope_list = self.class.name.split("::").inject([Module]) { |nesting, next_const| nesting + [nesting.last.const_get(next_const)] }.reverse + scope_list.each do |scope| + component = name_list.inject(scope) do |scope, class_name| + scope.const_get(class_name) + end rescue nil + return component if component and component.method_defined? :render + end + nil + end + + def method_missing(n, *args, &block) + return params[n] if params.key? n + name = n + if name =~ /_as_node$/ + node_only = true + name = name.gsub(/_as_node$/, "") + end + unless (React::HTML_TAGS.include?(name) || name == 'present' || name == '_p_tag' || (name = component?(name, self))) + return super + end + + if name == "present" + name = args.shift + end + + if name == "_p_tag" + name = "p" + end + + if node_only + React::RenderingContext.build { React::RenderingContext.render(name, *args, &block) }.to_n + else + React::RenderingContext.render(name, *args, &block) + end + + end + + def watch(value, &on_change) + React::Observable.new(value, on_change) + end + + def define_state(*args, &block) + React::State.initialize_states(self, self.class.define_state(*args, &block)) + end + + attr_reader :waiting_on_resources + + def _render_wrapper + React::State.set_state_context_to(self) do + RenderingContext.render(nil) {render || ""}.tap { |element| @waiting_on_resources = element.waiting_on_resources if element.respond_to? :waiting_on_resources } + end + rescue Exception => e + self.class.process_exception(e, self) + end + + module ClassMethods + + def backtrace(*args) + @backtrace_on = (args.count == 0 or (args[0] != :off and args[0])) + end + + def process_exception(e, component, reraise = nil) + message = ["Exception raised while rendering #{component}"] + if @backtrace_on + message << " #{e.backtrace[0]}" + message += e.backtrace[1..-1].collect { |line| line } + else + message[0] += ": #{e.message}" + end + message = message.join("\n") + `console.error(message)` + raise e if reraise + end + + def validator + @validator ||= React::Validator.new + end + + def prop_types + if self.validator + { + _componentValidator: %x{ + function(props, propName, componentName) { + var errors = #{validator.validate(Hash.new(`props`))}; + var error = new Error(#{"In component `" + self.name + "`\n" + `errors`.join("\n")}); + return #{`errors`.count > 0 ? `error` : `undefined`}; + } + } + } + else + {} + end + end + + def default_props + validator.default_props + end + + def params(&block) + validator.build(&block) + end + + def define_param_method(name, param_type) + if param_type == React::Observable + (@two_way_params ||= []) << name + define_method("#{name}") do + params[name].instance_variable_get("@value") + end + define_method("#{name}!") do |*args| + if args.count > 0 + current_value = params[name].instance_variable_get("@value") + params[name].call args[0] + current_value + else + current_value = params[name].instance_variable_get("@value") + params[name].call current_value unless @dont_update_state rescue nil # rescue in case we in middle of render + params[name] + end + end + elsif param_type == Proc + define_method("#{name}") do |*args, &block| + params[name].call *args, &block + end + else + define_method("#{name}") do + @processed_params[name] ||= if param_type.respond_to? :_react_param_conversion + param_type._react_param_conversion params[name] + elsif param_type.is_a? Array and param_type[0].respond_to? :_react_param_conversion + params[name].collect { |param| param_type[0]._react_param_conversion param } + else + params[name] + end + end + end + end + + def required_param(name, options = {}) + validator.requires(name, options) + define_param_method(name, options[:type]) + end + + alias_method :require_param, :required_param + + def optional_param(name, options = {}) + validator.optional(name, options) + define_param_method(name, options[:type]) + end + + def define_state(*states, &block) + default_initial_value = (block and block.arity == 0) ? yield : nil + states_hash = (states.last.is_a? Hash) ? states.pop : {} + states.each { |name| states_hash[name] = default_initial_value } + (self.initial_state ||= {}).merge! states_hash + states_hash.each do |name, initial_value| + define_state_methods(self, name, &block) + end + end + + def export_state(*states, &block) + default_initial_value = (block and block.arity == 0) ? yield : nil + states_hash = (states.last.is_a? Hash) ? states.pop : {} + states.each { |name| states_hash[name] = default_initial_value } + React::State.initialize_states(self, states_hash) + states_hash.each do |name, initial_value| + define_state_methods(self, name, self, &block) + define_state_methods(singleton_class, name, self, &block) + end + end + + def define_state_methods(this, name, from = nil, &block) + this.define_method("#{name}") do + React::State.get_state(from || self, name) + end + this.define_method("#{name}=") do |new_state| + yield name, React::State.get_state(from || self, name), new_state if block and block.arity > 0 + React::State.set_state(from || self, name, new_state) + end + this.define_method("#{name}!") do |*args| + #return unless @native + if args.count > 0 + yield name, React::State.get_state(from || self, name), args[0] if block and block.arity > 0 + current_value = React::State.get_state(from || self, name) + React::State.set_state(from || self, name, args[0]) + current_value + else + current_state = React::State.get_state(from || self, name) + yield name, React::State.get_state(from || self, name), current_state if block and block.arity > 0 + React::State.set_state(from || self, name, current_state) + React::Observable.new(current_state) do |new_value| + yield name, React::State.get_state(from || self, name), new_value if block and block.arity > 0 + React::State.set_state(from || self, name, new_value) + end + end + end + end + + def export_component(opts = {}) + export_name = (opts[:as] || name).split("::") + first_name = export_name.first + Native(`window`)[first_name] = add_item_to_tree(Native(`window`)[first_name], [React::API.create_native_react_class(self)] + export_name[1..-1].reverse).to_n + end + + def add_item_to_tree(current_tree, new_item) + if Native(current_tree).class != Native::Object or new_item.length == 1 + new_item.inject do |memo, sub_name| {sub_name => memo} end + else + Native(current_tree)[new_item.last] = add_item_to_tree(Native(current_tree)[new_item.last], new_item[0..-2]) + current_tree + end + end + + end + + module API + #include Native + + alias_native :dom_node, :getDOMNode + alias_native :mounted?, :isMounted + alias_native :force_update!, :forceUpdate + + def set_props(prop, &block) + raise "No native ReactComponent associated" unless @native + %x{ + #{@native}.setProps(#{prop.shallow_to_n}, function(){ + #{block.call if block} + }); + } + end + + def set_props!(prop, &block) + raise "No native ReactComponent associated" unless @native + %x{ + #{@native}.replaceProps(#{prop.shallow_to_n}, function(){ + #{block.call if block} + }); + } + end + + def set_state(state, &block) + raise "No native ReactComponent associated" unless @native + %x{ + #{@native}.setState(#{state.shallow_to_n}, function(){ + #{block.call if block} + }); + } + end + + def set_state!(state, &block) + raise "No native ReactComponent associated" unless @native + %x{ + #{@native}.replaceState(#{state.shallow_to_n}, function(){ + #{block.call if block} + }); + } + end + end + + end +end diff --git a/lib/react/element.rb b/lib/opal-react/element.rb similarity index 60% rename from lib/react/element.rb rename to lib/opal-react/element.rb index ca6ce6e..21b4df7 100644 --- a/lib/react/element.rb +++ b/lib/opal-react/element.rb @@ -1,4 +1,4 @@ -require "react/ext/string" +require "opal-react/ext/string" module React class Element @@ -6,8 +6,17 @@ class Element 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) + def initialize(native_element, type, properties, block) + @type = type + @properties = properties + @block = block @native = native_element end @@ -28,6 +37,21 @@ def on(event_name) end self end + + def method_missing(class_name, args = {}, &new_block) + class_name = class_name.split("__").collect { |s| s.gsub("_", "-") }.join("_") + new_props = properties.dup + new_props["class"] = "#{new_props['class']} #{class_name} #{args.delete("class")} #{args.delete('className')}".split(" ").uniq.join(" ") + new_props.merge! args + RenderingContext.replace( + self, + React::RenderingContext.build { React::RenderingContext.render(type, new_props, &new_block) } + ) + end + + def delete + RenderingContext.delete(self) + end def children nodes = self.props.children diff --git a/lib/react/event.rb b/lib/opal-react/event.rb similarity index 100% rename from lib/react/event.rb rename to lib/opal-react/event.rb diff --git a/lib/react/ext/hash.rb b/lib/opal-react/ext/hash.rb similarity index 100% rename from lib/react/ext/hash.rb rename to lib/opal-react/ext/hash.rb diff --git a/lib/react/ext/string.rb b/lib/opal-react/ext/string.rb similarity index 100% rename from lib/react/ext/string.rb rename to lib/opal-react/ext/string.rb diff --git a/lib/opal-react/observable.rb b/lib/opal-react/observable.rb new file mode 100644 index 0000000..b6f1818 --- /dev/null +++ b/lib/opal-react/observable.rb @@ -0,0 +1,33 @@ +module React + + class Observable + + def initialize(value, on_change = nil, &block) + @value = value + @on_change = on_change || block + end + + def method_missing(method_sym, *args, &block) + @value.send(method_sym, *args, &block).tap { |result| @on_change.call result } + end + + def respond_to?(method, *args) + if [:call, :to_proc].include? method + true + else + @value.respond_to? method, *args + end + end + + def call(new_value) + @on_change.call new_value + @value = new_value + end + + def to_proc + lambda { |arg = @value| @on_change.call arg } + end + + end + +end \ No newline at end of file diff --git a/lib/opal-react/prerender_data_interface.rb b/lib/opal-react/prerender_data_interface.rb new file mode 100644 index 0000000..a469172 --- /dev/null +++ b/lib/opal-react/prerender_data_interface.rb @@ -0,0 +1,90 @@ +module React + + class PrerenderDataInterface + + attr_reader :while_loading_counter + + class << self + + def get_next_while_loading_counter(i) + PrerenderDataInterface.load!.get_next_while_loading_counter(i) + end + + ["preload_css", "css_to_preload!", "cookie"].each do |method_name| + define_method(method_name) do |*args| + PrerenderDataInterface.load!.send(method_name, *args) + end + end + + def load! + (@instance ||= new) + end + + def on_opal_server? + RUBY_ENGINE == 'opal' and !(`typeof window.ServerSidePrerenderDataInterface === 'undefined'`) + end + + def on_opal_client? + RUBY_ENGINE == 'opal' and `typeof window.ServerSidePrerenderDataInterface === 'undefined'` + end + + end + + def on_opal_server? + PrerenderDataInterface.on_opal_server? + end + + def on_opal_client? + PrerenderDataInterface.on_opal_client? + end + + def initialize(controller = nil) + require 'opal-jquery' if RUBY_ENGINE == 'opal' and `typeof window.ServerSidePrerenderDataInterface === 'undefined'` + @controller = controller + @css_to_preload = "" + @while_loading_counter = `ClientSidePrerenderDataInterface.InitialWhileLoadingCounter` unless on_opal_server? rescue nil + @while_loading_counter ||= 0 + end + + attr_accessor :initial_while_loading_counter + + def get_next_while_loading_counter(i) + if on_opal_server? + `window.ServerSidePrerenderDataInterface.get_next_while_loading_counter(1)`.to_i + else + # we are on the server and have been called by the opal side, OR we are the client both work the same + @while_loading_counter += 1 + end + end + + def preload_css(css) + if on_opal_server? + `window.ServerSidePrerenderDataInterface.preload_css(#{css})` + elsif RUBY_ENGINE != 'opal' + @css_to_preload << css << "\n" + end + end + + def css_to_preload! + @css_to_preload.tap { @css_to_preload = "" } + end + + def cookie(name) + if @controller + @controller.send(:cookies)[name] + elsif on_opal_server? + `window.ServerSidePrerenderDataInterface.cookie(#{name})` + end + end + + def generate_next_footer + ("\n\n" + ).html_safe + end unless RUBY_ENGINE == 'opal' + + end + +end diff --git a/lib/opal-react/rendering_context.rb b/lib/opal-react/rendering_context.rb new file mode 100644 index 0000000..cfc30d5 --- /dev/null +++ b/lib/opal-react/rendering_context.rb @@ -0,0 +1,108 @@ +module React + + class RenderingContext + + class << self + attr_accessor :waiting_on_resources + end + + def self.render(name, *args, &block) + #puts "RenderingContext.render(#{name}, [#{args}], #{!!block}), (#{waiting_on_resources})" + @buffer = [] unless @buffer + if block + element = build do + saved_waiting_on_resources = waiting_on_resources + self.waiting_on_resources = nil + result = block.call + # Todo figure out how children rendering should happen, probably should have special method that pushes children into the buffer + # i.e. render_child/render_children that takes Element/Array[Element] and does the push into the buffer + if !name and ( # !name means called from outer render so we check that it has rendered correctly + (@buffer.count > 1) or # should only render one element + (@buffer.count == 1 and @buffer.last != result) or # it should return that element + (@buffer.count == 0 and !(result.is_a? String or result.is_a? Element)) #for convience we will also convert the return value to a span if its a string + ) + #puts "render result incorrect: name: #{name}, @buffer: [#{@buffer}], result: #{result}, result.is_a? String (#{result.is_a? String})" + raise "a components render method must generate and return exactly 1 element or a string" + end + + @buffer << result.to_s if result.is_a? String # For convience we push the last return value on if its a string + @buffer << result if result.is_a? Element and @buffer.count == 0 + if name + #puts "about to create a new element #{name}, [#{args}], { [#{@buffer}] } #{@buffer.last.class.name}" + buffer = @buffer.dup + React.create_element(name, *args) { buffer }.tap do |element| + element.waiting_on_resources = saved_waiting_on_resources || !!buffer.detect { |e| e.waiting_on_resources if e.respond_to? :waiting_on_resources } + #puts "1 #{element}.waiting_on_resources set to #{element.waiting_on_resources}" + end + elsif @buffer.last.is_a? React::Element + @buffer.last.tap { |element| + #puts "2 #{element}.waiting_on_resources is = #{element.waiting_on_resources}" + element.waiting_on_resources ||= saved_waiting_on_resources + #puts "2 #{element}.waiting_on_resources set to #{element.waiting_on_resources}" + } + else + @buffer.last.to_s.span.tap { |element| + element.waiting_on_resources = saved_waiting_on_resources + #puts "3 #{element}.waiting_on_resources set to #{element.waiting_on_resources}" + } + end + end + else + element = React.create_element(name, *args) + element.waiting_on_resources = waiting_on_resources + #puts "4 #{element}.waiting_on_resources set to #{element.waiting_on_resources}" + end + @buffer << element + self.waiting_on_resources = nil + #puts "CLEARED WAITING ON RESOURCES" + element + #puts "HEY !!!!!!!!!!!! this is why it aint clearng #{e}" + #ensure + # waiting_on_resources = nil + # element + end + + def self.build(&block) + current = @buffer + @buffer = [] + return_val = yield @buffer + @buffer = current + return_val + #ensure + # @buffer = current + # return_val + end + + def self.as_node(element) + @buffer.delete(element) + element + end + + class << self; alias_method :delete, :as_node; end + + def self.replace(e1, e2) + @buffer[@buffer.index(e1)] = e2 + end + + end + + class ::Object + + alias_method :old_method_missing, :method_missing + + ["span", "para", "td", "th", "while_loading"].each do |tag| + define_method(tag) do | *args, &block | + args.unshift(tag) + return self.method_missing(*args, &block) if self.is_a? React::Component + React::RenderingContext.render(*args) { self.to_s } + end + end + + def br + return self.method_missing(*["br"]) if self.is_a? React::Component + React::RenderingContext.render("span") { React::RenderingContext.render(self.to_s); React::RenderingContext.render("br") } + end + + end + +end \ No newline at end of file diff --git a/lib/opal-react/serializers.rb b/lib/opal-react/serializers.rb new file mode 100644 index 0000000..b79c759 --- /dev/null +++ b/lib/opal-react/serializers.rb @@ -0,0 +1,15 @@ +[Bignum, FalseClass, Fixnum, Float, Integer, NilClass, String, Symbol, Time, TrueClass].each do |klass| + klass.send(:define_method, :react_serializer) do + as_json + end +end + +BigDecimal.send(:define_method, :react_serializer) { as_json } rescue nil + +Array.send(:define_method, :react_serializer) do + self.collect { |e| e.react_serializer }.as_json +end + +Hash.send(:define_method, :react_serializer) do + Hash[*self.collect { |key, value| [key, value.react_serializer] }.flatten(1)].as_json +end diff --git a/lib/opal-react/state.rb b/lib/opal-react/state.rb new file mode 100644 index 0000000..ce46c26 --- /dev/null +++ b/lib/opal-react/state.rb @@ -0,0 +1,95 @@ +module React + + class State + + class << self + + attr_reader :current_observer + + def initialize_states(object, initial_values) # initialize objects' name/value pairs + states[object].merge!(initial_values || {}) + end + + def get_state(object, name, current_observer = @current_observer) + # get current value of name for object, remember that the current object depends on this state, current observer can be overriden with last param + #puts "get_state(#{object}, #{name}) current_observer = #{current_observer}" + new_observers[current_observer][object] << name if current_observer and !new_observers[current_observer][object].include? name + states[object][name] + end + + def set_state(object, name, value) # set object's name state to value, tell all observers it has changed. Observers must implement update_react_js_state + #puts "set_state(#{object}, #{name}, #{value})" + states[object][name] = value + observers_by_name[object][name].dup.each do |observer| + observer.update_react_js_state(object, name, value) + end + value + end + + def will_be_observing?(object, name, current_observer) + #puts "will_be_observing(#{object}, #{name}, #{current_observer}) new_observers = #{new_observers}" + current_observer and new_observers[current_observer][object].include?(name) + end + + def is_observing?(object, name, current_observer) + #puts "is_observing?(#{object}, #{name}, #{current_observer}) #{observers_by_name[object][name]}" + current_observer and observers_by_name[object][name].include?(current_observer) + end + + def update_states_to_observe(current_observer = @current_observer) # should be called after the last after_render callback, currently called after components render method + #puts "update states to observe current_observer: #{current_observer}, new_observers: [#{new_observers[current_observer]}]" + raise "update_states_to_observer called outside of watch block" unless current_observer + current_observers[current_observer].each do |object, names| + names.each do |name| + observers_by_name[object][name].delete(current_observer) + end + end + observers = current_observers[current_observer] = new_observers[current_observer] + new_observers.delete(current_observer) + observers.each do |object, names| + names.each do |name| + observers_by_name[object][name] << current_observer + end + end + end + + def remove # call after component is unmounted + raise "remove called outside of watch block" unless @current_observer + current_observers[@current_observer].each do |object, names| + names.each do |name| + observers_by_name[object][name].delete(@current_observer) + end + end + current_observers.delete(@current_observer) + end + + def set_state_context_to(observer) # wrap all execution that may set or get states in a block so we know which observer is executing + saved_current_observer = @current_observer + @current_observer = observer + return_value = yield + ensure + @current_observer = saved_current_observer + return_value + end + + def states + @states ||= Hash.new { |h, k| h[k] = {} } + end + + def new_observers + @new_observers ||= Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = [] } } + end + + def current_observers + @current_observers ||= Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = [] } } + end + + def observers_by_name + @observers_by_name ||= Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = [] } } + end + + end + + end + +end \ No newline at end of file diff --git a/lib/react/top_level.rb b/lib/opal-react/top_level.rb similarity index 87% rename from lib/react/top_level.rb rename to lib/opal-react/top_level.rb index f9cfff4..ed94825 100644 --- a/lib/react/top_level.rb +++ b/lib/opal-react/top_level.rb @@ -1,5 +1,6 @@ require "native" require 'active_support' +require 'opal-react/component' module React HTML_TAGS = %w(a abbr address area article aside audio b base bdi bdo big blockquote body br @@ -21,12 +22,13 @@ module React readOnly rel required role rows rowSpan sandbox scope scrolling seamless selected shape size sizes span spellCheck src srcDoc srcSet start step style tabIndex target title type useMap value width wmode dangerouslySetInnerHTML) - + def self.create_element(type, properties = {}, &block) React::API.create_element(type, properties, &block) end def self.render(element, container) + container = `container.$$class ? container[0] : container` component = Native(`React.render(#{element.to_n}, container, function(){#{yield if block_given?}})`) component.class.include(React::Component::API) component @@ -37,14 +39,15 @@ def self.is_valid_element(element) end def self.render_to_string(element) - `React.renderToString(#{element.to_n})` + React::RenderingContext.build { `React.renderToString(#{element.to_n})` } end def self.render_to_static_markup(element) - `React.renderToStaticMarkup(#{element.to_n})` + React::RenderingContext.build { `React.renderToStaticMarkup(#{element.to_n})` } end def self.unmount_component_at_node(node) - `React.unmountComponentAtNode(node)` + `React.unmountComponentAtNode(node.$$class ? node[0] : node)` end + end diff --git a/lib/react/validator.rb b/lib/opal-react/validator.rb similarity index 55% rename from lib/react/validator.rb rename to lib/opal-react/validator.rb index 961165f..40e19d7 100644 --- a/lib/react/validator.rb +++ b/lib/opal-react/validator.rb @@ -1,9 +1,13 @@ module React class Validator + def self.build(&block) - validator = self.new - validator.instance_eval(&block) - validator + self.new.build(&block) + end + + def build(&block) + instance_eval(&block) + self end def initialize @@ -21,7 +25,16 @@ def optional(prop_name, options = {}) options[:required] = false @rules[prop_name] = options end - + + def type_check(errors, error_prefix, object, klass) + is_native = !object.respond_to?(:is_a?) rescue true + if is_native or !object.is_a? klass + unless klass.respond_to? :_react_param_conversion and klass._react_param_conversion object, :validate_only + errors << "#{error_prefix} could not be converted to #{klass}" unless klass._react_param_conversion object, :validate_only + end + end + end + def validate(props) errors = [] props.keys.each do |prop_name| @@ -34,14 +47,19 @@ def validate(props) (@rules.keys - props.keys).each do |prop_name| errors << "Required prop `#{prop_name}` was not specified" if @rules[prop_name][:required] end - - # type + # type checking props.each do |prop_name, value| if klass = @rules[prop_name][:type] - if klass.is_a?(Array) - errors << "Provided prop `#{prop_name}` was not an Array of the specified type `#{klass[0]}`" unless value.all?{ |ele| ele.is_a?(klass[0]) } + is_klass_array = klass.is_a?(Array) and klass.length > 0 rescue nil + if is_klass_array + value_is_array_like = value.respond_to?(:each_with_index) rescue nil + if value_is_array_like + value.each_with_index { |ele, i| type_check(errors, "Provided prop `#{prop_name}`[#{i}]", ele, klass[0]) } + else + errors << "Provided prop `#{prop_name}` was not an Array" + end else - errors << "Provided prop `#{prop_name}` was not the specified type `#{klass}`" unless value.is_a?(klass) + type_check(errors, "Provided prop `#{prop_name}`", value, klass) end end end @@ -52,7 +70,7 @@ def validate(props) errors << "Value `#{value}` for prop `#{prop_name}` is not an allowed value" unless values.include?(value) end end - + errors end diff --git a/lib/opal-react/version.rb b/lib/opal-react/version.rb new file mode 100644 index 0000000..9e4284f --- /dev/null +++ b/lib/opal-react/version.rb @@ -0,0 +1,3 @@ +module React + VERSION = "0.2.2" +end diff --git a/lib/opal-react/while_loading.rb b/lib/opal-react/while_loading.rb new file mode 100644 index 0000000..987ed08 --- /dev/null +++ b/lib/opal-react/while_loading.rb @@ -0,0 +1,189 @@ +module React + + # Adds while_loading feature + # to use attach a .while_loading handler to any element for example + # div { "displayed if everything is loaded" }.while_loading { "displayed while I'm loading" } + # the contents of the div will be switched (using jQuery.show/hide) depending on the state of contents of the first block + + # To notify React that something is loading use React::WhileLoading.loading! + # once everything is loaded then do React::WhileLoading.loaded_at message (typically a time stamp just for debug purposes) + + class WhileLoading + + include React::Component + + required_param :loading + required_param :loaded_children + required_param :loading_children + required_param :element_type + required_param :element_props + optional_param :display, default: "" + + class << self + + def loading! + #puts "loading! current_observer: #{React::State.current_observer}" + React::RenderingContext.waiting_on_resources = true + React::State.get_state(self, :loaded_at) + end + + def loaded_at(loaded_at) + React::State.set_state(self, :loaded_at, loaded_at) + end + + def add_style_sheet + @style_sheet ||= %x{ + $('').appendTo("head") + } + end + + end + + before_mount do + @uniq_id = PrerenderDataInterface.get_next_while_loading_counter + PrerenderDataInterface.preload_css( + ".reactive_record_while_loading_container_#{@uniq_id} > :nth-child(1n+#{loaded_children.count+1}) {\n"+ + " display: none;\n"+ + "}\n" + ) + end + + after_mount do + @waiting_on_resources = loading + WhileLoading.add_style_sheet + %x{ + var node = #{@native}.getDOMNode(); + $(node).children(':nth-child(-1n+'+#{loaded_children.count}+')').addClass('reactive_record_show_when_loaded'); + $(node).children(':nth-child(1n+'+#{loaded_children.count+1}+')').addClass('reactive_record_show_while_loading'); + } + end + + after_update do + @waiting_on_resources = loading + end + + def render + #puts "#{self}.render loading: #{loading} waiting_on_resources: #{waiting_on_resources}" + props = element_props.dup + classes = [props[:class], props[:className], "reactive_record_while_loading_container_#{@uniq_id}"].compact.join(" ") + props.merge!({ + "data-reactive_record_while_loading_container_id" => @uniq_id, + "data-reactive_record_enclosing_while_loading_container_id" => @uniq_id, + class: classes + }) + React.create_element(element_type, props) { loaded_children + loading_children } + end + + end + + class Element + + def while_loading(display = "", &loading_display_block) + + loaded_children = [] + loaded_children = block.call.dup if block + + loading_children = [display] + loading_children = RenderingContext.build do |buffer| + result = loading_display_block.call + buffer << result.to_s if result.is_a? String + buffer.dup + end if loading_display_block + + RenderingContext.replace( + self, + React.create_element( + React::WhileLoading, + loading: waiting_on_resources, + loading_children: loading_children, + loaded_children: loaded_children, + element_type: type, + element_props: properties) + ) + end + + def hide_while_loading + while_loading + end + + end + + module Component + + alias_method :original_component_did_mount, :component_did_mount + + def component_did_mount(*args) + #puts "#{self}.component_did_mount" + original_component_did_mount(*args) + reactive_record_link_to_enclosing_while_loading_container + reactive_record_link_set_while_loading_container_class + end + + alias_method :original_component_did_update, :component_did_update + + def component_did_update(*args) + #puts "#{self}.component_did_update" + original_component_did_update(*args) + reactive_record_link_set_while_loading_container_class + end + + def reactive_record_link_to_enclosing_while_loading_container + # Call after any component mounts - attaches the containers loading id to this component + # Fyi, the while_loading container is responsible for setting its own link to itself + + %x{ + var node = #{@native}.getDOMNode(); + if (!$(node).is('[data-reactive_record_enclosing_while_loading_container_id]')) { + var while_loading_container = $(node).closest('[data-reactive_record_while_loading_container_id]') + if (while_loading_container.length > 0) { + var container_id = $(while_loading_container).attr('data-reactive_record_while_loading_container_id') + $(node).attr('data-reactive_record_enclosing_while_loading_container_id', container_id) + } + } + } + + end + + def reactive_record_link_set_while_loading_container_class + + %x{ + + var node = #{@native}.getDOMNode(); + var while_loading_container_id = $(node).attr('data-reactive_record_enclosing_while_loading_container_id'); + if (while_loading_container_id) { + var while_loading_container = $('[data-reactive_record_while_loading_container_id='+while_loading_container_id+']'); + var loading = (#{waiting_on_resources} == true); + if (loading) { + $(node).addClass('reactive_record_is_loading'); + $(node).removeClass('reactive_record_is_loaded'); + $(while_loading_container).addClass('reactive_record_is_loading'); + $(while_loading_container).removeClass('reactive_record_is_loaded'); + + } else if (!$(node).hasClass('reactive_record_is_loaded')) { + + if (!$(node).attr('data-reactive_record_while_loading_container_id')) { + $(node).removeClass('reactive_record_is_loading'); + $(node).addClass('reactive_record_is_loaded'); + } + if (!$(while_loading_container).hasClass('reactive_record_is_loaded')) { + var loading_children = $(while_loading_container). + find('[data-reactive_record_enclosing_while_loading_container_id='+while_loading_container_id+'].reactive_record_is_loading') + if (loading_children.length == 0) { + $(while_loading_container).removeClass('reactive_record_is_loading') + $(while_loading_container).addClass('reactive_record_is_loaded') + } + } + + } + + } + } + + end + + end + +end \ No newline at end of file diff --git a/lib/rails-helpers/react_component.rb b/lib/rails-helpers/react_component.rb new file mode 100644 index 0000000..ff946d8 --- /dev/null +++ b/lib/rails-helpers/react_component.rb @@ -0,0 +1,39 @@ +begin + + require 'react-rails' + require 'opal-react' #'/prerender_data_interface' + + module React + module Rails + module ViewHelper + + alias_method :pre_opal_react_component, :react_component + + def react_component(module_style_name, props = {}, render_options={}, &block) + js_name = module_style_name.gsub("::", ".") + @prerender_data_interface ||= React::PrerenderDataInterface.new(self) + @prerender_data_interface.initial_while_loading_counter = @prerender_data_interface.while_loading_counter + if render_options[:prerender] + if render_options[:prerender].is_a? Hash + render_options[:prerender][:context] ||= {} + elsif render_options[:prerender] + render_options[:prerender] = {render_options[:prerender] => true, context: {}} + else + render_options[:prerender] = {context: {}} + end + + render_options[:prerender][:context].merge!({"ServerSidePrerenderDataInterface" => @prerender_data_interface}) + + end + + component_rendering = raw(pre_opal_react_component(js_name, props.react_serializer, render_options, &block)) + initial_data_string = raw(@prerender_data_interface.generate_next_footer) #render_options[:prerender] ? @prerender_data_interface.generate_next_footer : "") + + component_rendering+initial_data_string + end + end + end + end + +rescue LoadError +end diff --git a/lib/react.rb b/lib/react.rb deleted file mode 100644 index ed2d044..0000000 --- a/lib/react.rb +++ /dev/null @@ -1,16 +0,0 @@ -if RUBY_ENGINE == 'opal' - require "react/top_level" - require "react/component" - require "react/element" - require "react/event" - require "react/version" - require "react/api" - require "react/validator" -else - require "opal" - require "react/version" - require "opal-activesupport" - - Opal.append_path File.expand_path('../', __FILE__).untaint - Opal.append_path File.expand_path('../../vendor', __FILE__).untaint -end diff --git a/lib/react/api.rb b/lib/react/api.rb deleted file mode 100644 index 79e41a1..0000000 --- a/lib/react/api.rb +++ /dev/null @@ -1,104 +0,0 @@ -module React - class API - @@component_classes = {} - - def self.create_element(type, properties = {}, &block) - params = [] - - # Component Spec or Nomral DOM - if type.kind_of?(Class) - raise "Provided class should define `render` method" if !(type.method_defined? :render) - @@component_classes[type.to_s] ||= %x{ - React.createClass({ - propTypes: #{type.respond_to?(:prop_types) ? type.prop_types.to_n : `{}`}, - getDefaultProps: function(){ - return #{type.respond_to?(:default_props) ? type.default_props.to_n : `{}`}; - }, - getInitialState: function(){ - return #{type.respond_to?(:initial_state) ? type.initial_state.to_n : `{}`}; - }, - componentWillMount: function() { - var instance = this._getOpalInstance.apply(this); - return #{`instance`.component_will_mount if type.method_defined? :component_will_mount}; - }, - componentDidMount: function() { - var instance = this._getOpalInstance.apply(this); - return #{`instance`.component_did_mount if type.method_defined? :component_did_mount}; - }, - componentWillReceiveProps: function(next_props) { - var instance = this._getOpalInstance.apply(this); - return #{`instance`.component_will_receive_props(`next_props`) if type.method_defined? :component_will_receive_props}; - }, - shouldComponentUpdate: function(next_props, next_state) { - var instance = this._getOpalInstance.apply(this); - return #{`instance`.should_component_update?(`next_props`, `next_state`) if type.method_defined? :should_component_update?}; - }, - componentWillUpdate: function(next_props, next_state) { - var instance = this._getOpalInstance.apply(this); - return #{`instance`.component_will_update(`next_props`, `next_state`) if type.method_defined? :component_will_update}; - }, - componentDidUpdate: function(prev_props, prev_state) { - var instance = this._getOpalInstance.apply(this); - return #{`instance`.component_did_update(`prev_props`, `prev_state`) if type.method_defined? :component_did_update}; - }, - componentWillUnmount: function() { - var instance = this._getOpalInstance.apply(this); - return #{`instance`.component_will_unmount if type.method_defined? :component_will_unmount}; - }, - _getOpalInstance: function() { - if (this.__opalInstance == undefined) { - var instance = #{type.new(`this`)}; - } else { - var instance = this.__opalInstance; - } - this.__opalInstance = instance; - return instance; - }, - render: function() { - var instance = this._getOpalInstance.apply(this); - return #{`instance`.render.to_n}; - } - }) - } - - params << @@component_classes[type.to_s] - else - raise "#{type} not implemented" unless HTML_TAGS.include?(type) - params << type - end - - # Passed in properties - props = {} - properties.map do |key, value| - if key == "class_name" && value.is_a?(Hash) - props[lower_camelize(key)] = `React.addons.classSet(#{value.to_n})` - else - props[React::ATTRIBUTES.include?(lower_camelize(key)) ? lower_camelize(key) : key] = value - end - end - params << props.shallow_to_n - - # Children Nodes - if block_given? - children = [yield].flatten.each do |ele| - params << ele.to_n - end - end - - return React::Element.new(`React.createElement.apply(null, #{params})`) - end - - def self.clear_component_class_cache - @@component_classes = {} - end - - private - - def self.lower_camelize(snake_cased_word) - words = snake_cased_word.split("_") - result = [words.first] - result.concat(words[1..-1].map {|word| word[0].upcase + word[1..-1] }) - result.join("") - end - end -end diff --git a/lib/react/component.rb b/lib/react/component.rb deleted file mode 100644 index e454f29..0000000 --- a/lib/react/component.rb +++ /dev/null @@ -1,209 +0,0 @@ -require "react/ext/string" -require 'active_support/core_ext/class/attribute' -require 'react/callbacks' -require "react/ext/hash" - -module React - module Component - def self.included(base) - base.include(API) - base.include(React::Callbacks) - base.class_eval do - class_attribute :init_state, :validator - define_callback :before_mount - define_callback :after_mount - define_callback :before_receive_props - define_callback :before_update - define_callback :after_update - define_callback :before_unmount - end - base.extend(ClassMethods) - end - - def initialize(native_element) - @native = native_element - end - - def params - Hash.new(`#{@native}.props`) - end - - def refs - Hash.new(`#{@native}.refs`) - end - - def state - raise "No native ReactComponent associated" unless @native - Hash.new(`#{@native}.state`) - end - - def emit(event_name, *args) - self.params["_on#{event_name.to_s.event_camelize}"].call(*args) - end - - def component_will_mount - self.run_callback(:before_mount) - end - - def component_did_mount - self.run_callback(:after_mount) - end - - def component_will_receive_props(next_props) - self.run_callback(:before_receive_props, Hash.new(next_props)) - end - - def should_component_update?(next_props, next_state) - self.respond_to?(:needs_update?) ? self.needs_update?(Hash.new(next_props), Hash.new(next_state)) : true - end - - def component_will_update(next_props, next_state) - self.run_callback(:before_update, Hash.new(next_props), Hash.new(next_state)) - end - - def component_did_update(prev_props, prev_state) - self.run_callback(:after_update, Hash.new(prev_props), Hash.new(prev_state)) - end - - def component_will_unmount - self.run_callback(:before_unmount) - end - - def p(*args, &block) - if block || args.count == 0 || (args.count == 1 && args.first.is_a?(Hash)) - _p_tag(*args, &block) - else - Kernel.p(*args) - end - end - - def method_missing(name, *args, &block) - unless (React::HTML_TAGS.include?(name) || name == 'present' || name == '_p_tag') - return super - end - - if name == "present" - name = args.shift - end - - if name == "_p_tag" - name = "p" - end - - @buffer = [] unless @buffer - if block - current = @buffer - @buffer = [] - result = block.call - element = React.create_element(name, *args) { @buffer.count == 0 ? result : @buffer } - @buffer = current - else - element = React.create_element(name, *args) - end - - @buffer << element - element - end - - - module ClassMethods - def prop_types - if self.validator - { - _componentValidator: %x{ - function(props, propName, componentName) { - var errors = #{validator.validate(Hash.new(`props`))}; - var error = new Error(#{"In component `" + self.name + "`\n" + `errors`.join("\n")}); - return #{`errors`.count > 0 ? `error` : `undefined`}; - } - } - } - else - {} - end - end - - def initial_state - self.init_state || {} - end - - def default_props - self.validator ? self.validator.default_props : {} - end - - def params(&block) - self.validator = React::Validator.build(&block) - end - - def define_state(*states) - raise "Block could be only given when define exactly one state" if block_given? && states.count > 1 - - self.init_state = {} unless self.init_state - - if block_given? - self.init_state[states[0]] = yield - end - states.each do |name| - # getter - define_method("#{name}") do - return unless @native - self.state[name] - end - # setter - define_method("#{name}=") do |new_state| - return unless @native - hash = {} - hash[name] = new_state - self.set_state(hash) - - new_state - end - end - end - end - - module API - include Native - - alias_native :dom_node, :getDOMNode - alias_native :mounted?, :isMounted - alias_native :force_update!, :forceUpdate - - def set_props(prop, &block) - raise "No native ReactComponent associated" unless @native - %x{ - #{@native}.setProps(#{prop.shallow_to_n}, function(){ - #{block.call if block} - }); - } - end - - def set_props!(prop, &block) - raise "No native ReactComponent associated" unless @native - %x{ - #{@native}.replaceProps(#{prop.shallow_to_n}, function(){ - #{block.call if block} - }); - } - end - - def set_state(state, &block) - raise "No native ReactComponent associated" unless @native - %x{ - #{@native}.setState(#{state.shallow_to_n}, function(){ - #{block.call if block} - }); - } - end - - def set_state!(state, &block) - raise "No native ReactComponent associated" unless @native - %x{ - #{@native}.replaceState(#{state.shallow_to_n}, function(){ - #{block.call if block} - }); - } - end - end - end -end diff --git a/lib/react/version.rb b/lib/react/version.rb deleted file mode 100644 index 6681dbd..0000000 --- a/lib/react/version.rb +++ /dev/null @@ -1,3 +0,0 @@ -module React - VERSION = "0.0.2" -end diff --git a/logo1.png b/logo1.png new file mode 100644 index 0000000..c845d80 Binary files /dev/null and b/logo1.png differ diff --git a/logo2.png b/logo2.png new file mode 100644 index 0000000..3ddb790 Binary files /dev/null and b/logo2.png differ diff --git a/logo3.png b/logo3.png new file mode 100644 index 0000000..aad01a8 Binary files /dev/null and b/logo3.png differ diff --git a/react.rb.gemspec b/react.rb.gemspec index d862e7d..7cd16ce 100644 --- a/react.rb.gemspec +++ b/react.rb.gemspec @@ -1,8 +1,8 @@ # -*- encoding: utf-8 -*- -require File.expand_path('../lib/react/version', __FILE__) +require File.expand_path('../lib/opal-react/version', __FILE__) Gem::Specification.new do |s| - s.name = 'react.rb' + s.name = 'opal-react' s.version = React::VERSION s.author = 'David Chang' s.email = 'zeta11235813@gmail.com' @@ -16,7 +16,7 @@ Gem::Specification.new do |s| s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.require_paths = ['lib', 'vendor'] - s.add_runtime_dependency 'opal', '~> 0.7.0' + s.add_runtime_dependency 'opal'#, '~> 0.7.0' s.add_runtime_dependency 'opal-activesupport' s.add_development_dependency 'react-source', '~> 0.12' s.add_development_dependency 'opal-rspec' diff --git a/spec/component_spec.rb b/spec/component_spec.rb index 96fa5c9..0444473 100644 --- a/spec/component_spec.rb +++ b/spec/component_spec.rb @@ -351,7 +351,7 @@ def render; div; end } renderToDocument(Foo, bar: 10, lorem: Lorem.new) `window.console = org_console;` - expect(`log`).to eq(["Warning: In component `Foo`\nRequired prop `foo` was not specified\nProvided prop `bar` was not the specified type `String`"]) + expect(`log`).to eq(["Warning: Failed propType: In component `Foo`\nRequired prop `foo` was not specified\nProvided prop `bar` was not the specified type `String`"]) end it "should not log anything if validation pass" do diff --git a/spec/reactjs/index.html.erb b/spec/reactjs/index.html.erb index 8720693..ab3f2ca 100644 --- a/spec/reactjs/index.html.erb +++ b/spec/reactjs/index.html.erb @@ -6,5 +6,6 @@ <%= javascript_include_tag 'react-with-addons' %> <%= javascript_include_tag @server.main %> +
diff --git a/spec/tutorial/tutorial_spec.rb b/spec/tutorial/tutorial_spec.rb new file mode 100644 index 0000000..78bb739 --- /dev/null +++ b/spec/tutorial/tutorial_spec.rb @@ -0,0 +1,37 @@ +require "spec_helper" + +class HelloMessage + include React::Component + def render + div { "Hello World!" } + end +end + +describe "An Example from the react.rb doc" do + + it "produces the correct result" do + expect(React.render_to_static_markup(React.create_element(HelloMessage))).to eq('
Hello World!
') + end + +end + +class HelloMessage2 + include React::Component + define_state(:user_name) { '@catmando' } + def render + div { "Hello #{user_name}" } + end +end + +describe "Adding state to a component (second tutorial example)" do + + it "produces the correct result" do + expect(React.render_to_static_markup(React.create_element(HelloMessage2))).to eq('
Hello @catmando
') + end + + it "renders to the document" do + React.render(React.create_element(HelloMessage2), `document.getElementById('render_here')`) + expect(`document.getElementById('render_here').innerHTML`) =~ 'Hello @catmando' + end + +end \ No newline at end of file