Clearwater, a front-end app framework in Ruby

In case you haven’t heard about it, I’ve been working on a front-end framework in Ruby called Clearwater. These are a few of its main features:

  • Component architecture
  • Routing
  • Virtual DOM

Components

Components are your bread and butter. They’ll make up most of your app. Here is an example of a Clearwater component:

class Layout
  include Clearwater::Component

  def render
    div({ class_name: 'foo' }, [
      h1({ id: 'hello-world' }, 'Hello, world!'),
    ])
  end
end

Notice how it’s a plain Ruby class that includes the Clearwater::Component mixin and responds to render. The return value from render needs to be one of the following:

  • String
  • Numeric
  • nil
  • VirtualDOM::Node
  • Another Clearwater::Component (or, really, something that responds to render and returns any of these)
  • An array containing any of the above

Also notice the DOM DSL, how we call a div method and an h1 method. Those are defined by the Clearwater::Component mixin. They just generate VirtualDOM::Node objects of the appropriate kind. There are similar methods for each of the HTML5 element types.

Element DSL

The element DSL methods are simple.

div(attributes, content)

The attributes is either a Hash of attributes or nil. The important thing to remember is that you must use the JS DOM API properties, not necessarily HTML-style attributes. For example, the DOM property representing the HTML class attribute is actually className. Because Ruby is traditionally snake_cased rather than camelCased, you can use snake-cased attribute names with Clearwater components, like div({ class_name: 'foo' }).

The content value is any valid return value for render.

Routing

Routes are simple:

router = Clearwater::Router.new do
  route 'blog' => Blog.new do
    route ':article_id' => ArticleReader.new
  end
end

In the above router, the path /blog maps to the Blog component. The path /blog/123 will map to both the Blog and the ArticleReader components and will set params[:article_id] = '123' within your routed components.

Application

Your application wraps your router and your primary component:

app = Clearwater::Application.new(
  component: Layout.new,
  router: router
)

Then, once your document is loaded, you send the app the call message:

$document.ready { app.call }

Any time you need to rerender your app, you simply call app.render. Clearwater will coalesce multiple render calls that come in faster than once per animation frame (about 16.67ms) into a single render, so you can call it as much as you need to without worrying that you’re rendering too frequently.

You also don’t need to throttle render calls while the app isn’t visible to save on CPU cycles. Clearwater will withhold render calls while the app is hidden and automatically rerender when the app becomes visible again.

Example app

This is a fully functional Clearwater application:

require 'opal'
require 'clearwater'

class Layout
  include Clearwater::Component

  def render
    h1(nil, "Hello, world!")
  end
end

app = Clearwater::Application.new(
  component: Layout.new
)

$document.ready { app.call }

I’ll be working on some screencasts to show off how simple it is to develop Clearwater apps. I hope it’s as fun for you as it has been for me!

4 Likes

How easy would it be to just use the VirtualDOM stuff, and not the rest of the framework?

1 Like

I’m not sure I like all the brackets required for this:

div({ class_name: 'foo' }, [
  h1({ id: 'hello-world' }, 'Hello, world!'),
])

It looks less readable to me as I’m accustomed to HTML. But from a dynamic programming stand point I love the design of div(attributes, content). I consider this great for writing meta-programming, but not so great for purely static content. I understand that may be the point in which case this is great!

3 Likes

Great post @jgaskins Clearwater looks really good!

I think @danielpclark makes a valid point, I wonder if you could do something like

div class_name: 'foo' do
  h1 id: 'hello-world' do
    'Hello, world!'
  end
end 

Just out of curiosity, what made you decide on Clearwater as a name?

How easy would it be to just use the VirtualDOM stuff, and not the rest of the framework?

To what end?

To answer your question, it’s pretty easy. Most of the interaction with the virtual DOM is in opal/clearwater/application.rb.

The bracket usage is a little unfortunate, but they’re required for it to parse correctly. The good thing that comes out of that is that it encourages you to use small components.

@AstonJ I thought about that, but I’m not sure I like how it’d need to be implemented, for two reasons:

  1. The idea behind the DSL is that it just returns a value. The block syntax would require in-place modification of the component to track nesting levels and such.
  2. I have concerns about the performance of the block syntax, though admittedly I haven’t benchmarked it. I doubt it could be any faster than an array, though.

I’ll play with it this weekend and see what I can come up with.

2 Likes

To what end?

I’m interested in virtual dom bindings from Ruby. I’m really not interested in full blown front end frameworks (routing, component architectures, etc.).

I’m interested in virtual dom bindings from Ruby. I’m really not interested in full blown front end frameworks (routing, component architectures, etc.).

You might want to check out react.rb, then.

