Learn how Scrivito CMS can help you deliver amazing digital experiences
See Scrivito CMS in action

Creating a Products Widget

Creating a Products Widget

Wouldn't it be nice to be able to place one or more Solidus products onto a page managed by Scrivito? Let's create a widget for this!

The basics

Run rails g scrivito:widget SolidusProductsWidget to create the widget model and its views. Then open the model file and add a stringlist attribute named product_ids. Also, provide it with a products method that returns Spree::Product instances of the products stored in product_ids. This is what the model file should finally contain:

# app/models/solidus_products_widget.rb class SolidusProductsWidget < Widget attribute :product_ids, :stringlist def products return [] unless product_ids.present? spree_products = Spree::Product.find(product_ids) # Spree API may return products in different order! product_ids.map { |id| spree_products.detect { |p| p.id.to_s == id } } end end

For letting editors specify the IDs of the products the widget should render, extend the details view of the SolidusProductsWidget so that the product_ids attribute becomes editable. The view should finally contain the following:

<!-- app/views/solidus_products_widget/details.html.erb --> <%= scrivito_medium_dialog do %> <%= scrivito_details_for SolidusProductsWidget.description_for_editor do %> <%= scrivito_details_for "Products" do %> <%= scrivito_tag :div, widget, :product_ids %> <% end %> <% end %> <% end %>

To have a SolidusProductsWidget instance render the products, set the contents of its show view to the following:

<!-- app/views/solidus_products_widget/show.html.erb --> <% if widget.products.any? %> <%= render partial: 'spree/shared/products', locals: { products: widget.products } %> <% else %> <p> No products selected. Edit widget properties to select products to display here. </p> <% end %>

As you can see, we simply utilize the spree/shared/products partial, which is provided by Solidus/Spree.


We would like to be able to auto-complete Solidus products on the details page of a SolidusProductsWidget instance. For this, we provide a custom inplace editor. This editor uses the API provided by Solidus/Spree.

First, add the following gems to your Gemfile (if they are not present yet):

gem 'underscore-rails'
gem 'coffee-rails', '~> 4.2'

Run bundle install. Then extend the widget's details view so that it renders the spree_api_key of the current user:

<%= scrivito_tag :div, widget, :product_ids, {data: {solidus_api_key: current_spree_user.spree_api_key}} %>

The details view should finally look like this (we also changed the dialog size from medium to large):

<!-- app/views/solidus_products_widget/details.html.erb --> <%= scrivito_large_dialog do %> <%= scrivito_details_for SolidusProductsWidget.description_for_editor do %> <%= scrivito_details_for "Products" do %> <%= scrivito_tag :div, widget, :product_ids, {data: {solidus_api_key: current_spree_user.spree_api_key}} %> <% end %> <% end %> <% end %>

The Spree API can be configured to not require an API key for read-only access: Just set config.requires_authentication to false during Spree::Api::Config.configure. In this case it is, of course, not necessary to render the user's API key as shown above.

Next, create app/assets/javascripts/products_editor.js.coffee and set its contents to:

# app/assets/javascripts/products_editor.js.coffee class ProductCache constructor: -> @cache = {} add_raw: (product) -> id = String(product.id) @cache[id] = id: id value: product.name + ' (' + product.id + ')' find_by_id: (id) -> @cache[id] find_by_value: (value) -> _(@cache).detect (item) -> item.value == value # fetch products via solidus API and add them to the cache fetch_products = (elem, cache, data) -> $.ajax( url: '/api/products', dataType: 'json', beforeSend: (xhr) -> xhr.setRequestHeader('X-Spree-Token', elem.data('solidus-api-key')), data: data, ).then (data) -> data.products.map (product) -> cache.add_raw(product) init_editor = (elem, cache, initial_values) -> elem.tagEditor # jQuery tagEditor initialTags: initial_values forceLowercase: false autocomplete: source: (request, response) -> fetch_products(elem, cache, 'q[name_cont]': request.term).done (products) -> response(products.map (product) -> product.value) onChange: (field, editor, values) -> product_ids = values.map (value) -> cache.find_by_value(value).id field.scrivito('save', product_ids) products_editor = can_edit: (element) -> $(element).data('scrivito-field-name') == 'product_ids' && $(element).data('scrivito-field-obj-class') == 'SolidusProductsWidget' activate: (element) -> elem = $(element) cache = new ProductCache() current_item_ids = elem.scrivito('content') if current_item_ids.length fetch_products(elem, cache, ids: current_item_ids.join(',')).then (products) -> # reiterate current_item_ids, since fetch_products does not response in the correct order intial_values = current_item_ids.map (item_id) -> cache.find_by_id(item_id).value init_editor(elem, cache, intial_values) else init_editor(elem, cache, []) scrivito.on 'load', () -> scrivito.define_editor 'products_editor', products_editor scrivito.select_editor (element, editor) -> editor.use('products_editor')

Then, add the following to app/assets/javascripts/cms.js:

//= require underscore //= require products_editor

It is important that the JavaScript of the products_editor is inserted after //= require scrivito.

If you restart your server now, you should be able to auto-complete products in the properties dialog of any SolidusProductsWidget instance. Just start typing, e.g. Spa”. You can also reorder the products in the list.

Note: The instructions do not include error handling in the case of an invalid product being added. This was consciously done in an effort to keep the tutorial simple.