Ruby
Article

Create a Ruby Gem for a jQuery Plugin: Advanced

By Imran Latif

Screenshot 2014-10-12 11.39.13

In the previous article we created a gem from scratch that pulls in a jQuery plugin and adds it to a Rails application. In this post, let’s look into some advanced concepts to make our gem even better.

We will be using same gem and Rails application used in the previous post.

Custom Helper

So far, we have successfully applied the zoom effect to images by simply adding the image_zoomer CSS class. But, don’t you think our gem deserves a custom helper? I mean, wouldn’t it be better to use our own helper tag and free the user from haveing to add the image_zoomer CSS class?

Let’s create a custom helper called zoom_image_tag. Images added with zoom_image_tag will have the zoom effect applied, by default. Create a file called zoom_image_helper.rb under lib/image_zoomer directory in the gem and paste following code:

module ImageZoomer
  def zoom_image_tag(*args, &block)
    options = insert_zoom_class(args.extract_options!)
    args << options
    image_tag(*args, &amp;block)
  end

  def insert_zoom_class(options)
    class_name = "image_zoomer"
    if options.key?(:class)
      options[:class] += " #{class_name}"
    elsif options.key?('class')
      options['class'] += " #{class_name}"
    else
      options[:class] = class_name
    end
    options
  end
end

We have defined two methods on the ImageZoomer module. The zoom_image_tag method receives multiple arguments and a block just like the image_tag helper.

options = insert_zoom_class(args.extract_options!) is calling the extract_options! method provided by ActiveSupport. It is used to extracts options from a set of arguments. It removes and returns the last element in the array if it’s a hash, otherwise it returns a blank hash. The returned hash is passed to insert_zoom_class.

insert_zoom_class is a straightforward method used to insert the class into the provided hash. The key could be a symbol, string, or not provided at all. Here, the image_zoomer class attribute is returned as part of the hash.

args < < options appends the options hash to args. Since extract_options! removes the options from args, we have to append them back.

image_tag(*args, &block) is the standard Rails image_tag helper with our modified arguments. The takeaway here is zoom_image_tag is simple a wrapper for image_tag. Users of our gem now have a convenience method to make their lives easier.

Navigate to the test Rails app and open app/views/site/index.html.erb under your Rails app and replace image_tag with:

<%= zoom_image_tag("test.jpg"); %>

Start the web server and open http://localhost:3000/site/index in the browser. You will see an error stating something like undefined method 'zoom_image_tag'. What is happening?? We didn’t tell Rails that zoom_image_tag is a helper method. Let’s do this now.

We know that image_tag helper is defined in the ActionView::Helpers::AssetTagHelper module. We can simply reopen this modules and add the ImageZoomer module. Open lib/image_zoomer.rb (in the gem) and add:

require "image_zoomer/zoom_image_helper"

module ActionView
  module Helpers
    module AssetTagHelper
      include ImageZoomer
    end
  end
end

Now, zoom_image_tag will become part of the ActionView::Helpers::AssetTagHelper methods, joining the league of other helpers such as image_tag, javascript_include_tag, etc.

Restart your web server and refresh the page. The image zooming effect is applied using our new helper. Cool.

An Important Refactoring

Including ImageZoomer in an existing module is a form of Monkey Patching, and is considered harmful. Luckily, Rails provides an elegant way of adding custom helpers, called Railties, that doesn’t use Monkey Patching. Railties provide several hooks to participate in the initialization process. Read more about Railtie here.

First, remove the newly inserted code from lib/image_zoomer.rb so it consists only of the three require statements. Create a file railtie.rb in the lib/image_zoomer/ directory with:

class Railtie < Rails::Railtie
  initializer "image_zoomer.initialize_zoom_image_helper" do |app|
    ActiveSupport.on_load(:action_view) do
      include ImageZoomer
    end
  end
end

Here is our Railtie class that is subclassed from Rails::Railtie as per the Rails documentation. The call to the initializer method takes a name and a block. The block does the our magic, calling ActiveSupport.on_load(:action_view) and passing another block to include the ImageZoomer module. Our ImageZoomer module will only be included when ActionView is fully loaded. This is the standard way of adding helpers since Rails 3. Read more about this here.