I think what I really want is ruby bindings over GitHub - Matt-Esch/virtual-dom: A Virtual DOM and diffing algorithm

That’s what Clearwater uses for its virtual DOM.

I thought so. That’s why I was asking if it was decoupled enough that the VirtualDOM stuff could be used without buying into the entire framework.

I need to extract it and probably clean it up a bit, but there’s nothing in it that relies on Clearwater.

I had some spare time to try out an implementation of the block-style components that @AstonJ suggested. I’m not 100% sure it’s complete, but here are my findings:

It allows syntax like he suggested:

class Foo
  include Clearwater::BlockComponent

  def render
    div class_name: 'foo' do
      h1 { 'Hello world' }
    end
  end
end

There are three monkey wrenches here. First, because each block has to be executed in the context of a Component::Buffer (a class added to make this syntax work), using bound methods as event handlers breaks. For example, with the existing style, you can do this:

class MyButton
  include Clearwater::Component
  
  def render
    button({ onclick: method(:do_the_thing) }, 'Do the thing')
  end

  def do_the_thing
    alert "You did the thing!"
  end
end

Since the context of the block is the Component::Buffer, which doesn’t have the method that you’re telling it about. The buffer has a delegate, which refers to the component. It will delegate all calls to it via method_missing, but the method method doesn’t respect method_missing. It also causes a problem when using a component’s instance variables — they’re completely inaccessible (without using instance_variable_get, of course, but that’s not at all graceful) from the block.

The second weird thing was that adding new components to the buffer requires passing it to a method.

class Foo
  include Clearwater::BlockComponent

  def render
    div do
      component Bar.new
      component Baz.new
    end
  end
end

One of the things I like about the array style is that I could just stick the component itself in the array.

The other concern I had was performance. For apps that render complex documents, I want it to remain fast. I’m not sure yet about very complex ones, but there is a 4-18% performance penalty on rendering lots of small ones (just a div with an h1 in it). I couldn’t narrow it down much more than that. I’m assuming the high standard deviation is due to GC pauses, which I don’t know if we can remove in the browser.

Rendered 100000 block-style components in 6161ms
Rendered 100000 array-style components in 5520ms

I’m not sold on the block syntax but I’m not totally against it, either. It’s undeniably easier to read, but I hope we can figure out a way around at least the performance and the delegate-scope issue. The nested-component issue, I’m pretty sure, we’d just have to live with.

1 Like

Maybe it’s because of smalltalk or something, but I don’t find the array syntax that distasteful. Building up components in the array and returning them from a method would certainly alleviate the ugliness, and perhaps encourage better habits; I dread one day reading through a nest of a nest of a nest of blocks.

That’s my view on it, as well. David Brady wrote a blog post about parentheses in Ruby, for which I am failing to find a link. The basic gist was that omitting parentheses in his code forces him to do things in smaller pieces because otherwise the parser will barf.

Making small components look nice while large ones look ugly was one of the great side effects of the current design. It’s an example of “make doing the right thing easy and the wrong thing hard” (for my own interpretations of the right and wrong things).

Take these components, for example:

class Layout
  include Clearwater::Component

  def render
    div(nil, [
      AppHeader.new,
      outlet,
      AppFooter.new,
    ])
  end
end

class AppHeader
  include Clearwater::Component

  def render
    header(attributes, [
      img(src: logo_url),
      h1(nil, 'My App'),
      AppNavigation.new,
    ])
  end

  def logo_url
    # ...
  end

  def attributes
    {
      style: {
        # ...
      }
    }
  end
end

class AppFooter
  include Clearwater::Component

  def render
    footer(nil, [
      copyright,
    ])
  end

  def copyright
    p(nil, "Copyright #{Time.now.year} Me, Myself, and I")
  end
end

I don’t find them hard to read, but I understand some people might and I understand that a lot of people won’t factor out parts of the component this way. People will write massive components and then complain that Clearwater is ugly. I don’t know what to do about that.

1 Like

Sorry about the delay in replying - just been a hectic day.

I agree about the performance hit but can’t make my mind up on the block style. Could you give us an example of a typical page please Jamie? I think seeing one side by side with the two styles might help.

Sure, here’s the layout for the Clearwater TodoMVC app: https://github.com/jgaskins/clearwater_todomvc/blob/master/app/components/layout.rb

Demo here: https://clearwater-todomvc.herokuapp.com

That’s interesting about the no paren style. I’ve never read that article, but I also prefer no paren style (I suspect another smalltalk thing) and I tend to break my code up into small bits.

Do you think a templating engine might be an avenue worth pursuing Jamie? I’d worry that otherwise front end devs and designers may find things overwhelming?

I did work on a haml to react.js converter before. That could be converted to work on top of virtualdom etc

2 Likes