More than once writing view code for a Rails application ends up with a messy template file. The separation of structure and style doesn’t always work out as intended. Frameworks like Bootstrap force you to use nested structures and lots of class attributes. The view code ends up with a lot of duplication and is hard to read as a result. You’ll get away with that for a while, constantly fearing the next redesign…
To tackle this, you probably start writing helper methods. They help to remove logic out of the template and hide overly complex structures behind a nice interface. Unfortunately some view components are more complex to implement and require more than just a single method. This is even more true when you need to configure the view components in some way or another.
Introducing ActionWidgets
In the following, I’ll present you a different approach to this issue: ActionWidget. The gem by Konstantin Tennhard takes helper methods to the next level. The basic idea is simple, instead of writing a complex helper method, you’ll write a class inheriting from ActionWidget::Base
and put it into the app/widgets
directory. The only method you have to implement is the render
method. The ActionWidget gem, will provide you with a simple helper method to use your widget automatically. Let me show you a simple example.
A simple button widget
To illustrate the basic usage of ActionWidget, let’s write a widget to represent a button. The button will have a title, a target, a type (default, primary, or danger) and a size (small, default, or large). For convenience, the gem provides a generator to get you started:
rails generate action_widget:widget button
The generator creates a file called app/widgets/button_widget.rb
for you. This is where the implementation of the button widget lives. An example implementation might look something like this:
# app/widgets/button_widget.rb
class ButtonWidget < ActionWidget::Base
property :title,
:converts => :to_s,
:required => true
property :target,
:converts => :to_s,
:required => true
property :type,
:converts => :to_sym,
:accepts => [:default, :primary, :danger],
:default => :default
property :size,
:converts => :to_sym,
:accepts => [:small, :default, :large],
:default => :default
def render
content_tag(:a, title, :href => target, :class => css_classes)
end
protected
def css_classes
css_classes = ['btn']
css_classes << "btn-#{size}" unless size == :default
css_classes << "btn-#{type}" unless type == :default
css_classes
end
end
The ActionWidget gem depends on Smart Properties to provide the property
macro method. This allows you to add simple configuration values to your widget. It’ll also do conversion and input validation for you. With the implementation above, you’ll be forced to always provide a title and a target attribute for every button. Otherwise the widget will complain with an exception.
Using the button widget in your view is as simple as a call to the (automatically generated) button_widget
method:
button_widget :title => 'Sign Up',
:target => signup_path,
:type => :primary,
:size => :large
Now that you have a basic understanding of the idea behind ActionWidgets, let’s dive into a more complex example.
A more advanced example
Tabs are a pretty common user interface element and their implementation is pretty straight forward. Let’s look at the implementation of tabs, on a site using Twitter Bootstrap:
<ul class="nav nav-tabs" id="myTab">
<li><a href="#first">First</a></li>
<li class="active"><a href="#second">Second</a></li>
<li><a href="#third">Third</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane" id="first">...</div>
<div class="tab-pane active" id="second">...</div>
<div class="tab-pane" id="third">...</div>
</div>
To implement the tabs, you need a list with links for each tab and a collection of tab panes. For every href
attribute of each link, there must be a corresponding tab pane with its id
attribute set accordingly. Additionally, the active
class has to match, or your users will be a bit confused when clicking through the tabs. Of course when adding the tabs to one of your pages, you’ll have to remember all the details of the markup that’s required. There has to be both the nav
and nav-tabs
class in the ul
tag, and don’t forget the wrapping div
tag with class tab-content
around all the tab-pane
elements. That’s a lot to remember, and a lot to mess up. If you don’t screw it up now, then probably later when you try to change something quickly. To make your life a little easier, I’ll show you how to use ActionWidgets to hide the markup behind this expressive interface.
= tabs_widget do |t|
= t.tab 'First' do
Content of the first tab
= t.tab 'Second', :active => true do
Content of the second tab
= t.tab 'Third' do
Content of the third tab
I’m using Haml in this example, but that’s not a requirement. ActionWidgets work with all of your templates, as long as they support helper methods.
Building the TabsWidget
Let’s start by generating the TabsWidget
class. Again, it’s as simple as creating a file called app/widgets/tabs_widget.rb
or running the generator.
class TabsWidget < ActionWidget::Base
def render(&block)
navigation = content_tag(:ul, :class => ['nav', 'nav-tabs']) do
capture(self, &block)
end
contents = content_tag(:div, :class => ['tab-content']) do
tabs.each do |tab|
concat(tab.render)
end
end
navigation + contents
end
def tab(*args)
# We'll implement this soon...
end
private
def tabs
@tabs ||= []
end
end
Every ActionWidget has to implement a render
method. If the generated helper method gets passed a block, it will be handed to this method as well. Every ActionWidget has delegators to all the methods in your view. We use this to our advantage and use capture
to get the contents of the block. We also pass self
, which makes it possible to call the widget’s tab
method from within the block. The captured contents get wrapped in the necessary ul
tag.
Next we create a div
tag to wrap the tab contents. We iterate over the collection of tabs and render each one. By using concat
we make sure, that the rendered tab-pane actually gets appended to the view.
With this code in place, we don’t get an error when trying to view the page we’re embedding the tabs widget in, but we don’t see the tabs either.
Adding a nested TabWidget
The general idea behind ActionWidget is to use objects instead of a set of methods to generate markup code. In this spirit, let’s add another ActionWidget to represent a single tab. It’s pretty useless without the wrapping tabs widget, so let’s add it as a nested class:
class TabsWidget < ActionWidget::Base
class Tab < ActionWidget::Base
property :name,
:required => true,
:converts => :to_s
property :active,
:accepts => [true, false],
:default => false
property :target,
:converts => :to_s
property :content,
:required => true
def render
content_tag(:div, :id => target, :class => ['tab-pane'] + css_classes, &content)
end
def render_navigation
content_tag(:li, :class => ['tab'] + css_classes) do
link_to(name, '#' + target)
end
end
def target
super || name.parameterize
end
private
def css_classes
css_classes = []
css_classes << 'active' if active
css_classes
end
end
# ...
end
As you can see, the class is quite simple. Apart from defining a set of properties, it has two render methods. One to render the tab pane, and another one to render the navigation link. They both make use of the target
method so the link’s href
attribute matches the tab pane’s id
attribute. The private css_classes
method makes sure, both elements get the active
class when the tab is active.
Now everything we need to do is to tie the TabsWidget
and the TabsWidget::Tab
together, usin the TabsWidget#tab
method.
def tab(name, options = {}, &block)
options.merge!({
:name => name,
:content => block
})
tab = Tab.new(view, options)
tabs << tab
tab.render_navigation
end
This method creates a new instance of the TabsWidget::Tab
widget, while passing both the view context and a options hash to it. Afterwards it adds the tab to the collection and renders the navigation link.
Now we have everything in place to render tab widgets with a few lines of nice and expressive code. Should you ever need to change the implementation of all tab widgets on your website, you now have a single place to do this.
Conclusion
ActionWidget allows you to separate the concept of a view component from the actual implementation. In contrast to the presenter pattern, the widgets don’t necessarily need to wrap a model object. As they are implemented in classes, it’s easy to extend them. Additionally the possibility to use private methods internally makes their code easier to maintain than standard helper methods.
Hopefully this short introduction made you curious about the ActionWidget approach and gives you some pointers to implement some (or all?) of your user interface components using this gem. Unfortunately there’s no documentation for it yet, but as the gem’s code is pretty simple and I covered the basic idea in this article, you should be able to get started easily.