(← Part I, for those who missed it.)
Wow! I didn’t realize plugins would be so popular. It’s tempting to name my next post “The Complete Guide to What Geoff Ate For Dinner: Part VII.”
I did have the misfortune of writing a popular article during a spell of mayhem at Dreamhost. Several people blogged about the difficulty of keeping Typo running smoothly at the end of last week. So, I did what I should have done a while ago and switched this blog to a speedy VPS server at Rimuhosting. Shared hosts are still a great place for sites with a small codebase or for sites that can use page caching (where an entire HTML file is written to disk). I still have a few sites at Dreamhost.
As it turns out, I’ll be switching again to a dedicated host in a few weeks. I’ll be sharing it with one or two other Rails developers. So you can expect “The Complete Guide to Shared, Virtual, and Dedicated Hosting in Eighteen Parts” sometime this summer.
Today I’ll show you how to package a helper method as a plugin, which will show the basics of making any kind of code-based plugin. I’ll be using a plugin I wrote today based on code from Jeremy Voorhis and Typo. It’s a helper that fills in some missing header tag functionality, such as printing a
doctype declaration at the top of your HTML pages or rendering a
meta_tag for your site’s description and keywords.
The final project is the meta_tags plugin.
Here are some of the methods it defines:
xhtml_doctype :strict html_tag :lang => 'zh' meta_tag 'keywords', 'plugins, rails, tutorials'
The basics steps are:
includes your module into the appropriate class
There are several ways to write code that enhances Rails, but the cleanest way to do it is with a
mixin is a simple module that defines a few methods. The difference is that a mixin module is basically worthless on its own until it is combined with an existing class. Because it is designed to be combined with an existing class, it can use any of the variables or methods of the class it will be a part of.
When you design a plugin, you have to decide how it will be used:
include SomeSpecialModuleto the relevant classes. Your plugin will work this way if you leave
includethe module in your plugin’s
Some classes you might want to automatically enhance are:
ActionView::Basefor helpers. Example: calendar_helper
ActiveRecord::Basefor models. Example: The newer acts_as_taggable
Test::Unit::TestCasefor adding your own
ActionController::Basefor controller methods (but not full actions). Rarely done, but possible. account_location could be used this way (but is implemented differently).
There are several methods in the meta_tags plugin, but the
meta_tag is one of the simplest. Here it is:
# Goes in lib/meta_tag_helper.rb module MetaTagHelper # Renders a meta tag for use in the HEAD section of an html document. def meta_tag(name, value) tag :meta, :name => name, :content => value unless value.blank? end end
Here are a few things to note about this method:
taghelper from Rails. You don’t see the
tagmethod defined in this module because we will mixin to
Topfunky::MetaTagHelper. The Java convention is to use your domain name in reverse, but Ruby doesn’t have a convention like that. It’s up to you to choose something unique.
I want this method to be automatically available to all my views as if it were built-in. For that, I add one line to
# Contents of init.rb ActionView::Base.send :include, MetaTagHelper
ActionView to call itself with the
include method, and to pass my
MetaTagHelper module as the argument. All the methods in
MetaTagHelper are now part of
ActionView and can be called from any view template. That was easy!
In a normal Ruby library I would have to
require 'meta_tag_helper', but Rails does this automatically. In fact, I could drop a full model file into the
lib folder and it would be picked up by Rails without any extra code. My Mint plugin has several models that work this way.
A surprisingly large number of plugins have no tests at all. Part of the reason might be that writing a plugin test is a little bit harder than writing a normal unit test. Since a module is worthless on its own, you must add the rest of the functionality for any Rails methods you call in your new helper. If your method doesn’t use any built-in methods, your job is easier.
There are two ways to run the tests for your plugin. First, you can use the Rake task created by the plugin generator. Navigate to your plugin’s directory and type
rake. Your tests will be run.
The second way is to test all your plugins from the root of your Rails app. Type
rake test:plugins and all tests will be run. NOTE This uses a global test task. Changes to the
Rakefile in your plugin’s directory will not affect the behavior of the
Of course, you need to write some tests first!
A simple method that doesn’t call any built-in Rails methods can be tested like this:
require 'test/unit' # Add your module file here require File.dirname(__FILE__) + '/../lib/meta_tag_helper' class MetaTagTest < Test::Unit::TestCase # Explicitly include the module include MetaTagHelper def test_end_html_tag assert_equal "</html>", end_html_tag end end
The important thing is that you
require your helper file and that you
include your module.
You may remember that I used the built-in
tag helper in my own helper. The steps are the same, but are slightly more complicated since you need to
require the relevant file from Rails and also
include the relevant helper.
Here’s how I do it:
# Rubygems is where Rails is located require 'rubygems' require 'test/unit' require File.dirname(__FILE__) + '/../lib/meta_tag_helper' # Here's the helper file we need require 'action_view/helpers/tag_helper' class MetaTagTest < Test::Unit::TestCase # This is the helper with the 'tag' method include ActionView::Helpers::TagHelper include MetaTagHelper def test_meta_tag output = meta_tag('keywords', 'cat, mouse, squirrel') assert_equal "<meta content=\"cat, mouse, squirrel\" name=\"keywords\" />", output end end
That’s a little bit of extra work, but it works!
Someone asked how to test models that are part of a plugin. The full answer is quite complicated, but here’s are a few options to explore:
Generators and other assorted fun facts.