Spree Dropdown boxes for variant option values
Asked Answered
S

2

6

I'm learning Spree 3.0 and I have a setup a test shop that sells shorts.

Shorts has multiple option types: Size, Color, Length

I wanted to change the way it displays the variant options on the frontend from a radio checkbox to a drop down box.

Currently, Spree displays the option types as radio buttons:

Current Spree setup

I want to change this to use drop down menus for each option type, like this:

What I want Spree to do

I've tried the following:

<%= select_tag "variant_id", options_for_select(@product.variants_and_option_values(current_currency).collect{ |v| ["#{variant_options(v)}  #{variant_price(v)}", v.id] })%>

But it simply displays the values of all option types in each tag:

Dropdown with all the option types 1

Dropdown with all the option types 2

I wanted to know the best way to split the option values into individual dropdown menus?

Any assistance is my much appreciated, thank you.

Sewellel answered 20/1, 2016 at 11:18 Comment(0)
B
5

This is not as easy as it looks since you will be using Spree::OptionValue records instead of variants and at some point you will want to convert back to variants in order to add it to your cart. Combinations might not be possible and/or out of stock so it is highly unpractical to work with option_values.

But nonetheless, you wanted to know how so i set up the following:

@options = Spree::OptionValue.joins(:option_value_variants).where(spree_option_value_variants: { variant_id: @product.variant_ids }).group_by(&:option_type)

This will give you a hash with the keys of the hash being option_types (Size, Color, Length in your case) and the values being arrays of option_values.

You can easily form this into radios like this:

<% @options.each do |option_type, option_values| %>
  <%= content_tag :h2, option_type.presentation %>
  <% option_values.each do |option_value| %>
    <%= radio_button_tag option_type.name, option_value.id %>
    <%= label_tag option_value.presentation %>
  <% end %>
<% end %>

Or for dropdowns:

<% @options.each do |option_type, option_values| %>
  <%= content_tag :h2, option_type.presentation %>
  <%= collection_select :variants, option_type.name, option_values, :id, :presentation %>
<% end %>

And in your controller you would want to find a variant matching those 3 criteria, check if it is in_stock, backorderable or track_inventory? is false and respond with errors or an updated cart :)

I hope this helped

Bock answered 20/1, 2016 at 20:53 Comment(3)
Thanks for the response Fabian. It looks difficult. I was looking at dropdown boxes specifically. Maybe, a Javascript solution might be the answer, hiding the radios.Sewellel
Radio's can be converted to a select easily. its the same kind of thing, pick one option from an array of options within a context. I have implemented this before in a SPA and i used the api to get all the variants from a product, then for the variants which where in_stock: true i collected the option values and displayed only those option_values to the user. Or maybe gray out the options for out of stock variantsBock
Thanks again @Fabian de Pabian, I'll need to digest this and figure out how to implement it. I can simply convert radio to dropdown but it has options types per product on a single dropdown, instead of seperate dropdown for color; size; etc..Sewellel
A
1

This is what I did to solve this problem. It basically takes the variant_id parameter that was controlled by the radio buttons and turns it into a hidden field controlled by jQuery and AJAX with additional notifications.

I hope this helps someone.

config/routes.rb

# Mount the core routes
Rails.application.routes.draw do 
  mount Spree::Core::Engine, at: '/'
end

# Create new routes
Spree::Core::Engine.routes.draw do 
  post "products/:product_id/get_variant",
       to: "products#toggle_like",
       as: "get_variant",
       constraints: { :format => /(js)/ }
end

app/models/spree/product_decorator.rb

Spree::Product.class_eval do

  # Find the Product's Variant from an array of OptionValue ids
  def find_variant_by_options(array)
    option_values = Spree::OptionValue.where(id: array)
    variants = []
    option_values.each do |option_value|
      variants.push(option_value.variants.ids)
    end
    self.variants.find_by(:id => variants.inject(:&).first)
  end
end

app/controllers/spree/products_controller_decorator.rb

Spree::ProductsController.class_eval do

  # Get the Variant from params[:ids], respond with JavaScript
  def get_variant
    @product = Spree::Product.find_by :slug => params[:product_id]
    @variant = @product.find_variant_by_options(params[:ids].split(','))

    respond_to do |format|
      format.js
    end
  end
end

app/views/spree/products/get_variant.js.erb

// Update hidden field #varient_id's value. 
$("#variant_id").val("<%= @variant.id %>") 
// Update price
$(".price.selling").html("<%= number_to_currency @variant.price %>");
<% if @variant.in_stock? && @variant.available? %> 
// If in stock and available
  $("#add-to-cart-button").prop("disabled", false); // Enable button
  $(".out-of-stock").hide(); // Hide 'out of stock' message
<% else %> 
// Otherwise
  $("#add-to-cart-button").prop("disabled", true); // Disable button
  $(".out-of-stock").show(); // Show 'out of stock' message
<% end %>

app/views/spree/products/_cart_form.html.erb

<%= form_for order, url: populates_orders_path do |f| %>
  ...
  <% if @product.variants_and_option_values(current_currency).any? %>
    <div id="product_variants" class="col-md-6">
      <h3 class="product-section-title"><%= Spree.t(:variants) %></h3>
      <% @product.option_types.each do |option_type| %>
        <%= f.label "option_type_#{option_type.id}", option_type.name %>
        <br>
        <%= f.select "option_type_value_#{option_type.id}",
                     option_type.option_values.all.collect { |v| [ v.name, v.id ] },
                     { include_blank: true },
                     { class: "form-control" } %>
        <br>
      <% end %>
      <%= hidden_field_tag "variant_id", value: "0" %>
      ...
     </div>
  <% else %>
    <%= hidden_field_tag "variant_id", @product.master.id %>
  <% end %>
  ...
    <span class="price selling" 
          itemprop="price" 
          content="<%= @product.price_in(current_currency).amount.to_d %>">
      <%= display_price(@product) %>
    </span>
  ...
    <%= button_tag class: "btn btn-success",
                      id: "add-to-cart-button",
                disabled: @product.variants.any?,
                    type: :submit do %>
      <%= Spree.t(:add_to_cart) %>
    <% end %>
  ...
  <span class="out-of-stock" style="display: none;">
    <%= Spree.(:out_of_stock) %>
  </span>
<% end %>

<script>
  // Call AJAX if all options are selected, otherwise clean up.
  $("#product-variants select").change(function(){
    var option_value_ids = [];
    $("#product-variants select").each(function(){
      option_value_ids.push($(this).val());
    });
    if(option_value_ids.indexOf("") == -1){
      $.ajax({
        url: "<%= get_variant_path(@product) %>?ids=" + option_value_ids.join(','),
        method: "post"
      });
    }else{
      $("#variant_id").val("0");
      $("#add-to-cart-button").prop("disabled", true);
      $(".out-of-stock").hide();
    }
  });
</script>
Attending answered 29/8, 2017 at 15:0 Comment(1)
That's awesome! Some corrections through: in routes products#get_variant instead of products#toggle_like. Also for some reason the find_variant_by_options method was not working for me, so I made a full query, which looks like this: @variant = Spree::Variant.joins(:option_value_variants).where( spree_option_value_variants: {option_value_id: params[:ids].split(',')}, product: @product).first. Haven't fully tested it though.Cowbane

© 2022 - 2024 — McMap. All rights reserved.