Dead simple view layer with Opal and jQuery

Dead simple view layer with Opal and jQuery

Lately I noticed that I started to port from project to project a simple class that implements a basic view layer with Opal and jQuery.

In this post we will rebuild it from scratch, you can consider it as an introduction for both Opal and Ruby, and maybe also a bit of OOP

The View class

If you still generate your HTML on the server (making happy search engines) and progressively add functionality via CSS and JavaScript then this class is probably a good fit.

Let’s start by defining our API. We want an object that:

  • takes charge of a piece of HTML
  • can setup listeners and handlers for events on that HTML node
  • can be easily composed with other objects
  • exposes behavior, hiding implementation

Step 1: An object, representing a piece of HTML

Say we have this HTML, representing a search bar:

<section class="search">
  <form>
    <input type="search" placeholder="Type here"></input>
    <input type="submit" value="Search">
  </form>
</section>

This is how we want to instantiate our view:

Document.ready? do
  element = Element.find('section.search')
  search_bar = SearchBar.new(element)
  search_bar.setup
end

The SearchBar class can look like this:

class SearchBar
  def initialize(element)
    @element = element
  end

  attr_reader :element

  def setup
    # do your thing here…
  end
end

Step 2: Adding behavior

Now that we have a place let’s add some behavior. For example we want to clear the field if the ESC key is pressed and to block the submission if the field is empty. We need to concentrate on the #setup method.

feature 1: “clear the field on ESC”

<pre><code class="ruby">class SearchBar

# …

ESC_KEY = 27

def setup
element.on :keypress do |event|
clear if event.key_code == ESC_KEY
end
end

private

def clear
input.value = ”
end

def input
@input ||= element.find(‘input’)
end
end

As you may have noted we’re memoizing #input and we’re searching the element inside our current HTML subtree. The latter is quite important, especially if you’re coming from jQuery and used to $-search everything every time. Sticking to this convention will avoid down the road those nasty bugs caused by selector ambiguities.

feature 2: “prevent submit if the field’s empty”

<pre><code class="ruby">class SearchBar

# …

def setup
element.on :keypress do |event|
clear if event.key_code == ESC_KEY
end

form.on :submit do |event|
  event.prevent_default if input.value.empty?
end

end

private

def form
@form ||= element.find(‘form’)
end
end

YAY! Sounds like we’re almost done!

Step 3: Extracting the View class

Now seems a good time to extract our view layer, thus leaving the SearchBar class with just the business logic:

class View
  def initialize(element)
    @element = element
  end

  attr_reader :element

  def setup
    # noop, implementation in subclasses
  end
end


class SearchBar < View
  def setup
    # …
  end

  private

  # …
end

Step 4: Topping with some “Usability”

Now that the View class has come to life we can add some sugar to make out lives easier.

Default Selector

We already know that the class will always stick to some specific HTML and its selector, it’s a good thing then to have some sensible defaults while still allowing to customize. We’ll define a default selector at the class definition, this way:

class SearchBar < View
  self.selector = 'section.search'
end

Document.ready? { SearchBar.new.setup }

The implementation looks like this:

class View
  class << self
    attr_accessor :selector
  end

  def initialize(options)
    parent   = options[:parent] || Element
    @element = options[:element] || parent.find(self.class.selector)
  end
end

ActiveRecord style creation

I’d also like to get rid of that Document.ready? that pops up every time. As always let’s define the API first:

class SearchBar < View
  self.selector = 'section.search'
end

SearchBar.create

And then the implementation

class View
  # …

  def self.create(*args)
    Document.ready? { create!(*args) }
    nil
  end

  def self.create!(*args)
    instance = new(*args)
    if instance.exist?
      instances << instance
      instance.setup
    end
    instance
  end

  def exist?
    element.any?
  end

  def self.instances
    @instances ||= []
  end
end

While there we also added a check on element existence and a list of active instances so that we can play nice with JS garbage collection and instantiate the class even if the HTML it needs is missing (e.g. we’re on a different page).

Conclusion

Hope you enjoyed and maybe learned a couple of things about Opal and opal-jquery, below are some links for you:

The full implementation is available in this gist: https://gist.github.com/elia/50a723e6133a645b4858

Opal’s website: http://opalrb.org
Opal API documentation: http://opalrb.org/docs/api
Opal-jQuery API documentation: http://opal.github.io/opal-jquery/doc/master/api
jQuery documentation: http://jqapi.com (unofficial but handy)
Vienna, a complete MVC framework: https://github.com/opal/vienna (unofficial but handy)

Leave a Reply

wpDiscuz