3
\$\begingroup\$

I have a library that will be called in multiple places. Here is my implementation:

var vessel1 = new Vessel();
var vessel2 = new Vessel();

vessel2 should use the same instance as vessel1.

The reason I need a singleton pattern here is because of my _initialize() function. It should only ever be called once and with my current pattern I've achieved that, however I'm not sure if this is the best way to do it.

What I'm trying to do is basically have this library being used from multiple other libraries/html files but since they may all be on one page, I want the Vessel to only ever be instantiated once since the _initialize() methods does things I don't want to be run multiple times on page load cause it causes lag.

Also, is my way of declaring "methods" to the Vessel class also ideal? I see people using prototypes often.

Here is my current implementation. How can it improve?

/**
 * Vessel library.
 *
 * @returns {object}
 * @constructor
 */
var Vessel = function() {
    'use strict';

    if (Vessel._instance) {
        return Vessel._instance;
    }

    Vessel._instance = {
        add: add,
        addFilter: addFilter,
        remove: remove,
        showItemList: showItemList,
        closeItemList: closeItemList,
        removeFilter: removeFilter,
        reset: reset,
        toggleSkipDeliveryText: toggleSkipDeliveryText,
        selectWeekInDelivery: selectWeekInDelivery,
        selectDropdownWeek: selectDropdownWeek,
        getSelectedWeek: getSelectedWeek
    };

    var self = this;
    self.currentBlockTag = 'block[data-block="checkout.step.delivery"]';
    self.selectedFilters = [];
    self.IN_DELIVERY_MODE = (window.location.href.indexOf('delivery') > -1);
    self.IN_CHECKOUT_MODE = !self.IN_DELIVERY_MODE;

    _initialize();

    return Vessel._instance;

    /**
     * Pass in a date in Y-m-d format and get returned a date in the same format
     * seven days later. For example, 2016-07-15 will return 2016-07-22
     *
     * @param date
     * @returns {*}
     * @private
     */
    function _addSevenDays(date) {
        var dateArray = date.split('-');

        // Add 7 days to the date to get the end date
        return (
            dateArray[0] + '-' + dateArray[1] + '-' + ( parseInt(dateArray[2]) + 7 )
        ).toString();
    }

    /**
     * Adjust the font sizes in the vessel message boxes based on the width
     * of the outer/inner box.
     *
     * @private
     */
    function _adjustMessageFontSize() {
        $j('.vessel-message').each(function(index, message) {
            var originalH4FontSize = 18;
            var originalPFontSize = 20;

            var divWidth = $j(message).width();
            var spanWidth = $j(message).find('.vessel-message-inner').width();

            var h4 = $j(message).find('h4');
            var p = $j(message).find('p');

            var newH4FontSize = (divWidth / spanWidth) * originalH4FontSize;
            var newPFontSize = (divWidth / spanWidth) * originalPFontSize;

            h4.css({
                "font-size": newH4FontSize,
                "line-height": newH4FontSize / 1.2 + "px"
            });

            p.css({
                "font-size": newPFontSize,
                "line-height": newPFontSize / 1.2 + "px"
            });
        });
    }

    /**
     * Vessel initializers, such as matching height of vessel item text, etc...
     *
     * @private
     */
    function _initialize() {
        _matchVesselItemTextHeight();

        _adjustMessageFontSize();

        _watchDateChange();

        // Call only if the vessel is in the context of the upcoming deliveries
        if (self.IN_DELIVERY_MODE) {
            _initStickyCapacityInfoContainer();
        }
    }

    /**
     * Sets the configuration for the stickyness of the capacity info container.
     *
     * @private
     */
    function _initStickyCapacityInfoContainer() {
        $j('.capacityinfo-container').affix({
            offset: {
                top: $j('#mainHeader').outerHeight(true)
            }
        });
    }

    /**
     * Set the html passed to the respective area based on what page the vessel
     * is currently being viewed on.
     *
     * @param html
     * @private
     */
    function _setNewVessel(html) {
        // Viewing vessel in checkout process
        if ($j('body').hasClass('tksubscriptions-index-index')) {
            $j(self.currentBlockTag).html('').html(html);
        }
        // Viewing vessel in upcoming deliveries
        else if ($j('body').hasClass('tksubscriptions-delivery-index')) {
            $j('.vessel-container').parent().html('').html(html);
        }
    }

    /**
     * Loop through the items in the selection popup and show only the ones that have
     * a type matching a type in the selectedFilters property. If no filters are selected,
     * show all items (this would also be the default).
     *
     * @private
     */
    function _updatedFilteredItems() {
        $j('body')
            .find('.vessel-selection-container .vessel-item')
            .each(function(index, item) {
                var itemTypes = $j(item).attr('data-type').split(',');
                var matchFound = false;

                for (var i = 0; i < itemTypes.length; i++) {
                    for (var y = 0; y < self.selectedFilters.length; y++) {
                        if (itemTypes[i].toString() === self.selectedFilters[y].toString()) {
                            matchFound = true;
                            break;
                        }
                    }
                }

                if (self.selectedFilters.length == 0) {
                    $j(item).show();
                } else {
                    matchFound ? $j(item).show() : $j(item).hide();
                }
            });
    }

    /**
     * Match the height of the vessel item titles and descriptions.
     *
     * @private
     */
    function _matchVesselItemTextHeight() {
        $j('.vessel-title').matchHeight();
        $j('.vessel-description').matchHeight();
        $j('.vessel-item').matchHeight();
    }

    /**
     * Fetch the vessel showing the items on the selected date of the dropdown.
     *
     * @private
     */
    function _watchDateChange() {
        $j('#vessel-date-select').change(function(e) {
            e.preventDefault();

            var date = $j(this).val();

            var dates = {
                startDate: date,
                endDate: _addSevenDays(date)
            };

            selectWeekInDelivery(dates, function() {
                calendar.setActiveWeek(false, date);
            });
        });
    }

    /**
     * Take the sku of the selected item and send to server.
     *
     * @param item
     */
    function add(item) {
        $j.ajax({
                url: '/subscriptions/index/addItemToVessel',
                method: 'POST',
                data: {
                    sku: item.attr('data-sku')
                }
            })
            .done(function(response) {
                // Close vessel selection popup (in case it was open when adding product)
                closeItemList();

                // Close item detail popup (in case it was open when adding product)
                $j.fancybox.close();

                reset(response.blocks['checkout.step.delivery']);
            })
            .fail(function(response) {
                console.log('Error adding item: ', response);
            });
    }

    /**
     * Add the selected filter from the selectedFilters property and update the
     * selected items list accordingly.
     *
     * @param selectedType
     */
    function addFilter(selectedType) {
        self.selectedFilters.push(selectedType);

        _updatedFilteredItems();
    }

    /**
     * Take the sku of the selected item and send to server.
     *
     * @param item
     */
    function remove(item) {
        $j.ajax({
            url: '/subscriptions/index/removeItemFromVessel',
            method: 'POST',
            data: {
                sku: item.attr('data-sku')
            }
        })
        .done(function(response) {
            reset(response.blocks['checkout.step.delivery']);
        })
        .fail(function(response) {
            console.log('Error removing item: ', response);
        });
    }

    /**
     * Show the popup to select items.
     */
    function showItemList() {
        $j('body').css('overflow-y', 'hidden');
        $j('#checkoutHeader').css('z-index', '0');
        $j('.vessel-selection-container').show();
    }

    /**
     * Close the popup of items to select.
     */
    function closeItemList() {
        $j('body').css('overflow-y', 'scroll');
        $j('#checkoutHeader').css('z-index', '9999');
        $j('.vessel-selection-container').hide();
    }

    /**
     * Remove the selected filter from the selectedFilters property and update the
     * selected items list accordingly.
     *
     * @param selectedType
     */
    function removeFilter(selectedType) {
        var typeIndex = self.selectedFilters.indexOf(selectedType);

        if (typeIndex > -1) {
            self.selectedFilters.splice(typeIndex, 1);
        }

        _updatedFilteredItems();
    }

    /**
     * This takes the new vessel HTML and resets the vessel in the DOM
     * as well as re-initializing anything else such as product heights,
     * font sizes, etc.
     *
     * @param html
     * @param callback
     */
    function reset(html, callback) {
        _setNewVessel(html);

        $j('body').children('.vessel-selection-container').remove();

        $j('.vessel-item').matchHeight();

        _adjustMessageFontSize();

        if (typeof callback === 'function') {
            callback();
        }
    }

    /**
     * Only toggle the "skip/unskip delivery" text if the current week in the vessel matches
     * any week that is selected in the calendar.
     *
     * @param dateOrDates
     */
    function toggleSkipDeliveryText(dateOrDates) {
        var isCurrentDeliveryChecked = false;

        if (Array.isArray(dateOrDates)) {
            dateOrDates.forEach(function(date, index) {
                if ($j('#vessel-date-select').val() == date) {
                    isCurrentDeliveryChecked = true;
                }
            });
        } else {
            if ($j('#vessel-date-select').val() == dateOrDates) {
                isCurrentDeliveryChecked = true;
            }
        }

        if (isCurrentDeliveryChecked) {
            $j('.vessel-container .skip-delivery-container')
                .children()
                .each(function(index, item) {
                    $j(item).hasClass('hidden')
                        ? $j(item).removeClass('hidden')
                        : $j(item).addClass('hidden');
                });
        }
    }

    /**
     * Pass in a date to select an option in the dropdown matching that date.
     *
     * @param date
     * @private
     */
    function selectDropdownWeek(date) {
        var options = $j('#vessel-date-select option');

        for (var i = 0; i < options.length; i++) {
            if ($j(options[i]).val() == date) {
                $j(options[i]).prop('selected', true);
                break;
            }

            $j(options[i]).prop('selected', false);
        }
    }

    /**
     * Post a date in Y-m-d format to the server to fetch the vessel containing
     * items for that delivery week.
     *
     * @param dates
     * @param callback
     */
    function selectWeekInDelivery(dates, callback) {
        $j.ajax({
                url: '/subscriptions/index/selectWeekInDelivery/',
                method: 'POST',
                data: {
                    startDate: dates.startDate,
                    endDate: dates.endDate
                }
            })
            .done(function(response) {
                //var newVesselHtml = response.blocks['checkout.step.delivery'];

                //reset(newVesselHtml);

                if (typeof callback === 'function') {
                    callback();
                }
            })
            .fail(function(response) {
                console.log('Error', response);
            });
    }

    /**
     * Get the date that is currently selected in the dropdown.
     *
     * @returns {*}
     */
    function getSelectedWeek() {
        return $j('#vessel-date-select').val();
    }
};
\$\endgroup\$
3
  • 2
    \$\begingroup\$ Please include the rest of your code, especially your _initialize function. This seems like a really bad idea. Please edit your title with what are you're trying to solve with the code itself, not what you're seeking from a review. \$\endgroup\$ Commented Jun 28, 2016 at 15:27
  • \$\begingroup\$ Why are you doing 'new Vessel()' at all then? Why not just create a shared vessel somewhere and pass it around? You could use a provider VesselProvider.GetVessel() for example - or just skip the new pretense all together and do Vessel.instance={} without the constructor approach. \$\endgroup\$ Commented Jun 30, 2016 at 14:27
  • \$\begingroup\$ Because I wanted to avoid using a global variable, but good idea on the getInstance approach. Thanks! \$\endgroup\$ Commented Jun 30, 2016 at 14:35

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.