Opal is a transpiler that converts Ruby code into browser-friendly JavaScript, opening the door for developers to build frontend web applications with Ruby. It’s a particularly intriguing option for backend Ruby developers who want to use the same language across their entire stack.

I recently gave Opal a try myself, incorporating it into a simple realtime todo list demo that I built with RethinkDB and Ruby. My demo is a full-stack Ruby application, with Sinatra on the backend and an Opal-based library called React.rb on the frontend. React.rb wraps Facebook’s popular React framework, adapting it to support idiomatic Ruby conventions. Developers can build React components in native Ruby, using a domain-specific language (DSL) to describe the generated HTML markup.

After a fair amount of tinkering, I succeeded in making my demo application work as expected. Although I found that both Opal and React.rb are still somewhat experimental projects that need further development before they are ready for serious use in production environments, they offer some fascinating possibilities.

Build the backend with RethinkDB and Sinatra

RethinkDB 2.0, released earlier this year, introduced EventMachine integration in the Ruby client driver. Developers can use EventMachine to perform RethinkDB queries in the background, which is ideal for consuming changefeeds. My demo application uses a changefeed to track updates on the todo list and broadcast them to clients via WebSocket:

EM . next_tick do conn = r . connect () r . table ( "todo" ). changes . em_run ( conn ) do | err , change | @clients . each { | c | c . send change . to_json } end end

The backend also uses WebSockets to accept commands from the frontend. The following code tracks WebSocket client connections and handles the incoming commands, adding and updating todo list records as needed:

def query rql rql . run ( conn = r . connect ()). to_json ensure conn . close end def setup_websocket ws ws . on ( :close ) { @clients . delete ws } ws . on ( :open ) { @clients << ws } ws . on :message do | msg | data = JSON . parse msg . data case data [ "command" ] when "add" query r . table ( "todo" ). insert text: data [ "text" ], status: false when "update" query r . table ( "todo" ). get ( data [ "id" ]). update status: data [ "status" ] when "delete" query r . table ( "todo" ). get ( data [ "id" ]). delete () end end end

The main Sinatra route handler checks to see if the incoming request is a WebSocket connection or a regular HTTP request. It will respond accordingly, conveniently making it possible to handle both on the same port:

get "/" do if Faye :: WebSocket . websocket? request . env ws = Faye :: WebSocket . new request . env setup_websocket ws ws . rack_response else haml :index end end

I also use Sinatra to add conventional REST endpoints as needed. For example, I make it possible to retrieve current todo list items with a simple GET request:

get "/api/items" do query r . table ( "todo" ). coerce_to ( "array" ) end

In my config.ru file, I used Opal’s built-in Sprockets integration to configure an asset pipeline. This makes it possible for Sinatra to dynamically serve transpiled assets in response to requests:

Faye :: WebSocket . load_adapter ( "thin" ) react_path = :: React :: Source . bundled_path_for ( "react-with-addons.js" ) $opal = Opal :: Server . new do | s | s . append_path File . dirname react_path s . append_path "client" s . main = "main" end map "/assets" do run $opal . sprockets end $opalinit = Opal :: Processor . load_asset_code ( $opal . sprockets , 'main' )

I also take the opportunity to add React.rb into the mix and configure the Faye WebSockets library to use Thin, the underlying web server that runs the application. I chose Thin because it plays nicely with EventMachine, which I need for my background changefeed.

The global $opalinit variable contains a string of JavaScript code that the page must execute to bootstrap the Opal environment. All you have to do is put its contents into a script tag in the HAML template:

!!! %html %head %title Test %script ( src= "/assets/react-with-addons.js" ) %script ( src= "/assets/main.js" ) %script = $opalinit %body

Build the frontend with React.rb and Opal

Developers who have previous experience with React can expect a relatively easy transition working with React.rb. It’s largely a wrapper, but it does a nice job of adapting React’s underlying concepts so that you can express all of the same functionality in Ruby.

React applications consist of components, which the developer composes to build their frontend. Inside of the component, there’s a render function that generates the associated HTML markup. Components can also emit signals that are handled by parent components. In general, React developers try to minimize the amount of stateful data managed by individual components.

