oh yeah, and it all works with bootstrap as well
Here is a more detailed progress report. I know a demo would be even better, but frankly we are working around the clock on rebuilding our current site using Opal + React and just have not had the time! Feel free to view our progress here: http://catprintdemo.fwd.wf/ Hopefully we will be able to extract some demos and tutorials once our conversion is complete next month.
Meanwhile that said I do want to let folks know what we are doing, because we frankly think its fantastically productive.
The basic setup is React.js at the base with an Opal-Ruby wrapper on top of that. The wrapper follows the base semantics of React, but adds a lot of features and syntactic helpers. For example react params, and states are directly accessible as methods in the component. We never interact directly with React.js
Here is a simple component:
class MyComponent
include React::Component
define_state :input_string #states are basically instance variables that cause
#rerendering when they change
def render
div do
input(type: :text, placeholder: "enter some text").
on(:change) { |e| input_string! e.target.value }
if input_string.delete(' ').reverse == input_string.delete(' ')
"that's a palindrome!".span(class: :palindrome)
else
"that is not a palindrome".span(class: :not_palindrome)
end
end
end
end
On top of this is a modified version of react-rails, plus a very simple helper that allows you to directly render a component from a controller, thus the top level component can replace a view. For example
class UserController < ApplicationController
def index
render_component users: Users.active # renders Components::Users::Index passing it the active users
end
end
React-rails also provides prerendering support for your react components. This means that the components will be prerendered on the server and delivered as ready to show html to the browser. All the react code will follow later, and react will take over managing the components state change once its loaded.
On top of this is the reactive_record gem, which provides access to your active record models from within react.
Within your opal-react component you have full access to your ActiveRecord models, and ReactiveRecord provides a stubbed version of ActiveRecord that works both during prerendering and on the client to access your AR models seamlessly within the React
So in Components::Users::Index render method you might have code like this:
users.each do |user|
email_class.div {user.email} # thanks to @dancinglightning for the idea of haml like class prefixes!
name_class.div {user.name}
end
If these lines are executed during prerendering then we are on the server and we simply execute the AR query as normal and would be equivalent to the following ERB
<% users.each do |user| %>
<div class="email-class"><%= user.email %></div>
<div class="name-class"><%= user.name %></div>
<% end %>
Lets say now the value of users changes on the client, for example a new search for users was made. Everything is asynchronous so we don’t know any values yet, so reactive_record will provide dummy values (i.e. empty strings for attributes, and arrays with 1 item for scopes) and queue up a request to the server to get the actual data. When the actual data returns its state changes thus triggering a re-render of any effected components. The requests for data are bundled together and processed on the server as one request to minimize hits to the database as well.
The default behavior is that the user will see the structure of the page form and then a second later get filled with actual values.
The developer need do nothing. However its also possible to add a while_loading attribute to the code block which will be displayed while the data is being fetched. For example:
users.each do |user|
email_class.div {user.email}
name_class.div {user.name}
end.while_loading do
spinner.div {}
end
The last piece of the puzzle is updating the AR models. This all works as expected, you can update any AR model instance (for example user.name = “fred”) and then save the model. The only difference is because things are asynchronous you might attach while_saving, and on_save handlers.
Each attribute and each AR record has its own react state variable, so changes cause immediate update to the appropriate part of the display with no additional code.
Whoops - not quite the last piece: A quick word on testing. Opal-rails comes with Opal-Rspec and we have some little helpers that let you define test cases on components nicely in rspec…
Oh boy - was just reminded that I did not mention react-router. we have an opal-react-router gem that provides a nice wrapper on the react-router library. Again the power of ruby meta-programming allowed us to make a very clean API. Here is an example:
module Components
module Jobs
class Doodle
include React::Router
routes do
route(name: "Gallery", path: "jobs/gallery", handler: Components::Jobs::Gallery)
route(name: "Personalize", path: "jobs/personalize/:background_id", handler: Components::Jobs::Personalize)
redirect(from: "", to: "Gallery")
end
def show
div do
route_handler
end
end
end
end
end
Why???
We needed to solve 3 problems:
-
We were sick and tired of having to deal with 4 or even 5 different languages to deliver functionality to our users. HTML, Ruby, Javascript, ERB, and possibly Coffeescript. Now we have one: We write in ruby. Period.
-
We needed a way to write highly maintainable UI code quickly and efficiently. The problem with UI coding is you get hit at any time from any direction with user inputs, and you have to respond, and keep the UI state correct, and have this state reflected appropriately on the server. Whew. The beauty of React is that it takes this whole problem and says: Just draw the UI based on the current state. Done. Period. Under the hood React will take care of doing that efficiently but you can just think of your UI components as functional entities. State + parameters in, UI out.
-
We had to work within our existing rails code base. Opal and React solve 1 + 2 without requiring any rethinking of our existing system. None. React components simply replace views, and reactive_record allows us to use our existing AR models.
I have been asked several times about why we don’t rewrite react in Opal. To us React is a low level implementation, its a black box, and its more efficiently implemented directly in JS than in Opal, and besides its maintained. There would be no advantage of rewriting it, and its basic feature set and model of operation is so simple its hard to image an alternative model that would be better.
Other things we have thought about and experimented with:
Node.js + React - Does not solve 1) or 3). Besides not to get into a war here, but Javascript is ugly. Sorry. And besides that why would i give up the rich code base available to me in the rails ecosystem, plus a working and simple tool chain?
Ember. This was my original direction a couple of years ago. Does not solve 1, or 3, and in the end it was way to hard to understand to practically solve 2.
Volt. Love volt, and some key ideas for reactive-record came from volt. Originally I tried developing a “third-rail” gem that would interface volt with rails. I gave up. The problem is that it is too monolithic for our use. I still recommend volt to anybody who has a clean sheet project that is mainly UI and will use volts many advantages.
Where are we at?
We almost all 60 of our current views at least partially done, and have about 1 month of coding and testing to go. Its frankly just too easy.
As a side effect we have had a lot more time to concentrate on just making the pages “nice”, with things like on page routing, and responsive UIs. (try out http://catprintdemo.fwd.wf/jobs/gallery )
One fun note - because of the prerendering, there are sections of code that are “meta-isomorphic” in that they actually run in three different environments:
Opal client (the browser), Opal server (during prerendering) and on the server proper.