Marketing | Creative | Strategy

Your faithful partner in the discovery of endless digital opportunity.

Take the First Step

How To Develop a "Build a Box" Product In Shopify - Part 3: Building A Necklace With A "Drag And Drop" User Interface

We previously wrote articles explaining how to develop a Build a Box product in Shopify and how to remove the limit on flavors in the box. In this article we are going to revisit the Build a Box product, but this time we will be adding a slightly different spin on it. I know what you’re thinking, yet another sequel in a series, but trust me, this one is worth checking out… unlike some of the other sequels that may have been less than stellar.


While in our last two articles, we used the idea of building a box of chocolates, this time we will be going in a slightly different direction and using the same core approach to build a necklace. While this is obviously an entirely different type of product to a box of chocolates, the same idea applies in that we will be using a parent necklace product containing multiple charm products rather than a parent box product containing multiple chocolate flavors.


We will also be adding a whole new user interface to this, because we want to build a product that allows the user to see a visual representation of how their finished product will actually look when they receive it in the mail. This is the primary difference here, because you don’t need to be able to see your box of chocolates that you built, but a necklace is a whole different story, and offering your users an interactive and visual display of what they will get is a bonus that not many other sellers will have. So we will be pushing things even further this time around, to add a “drag and drop” feature so that the user can build their necklace exactly how they want.


The first thing we want to do is add the core HTML markup for the build steps. We will be breaking each step out into separate list items which we show or hide accordingly.

<ul id="builder-steps">
  <li id="step1"></li>
  <li id="step2"></li>
  <li id="step3"></li>
</ul>

Within each list item, we also add a button to click through to the next step...

<div class="next">Next</div>

We then hide the list items by default with the CSS #builder-steps { display: none }  and then add the following javascript to show each successive step as the user clicks next at each step.

$('#builder-steps li .next').click(function() {
  $(this).parent().hide();
  $(this).parent().next().show();
});

STEP 1

In this step, we choose the necklace. Exactly like in our original article, we add the necklaces as variants of our builder product.

We add two divs to the markup of step one inside the <li id=”step1”>. One containing a list of the necklaces the user can choose from and another containing the drop zone where the user drags and drops the necklace to

<li id="step1">

<div class="list">
    {% for option in product.options_with_values %}
        {% if option.position == 1 %}
        <ul>
            {% for value in option.values %}
            {% for variant in product.variants %}
                {% if variant.option1 == value %}
                <li>
                    <div class="draggable" data-price="{{ variant.price | money }}" data-title="{{ variant.option1 }}" data-variant-id="{{ variant.id }}">
                    <div class="image">
                    <div class="thumbnail" style="background-image: url({{ variant.featured_image | img_url: '1600x' }})" data-full="{{ variant.featured_image | img_url: '995x' }}"></div>
                    </div>
                    <div class="text">
                    <label class="form-check-label" for="{{ variant.id }}">{{ value | escape }}</label>
                    </div>
                </div>
                <div class="price">{{ variant.price | money }}</div>
                </li>
                {% endif %}
            {% endfor %}
        </ul>
        {% endif %}
      {% endfor %}
    </div>
    <div class="drop-zone">
    <div class="image-holder">
        <div class="image"></div>
    </div>
    <div class="placeholder">Drag and Drop Necklace Here</div>

    <div class="next">Next</div>
    </div>
</li>

Then we add the CSS for this step...

#builder-steps {
    .list {
      position: relative; margin-bottom: 20px;
      .image { float: left; width: 25%; max-width: 100px; }
      .text { float: left; width: 50%; height: 75px; display: flex; justify-content: center; flex-direction: column; font-size: 13px; padding: 0 15px; }
      .price { float: right; width: 25%; height: 75px; display: flex; justify-content: center; flex-direction: column; text-align: right; padding-right:

30px; font-size: 13px; }
    }
    .drop-zone {
        .image-holder {
            position: absolute; top: 50%; left: 50%; width: 100%; height: auto; transform: translate(-50%, -50%); padding: 30px;
            .image {
            display: block; max-width: 100%; height: auto; background-size: contain; background-position: center; background-repeat: no-repeat;
            &:after { content: ''; display: block; padding-top: 50%; }
            }
        }
      .placeholder { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 100%; text-align: center; text-transform: uppercase; color: #ccc; font-size: 30px; }
    }
}

Which gives us this...

The final thing we need, is to add the drag and drop functionality. For this we use the jQuery UI Draggable and Droppable libraries and call them with this code