To create a component with React.rb, you define a class and include the React::Component mixin. Inside of the component’s render method, you can use the React.rb templating DSL to build your markup. The following component enables the user to input a new todo list item:

class TodoAdd include React :: Component def render div do input ( type: "text" , placeholder: "Input task name" , ref: "text" ) button { "Add" }. on ( :click ) do text = self . refs [ :text ]. dom_node . value self . emit :add , text: text end end end end

It includes a text entry field and a submission button. When the user clicks the button, the application will execute the on(:click) block, which emits a signal with the contents of the entry box. I also made a component that displays all of the todo list items, emitting a signal when the user toggles one of the checkboxes:

class TodoList include React :: Component def render ul do params [ :items ]. each do | item | li do label do input ( type: "checkbox" , checked: item [ "status" ]). on ( :click ) do | e | self . emit :toggle , id: item [ "id" ], status: e . current_target . checked end span { item [ "text" ] } end end end end end end

The todo list component doesn’t actually store the todo list items itself, it attaches to an items parameter that is provided by the parent component in which it is instantiated. Here’s the top-level component that sets up the application:

class App include React :: Component define_state ( :items ) { [] } after_mount :setup def setup Browser :: HTTP . get ( "/api/items" ). then do | res | self . items = res . json setup_websocket end end def setup_websocket @ws = Browser :: Socket . new () @ws . on ( :open ) { p "Connection opened" } @ws . on ( :close ) { p "Socket closed" } # ... handle WebSocket messages here end def transmit data @ws . puts data . to_json end def render div do present ( TodoList , items: self . items ). on :toggle do | data | transmit command: "update" , id: data [ "id" ], status: data [ "status" ] end present ( TodoAdd ). on :add do | data | transmit command: "add" , text: data [ "text" ] end end end end $document . ready do React . render ( React . create_element ( App ), `document.body` ) end

It creates the items property, which it initially populates by fetching data from the API endpoint defined in Sinatra. In the render method, it displays the TodoList component and the TodoAdd component. When the user adds or toggles a todo list item, the signal handlers will send the appropriate command to the backend via WebSocket.

The Browser::Socket class, which I use to create the WebSocket client connection, comes from a library called [opal-browser][]. In addition to WebSocket support, the library provides Ruby-friendly wrappers around many different standard browser APIs.

With all of that plumbing in place, all the application needs is an on(:message) handler to perform the necessary changes to the todo list when the application receives live updates from the backend changefeed via WebSocket:

@ws . on ( :message ) do | e | data = JSON . parse e . data puts "Received:" , data # Add new item if data [ :new_val ] && ! data [ :old_val ] self . items = self . items << data [ :new_val ] # Update existing item elsif data [ :new_val ] && data [ :old_val ] self . items = self . items . map do | i | i [ "id" ] == data [ :new_val ][ "id" ] ? data [: new_val ] : i end # Remove deleted item elsif ! data [ :new_val ] && data [ :old_val ] self . items = self . items - [ data [ :old_val ]] end end

Caveats and next steps

Although I’m enthusiastic about Opal’s long-term potential, I did face some challenges while I was attempting to build the demo. Parts of the Opal project, particularly the associated libraries like opal-browser, are fragile and under-documented. My initial attempts at using the Opal project’s Vienna framework instead of React.rb were largely unsuccessful.

I also faced some difficulty with debugging while working with Opal. Unlike CoffeeScript, Opal doesn’t really produce a clean 1:1 translation of the code. In order to provide some of the capabilities and behaviors that you would expect to find in a real Ruby environment, it uses a fair amount of runtime code. As a result, the tracebacks and error messages are often very unhelpful. There is, however, some support for source maps, which can take some of the pain out of debugging.

All of those caveats aside, building a demo app that uses Ruby on both the frontend and the backend was an exhilarating experience. Transpilers play an increasingly foundational role in web application development. As projects like WebAssembly gain momentum and make JavaScript a better target for transpilers, I think we can expect to see projects like Opal become better and more practical for day-to-day use. It might not take long for this kind of full-stack Ruby web development to evolve from an esoteric curiosity to a real-world option.

To get started with RethinkDB today, visit our ten-minute guide. To learn more about Opal, visit the Opal website.

Resources: