Open Source Adventures: Episode 63: Accessing Browser APIs Directly with Opal Ruby
Tags: javascript, ruby
Published: 2022-05-25 13:40:39

Opal Ruby really wants to be used with some wrappers and frameworks, but let's start by accessing browser APIs directly.

Note that this is not how you'd use Opal Ruby in the real world. You'd use various wrappers, and for basic browser APIs there's a lot of choice.

However if you write anything nontrivial, you'll need to interact with various new APIs and JavaScript packages, and then you'll have a choice of either going in raw, or writing wrappers.

First, let's get through all the boilerplate.

### `index.html`

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Click Counter</title>
    <link rel="stylesheet" href="./app.css">
  </head>
  <body>
    <button>Click</button>
    <div id="summary""></div>
    <script src="./app.js"></script>
  </body>
</html>
```

### `app.css`

```css
body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5px;
}
```

### `Gemfile`

We just need `opal`, and `rake` for build command.

```ruby
source "https://rubygems.org"

gem "opal", "~> 1.5"
gem "rake", "~> 13.0"
```

### `Rakefile`

```ruby
task default: "app.js"

file "app.js" => "app.rb" do
  sh "opal -c app.rb >app.js"
end
```

### `app.rb`

And now the actual code!

```ruby
win = `window`
document = win.JS[:document]
summary = document.JS.querySelector("#summary")
button = document.JS.querySelector("button")

count = 0

button.JS.addEventListener("click") do
  count += 1
  summary.JS[:innerText] = "You clicked #{count} times"
end
```

The first thing to note is that you can run any JavaScript code with backticks. We'll need it to access the `window` object.

We'd like to name that variable `window`, but that would then compiles to `var window = window` which doesn't actually work.

If we have a JavaScript object, we can use it in two ways - `.JS[]` accesses JavaScript property, and `.JS.foo()` does a function call. We need both syntaxes as `win.JS.document` would compile to `win.document()`, not quite what we want.

Let's see what all this compiles to:

```javascript
Opal.queue(function(Opal) {/* Generated by Opal 1.5.0 */
  var nil = Opal.nil, $rb_plus = Opal.rb_plus, win = nil, document = nil, summary = nil, button = nil, count = nil;

  Opal.add_stubs('+');

  win = window;
  document = win["document"];
  summary = document.querySelector("#summary");
  button = document.querySelector("button");
  count = 0;
  return button.addEventListener("click", function $$1(){
    count = $rb_plus(count, 1);
    return summary["innerText"] = "You clicked " + (count) + " times";}, 0);
});
```

There's a bunch of extra `return`s due to mismatch between Ruby and JavaScript, but they're mostly harmless. If you'd like to get rid of them, just add some `nil`s at ends of blocks. `return` at end of the whole script makes a lot less sense, but I guess they didn't care enough to special case it.

### `this` problem

Did you spot another problem? Why is Opal Ruby using `function` not `=>` there, and wouldn't that cause issues?

Of course it does. Let's modify the app slightly, by adding some `puts`, which performs `console.log`:

```ruby
win = `window`
document = win.JS[:document]
summary = document.JS.querySelector("#summary")
button = document.JS.querySelector("button")

count = 0

button.JS.addEventListener("click") do
  puts "Button clicked!"
  count += 1
  summary.JS[:innerText] = "You clicked #{count} times"
end

puts "Application started!"
```

This gets compiled to:

```javascript
Opal.queue(function(Opal) {/* Generated by Opal 1.5.0 */
  var self = Opal.top, nil = Opal.nil, $rb_plus = Opal.rb_plus, win = nil, document = nil, summary = nil, button = nil, count = nil;

  Opal.add_stubs('puts,+');

  win = window;
  document = win["document"];
  summary = document.querySelector("#summary");
  button = document.querySelector("button");
  count = 0;
  button.addEventListener("click", function $$1(){var self = $$1.$$s == null ? this : $$1.$$s;
    self.$puts("Button clicked!");
    count = $rb_plus(count, 1);
    return summary["innerText"] = "You clicked " + (count) + " times";}, {$$arity: 0, $$s: self});
  return self.$puts("Application started!");
});
```

`Application started!` part works just fine, but `Button clicked!` crashes miserably, as it incorrectly changes `self` to the wrong one.

If Opal Ruby was updated to use `=>` like modern JavaScript code, `this` inside the block would be correct, so the problem would be averted.

### Story so far

As you can see, Opal Ruby interoperability with JavaScript world is quite poor. Fortunately we can sweep that nasty stuff into a wrapper, and there's plenty of wrappers for all the common things like browser APIs. If you want to seriously use Opal Ruby, you'll be writing a lot of wrappers as well.

And it's also high time for Opal Ruby to target ES6 - sticking to pre-ES6 code causes unnecessary issues like `this` issue we ran into.

I deployed this on GitHub Pages, [you can see it here](https://taw.github.io/open-source-adventures/episode-63/).

[All the code is on GitHub](https://github.com/taw/open-source-adventures).

### Coming next

That's enough Opal Ruby for now. In the next episode we'll explore another technology.