$('#builder-steps .draggable').draggable({
  helper: 'clone',
  containment: 'window',
  appendTo: 'body',
  scroll: false,
  start: function(event, ui) {

 var width = $(event.target).find('.thumbnail').width();
      var height = $(event.target).find('.thumbnail').height();
      $(ui.helper).css({'width': '${ width }px', 'height': '${ height }px'});
      $(this).draggable('instance').offset.click = {
        left: Math.floor(width / 2),
        top: Math.floor(height / 2)
      };
  }
});

This allows the user to click on a necklace in the list and drag and drop it into the drop zone. We use the clone helper to allow us to drag the necklace from the list while also keeping the original in the list (we don’t want to move it but instead copy it over the drop zone).

 

We added some code inside the drag start event to set the dimensions of the draggable image and also center the mouse cursor inside the image as it is dragged. We also add this code to initialize the drop zone to accept the necklace images...

$('#builder-steps #step1 .drop').droppable({
  accept: '.draggable',
  tolerance: 'pointer',
  drop: function(event, ui) {
    /* Grab the dropped element */
    var droppedElement = ui.draggable;

    /* Apply necklace image to canvas div */
    $(this).find('.image').css('background-image', 'url(' + $(droppedElement).find('.thumbnail').attr('data-full') + ')');

    /* Hide placeholder text */
    if($(this).find('.placeholder').is(':visible')) {
      $(this).find('.placeholder').hide();
    }

    /* Assign selected class to selected necklace in the list */
    $(droppedElement).parent().siblings().removeClass('selected');
    $(droppedElement).parent().addClass('selected');
  }
});

Now, we apply the full size necklace image to the drop zone and hide the placeholder text, giving us this...

STEP 2

 

This is where the user adds charms to the necklace. We add the charms as separate products to the master necklace product. They are linked to their parent necklace product by their title, which is the same as the necklace product but also with the word charms in it. Each charm product contains the individual charms set as variants, exactly as we did with the necklaces.

The markup we use for step 2 is this...

<li id="step2">
    <div class="list">
        <ul>
        {% assign productIndex = 0 %}
        {% assign necklaceProductTitle = product.title %}
        {% paginate collections['all'].products by 1000 %}
            {% for product in collections['all'].products %}
            {% if product.title contains necklaceProductTitle and product.title contains 'charms' %}
                {% for variant in product.variants %}
                <li id="charm-{{ variant.id }}">
                    <div class="draggable charm" data-price="{{ variant.price | money }}" data-title="{{ variant.option1 }}" data-variant-id="{{ variant.id }}">
                    <div class="image">
                        <div class="thumbnail" style="background-image: url({{ variant.featured_image | img_url: '100x' }})" data-full="{{ variant.featured_image | img_url: '100x' }}"></div>
                    </div>
                    </div>
                    <div class="text">
                        <label class="form-check-label" for="{{ variant.id }}">{{ variant.title }}</label>
                    </div>
                    <div class="price">{{ variant.price | money }}</div>
                </li>
                {% assign productIndex = productIndex | plus: 1 %}
                {% endfor %}
            {% endif %}
            {% endfor %}
        {% endpaginate %}
        </ul>
    </div>
    <div class="drop-zone">
        <div class="placeholder">Drag and Drop Your Charms</div>
        <div class="image-holder">
            <div class="image"></div>
        </div>
        <div id="charm-slots" class="clearfix">
            <div class="slot first">
            <div class="drop" data-index="1"></div>
            </div>
            <div class="slot">
            <div class="drop" data-index="2"></div>
            </div>
            <div class="slot">
            <div class="drop" data-index="3"></div>
            </div>

<div class="slot">
            <div class="drop" data-index="4"></div>
            </div>
            <div class="slot">
            <div class="drop" data-index="5"></div>
            </div>
            <div class="slot">
            <div class="drop" data-index="6"></div>
            </div>
            <div class="slot">
            <div class="drop" data-index="7"></div>
            </div>
            <div class="slot">
            <div class="drop" data-index="8"></div>
            </div>
            <div class="slot">
            <div class="drop" data-index="9"></div>
            </div>
            <div class="slot">
            <div class="drop" data-index="10"></div>
            </div>
            <div class="slot">
            <div class="drop" data-index="11"></div>
            </div>
            <div class="slot">
            <div class="drop" data-index="12"></div>
            </div>
        </div>

        <div class="next">Next</div>

    </div>
</li>

Plus the following CSS...

