Blog An exploration of the art and
craft of software development

Yui4Rails Component Tutorial

Posted by Marty Haught on Friday, May 23, 2008

Building YUI components in Yui4Rails

I’m going to go over the process for adding a YUI component into YUI4Rails. The basic philosophy that I have been following is the end result should be a concise, easy to use api for the developer yet allow flexibility for the myriad options that the YUI components offer. Let’s explore this approach with the latest widget that I’ve added, the carousel.

Before we dive into the code, let’s look at how the developer would use the carousel widget on one of his pages.

Using the component

1) Add the yui_includes call to the HEAD section of your layout:

<%= yui_includes :reset, :fonts %>

In the above example, we’ve decided that we also want the reset and fonts css components added to all pages that use this layout. You can just call yui_includes with no arguments.

2) Add the yui_carousel call to the page’s template:

<% yui_carousel("featured_photos", @featured_photos, {:scrollInc => 1, :navMargin => 82, :numVisible => 4}) do %>
    <div class="carousel-prev">
        <div id="prev_arrow"><span class="access">&#171; Left</span></div>
    </div>
    <div class="carousel-next">
        <div id="next_arrow"><span class="access">Right &#187;</span></div>
    </div>

    <div class="carousel-clip-region">
			<ul class="carousel-list">
<%= render :partial => '/photos/featured_photo', :collection => @featured_photos %>
			</ul>
    </div>
<% end %>

Here, we have identified a carousel, featured_photos, that will display a set of four images at once with a scrolling increment of one per click. The first parameter is the id name of the carousel object that we want to use. It is followed by the collection of photos that will populate the carousel list. Next we set several options that differ from the component’s defaults. Finally, the yui_carousel method takes a block which contains the html that you wish to put inside the carousel. Since the carousel control expects very specific html elements to work, I’ve chosen to wrap it but allow the developer to tweak css and html specifics to meet their needs.

Those are the only two steps. Let’s look into what happens under the hood and follow the flow as a component author.

Authoring a component

1) Widget class

The first step for writing a component in Yui4Rails is to write a Widget class. We’ll look at /lib/yui4rails/widgets/carousel.rb as our example.

The intent of a widget class is to take in the required arguments as well as optional ones and output the required pieces of javascript and html that will be required to make the YUI component work.

You start off placing the class into the appropriate module space, Yui4Rails::Widgets::Carousel.

module Yui4Rails
	module Widgets
	  class Carousel
	    def initialize(carousel_id, collection, options = {})	
				@carousel_id = carousel_id
				@collection = collection
				@options = defaults.merge(options)
				@options[:size] = @collection.size
				render_head_script
	    end
	...

The class’s initialize method requires the carousel_id, collection and a hash of options. We merge the options with default values that are defined in the class as well as put the size of the collection into the options. Finally we call the render_head_script method.

render_head_script is simply a method that contains the script output that will go into the head section of the page.


def render_head_script
Yui4Rails::AssetManager.manager.add_script <<-PAGE
YAHOO.util.Event.addListener(window, “load”, function()
{
new YAHOO.extension.Carousel(“#{@carousel_id}”,
{#{@options.keys.map{|key| optional_value(key)}.join(", ")}}
);
});
PAGE
end

We leverage the Yui4Rails::AssetManager’s add_script method to add our script content. This will keep hold of all the head script until the layout is rendered which is handy as it allows us to construct many components before anything is outputted to the page. Also of note here is that AssetManager is a singleton that is referenced through the class method manager. The rest of the content here should be familiar to anyone using YUI components on their own.

The only other methods in the class are private. One is defaults which just contains all the default parameters that I, as a component author, have decided should be automatic in widget’s constructor.

	def defaults
		{
			:numVisible => 3,
			:animationSpeed => 0.5,
			:scrollInc => 3,
			:navMargin => 60,
			:prevElement => "prev_arrow",
			:nextElement => "next_arrow",
			:wrap => true
		}
	end

One note here is that I’ve decided that I want to make a app-wide override for these. So that if you want to use the carousel multiple times on your site and that there are one or more options that you always set you can do it in one place instead of each time.

The other private method is optional_value which will either output the javascript parameters as a hash if they’re present as a key and value in the options hash.

	def optional_value(option, carousel_key = nil)
		carousel_key ||= option
		if @options.has_key?(option) && !@options[option].nil? 
			%\#{carousel_key}: #{@options[option].is_a?(String) ? "'#{@options[option]}'" : @options[option] }\
		else
			""
		end	
	end

2) Asset Manager

The asset manager’s role is to give a convenient and unified interface to indicating what YUI resources you will need for your component. Furthermore as we have seen, it gives a place to add custom script that will be outputted into the head section after template processing is complete.

