Hi everybody! Last week I showed you the extension Spree It is a present I created for Be Bellón and I promised you that this week I would tell you how to create these extensions. A promise is a promise, so taking this extension as an example, techie post is coming 😉
CREATE AN EXTENSION FOR SPREE COMMERCE
In order to create the extension Spree It is a present I based myself on Spree’s guide to create extensions. As you can imagine, albeit this guide is very complete and very well written, it cannot cover every detail, so for the things I did not find in the guides I took a look to other already existing Spree extensions, and in order that you have everything in a single place, I will describe here the whole process to you. Let’s go!
1. INSTALL SPREE COMMERCE
The first thing you need to do is to make sure you have the spree gem installed so that you can use its extension’s generator. Assuming you already have ruby installed on your machine, on a terminal run:
gem install spree
This command should install spree and all necessary dependencies.
2. CREATE YOUR EXTENSION’S STRUCTURE WITH THE GENERATOR
Spree gem includes an extension generator that gives you almost everything ready to start coding. It creates the structure of your extension, includes an open-source BSD 3 license, which allows everyone to use your software exonerating you from all liability in case it creates any sort of damage, and adds a very decent README file, you should only need to adjust a few things in the Gemspec file and start programming!
To use the generator simply execute the following command indicating the name you want to give to your extension. In spree_it_is a present‘s case it was:
spree extension it_is_a_present
The result should be something similar to this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
create spree_it_is_a_present create spree_it_is_a_present/app create spree_it_is_a_present/app/.gitkeep create spree_it_is_a_present/app/controllers/.gitkeep create spree_it_is_a_present/app/models/spree_it_is_a_present/configuration.rb create spree_it_is_a_present/app/models/.gitkeep create spree_it_is_a_present/app/services/.gitkeep create spree_it_is_a_present/app/views/.gitkeep create spree_it_is_a_present/lib create spree_it_is_a_present/lib/spree_it_is_a_present.rb create spree_it_is_a_present/lib/spree_it_is_a_present/engine.rb create spree_it_is_a_present/lib/spree_it_is_a_present/factories.rb create spree_it_is_a_present/lib/spree_it_is_a_present/version.rb create spree_it_is_a_present/lib/generators/spree_it_is_a_present/install/install_generator.rb create spree_it_is_a_present/bin create spree_it_is_a_present/bin/rails create spree_it_is_a_present/spec create spree_it_is_a_present/spec/.rubocop.yml create spree_it_is_a_present/spec/spec_helper.rb create spree_it_is_a_present/gemfiles create spree_it_is_a_present/gemfiles/spree_4_2.gemfile create spree_it_is_a_present/gemfiles/spree_master.gemfile chmod spree_it_is_a_present/bin/rails create spree_it_is_a_present/spree_it_is_a_present.gemspec create spree_it_is_a_present/Gemfile create spree_it_is_a_present/.gitignore create spree_it_is_a_present/LICENSE create spree_it_is_a_present/Rakefile create spree_it_is_a_present/README.md create spree_it_is_a_present/config/routes.rb create spree_it_is_a_present/config/locales/en.yml create spree_it_is_a_present/.rspec create spree_it_is_a_present/.travis.yml create spree_it_is_a_present/Appraisals create spree_it_is_a_present/.rubocop.yml Your extension has been generated with a gemspec dependency on Spree 4.2.3.1. |
As you can see, the generator creates a directory with the name you indicated, prefixed with spree_. In our case spree_it_is_a_present. Change to that directory and open your favorite editor:
cd spree_it_is_a_present
And install your extension dependencies:
bundle install
3. CREATE MIGRATIONS
If your extension needs to persist some data you will need to add a migration.
Spree’s official extension guide shows how to add a field to an already existing model.
In our case we needed to add a completely new model ,which we named Spree::PresentNote
. You should add a migration as you would do in other Rails’ project, in our case, by just executing the following command:
bundle exec rails g migration CreateSpreePresentNotes recipient_name dedication
Once the migration file has been generated, we change it to match our needs:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
require 'spree_extension/migration' class CreateSpreePresentNotes < SpreeExtension::Migration[6.1] def change create_table :spree_present_notes do |t| t.belongs_to :order, index: { unique: true }, null: false t.string :recipient_name t.text :dedication t.timestamps end end end |
Pay attention to the following details here. First see how spree_present_notes
 references an already existing Spree’s model (Spree::Order
), which although here it is reference as :order
, later you will see what do we need to add to the model so that Rails knows which model should this be related to. And second, in your other Rails projects your migrations will typically inherit from ActiveRecord::Migration
. Here, instead of that, add require 'spree_extension/migration'
 and make them inherit from SpreeExtension::Migration
