A take on Spree + deface

Firstly, kudos to Brian Quinn aka BDQ for the great ‘Deface‘ integration that leverage the Spree hook to a new level [1]. The secret lies in the power of the Deface library that allow targeting an html element with CSS selector and replace, remove, insert_before, insert_after, insert_top and insert_bottom new markup to that target. This mechanism shares resemblance to jQuery selector and manipulator.

Pros:

  • Flexible to target element that is not pre-defined with hooks
  • Able to target even ruby code
  • Memory safe as all overrides are applied on template once and included in the Rails compiled template
  • Easy to pick up as long as you are proficient with CSS selector rules
  • 99% percent backward-compatible with old hooks

Cons:

  • Bleeding edge technology is not always stable
  • 1% hook-related stuff might breaks

So the question is how well this feature work? Does it work with all CSS selector rules? Does it support the new CSS3 selectors? How inheritances works with multiple overrides on same target? Well, I hold no answer, so why don’t we venture into the code and work it out?

The Setup

At the time of this writing, BDQ has not yet merged the branch with master, so we have to manually pull deface branch to test it. Deface is now merged with master branch of spree.

  1. Fetch the upstream spree from GitHub
    git clone git://github.com/spree/spree.git
  2. Fetch and track BDQ’s deface branch.This step is not required any longer.
    cd spree
    git remote add bdq git://github.com/BDQ/deface.git
    git fetch bdq
    git checkout -b deface --track bdq/master
  3. Install all Gem dependencies and generate sandbox
    bundle install
    rake sandbox

Next, we need to a placeholder for hook. I’d recommend to create a new file called 'spree_site_hook.rb' in your sandbox’s lib. Then we need to include that file in our SpreeSite engine, so append require 'spree_site_hooks' into the first line of 'lib/spree_site.rb'

# lib/spree_site.rb
require 'spree_site_hooks'

module SpreeSite
class Engine < Rails::Engine
def self.activate
# Add your custom site logic here
end

def load_tasks
end

config.to_prepare &method(:activate).to_proc
end
end

There isn’t convention for the hook placeholder or how it should be loaded. You don’t have to initialize the hook file in your Engine file, in fact you can place file 'spree_site_hook.rb' in folder 'config/initializers'.