Require railtie.rb in lib/image_zoomer.rb with the following code:

require "image_zoomer/railtie"

Save your changes, restart the web server, notice that the zoom effect is still working. Now, zoom_image_tag is being inserted by Rails built-in hooks and we have eliminated Monkey Patching.

Our final version of lib/image_zoomer.rb looks like this:

require "image_zoomer/version"
require "image_zoomer/engine"
require "image_zoomer/zoom_image_helper"
require "image_zoomer/railtie"

Overriding Default Configurations

Our gem is working fine and we could call it “done”, but is it really extensible? By “extensible” I mean, can a developer change the default configuration of the jQuery plugin? By default, the height and width of the zoom lens is 90px and zoom-level is 1.5. If a developer is not able to update these settings, our gem is not extensible. In other word, we aren’t done yet.

Let’s add some extensibility so that developers can easily override default settings.

Configuration Class

Create a class to hold our configuration in a file called lib/image_zoomer/configuration.rb in your gem and add:

module ImageZoomer
  class Configuration
    attr_accessor :width, :height, :zoom_level

    def initialize
      @width = 90
      @height = 90
      @zoom_level = 1.5
    end
  end
end

We have simply created a class in our ImageZoomer module. The Configuration class has three properties: width, height and zoom_level. Instancs of this class will get default value that match those defined by the jQuery plugin.

Require configuration.rb in lib/image_zoomer.rb:

require "image_zoomer/configuration"

The best place to create an instance of this class is in lib/image_zoomer/railtie.rb. Add following code to our Railtie class:

initializer "image_zoomer.configuration" do |app|
  app.config.image_zoomer = ImageZoomer::Configuration.new
end

We are, again, calling the initializer method and passing it a name and a block. app here is represents Rails.application and app.config is an instance of Rails::Application::Configuration used to hold all configuration for the app. We have created an instance of ImageZoomer::Configuration and assigned it to app.config.image_zoomer.

Now, accessing Rails.application.config.image_zoomer will present an instance of ImageZoomer::Configuration. Restart your web server, refresh your browser and make sure we haven’t broken anything.

To immediately play with an instance of ImageZoomer::Configuration, launch the console (rails console) and type Rails.application.config.image_zoomer. If everything is setup correctly, you’ll see our ImageZoomer::Configuration in action.

Providing Settings to the Plugin

The gem now has a configuration class, we only need to use it. Create a new file called image_zoomer_options.js.erb in app/assets/javascripts under the gem with:

var __ImageZoomer = {
  options: {
    width: "<%= Rails.application.config.image_zoomer.width %>",
    height: "<%= Rails.application.config.image_zoomer.height %>",
    zoom_level: "<%= Rails.application.config.image_zoomer.zoom_level %>"
  }
}

The above code is pretty simple. Declare a JavaScript variable (__ImageZoomer) and assigning it an object with a property called options. The options object has three properties that correspond with a property from the Rails.application.config.image_zoomer object. This is an erb template because we want to execute some Ruby code, and Sprockets automatically handles execution of dynamic code in asset files. As such, image_zoomer_options.js.erb will point result in /assets/image_zoomer_options.js in the HTML response and will look like:

var __ImageZoomer = {
  options: {
    width: "90",
    height: "90",
    zoom_level: "1.5"
  }
}

As you can see, our dynamic code executed and width, height and zoom_level have default values that we defined in the constructor of ImageZoomer::Configuration.

With our new JavaScript object holding the configuration values, it’s time to modify the jQuery plugin initialization code to use it. Open app/assets/javascripts/image_zoomer_initializer.js under your gem and replace the existing code with:

$(function() {
  if (typeof __ImageZoomer == "undefined") {
    __ImageZoomer = {options: {}};
  }
  $(".image_zoomer").each(function() {
    $(this).image_zoomer(__ImageZoomer.options);
  });
});