The main thing you’ll need to do here is to make sure the javascript and css includes are handled. The current implementation is ultra simple and will be enhanced in the near future. There are two methods that will look at to understand how to add to the asset manager as designed

	def process_components			
		@yui_stylesheets = []
		@yui_javascript = []
		
		@components.flatten!

		@yui_stylesheets << "reset/reset-min" if @components.include?(:reset)
		@yui_stylesheets << "fonts/fonts-min" if @components.include?(:fonts)
		add_container_includes if @components.include?(:container)
		add_datatable_includes if @components.include?(:datatable)
		add_charts_includes if @components.include?(:charts)
		add_carousel_includes if @components.include?(:carousel)

		@stylesheets = @yui_stylesheets.uniq
		@javascripts = @yui_javascript.uniq
	end

Process components is where all the javascript files and css files are collected into unique arrays. They are simple strings to the path of the resources as consumed by the stylesheet_link_tag and javascript_include_tag helpers. The path “/yui/” will be prepended for all. Thus for the reset css file you can see it adds the string “reset/reset-min” into the yui_stylesheets array.

Most likely you will need something like what carousel uses. In this case I wrote a method, add_carousel_includes, to handle it for me. If the :carousel is passed in, it will invoke this method.

	def add_carousel_includes
		@yui_stylesheets << "carousel/assets/carousel"
		@yui_javascript << "yahoo-dom-event/yahoo-dom-event"
		@yui_javascript << "animation/animation-min"
		@yui_javascript << "container/container-min"
		@yui_javascript << "carousel/carousel_min"	
	end

As you can see here I add in each css and js file needed into their respective arrays. Since these arrays are uniq’d later on we don’t need to worry too much about duplication.

The eventual design will not require you to do anything in here. You will simply pass the resource names (as symbols) in and it will determine dependencies and include them, in the most optimal way. Thus for Drag n Drop you’d be able to pass in :dragdrop. This will be identical to what is defined in YUI Loader as the YUI module names. It would then make sure these dependencies are there: yahoo-dom-event.js, dragdrop-min.js. I will look to make sure it minifies and compresses based on site-wide preferences and such.

3) Helper method

The last important piece is writing a helper method to make it convenient for the developer to invoke. In this case I’ve added the carousel’s method to /lib/yui4rails/helpers_extension.rb.

	def yui_carousel(carousel_id, collection, options = {}, &block)
		asset_manager.add_components :carousel
		carousel = Yui4Rails::Widgets::Carousel.new(carousel_id, collection, options)
		
		concat(%{<div id="#{carousel_id}" class="carousel-component">}, block.binding)
		yield block
		concat("</div>", block.binding)			
	end

In this helper, you can see we take in the arguments that we first talked about. The first order of business is it calls asset_manager.add_components with the key name that AssetManager’s process_components is aware of. There is another helper module, include_extension.rb, that provide a convenience method to get the AssetManager’s instance via asset_manager. We use it here but you don’t have to. Additionally, you could call add_components from the initialize method of the Widget class.

The next thing I do is construct the widget class passing through the parameters it needs. One nice thing with helpers is you could provide multiple helpers that use the same widget class varying their parameters. Finally, I output the required div as a wrapper around the block that they specified.

4) Testing

In reality, I wouldn’t do testing as the last step in my workflow. However, I wanted to mention it after you have seen the different parts and how they fit together. Testing, or specs, in this plugin have not been as strong as I would have liked. Part of that is it’s challenging to test this sort of code. Javascript is not something you can 100% test in a framework like Rails with any confidence. Other scripts, browser differences and html will keep it from being so simple. That said, you can test some things. Let’s look at /spec/models/carousel_spec.rb to see an example of what I’ve done so far.

We’ll only look briefly at the render_head_script describe block.

	describe ".render_head_script" do
		before(:each) do
		  @carousel = Yui4Rails::Widgets::Carousel.new(@carousel_id, @collection)
		end

		it "should register the code as an anonymous function on the page load event" do
			@carousel.render_head_script.should match(/YAHOO\.util\.Event\.addListener\(window, "load", function\(\)/)
		end

	  it "should render a YAHOO.extension.Carousel with our carousel_id" do
	    @carousel.render_head_script.should match(/new YAHOO\.extension\.Carousel\("#{@carousel_id}",/)
	  end
	
		it "should set the collection size into the size option" do
			@carousel.render_head_script.should match(/size: #{@collection.size},/)
		end

		it "should include default options" do
			@carousel.render_head_script.should match(/numVisible: 3,/)
		end
	...

You can see I’m listing several specifications that I expect for the output to be valid. I use regex matches to confirm parts of the script are there. Clearly, this will not guarantee that the script is valid javascript but it will catch is major, important pieces are missing and that’s good enough for me here. I would suggest you take a similar approach.

That is essentially it for the steps you will need to take as a component author. As this project has grown organically out of YUI integration that I’ve used on a couple of my projects, it’s likely that we’ll need to enhance things as we go forward. The AssetManager is a great example.

One last thing to note on AssetManager. There is a class method called reset that will nil out the class instance of the manager. This is automatically prepended to your before filters so you never have to worry about stale AssetManager state biting you. The main motivator for this behavior is to allow the AssetManager instance to be available to any object that wishes to call it whether it be a template helper method or isolated Yui4Rails class that has no reference to the template or its variables.

Hopefully, this walk through will give you more confidence on writing components. Go forth and add your favorite components as you will.

blog comments powered by Disqus