. This will make your extensions compatible with Rails versions earlier than 5.x, (See SpreeExtension::Migration).
This migrations will be copied into the applications that consume our extension when they install it, let’s see how it is done.
4. ADD AND INSTALL YOUR EXTENSION INTO YOUR SPREE APPLICATION
The earlier you start using your extension, the earlier you will know how it behaves. On top of that it is really cool to see how it gets real!
Let’s add your newborn extension to your Spree application. In your Gemfile, add your extension indicating the path where it is located in your machine (Adjust this path to your needs)
gem ‘spree_it_is_a_present’, path: ‘../path/spree_it_is_a_present’ # Use the path where your gem is located in your computer
Run bundle install
bundle install
Once ready, run this command to install your extension. If prompted to run your migrations, enter Yes:
bundle exec rails g spree_it_is_a_present:install
5. ADD YOUR OWN MODELS…
This part is pretty straight forward since the only thing you need to do is add the code for your new model. In this case we create the model Spree::PresentNote
(app/models/spree/present_note.rb).
1 2 3 4 5 |
module Spree class PresentNote < Spree::Base belongs_to :order, class_name: 'Spree::Order' end end |
As a detail that does not come in the official guides and might be relevant to you, take a look at how I relate Spree::PresentNote
with the already existing Spree::Order
model.
At this point you might want to add factories or fixtures for your tests, if you would like to know how did I add them here is the link to that exact commit.
…AND/OR EXTEND OTHER ALREADY EXISTING MODELS
Once we have created Spree::PresentNote
we need to associated it with Spree::Order
from the latter so that we can navigate through objects. As Spree::Order
already exists, we need to extend it, to do that we create a decorator for Spree::Order
using ActiveSupport::Concern
in app/models/spree_it_is_a_present/spree/order_decorator.rb
As you can see, on top of adding a relations, we also indicate that from now on Spree::Order
will accept nested attributes for :present_note
. This is important so that we can persist this in formation within the checkout’s form later.
At this point it would be convenient to add some tests. I used RSpec and FactoryBot, but you can also use other frameworks.
What we would like to be sure is that Spree::Order
is able to also persist data about its related Spree::PresentNote
.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
require 'spec_helper' describe Spree::Order do describe 'Spree::PresentNote nested attributes' do it 'accepts nested attributes for Spree::PresentNote' do attributes = attributes_for(:order, user: create(:user)).merge({ present_note_attributes: attributes_for(:present_note) }) expect(described_class.new(attributes)).to be_valid end end end |
On top of that, it would also be interesting to know if our new model works as expected, and although these tests could not be necessary in other projects, I think they are useful here to avoid future regressions since I hope this gem will be shared among many people.
1 2 3 4 5 6 7 8 9 10 11 12 |
require 'spec_helper' describe Spree::PresentNote do describe 'attributes' do it { is_expected.to respond_to(:recipient_name) } it { is_expected.to respond_to(:dedication) } end describe 'relationships' do it { is_expected.to respond_to(:order) } end end |
6. SHOW YOUR FORM IN THE FRONTEND
The first thing you need to show something is to have something to show. For that, we will create a partial that contains our form and we will display it using Deface. The partial would be like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<div class="row"> <div class="col-12 mb-4"> <div id="present_note"> <h5 class="text-uppercase checkout-content-header"> <%= Spree.t('it_is_a_present.form.header') %> </h5> <%= form.fields_for :present_note, order.present_note_with_default do |present_note_form| %> <div class="inner checkout-content-inner" data-hook='present_note_inner'> <div class="form-group checkout-content-inner-field has-float-label"> <%= present_note_form.text_field :recipient_name, class: 'required spree-flat-input', placeholder: Spree.t('it_is_a_present.form.recipient_name') %> <%= present_note_form.label :name, Spree.t('it_is_a_present.form.recipient_name'), class: 'text-uppercase' %> </div> <div class="form-group checkout-content-inner-field has-float-label"> <%= present_note_form.text_area :dedication, class: 'required spree-flat-input', placeholder: Spree.t('it_is_a_present.form.dedication') %> <%= present_note_form.label :dedication, Spree.t('it_is_a_present.form.dedication'), class: 'text-uppercase' %> </div> </div> <% end %> </div> </div> </div> |
Modifying views with Deface is very easy because Spree Commerce’s most relevant HTML elements provide IDs for this purpose. To be honest I do not know if using Deface will be also the way to go in the future since Spree’s core team decided to stop including this dependency, but for the time being the main gems in spree-contrib
continue using it. As the upgrading guide says, if you use this kind of extensions you need to add Deface to your Gemfile.
Here you can see how I added the form partial to the checkout form:
1 2 3 4 5 |
Deface::Override.new( virtual_path: 'spree/checkout/_address', text: "<%= render partial: 'spree/present_notes/form', locals: { form: form, order: @order } %>", name: 'present_notes_form', insert_before: '#delete-address-popup') |
7. PERSIST YOUR DATA
Great, we already have the models (step 5 ) and we also have the views, What else do we need? Exactly, the glue, the controller.
Spree’s default checkout controller responsible of persisting Spree::Order
instances has no idea about how to persist Spree::PresentNote
records. In step 5 we told Spree::Order
it should accept nested attributes for Spree::PresentNote
, but at some place we should provide these. That place will be Spree::CheckoutController
, which is where Spree gets all parameters related to orders and persists them. To do that, we will rely again on the Decorator pattern.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
module SpreeItIsAPresent module Spree module CheckoutControllerDecorator extend ActiveSupport::Concern private def permitted_checkout_attributes super + [ present_note_attributes: present_note_attributes ] end def present_note_attributes %i[recipient_name dedication] end end end end Spree::CheckoutController.prepend SpreeItIsAPresent::Spree::CheckoutControllerDecorator |
WHAT COMES NEXT?
The last steps would be to add a form and a controller for the backend side. But as this is analogous to what we already saw in steps 6 and 7, I will not further extend this post with that. If you would like to take a look at it here you have the link to the Spree It is a commerce’s link in which I added it. After this you just need to polish the gem a little bit and share it with the world.
If you have worked with me in the past you already know I like to have a commit history as clean as possible, so if you are interested on knowing step by step how I added things to the gem you can check commit by commit here.
And this is the end of this week’s post! I hope you enjoyed it and… see you next week! Adiós!