#builder-steps {
  #step2 {
    #charm-slots {
      display: block; width: 90%; position: absolute; top: 40%; left: 5%; margin: 0; padding: 0; text-align: center;
      .slot {
      float: left; width: 8.33333333%; height: auto; position: relative; padding: 0 15px; margin-top: 35px;
      .drop { display: block; padding: 100% 0 0; height: 0; border: 1px solid; }
      }
    }
  }}

Which gives us this...

 

Similar to step one, we’ve got the list on the left and the drop zone on the right. We can see the necklace we added in step one along with 12 slots for us to add charms to. The draggable and droppable code we added for the last step also applies here too, so we don’t need to do anything more on the javascript on this step.

STEP 3

In the final step, the user reviews their built product and adds it to cart. 

The markup for this is...

<li id="step3">
    <div class="list">
        <h3>Your Necklace</h3>
        <ul id="order-contents" class="clearfix"></ul>
        <div id="order-subtotal" class="clearfix">
            <div id="order-subtotal-label">Subtotal</div>
            <div id="order-subtotal-value"></div>
        </div>
        <form action="/cart/add" method="post" enctype="multipart/form-data" id="AddToCartForm" class="builder">             
        <button type="submit" name="add" id="AddToCart" class="btn" disabled="disabled">
            <span id="AddToCartText">{{ 'products.product.add_to_cart' | t }}</span>
        </button>
    </form>
    </div>
    <div class="drop-zone">
    </div>
</li>

The drop zone div is empty because we use some javascript to copy the selected necklace and charms from step 2. We also populate the necklace and charms in the summary.

$('#builder-steps #step2 .next').click(function(e) {
    var necklace = $(this).siblings('.image-holder').children('.image').clone();
    var charms = $(this).siblings('#charm-slots').clone();
    $(charms).children('.slot').each(function(index, element) {
    if(!$(this).children('.drop').hasClass('filled')) {
        $(this).css('visibility', 'hidden');
    }
    });
    $(charms).find('.remove').remove();
    $('#builder-steps #step3').find('.drop-zone .image').remove();
    $('#builder-steps #step3').find('.drop-zone').append(necklace);
    $('#builder-steps #step3').find('.drop-zone #charm-slots').remove();
    $('#builder-steps #step3').find('.drop-zone').append(charms);
    var orderSummaryHtml = '<li class="clearfix"><div class="item">' + $('#builder-steps #step2').siblings('#step1').find('.list li.selected .draggable').attr('data-title') + '</div><div class="price">' + $('#builder-steps #step2').siblings('#step1').find('.list li.selected .draggable').attr('data-price') + '</div></li>';
    $('#builder-steps #step2 #charm-slots .slot .drop.filled').each(function() {
    orderSummaryHtml = orderSummaryHtml + '<li class="indented"><div class="item">' + $(this).children('.charm').attr('data-title') + '</div><div class="price">+' + $(this).children('.charm').attr('data-price') + '</div></li>';
    });

 $('#builder-steps #step3 #order-contents').html(orderSummaryHtml);
    $('#builder-steps #step3 #order-subtotal-value').html($('#builder-steps #step2 .list .subtotal-value').html());
});

We need the following CSS...

#builder-steps {
  #order-contents {
    padding: 0 30px 0 0; margin: 0;
    .item { width: 75%; float: left; }
    .price { width: 25%; float: right; text-align: right; }
    .indented { padding-left: 20px; }
  }
  #order-subtotal {
margin: 30px 0; padding: 0 30px 0 0; text-transform: uppercase; font-weight: bold; font-size: 16px; color: black;
    #order-subtotal-label { width: 75%; float: left; }
    #order-subtotal-value { width: 25%; float: right; text-align: right; }
  }
}

This javascript handles the add to cart functionality...

