News

Welcome to End Point’s blog

Ongoing observations by End Point people

Rails Ecommerce with Spree: Customizing with Hooks Tutorial

In the last couple months, there's been a bit of buzz around theme and hook implementation in Spree. The Spree team hasn't officially announced the newest version 0.9.5, but the edge code is available at http://github.com/railsdog/spree and developers have been encouraged to work with the edge code to check out the new features. Additionally, there is decent documentation here about theme and hook implementation. In this article, I'll go through several examples of how I would approach site customization using hooks in the upcoming Spree 0.9.5 release.

Background

I've been a big proponent of how WordPress implements themes, plugins, and hooks in the spree-user Google group. The idea behind WordPress themes is that a theme includes a set of PHP files that contain the display logic, HTML, and CSS for the customer-facing pages:

  • index
  • a post page
  • archive pages (monthly, category, tag archives)
  • search result page
  • etc.

In many cases, themes include sections (referred to as partial views in Rails), or components that are included in multiple template pages. An example of this partial view is the sidebar that is likely to be included in all of the page types mentioned above. The WordPress theme community is abundant; there are many free or at-cost themes available.

The concept behind WordPress plugins is much like Spree extension functionality - a plugin includes modular functionality to add to your site that is decoupled from the core functionality. Judging from the popularity of the WordPress plugin community, WordPress has done a great job designing the Plugin API. In most cases, the Plugin API is used to extend or override core functionality and add to the views without having to update the theme files themselves. An example of using the WordPress plugin API to add an action to the wp_footer hook is accomplished with the following code:

/* inside plugin */
function add_footer_text() {
    echo '<p>Extra Footer Text!!</p>';
}
add_action('wp_footer', 'add_footer_text');

WordPress themes and plugins with hooks are the building blocks of WordPress: with them, you piece together the appearance and functionality for your site. I reference WordPress as a basis of comparison for Spree, because like WordPress users, Spree users aim to piece together the appearance and functionality for their site. One thing to note is that the hook implementation in Spree is based on hook implementation in Redmine.

Spree Code

I grabbed the latest code at http://github.com/railsdog/spree. After examining the code and reviewing the SpreeGuides documentation, the first thing I learned is that there are four ways to work with hooks:

  • insert before a hook component
  • insert after hook component
  • replace a hook component's contents
  • remove a hook component

The next thing I researched was the hook components or elements. Below are the specific locations of hooks. The specific locations are more meaningful if you are familiar with the Spree views. The hooks are merely wrapped around parts of pages (or layouts) like the product page, product search, homepage, etc. Any of the methods listed above can act on any of the components listed below.

  • layout: inside_head, sidebar
  • homepage: homepage_sidebar_navigation, homepage_products
  • product search: search_results
  • taxon: taxon_side_navigation, taxon_products, taxon_children
  • view product: product_description, product_properties, product_taxons, product_price, product_cart_form, inside_product_cart_form
  • etc.

After I spent time figuring out the hook methods and components, I was ready to do stuff. First, I got Spree up and running (refer to the SpreeGuides for more information):


Spree startup with seed data and images.

Next, I updated the product list with a few pretend products. Let's take a quick look at the site with the updated products:


Spree with new product data for test site.

Example #1: Replace the logo and background styling.

First, I created an extension with the following code. Spree's extensions are roughly based off of Radiant's extension system. It's relatively simple to get an extension up and running with the following code (and server restart).

script/generate extension StephsPhotos

