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”
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”
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