Alternatively, you can create folder app/deface and chuck in files with _decorator naming (for example app/deface/site_decorator.rb. To do so, you should tell the Engine (lib/spree_site.rb) to include those decorators files.

# lib/spree_site.rb

module SpreeSite
class Engine < Rails::Engine
def self.activate
Dir.glob(File.join(File.dirname(__FILE__), "../app/**/*_decorator*.rb")) do |c|
  Rails.env.production? ? require(c) : load(c)
end
end

def load_tasks
end

config.to_prepare &method(:activate).to_proc
end
end

How does it work?

Says I want to change the line ‘Powered by Spree’ in the footer partial with ‘Hello world’. Let’s look into how the old partial looks like:

<div class="left">
    <%= hook :footer_left do %>
    <p>
      <%= t("powered_by") %> <a href="http://spreecommerce.com/">Spree</a>
    </p>
    <% end %>
  </div>

and this is the new partial:

  <div class="left" data-hook="footer_left">
    <p>
      <%= t("powered_by") %> <a href="http://spreecommerce.com/">Spree</a>
    </p>
  </div>

You can see that ruby hook is deprecated and replaced with html5 attribute tag data-hook. The change is very marginal but creates much differences.

Below is how you replace the footer text with old hook:

class SiteHooks < Spree::ThemeSupport::HookListener
  remove :footer_left
  replace :footer_left, :text => '<p>Hello world</p>'
end

The new way is:

# lib/spree_site_hook.rb
Deface::Override.new(:virtual_path => "shared/_footer",
                     :name => "site_footer_left",
                     :replace => "[data-hook='footer_left'] > p:first-child",
                     :text => "Hello world",
                     :disabled => false)

This example above is very simple yet this hinders a problem with the old hooks that is you could not target deeper than pre-defined hook. With the old way, we could not target the p tag that enclose the “Powered by Spree” text, thus we have remove existing content within that hook and replace it with new content. Whilst the new Deface way allows you to use CSS rule "[data-hook='footer_left'] > p:first-child" to explicitly just target the p tag. This example is very trivial but it demonstrates the inefficiency of the old hook system. Just imagine you have to work with a big complex template, it’d be an mission impossible with hooks. I often find the old hook way is too broad/narrow in term of hook scope, that usually makes me resolve to replacing a full view file with new file using hook (no DRY at all).

How to use it?

By creating new Deface::Override object with sets of params. Taking above example:

# lib/spree_site_hook.rb
Deface::Override.new(:virtual_path => "shared/_footer",
                     :name => "site_footer_left",
                     :replace => "[data-hook='footer_left'] > p:first-child",
                     :text => "Hello world",
                     :disabled => false)

The parameters are 3 parts: Target, Action and Source.

Target
:virtual_path is the partial where Deface should apply the override.

Action
:replace is one of 4 actions provided by Deface, ie :remove, :replace, :insert_after, :insert_before

Source
In the example, :text source is used to replace a matching element with text. You can replace matching element with :partial and :template too.

Optional
:name is an optional unique name so you can identify the object and modify it later
:disabled is a flag to disable/enable the object if something breaks, useful for debugging

Once the object initialization is finished, Deface targets the shared/_footer partial and try to match any element with [data-hook='footer_left'] > p:first-child CSS rule, then replace the first p tag of footer_left hook with Hello world text.

You can find more details on BDQ GitHub page at https://github.com/bdq/deface.

Advanced

Deface can also target ruby code thanks to Nokogiri support out-of-the box. Deface will temporarily convert ERB files with ruby code to pseudo HTML that can queried by Nokogiri.

<%= some ruby code %>

becomes

<code erb-loud> some ruby code </code>

and

<% other ruby code %>

becomes

<code erb-silent> other ruby code </code>

This feature is truly a killer feature of Deface. For example, if I want to replace link_to_cart helper from 'shared/_store_menu' partial with my own customized advanced_link_to_cart helper, how could I do that? Well, you could use CSS to match li.cart-indicator and replace it with a new partial that contains the helper. That’s not nifty enough, isn’t it? You can achieve that like this:

Deface::Override.new(:virtual_path => "shared/_store_menu",
                     :name => "remove_link_to_cart_helper",
                     :replace => "code[erb-loud]:contains('link_to_cart')",
                     :text => "<%= advanced_link_to_cart %>" )

The above code tells Deface to look for any instances of link_to_cart helper and replace it with advanced_link_to_cart helper. So you are probably wondering how things work at the background. First, deface will escape all ERB template to pseudo markup

The example markup is

<li><%= link_to t("home") , root_path %></li>
<li class="cart-indicator"><%= link_to_cart %></li>

becomes

<li><code erb-loud> link_to t(&quot;home&quot;) , root_path </code></li>
<li class="cart-indicator"><code erb-loud> link_to_cart </code></li>

Deface then queries our selector code[erb-loud]:contains('link_to_cart') and replace matchings with advanced_link_to_cart

BDQ also create a web-app test harness for you to test out Deface @ http://deface.heroku.com/. This tool comes very handy when you on complicated rules.

Issues

Update: BDQ has resolved below issue in the recent merge.
So far as I am aware, Deface parser has few problems with few odd ERB + Ruby combination. For example:

<tr <%= 'style="color:red;"' if product.deleted? %> id="<%= dom_id product %>" data-hook="admin_products_index_rows">

could not be parsed correctly. However worry not, BDQ is aware of it. For now, I’d recommend you not to mix up Ruby and ERB too much. So the above markup can be converted to:

<tr style="<%= "color:red" if product.deleted? %>" id="<%= dom_id product %>" data-hook="admin_products_index_rows">

Conclusion

I have to say Deface is a great addition to Spree stack, another step closer to a truly modular templating. Give it a try and remember to report issues to BDQ and most importantly please send a thank you to BDQ if you like it.

[1] https://github.com/spree/spree/commit/87c97bf7314eae0ca01a043bb44fa0812d578766

Advertisements

About Jones Lee

Nothing much about me..

2 responses to “A take on Spree + deface

  1. BDQ

    Hi JonesLee,
    Fantastic write up on Deface, it’ll really help people understand the new approach!

    The latest Deface release (0.5.0) has much improved ERB parsing so the issues you mentioned above should be solved now.

    Thanks again for all your contributions to the Spree community.

    BDQ

  2. Juzfoo!

    Great explanations. Thank you guys 🙂

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: