Jamie Gaskins

Ruby/Rails developer, coffee addict

Clearwater Pending Projects

Published Jan 10, 2016

I have been working on a lot of things with Clearwater, but I haven't been talking about it publicly as much as I would like. I'm sorry about that.

Here is a list of things I've gotten working well for Clearwater that are as yet unreleased:

  • Server rendering
  • Hot loading in development (updating running code without refreshing the page)
  • Trimmed-down JS payload
  • Referencing rendered DOM nodes

And there's more that I want to do:

  • Documentation
  • Screencasts

Server Rendering

Rendering a client-side app on the server is a hot topic. People want it for a few reasons, namely SEO and faster content delivery. There is an open pull request for it that I'm working on trying to get to a good point to merge in.

The performance impact of server rendering a Clearwater app is unnoticeable. For comparison, when server-rendering a React app with react-rails, the performance impact is immense. I've yet to see a Clearwater app take longer than 2ms to render server-side (that is, if you were passing serialized models to the client with gon or some similar implementation already to remove the need to fetch models after the app initializes, the difference in render times will be that trivial). I recommend trying it out with your app:

gem 'clearwater', github: 'clearwater-rb/clearwater', branch: 'server-render'

Hot loading in development

I've written a gem called clearwater-hot_loader (not yet released) that you can run on the server in development. It checks for changes to Ruby files in your app/assets and assets folders by default (in the case of Rails and Roda), then compiles them and pushes them to the browser over a websocket connection.

On the client side, this is all you need to make that work:

require 'clearwater/hot_loader`

This sets up the websocket, listens for changes, evaluates the updated code and re-renders any Clearwater apps mounted into the document. When figuring out styles and copy, this has been a wonderful time saver.

Trimmed-down JS payload

In Clearwater 0.3.1 and below, we use the opal-browser gem as the DOM abstraction. It was helpful in getting Clearwater going in the beginning, but it compiles to a massive JavaScript payload. The worst part is that about 90% of that code will never get executed in most Clearwater apps.

I wrote a gem called bowser, which provides the minimum DOM API needed to get most Clearwater apps going. It supports DOM elements, DOM events, setTimeout, setInterval, and requestAnimationFrame (this one is also used internally by Clearwater to coalesce renders). It also includes optional AJAX support if you require 'bowser/http'.

Before this change, one of my apps was 122KB minified and gzipped. Afterward, it was 83KB. This also reduced the number of assets from over 200 to about 70, dropping page-load times in development from 2-2.5 seconds down to well below 1 second.

This change has been merged into the master branch, but there hasn't been a gem release for it yet.

Referencing rendered DOM nodes

Using a virtual-DOM can make it difficult to get access to the rendered DOM nodes, but you may need them for a few different reasons. Here are the examples that I can think of just off the top of my head

  • Getting form input values
  • Using a third-party JS library that renders itself into an existing DOM node, like a Google Map, which requires you to own the rendering/updating of that node

Form inputs

I usually use the grand_central gem (disclosure: I wrote that, too) to manage my app state — which includes the values of most form inputs. However, we don't want to assume everyone's doing that. If you're not storing input values in some object that persists between renders, how do you do something like this?

def render
  form({ onsubmit: method(:handle_submit) }, [
    input(type: :email, placeholder: 'Email'),
    input(type: :password, placeholder: 'Password'),
    input(type: :submit, value: 'Login'),
  ])
end

How would you get the values of the email and password fields in this form in the onsubmit handler? You would need access to the input fields to figure that out.

Well, this can be done by giving the virtual-DOM node a Clearwater::DOMReference object:

require 'clearwater/component'
require 'clearwater/dom_reference'

class LoginForm
  include Clearwater::Component

  def initialize
    @email_field = Clearwater::DOMReference.new
    @password_field = Clearwater::DOMReference.new
  end

  def render
    form({ onsubmit: method(:handle_submit), [
      # Notice the dom_ref attribute here
      input(type: :email, dom_ref: @email_field),
      input(type: :password, dom_ref: @password_field),
      input(type: :submit, value: 'Login'),
    ])
  end

  def handle_submit event
    event.prevent
    # Calling .value on the DOMReference objects gives the input value
    email = @email_field.value
    password = @password_field.value
    # ...
  end
end

This feature hasn't been merged into master because I'm still testing it, but it's worked pretty well so far and should make it in soon.

Owning the node

Sometimes instead of just getting a reference of a DOM node, you need to own that node's contents. For example, you may not be rendering HTML-like content to it. You may be using a Google Map, which is updated by using API calls instead.

To accomplish this, we need to be able to tell the virtual-DOM engine to let us handle this node. We can do this using the Clearwater::BlackBoxNode mixin:

require 'clearwater/black_box_node'

class MapContainer
  # Notice we don't include Clearwater::Component here
  include Clearwater::BlackBoxNode

  # The definition of the node you want to use for this one.
  # It defaults to a blank div.
  def node
    Clearwater::Component.div(
      style: {
        width: '50%',
        height: '600px',
      }
    )
  end

  # This method is called when this object is first mounted into the DOM. Use this
  # to set up event listeners, render a map, etc.
  def mount(node)
    # node is the DOM node as a Bowser::Element

    Bowser.window.animation_frame do
      # do a Google Map thing in here. We need to wait until the next animation
      # frame because this actually gets called before the page reflow and GMaps
      # requires that this DOM node be within the rendered document.
    end
  end

  # Use this method to copy over or calculate new state from the previous instance
  # and update the DOM node.
  def update(previous, node)
    # previous is our previous instance
    # node is our DOM node, just as in the mount method
  end

  # unmount is called when this object is removed from the generated virtual-DOM
  # tree during the diff/patch process. Use this to remove event listeners, etc.
  def unmount(node)
    # ...
  end
end

This is also not merged yet until I am sure it works the way we need it to.

Documentation

I've begun work on a documentation site (using Clearwater, because of course I am), but I'm not actually that good at writing docs. I get too caught-up in the minutiae.

If someone else would like to help write docs, please feel free to contact me. You don't need to be an expert. I'll work with you on the docs; I'm just bad at doing it alone. :-)

Screencasts

I've been wanting to work on screencasts, but it's difficult at the moment. Recording and editing video is a time-consuming process. Turns out there's a reason professional screencasters only release 1-2 videos a week. :-)

I might just do a few live recordings with no editing (except maybe automatic noise reduction because that's a single click in iMovie) just to get something going.

Conclusion

Clearwater development is still pretty hot, even though I haven't been talking about it as much as I would prefer.

I'm planning on releasing a 1.0 beta when some of these are ready — especially the documentation. I want people to realize that Clearwater is not just a toy framework I play with in my spare time. I've introduced it at work as a way to improve frame rates in our most performance-intensive app. I've tested nearly every React experiment I've seen in the wild (still working on Ryan Florence's MagicMove, though) and they were all easier to write in Ruby.

How you can help:

  • Contribute documentation, even if it's just a page on the project wiki … which doesn't really exist yet but you can help with that, too! ;-)
  • Ask questions — this is about the most important thing. If you don't understand something about Clearwater, I won't know if you don't tell me. :-) Because I have very intimate knowledge about how Clearwater works, I may not realize that someone who isn't me is having trouble understanding certain concepts. It's okay not to understand something when you're used to a different web-development style.
  • Provide feedback using GitHub issues.

And if nothing else, you'd be surprised at the level of encouragement that a single tweet can provide.

TwitterGithubRss