Adding Custom Radius Tags
THIS ARTICLE IS A WORK IN PROGRESS
Radius tags are flexible tools. They allow you to wrap high level logic around content in order to get fine-grained control over your output with minimal effort.
This article will take you through creating an extension where you define a custom radius tag, via spec driven development. It will encapsulate a common task of wrapping a bit content in an HTML block that would be a pain to rewrite over and over again. The basic structure of the output of this tag will look like this:
<div class="box">
<h2>
<img src="/images/icons/happyface.png" />
Spiffy box title
</h2>
<div class="content">
This is where the main content of our box will go. Lots
of HTML or radius tags can go here.
</div>
</div>
That’s a lot of boiler plate HTML that we can encapsulate in a reusable and easy to remember radius tag. We will wrap that all up in a nice clean tag like this:
<r:box icon="happyface" title="Spiffy box title">
This is where the main content of our box will go. Lots
of HTML or radius tags can go here.
</r:box>
Any custom tags you implement will be added to form an extension. This way, if you create a tag that is useful to you, you can just move over the extension into other project very easily.
So let’s create the extension skeleton. Type this in a command prompt set to your radiant root directory.
./script/generate extension custom_tags
You should see that it created a bunch of files in vendor/extensions/custom_tags
. This is where your extension now lives. Open up your favorite code editor and look around in that directory. You should see the following items:
-
README
: Contains a description of your extension. What it does, how you use it, who to thank, that sort of thing. -
custom_tags_extension.rb
: Extension initialization file. All ruby code in here is ran when the server starts up. It’s used to load necessary files and inject functionality into Radiant. -
app
: Works just like theapp
folder in any rails application. It’s gotcontrollers
,helpers
,models
andviews
. Any classes added to these directories will be loaded and available in your radiant instance. -
db
: Stores migrations. If your extension modifies or adds to the database structure the migrations that make those changes will go in here. -
lib
: Any code libraries that dont strictly fit in the Rails model/view/controller approach will go here. Any rake tasks that your extension may need to add would go in thelib/tasks
folder. -
spec
: You should be developing your extensions with a spec driven process. This is where the proof that your extension works as expected should live.
Your first stop should be custom_tags_extension.rb
.
class CustomTagsExtension < Radiant::Extension
version "1.0"
description "Describe your extension here"
url "http://yourwebsite.com/custom_tags"
# define_routes do |map|
# map.connect 'admin/custom_tags/:action', :controller => 'admin/custom_tags'
# end
def activate
# admin.tabs.add "Custom Tags", "/admin/custom_tags", :after => "Layouts", :visibility => [:all]
end
def deactivate
# admin.tabs.remove "Custom Tags"
end
end
-
version
is the the current version of the extension. An important value for public extensions. -
description
is some brief text about what your plugin does -
url
is the address of your extension’s homepage. -
activate
is the method that is called when your extension turns on. This is where you can add tabs or do other initialization tasks. -
deactivate
is an artifact of an older version of Radiant where extensions were turned on or off. Turning extensions off didn’t work quite right, and there wasn’t a compelling reason to keep the functionality (since it’s just simpler to remove/uninstall the extension) so it was dropped. Thedeactivate
method is still generated for you, but at no point does the current Radiant call this method. Basically, you can ignore it.
You will also see a commented section showing how you can define custom routes for your extension. In the case of this tutorial, we only need to set the description in this file for now. Something like “Adds some useful custom tags for this site.” would do it.
Now let’s write some code.
Lets write a spec that will outline what we are trying to do. We have defined a syntax for our radius tag, and we know what the output is supposed to look like. With those two bits of information, and some snippets from the Radiant spec helpers, we have everything we need to write a spec.
Create a new directory at spec/lib
of your extension, and then create a new file in that directory named custom_tags_spec.rb
. Here is what the content should look like.
require File.dirname(__FILE__) + '/../spec_helper'
describe 'CustomTags' do
dataset :pages
describe '<r:box>' do
it 'should render the correct HTML' do
tag = '<r:box icon="happyface" title="Test Title">Content</r:box>'
expected = %{<div class="box">
<h2>
<img src="/images/icons/happyface.png" />
Test Title
</h2>
<div class="content">
Content
</div>
</div>}
pages(:home).should render(tag).as(expected)
end
end
end
Let’s unpack this a bit.
-
require
grabs and loads the radiant spec framework. This loads up your radiant instance, and gives you access to all kinds of methods and matchers will need in any specs you may write. -
describe CustomTags
tells us we will be creating a module or class named “CustomTags” (same as our extension name, imagine that). What goes inside the this describe block are specifications that this class will implement. -
dataset :pages
loads up an RSpec scenario defined by Radiant. It makes ready a small library of page objects that you can mess around with. In Radiant a page object is what does the rendering of tags, so we need one if we are going to spec our own tag rendering. -
describe '<r:box>'
sets up a description content for just this tag. As you implement other tags in the future, they can have their own section too. -
tag=
defines the text we would type into the radiant page administration screen in order to trigger the tag. -
expected=
defines what we want the output of the tag to be. -
pages(:home).should render(tag).as(expected)
performs the test.pages
is a method added by the:pages
scenario that allows us access to page object. The easiest one to find is the:home
page object. Radiant defines arender(tag).as(expected)
matcher for testing tag rendering. All you have to do is plug in the values and it will let you know if your rendering succeeded or failed.
Let’s run our spec and see how we did. To run the specs, go back to your command prompt and change into the root directory of your extension, and simply run rake spec (and if you not already have bootstraped your test db, do that now: rake test db:bootstrap).
cd vendor/extensions/custom_tags
rake spec
It should fail. This is good. In spec (or test) driven development, you implement a specification first, then make sure it fails. When it fails, you fix what it tells you the problem is, and then you run the spec again. Repeat this process until your spec passes, and now you know your work is done.
The spec failure should say something about “undefined tag ‘box’”. So its time to define our tag. Our approach will be to create a module that defines the tags, and then include that module into Radiant. Add a file to vendor/extensions/custom_tags/lib/custom_tags.rb
with this content:
module CustomTags
include Radiant::Taggable
desc "Creates an HTML box with a title, icon and body content"
tag "box" do |tag|
""
end
end
In order to define tags, you need to include Radiant’s Radiant::Taggable
module. This mainly gives you access to the tag
method that allows declaration of new radius tags. The tag
method accepts a name for the tag, and a block that defines what the block does. For now, just return an empty string, since our spec failures have not told us what to do there yet. The desc
method works like rake
tasks; you simply provide a string that details how to use your tag and what it does.
Run your specs again. The message should be the same. We have a module that defines a tag, but Radiant doesn’t know that. We need to include our module into Radiant so the tags it defines become usable. In your initialization file custom_tags_extension.rb
add the following line to your activate
method.
Page.send :include, CustomTags
This inserts the CustomTags
module into the Page
model class. Now tags that we define in CustomTags
should be found by Radiant.
Run your specs again.
'CustomTags <r:box> should render the correct HTML' FAILED
expected "<r:box icon=\"happyface\" title=\"Test Title\">\n Content\n</r:box>\n" to render as "<div class=\"box\">\n <h2>\n <img src=\"/images/icons/happyface.png\" />\n Test Title\n </h2>\n <div class=\"content\">\n Content\n </div>\n</div>\n", but got "\n"
The old failure message is gone! This means it found our tag. The failure now is that the expected content doesn’t match the rendered content. Now lets implement the content the tag renders. We can simply copy out from the spec the desired result, and paste it in our tag block. Now we can insert some ruby snippets in this string to bring it to life.
module CustomTags
include Radiant::Taggable
desc "Creates an HTML box with a title, icon and body content"
tag "box" do |tag|
%{<div class="box">
<h2>
<img src="/images/icons/#{tag.attr['icon']}.png" />
#{tag.attr['title']}
</h2>
<div class="content">
#{tag.expand}
</div>
</div>}
end
end
When you define a tag, the block yields a tag
object. This object represents the tag that is being rendered, and holds all its content and attributes that were defined by the content author. It has two useful methods for our goal.
-
tag.attr['attribute_name']
allows you hash based access to the attributes of the tag. In this case, we want thetitle
andicon
attribtues. -
tag.expand
will render the content of the tag (text between opening and closing tags). In our case this is just text, but Radiant will render any other radius tags that exist in its content as real radius tags. This is what allows you to fill radius tags with other radius tags allowing very dynamic content.
Run your specs.
.
Finished in 0.48277 seconds
1 example, 0 failures
Congrats! You should have a new working tag! Time to boot up radiant and try it out. Launch your server, and add:
<r:box icon="happyface" title="I did it!">Foo</r:box>
to some page content. View the source of the genrated page and it should be looking good.
You may not always want to specify an icon and a title. Perhaps your box has a default icon you want to use most of the time, and you want to be able to have a blank title. This is easy to add. But first, a spec!
Add this to your existing custom_tags_spec.rb
it "should have a default icon and allow a blank title" do
tag = '<r:box>Content</r:box>'
expected = %{<div class="box">
<h2>
<img src="/images/icons/sadface.png" />
</h2>
<div class="content">
Content
</div>
</div>}
pages(:home).should render(tag).as(expected)
end
(at least firefox does not render the four spaces in the empty title line, so be sure to include them in your spec test)
As you can see the tag to be rendered now includes no attributes. But in the expected output we expect an icon of “sadface” and a blank line for the title. Run your specs.
Failure, good. In the rendered output, the tag is returning /images/icons/.png
for the image url, and we are expecting /images/icons/sadface.png
. Time to updagrade the box tag logic a bit.
module CustomTags
include Radiant::Taggable
desc "Creates an HTML box with a title, icon and body content"
tag "box" do |tag|
%{<div class="box">
<h2>
<img src="/images/icons/#{tag.attr['icon'] || 'sadface'}.png" />
#{tag.attr['title']}
</h2>
<div class="content">
#{tag.expand}
</div>
</div>}
end
end
All that was added was tag.attr['icon'] || 'sadface'
. Since tag.attr
just returns a hash, it will return nil
for any key that is not present. So using the ||
operator will return the default of “sadface” if the icon
attribute wasn’t set. Without a title attribute, simply, nothing is rendered in that spot. So that already works as expected.
Run your specs.
..
Finished in 0.484197 seconds
2 examples, 0 failures
Full pass! Nice job.