In this blogpost, I’ll describe how one can build such a feature, more to create some understanding of how in Spree such customisations can be developed.
Spree has everything in place to add this, but it can be a bit daunting to find all the bits and pieces that could and should be overridden. Which I hope to clarify a bit. In order to built this feature, we re-open some
Controllers from Spree and we inject some HTML using deface.
You could write this as a spree extension, but that requires even more moving parts to be in place. And I think it is a better, more efficient way to first write the customisation in your main app and then, later on, when things have settled, extract it into a spree extension.
You could write this TDD, and you should really write at least some tests to spec out your changes. But testing overrides of methods, controller actions and so on, is really daunting in Spree: you’ll be stubbing and mocking a gazillion of unrelated before-filters, finders and scopes. Just to spec that your method adds one other scope, you might need over 100 lines of setup. This is a problem, but not one that I want to address in this post.
Let’s get rolling. First, override the ProductsController#index action. Add a file
The idea is to add a sorting scope that is already available on
Product. The ordering scopes I want add are
descend_by_master_price. When implementing this, you can add
sorting=ascend_by_updated_at or one of the other sorting scopes to the URL of the app (e.g. http://localhost:3000/orders?sorting=ascend_by_updated_at). This way we can finish the controller and then move on to the view.
Now that this works, it needs to be secured and cleaned up:
There is a lot going on here. But the general idea is to use method aliasing to be able to extend the original index.
We have, furthermore, split out the scope-selecting into several methods. This allows setting a default and whitelisting allowed scopes. You don’t want people to call just any string and call that as a method on our model (e.g.
sorting=delete_all). I’ve declared
sorting_param as a helper method because we’ll need it later on (I know, not very YAGNI, but for the sake of brevity, let’s already implement it here).
If you want to find out where a view, controller or model is defining something, you can either run
bundle open spree_core (or
spree_backend) or simply clone the stable branch into a directory and browse or search the code there. However, make very sure you have the correct version. For example, when using Github to browse the code, you have a good chance of copying the wrong (too old or too new) versions of a method into your project in order to override.
View and helpers
First iteration is to copy over Spree’s products partial to
app/views/spree/shared/_products.html.erb. We add the sorting links there. In a later iteration we’ll rely on deface to inject our code, rather then duplicating the partial.
But, for now, Rails will simply pick our file instead of Spree’s version. In it, we add our links:
For the links, we use
params.merge(...) in order to persist any search, paging or filter params.
On your development server, this will work, but when you look at products/index.html.erb, you can see
cache(cache_key_for_products). It uses [cache_key_for_products`](https://github.com/spree/spree/blob/3-0-stable/core/app/helpers/spree/products_helper.rb#L54). The list of products will be cached, which is good, because the queries can be very heavy. But that cache disregards our ordering and the active-state of our links. We need to add the sorting to the cache-key.
In order to override it, we have to add it to a file called
app/helpers/spree/products_helper_decorator.rb. Because we are changing a lot inside the method, we can’t really use the aliasing as used earlier, it won’t help us a lot. Instead, we simply override the entire method. And document our changes.
We now have a working sorting feature, but it needs improvement.
One thing we should clean up, is our override of the partial file. Unless you want to change how a file behaves, or want to alter its HTML structure, you should avoid copying them over. That makes upgrading a lot harder. And it can breeak a lot of addons that want to inject HTML into the views.
Deface is made for this, so let’s use it. Create a file
app/overrides/sorting_links_in_products.rb (And possibly restart the rails server, I’ve found that deface sometimes does not pick up new files otherwise):
And a partial
Some changes to the first iteration of this erb-code, are that we now use the locales to render strings, and that we only render the sorting-link as a link when it is not active.
I’ve also added an additional condition where I check for the controller. This is not the cleanest, but since the
products partial can be reused (it is a shared partial), and we inject into this partial regardless of who is requesting it, we should only add the sorting links when we are sure the partial is being renderd via the controller that can handle the sorting. For example, the partial is being used when rendering the products in a taxonomy. And there, the products already are sorted, so we don’t want to sort them. But the controller rendering them, there, will ignore the sorting param and won’t have the helper-methods we use.
Note tha we could clean this out even more by DRY-ing up the
link_to_unless current_sorting... repetition with e.g. another helper, or partials, or both, but IMHO that is overengineering. Some repetition in views is fine: they give us the freedom to place some icons, override a text and so on, much easier and cleaner.
We can now remove our version of
products partial, since it is no longer overriding the Spree version.
One cleanup was to use a
current_sorting? helper. Which does not
exist, so let’s create that with the familiar decorator/monkey-patching. Add it to
In this helper,
sorting_param is requested from the controller, which re-uses the default, that’s why we made it a helper earlier.
We now have a few sorting links that are implemented without hacking Spree, and without copying over entire classes or files. We can still lean on Spree and upgrade it quite safely.
Oh, and I’ll leave the CSS and declaration of localised strings as homework.
Do you often override these Spree core items? Do you know any tips and tricks on how to manage these during Spree upgrades?