Next, I wanted to try out the insert_after method to append a stylesheet to the default theme inside the <head> html element. I also wanted to remove the sidebar because my test site only has 8 products (lame!) and I don't need sidebar navigation. This was accomplished with the following changes:

  • First, I added the insert_after hook to add a view that contains my extra stylesheet. I also added the remove hook to remove the sidebar element:
    # RAILS_ROOT/vendor/extensions/stephs_photos/stephs_photos_hooks.rb
    insert_after :inside_head, 'shared/styles'
    remove :sidebar
    
  • Next, I added a new view in the extension to include the new stylesheet.
    # RAILS_ROOT/vendor/extensions/stephs_photos/app/views/shared/_styles.erb
    <link type="text/css" rel="stylesheet" href="/stylesheets/stephs_photos.css">
    
  • Next, I created a new stylesheet in the extension.
    /* RAILS_ROOT/vendor/extensions/stephs_photos/public/stylesheets/stephs_photos.css */
    body { background: #000; }
    body.two-col div#wrapper { background: none; }
    a, #header a { color: #FFF; text-decoration: none; }
    
    ul#nav-bar { width: 280px; line-height: 30px; margin-top: 87px; font-size: 1.0em; }
    ul#nav-bar li form { display: none; }
    
    .container { width: 750px; }
    #wrapper { padding-top: 0px; }
    
    .product-listing li { background: #FFF; height: 140px; }
    .product-listing li a.info { background: #FFF; }
    
    body#product-details div#wrapper { background: #000; }
    body#product-details div#content, body#product-details div#content h1 { color: #FFF; margin-left: 10px; }
    #taxon-crumbs { display: none; }
    #product-description { width: 190px; border: none; }
    .price.selling { color: #FFF; }
    #product-image #main-image { min-height: 170px; }
    
    /* Styling in this extension only applies to product and main page */
    
    div#footer { display: none; }
    

One more small change was required to update the logo via a Rails preference. I set the logo preference variable to a new logo image and uploaded the logo to RAILS_ROOT/vendor/extensions/stephs_photos/public/images/.

# RAILS_ROOT/vendor/extensions/stephs_photos/stephs_photos_extension.rb
def activate
 AppConfiguration.class_eval do
   preference :logo, :string, :default => 'stephs_photos.png'
 end
end

After restarting the server, I was happy with the new look for my site accomplished using the insert_after and remove methods:


New look for Spree acomplished with several small changes.

Note: You can also add a stylesheet with the code shown below. However, I wanted to use the hook method described above for this tutorial.

def activate
  AppConfiguration.class_eval do 
    preference :stylesheets, :string, :default => 'styles'
  end
end 
Example #2: Use insert_before to insert a view containing Spree core functionality.

The next requirement I imagined was adding promo functionality to the product listing page. I wanted to use core Spree logic to determine which promo image to use. The first promo image would be a 10% off discount to users that were logged in. The second promo image would be a 15% off discount offered to users who weren't logged in and created an account. I completed the following changes for this work:

  • First, I added the insert_before method to add the promo view before the homepage_products component, the component that lists the products on the homepage.
    # RAILS_ROOT/vendor/extensions/stephs_photos/stephs_photos_hooks.rb
    insert_before :homepage_products, 'shared/stephs_promo'
    
  • Next, I added the view using core Spree user functionality.
    # RAILS_ROOT/vendor/extensions/stephs_photos/app/views/shared/_stephs_promo.erb
    <% if current_user -%>
    <img src="http://www.blogger.com/images/promo10.png" alt="10 off" />
    <% else -%>
    <img src="http://www.blogger.com/images/promo15.png" alt="15 off" />
    <% end -%>
    
  • Finally, I uploaded my promo images to RAILS_ROOT/vendor/extensions/stephs_photos/public/images/

After another server restart and homepage refresh, I tested the logged in and logged out promo logic.


vs.


Spree core functionality used to display two different promo images inside a partial view.

Note: The promo coupon logic that computes the 10% or 15% off was not included in this tutorial.

Example #3: Use replace method to replace a component on all product pages.

In my third example, I imagined that I wouldn't have time to manage product descriptions when I was rich and famous. I decided to use the replace hook to replace the product description on all product pages. I completed the following steps for this change:

  • First, I added the replace method to replace the :product_description component with a rails partial view.
    # RAILS_ROOT/vendor/extensions/stephs_photos/stephs_photos_hooks.rb
    replace :product_description, 'shared/generic_product_description'
    
  • Next, I created the view with the generic product description.
    # RAILS_ROOT/vendor/extensions/stephs_photos/app/views/shared/_generic_product_description.erb
    all prints are 4x6 matte prints.<br />
    all photos ship in a folder.
    

After yet another server restart and product refresh, I tested the generic product description using the replace hook.


The replace hook was used to replace product descriptions on all product pages.

Intermission

OK, so hopefully you see the trend:

  1. Figure out which component you want to pre-append, post-append, replace, or remove.
  2. Modify extension_name_hooks.rb to include your hook method (and pass the view, if necessary).
  3. Create the new view in your extension.
  4. Restart server and be happy!

I'll note a couple other examples below.

Example #4: Bummer that there's no footer component

In the next step, I intended to add copyright information to the site's footer. I was disappointed to find that there was no hook wrapped around the footer component. So, I decided not to care for now. But in the future, my client (me) may make this a higher priority and the options for making this change might include:

  • Clone the default template and modify the template footer partial view.
  • Clone the default template, create a hook to wrap around the footer component, add the changes via a hook in an extension.
  • Add a view in the extension that overrides the theme footer view.
Example #5: Add text instead of partial view.

Since I couldn't add copyright information below the footer, I decided to add it using after the inside_product_cart_form component using the insert_after hook. But since it's a Friday at 5:30pm, I'm too lazy to create a view, so instead I'll just add text for now with the following addition to the extension hooks file:

# RAILS_ROOT/vendor/extensions/stephs_photos/stephs_photos_hooks.rb
insert_after :inside_product_cart_form, :text => '<p>&copy; stephpowell. all rights reserved.</p>'

Server restart, and I'm happy, again:


Text, rather than a partial view, was appended via a hook.

Hopefully my examples were exciting enough for you. There's quite a lot you can do with the hook methods, and over time more documentation and examples will become available through the Spree site, but I wanted to present a few very simple examples of my approach to customization in Spree. I've uploaded the extension to http://github.com/stephskardal/stephs_photos for this article.

Tomorrow, I'm set to publish closing thoughts and comments on the hook implementation since this article is now too long for a blog post. Stay tuned.

Learn more about End Point's general Rails development and Rails shopping cart development.

12 comments:

Anonymous said...

Thanks for the tutorial.

On the first step you wrote you put the hooks in the stephs_photos_hooks.rb.

How did you created the file???

Thanks

M.

Steph Powell said...

Hi,

The extension_name_hooks.rb file is created when you create the extension with the 'script/generate extension ExtensionName' command. This file is only created in Spree Version 0.9.5. If you are running Spree from a gem, I'm not sure if the 0.9.5 gem is available yet.

~Steph

xonic said...

hi there,

i just noticed that the folowing lines didn't work for me:

# RAILS_ROOT/vendor/extensions/stephs_photos/stephs_photos_extension.rb
def activate
AppConfiguration.class_eval do
preference :logo, :string, :default => 'images/stephs_photos.png'
end
end

had to change the path 'images/stephs_photos.png' to 'stephs_photos.png'. not sure if i've mistaken something or if it's a little typo in the tutorial.

thx
xonic

Steph Powell said...

Thanks xonic. It looks like the '/images/' location is added when the logo is included in the template with an image_tag.

Todd M Baur said...

Just wanted to say I spent about 3 hours trying to figure out how to make a sidebar, and I'm sure I hacked it in. Basically I created shared/_taxonomies.erb and put in my html there. Most of my time was spent thinking that I could call my sidebar 'sidebar.erb' and then in the hook do replace :sidebar, 'shared/sidebar' but nope.

Steph Powell said...

Todd, the way to override / replace views before hooks were introduced was by placing files of the same name in the extension that would automatically override the core views. This should have worked as expected. You should also be able to replace / override the sidebar via hooks. Perhaps you should explain the details to the spree-user google group and you'll get more advice?

bob<3sDogsInCostumes said...

This tutorial doesn't quite apply to the extension system used in Spree 0.30.0. However, I'd really like to work through it. If there is any way you could update it, or add a few notes for adapting to spree 0.30.0, I would really appreciate it. Thanks.

bob said...

Well I see that you already took care of explaining Spree 0.30 extensions here

One quick note about something I had to fix to handle missing .gitignore

Thanks

Anonymous said...

Thanks for the nice tutorial!

Is there a way to add CSS style to a custom logo?

Thx!

David said...

This is a great tutorial.
What about an update?

Anonymous said...

Is there any way you can upload your code to GITHUB?

Steph Skardal said...

Hi David, and Anonymous:

There have been several updates on Spree since I wrote this article a year and a half ago. I haven't had time to update this to work on the latest version of Spree, which has pretty significant changes in the way it approaches theming. For now, I'd recommend looking through the Spree docs for Spree theming advice.