$('#AddToCartForm.builder').submit(function() {
    Shopify.queue = []; // Define the empty queue
    var variantsToAdd = {}; // A json array that holds the titles of the charm variants the user has selected
    var charmChoices = []; // An array that holds the charm choices containing both title and quantity
    var totalPrice, charmString, variantId, size, charmSlotSelector;
    totalPrice = $('#builder-steps #AddToCartForm #order-subtotal-value').html(); // Used to store the final price for the necklace and charms combined
    charmString = ''; // Define empty string to add the charm line item properties to
    variantId = $('#builder-steps #step1 .list li.selected .draggable').attr('data-variant-id');
    charmSlotSelector = $('builder-steps #step2 #charm-slots .slot');

    // Loop through selected charms to append titles to the charmString variable
    $('#builder-steps #step2 #charm-slots .slot').each(function () {
        if($(this).children('.drop').hasClass('filled')) {
            charmString = charmString + $(this).find('.charm').attr('data-title') + ',';
        }
    });

 // Remove the trailing comma from the line item string
    charmString = charmString.replace(/,\s*$/, "");

    // Push the necklace to the queue
    Shopify.queue.push({
        variantId: variantId,
        quantity: 1,
        properties: {
            Charms: charmString,
            Price: totalPrice
        }
    });

    // Loop through the selected charms and populate the charmChoices array
    charmSlotSelector.each(function () {
      if($(this).children('.drop').hasClass('filled')) {
        var charmTitle = $(this).find('.charm').attr('data-title'); // Set charm title

        if(charmTitle.match(/^\d+$/)) {
        charmTitle = 'Charm ' + charmTitle;
        }

        var charmId = parseInt($(this).find('.charm').attr('data-variant-id')); // Set charm variant id

        if(variantsToAdd[charmId] != null) { // If this charm is already in the cart then we increment its quantity by the amount chosen
          variantsToAdd[charmId] = parseInt(variantsToAdd[charmId]) + 1; // Increment quantity
          charmChoices[charmTitle] = charmChoices[charmTitle] + 1;
          $.each(charmChoices, function (i, value) {
            if(charmChoices[i].title == charmTitle) {
              charmChoices[i].quantity = charmChoices[i].quantity + 1;
            }
          });
        }
        else { // Otherwise if the charm is not already in the cart then we add only the chosen quantity to the data object
          variantsToAdd[charmId] = 1;
          charmChoices[charmTitle] = 1;
          charmChoices.push({
            id: charmId,
            title: charmTitle,
            quantity: 1
          });
        }

  }
    });

    // Loop through the charmChoices array and push charm variants to queue
    $(charmChoices).each(function(i, element) {
      Shopify.queue.push({
        variantId: element.id,
        quantity: element.quantity
      });
    });

    Shopify.moveAlong = function() {
      // If we still have requests in the queue, let's process the next one.
      if (Shopify.queue.length) {
        var request = Shopify.queue.shift();
        var data = {};
        if(request.properties) {
            data = {
                id: request.variantId,
                quantity: request.quantity,
                properties: {
                    Charms: charmString,
                    Price: totalPrice
                }
            };
        }
        else {
          data = {
            id: request.variantId,
            quantity: request.quantity
          };
        }

        $.ajax({
          type: 'POST',
          url: '/cart/add.js',
          dataType: 'json',
          data: data,
          beforeSend: function(jqxhr, settings) {
            $body.trigger('beforeAddItem.ajaxCart', form);
          },
          success: function(line_item) {
            if ((typeof callback) === 'function') {
              callback(line_item, form);
            }
            else {
              ShopifyAPI.onItemAdded(line_item, form);
            }

 $body.trigger('afterAddItem.ajaxCart', [line_item, form]);
            Shopify.moveAlong();
          },
          error: function(XMLHttpRequest, textStatus) {
            if ((typeof errorCallback) === 'function') {
              errorCallback(XMLHttpRequest, textStatus);
            }
            else {
              ShopifyAPI.onError(XMLHttpRequest, textStatus);
            }
            $body.trigger('errorAddItem.ajaxCart', [XMLHttpRequest, textStatus]);
          },
          complete: function(jqxhr, text) {
            $body.trigger('completeAddItem.ajaxCart', [this, jqxhr, text]);
          }
        });
      }
    };
    Shopify.moveAlong();
});

And that’s a wrap! 

 

We have leveraged the “build a box” feature in a different context with a slick drag and drop interface to boot. Building a necklace complete with charms is something that no Shopify app can handle particularly elegantly, but with this approach you no longer need to rely on apps - you can do this yourself. Plus, the beauty of this approach is that it can be adapted to all kinds of product types and client needs. Play around with this feature and come up with your very own build-a-product!

 

 

 

Niall Gallagher
Senior Web Developer
If there is a new web app, I’ve tried it. If it’s integratable, optimizable, automatable, and customizable it is right up my alley. I manage and lead a team of developers that specialize in making a workable digital solution for your business out of nothing. I identify and make ongoing recommendations based on your business goals and the operational feasibility of what can be implemented to allow your business to scale and grow freely. Long story short, we’ll solve problems you didn’t know you had, in way you never thought possible.
close[x]

Get A Quote

Ready to take the next step? Please fill out the form below. We can't wait to hear from you!

Once submitted, a Slicedbread representative will be back in touch with you as soon as possible.

* Required Fields