I'm developing a JS-based app that allows you to customize a product. I still consider myself a super newbie to javascript and especially to object oriented programming. So far, so good, the app does what I want it to do, so no trouble there.
My background is mostly in jQuery, so my main concern is how I'm sharing and reusing variables between functions, and how I can make this code prettier and more maintainable through optimizing those functions and vars.
The point of the app is:
- Click a product option
- Select a finish type
- Select a finish color
- If there is an additional price for that finish, update the price
I broke up what was once one big file into 3 modules and am loading them using LABjs:
$LAB
.script(homeUrl + "/assets/js/product/product.js").wait()
.script(homeUrl + "/assets/js/product/product-color.js")
.script(homeUrl + "/assets/js/product/product-events.js")
PRODUCT.JS
(function (window, document, $) {
"use strict";
// Set some general element variables used throughout this script
var $nodes = {
list: $('.cust-list'),
steps: $('.steps'),
step: $('.cust-step'),
stepsSlide: $('.steps-slide'),
subtotal: $('.customizer .total .price'),
allTypes: $('.finish-type a'),
allColors: $('.finish-color a'),
options: $('.cust-option'),
checks: $('.cust-option-checklist a')
};
function Product (name) {
this.name = name;
this.options = [];
}
// Loops through the options slide divs
function optionsLoop($options, callback) {
for(var i = 0; i < $options.length; i++) {
callback(i, $options[i]);
}
}
// Loops through the array of product options within the product object
function productOptionsLoop(productOptions, callback) {
for(var i = 0; i < productOptions.length; i++) {
callback(i, productOptions[i]);
}
}
// Populate the product object with an array of options
function getProductOptions($nodes, product) {
optionsLoop($nodes.options, function(index,value) {
var $me = $(value),
name = $me.attr('data-option'),
type = $me.attr('data-option-type'),
option = {
option: name,
type: type
};
product.options.push(option);
});
}
// Change the cost according to the added options / variations
function updateCost(addCost, productOptions, totalPrice, $subtotal, productPrice, $nodes) {
var currentSubtotal = $subtotal.attr('data-subtotal');
addCost = 0;
// Go through all the product options, if an additional cost has been set, add them up
productOptionsLoop(productOptions, function(index,value){
var $me = value,
cost = $me.cost;
if(cost) {
addCost += cost;
}
});
productPrice = +productPrice;
totalPrice = productPrice + addCost;
animateNumber($nodes.subtotal, currentSubtotal, totalPrice);
// Update the data attribute on the subtotal to reflect the user's choices
$nodes.subtotal.attr('data-subtotal',totalPrice);
// animating number produces rounding errors, so shortly after we animate, update the text with the exact total
setTimeout(function(){
$nodes.subtotal.text(totalPrice).digits();
},325);
}
function updateOptions(productOptions, myOption, myName, myCost, myType) {
// Go through the array of options and add the selected color and cost to this option
productOptionsLoop(productOptions, function(index,value) {
var $this = value;
if($this.option === myOption){
$this.name = myName;
$this.cost = Math.floor(myCost);
if(myType) {
$this.type = myType;
}
return false;
}
});
}
$.extend(window, {
'$nodes': $nodes,
'Product': Product,
'optionsLoop': optionsLoop,
'productOptionsLoop': productOptionsLoop,
'getProductOptions': getProductOptions,
'updateCost': updateCost,
'updateOptions': updateOptions
});
}(window, document, jQuery));
PRODUCT-COLOR.JS
$(document).ready(function(){
var productName = $('#product').attr('data-product-name'),
// Create a new product object with the name of the currently viewed product
product = new Product(productName),
// This is set to true when the slide animation has completed, to avoid multiple fast clicks
animReady = true,
productPrice,
totalPrice,
productOptions,
addCost,
inAnim = {},
outAnim = {opacity: 0},
outCss = {opacity: 1};
getProductOptions($nodes, product);
productOptions = product.options;
productPrice = $nodes.subtotal.attr('data-subtotal');
// Color selecting
$nodes.checks.add($nodes.allColors).add($nodes.allTypes).on('click',function(){
if($(this).hasClass('current')) {
return false;
}
var $me = $(this),
$parent = $me.parent(),
$granpa = $me.parents('.cust-step'),
granpaEq = $granpa.index() - 1,
myOption = $granpa.attr('data-option'),
$myCheck = $('.option-list li:eq(' + granpaEq + ')'),
myCost = $me.attr('data-option-cost'),
myName = $me.attr('data-option-name'),
myType = null,
$optTypes,
$optColors,
$curColor,
$curType,
$myParentType,
$myColors,
className,
$add,
$remove,
isCheck = $me.is('.cust-option-checklist a'),
isColor = $me.is('.finish-color a'),
isType = $me.is('.finish-type a');
if(isCheck) {
var $curCheck = $granpa.find('a.selected');
className = 'selected';
$add = $me;
$remove = $curCheck;
}
if(isColor || isType) {
if(isColor) {
// If we're clicking a color, select the <a> links
myType = $parent.attr('data-finish-type');
$optColors = $granpa.find('.finish-color a');
} else {
// If we're clicking a color, select the divs containing each color <a>
myType = $me.attr('data-finish-type');
$optColors = $granpa.find('.finish-color');
}
// All types and colors for the current option
$optTypes = $granpa.find('.finish-type a');
$curColor = $optColors.filter('.current');
$curType = $optTypes.filter('.current');
if(isColor) {
var myBg = $me.css('backgroundColor'),
myBgImg = $me.css('backgroundImage'),
bg = myBgImg;
$myParentType = $optTypes.filter('[data-finish-type=' + myType + ']');
$remove = $curColor.add($optTypes);
$add = $me.add($myParentType);
className = 'current ic-check selected';
} else {
$myColors = $optColors.filter('[data-finish-type=' + myType + ']');
className = 'current';
$remove = $curColor.add($curType);
$add = $me.add($myColors);
}
}
// Add selected class to chosen finish + type
setCurrent($add,$remove,className);
if(isColor) {
$curType = $optTypes.filter('.current');
// Set the background image for parent type to reflect chosen color
if(myBgImg === 'none') {
bg = myBg;
}
$curType.css({'background' : bg});
}
// If you select a color or a checkbox, mark the list item as selected
if( (isColor || isCheck) && !$myCheck.hasClass('.selected') ) {
$myCheck.add($granpa).addClass('selected');
}
updateOptions(productOptions, myOption, myName, myCost, myType);
updateCost(addCost, productOptions, totalPrice, $nodes.subtotal, productPrice, $nodes);
// Remove existing price indicator
$myCheck.find('.price').remove();
// Add price indicator to checklist if there is an extra cost, or else
if(myCost > 0) {
$myCheck.addClass('extra').find('a').append('<span class="f9 price">$' + myCost + '</span>');
} else {
$myCheck.removeClass('extra');
}
});
// Navigation
$('.cust-btn:not(.go-back)').on('click',function(){
var $me = $(this),
$curStep = $nodes.step.filter('.cust-step-cur'),
$nextStep = $curStep.next(),
$prevStep = $curStep.prev(),
isPrev = $me.hasClass('prev'),
$tar = $nextStep,
curIndex,
offset,
speed = 350;
if(isPrev) {
$tar = $prevStep;
if($tar.length === 0) {
$tar = $nodes.step.filter(':last');
}
} else {
if($tar.length === 0) {
$tar = $nodes.step.filter(':first');
speed = 0;
}
}
setCurrent($tar, $curStep, 'cust-step-cur');
$curStep = $nodes.step.filter('.cust-step-cur');
curIndex = $curStep.index('.cust-step');
offset = curIndex * 160;
$nodes.stepsSlide.animate({
right: -offset
},speed);
});
// Checklist Click
$('.option-list a').on('click',function(){
var $me = $(this),
myIndex = ($me.parent().index()) + 1,
$mySlide = $nodes.step.eq(myIndex),
offset = myIndex * 160;
setCurrent($mySlide, $nodes.list, 'cust-step-cur');
$nodes.stepsSlide.animate({
right: -offset
}, 0);
});
$('.cust-btn.go-back').on('click',function(){
var $curStep = $nodes.step.filter('.cust-step-cur');
setCurrent($nodes.list, $curStep, 'cust-step-cur');
$nodes.stepsSlide.animate({
right: 0
}, 0);
});
});
PRODUCT-EVENTS.JS
$(document).ready(function(){
var $productImg = $('.customizer-wrap .image'),
productImgUrl = $productImg.find('img').attr('src'),
spinnner,
$stepsSlide = $nodes.stepsSlide,
slideCount = $stepsSlide.find('.cust-step').length,
slidesLength = slideCount * 160;
$stepsSlide.css({width: slidesLength});
// Initialize loading graphic
if(!productImgUrl) {
return false;
}
spinner = startSpinner('product-image', 10, 4, 15, false);
// Preload the big image, when loaded, fade in
$.imgpreload(productImgUrl, function(){
$productImg
.zoom();
$('.spinner').fadeOut(2000, function(){
spinner.stop();
$('html').removeClass('img-loading');
});
});
});
updateCost()
, why do you passaddCost
when you set it to 0? \$\endgroup\$