First, check if the __ImageZoomer variable exists and create it, if necessary. According to the jQuery Plugin usage, we can pass an object to the image_zoomer method and properties defined in that object will be applied. Passing __ImageZoomer.options applies our settings to change the behavior of image zooming.

Restart theyour web server, refresh the browser, and you should see same image zooming experience. __ImageZoomer points to default values, so there is no change in the zooming experience for the moment. But, we have taken an important step towards giving users the ability to override default settings.

Modifying Default Values in Rails

Rails uses initializers to modify configuration for gems and core Rails settings. We will do so as well. First, there should be an elegant way in ImageZoomer::Configuration to update properties. Open lib/image_zoomer/configuration.rb and add the following method to the class:

# Override default options with the provided ones in block
def set_options(&amp;block)
  block.yield(self)
end

We have created a method set_options which accepts a block that yieldsself (current instance of the configuration). Calling this method with appropriate block will update the default values of Image_Zoomer::Configuration from our Rails.application.config.image_zoomer initializer.

In the Rails app, create config/initializer/image_zoomer.rb with:

Rails.application.config.image_zoomer.set_options do |options|
  options.width = 50
  options.height = 50
end

When this code executes, width and height are changed to 50, and image_zoomer is still 1.5 because we haven’t changed it. Developers can easily modify this block according to their needs. Since we have already setup our jQuery code in such a way that it will pick latest values from our Rails.application.config.image_zoomer we are good to go.

Restart the web server, refresh the browser, and observe different width and height than before.

Fixing a Caveat

Wait, the width and height didn’t change. What’s up? This is due to a caveat.

Sprockets caches assets so that it doesn’t have to process them again and again. When any asset is served the first time, it gets cached and that cache will be served on subsequent requests. This cache will not update, even when we restart the web server. One way to update the cache for any asset is to change its source code, but that could lead to useless changes and silly git commits full of blank spaces/lines. Let’s look at a better way. Open lib/image_zoomer/railtie.rb and change the image_zoomer.configuration block like so:

initializer "image_zoomer.configuration" do |app|
  app.config.image_zoomer = ImageZoomer::Configuration.new
  # Clear the cache
  FileUtils.rm_rf("#{Rails.application.root}/tmp/cache/assets") if Rails.env.development?
end

Sprockets store the caches at a specified path in our Rails app. We are deleting that directory when the code in our lib/image_zoomer/railtie.rb is executed (which is when theserver starts and the gem is loaded). We are using Ruby’s built in methods to delete that folder in the development environment.

Restart the web server and you’ll finally see the zoom lens with different dimensions. Go ahead and change settings in config/initializers/image_zoomer.rb to your heart’s content.

Publishing the Gem

Our gem is ready. Publishing a gem is quite simple, thanks to the Rubygems core team. Bundler has already provided some tasks that are helpful in packaging and distributing gems. First, you must have a Rubygem account. Head over to http://rubygems.org and sign up.

After creating an account, update the relevant information in the image_zoomer.gemspec file. You’ll probably want to push this gem to Github, too.

Navigate to your gem’s root directory and issue the following command:

rake release

This command will compile all the required files into a .gem package, commit thelatest code along (with gem version) as a tag to GitHub, and upload the gem to Rubygems. While publishing you will be asked for your Rubygems credentials, provide those and you are done. Once the gem is successfully published you will see this in your dashboard on Rubygems.

If for some reason rake release fails, you can also build the gem using rake build. If that works, manually push the generated image_name-version.gem file to Rubygems with gem push path/to/image_name-version.gem.

One Thing Left

Now there is only one thing left, and that is to share your gem with your colleagues, friends, and other Rubyists. Don’t forget to shoot an email to Peter Cooper to get it published in RubyWeekly, which is one of the most widely read Ruby newsletters.

Conclusion

Building something and contributing to open source is an awesome experience and every developer should do it. I hope you have learned a lot in these two articles. Go ahead and make a gem for your favorite jQuery plugin.

No Reader comments

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Ruby, once a week, for free.