1
\$\begingroup\$

I put together a jQuery plugin that displays a month of dates using underscore templates. When the template has been populated and displayed in the UI, a user can click on a date which sends a request to the api(with the selected date parameter) to give a list of available deals on that date. I then display these deals in another underscore template.

So far, it all seems to be working well. But as this is my first jQuery, I have a couple of concerns and would appreciate any feedback or advise on how to improve it...

1. How I am getting the data from the API with getJson and callbacks. I'm not sure if I am doing it efficiently.

2. I am using the jQuery .delegate to attach events to future elements in the templates. Perhaps there is a better way of doing this or is this ok?

3. The structure of my jQuery plugin. As I mentioned its my first one and would like to hear how I could improve it.

(function( $ ){

$.fn.Calendar = function(options) {

    var settings = $.extend({
        dealID : null,
        calendarTitle: 'Choose arrival date'
    }, options);

    var el = this;
    $(el).addClass('deal-calendar');

    var Templates = {};

    // templates for the UI using underscore templates
    Templates.calendarDay = [
        '<h2>Choose arrival date</h2>',
        '<a class="clear-selection">Clear selection</a>',
        '<div class="deal-calendar-header">',
        '<a id="prev-month" class="change-month left" data-change-month="<%= allDaysData.getYearMonth(allDaysData.data.currentYear, allDaysData.data.currentMonth, false)%>">< <%= allDaysData.data.previousMonth %></a>',
        '<span class="month"><%= allDaysData.data.currentMonth %></span><span class="year"><%= allDaysData.data.currentYear %></span>',
        '<a id="next-month" class="change-month right"  data-change-month="<%= allDaysData.getYearMonth(allDaysData.data.currentYear, allDaysData.data.currentMonth, true)%>"><%= allDaysData.data.nextMonth %> ></a>',
        '</div>',
        '<table class="deal-calendar-dates">',
        '<thead>',
        '<tr>',
        '<th>Monday</th><th>Tuesday</th><th>Wednesday</th><th>Thursday</th><th>Friday</th><th>Saturday</th><th>Sunday</th>',
        '</tr>',
        '</thead>',
        '<tr>',
        '<% _.each(allDaysData.data.calendarDateView, function(item, index){ %>',
        '<td data-arrival-Date="<%= item.dateString %>" class="day <%= (item.dateUnavailable == true) ? "unavailable": "available" %>">',
        '<span class="date"><%= allDaysData.ordinalSuffix(item.dateString) %></span>',
        '<span class="from"><%= (item.dateUnavailable == true) ? "Not available": "From" %></span>',
        '<span class="price"><%= (item.dateUnavailable == true) ? "for arrival ": "£" + item.minPrice %></span>',
        '</td>',
        '<% if ((index + 1) % 7 == 0) { %>',
        '</tr>',
        '<tr>',
        '<% }}) %>',
        '</tr>',
        '</table>',
        '<div id="js-day-products"></div>'
    ].join("\n");
    Templates.dealProducts = [
        '<h2>Choose stay length</h2>',
        '<% _.each(productsData.data.calendarProductView,function(item){ %>',
        '<div class="product-container">',
        '<h2><%= item.friendlyName %></h2>',
        '<div class="deal-details">',
        '<div class="stay">',
        '<div><span class="label">Check in: </span><%= item.checkInDate %></div>',
        '<div><span class="label">Check out: </span><%= item.checkOutDate %></div>',
        '</div>',
        '<div class="choose-quantity">',
        '<a class="choose-product">Choose</a>',
        '<% if (item.showQuantity) { %>',
        '<div class="quantity-container">',
        '<label for="<%= item.dealProductId %>-product-quantity">Persons:</label>',
        '<a class="decrement quantity">-</a>',
        '<input id="<%= item.dealProductId %>-product-quantity" name="quantity" class="person-quantity" type="text" max="<%= item.userPurchaseCap %>" value="1">',
        '<a class="increment quantity">+</a>',
        '</div>',
        '<% } %>',
        '</div>',
        '<div class="savings">',
        '<div class="was">',
        '<span class="label">Was:</span>',
        '<span>£<%= item.originalPrice %></span>',
        '</div>',
        '<div class="now">',
        '<span class="label">Now:</span>',
        '<span class="price">£<%= item.price %></span>',
        '</div>',
        '<div class="save">',
        '<span class="label">Save:</span>',
        '<span><%= item.discountPercentage %>%</span>',
        '</div>',
        '</div>',
        '</div>',
        '<div class="purchase" id="<%= item.dealProductId %>-purchase">',

        '<a name="checkout"  class="checkout" data-dealProductId="<%= item.dealProductId %>" data-checkInDate="<%= item.checkInDate %>">Checkout</a>',
        '</div>',
        '</div>',
        ' <% }) %>'
    ].join("\n");

    //helpers that are called from the templates
    var viewHelpers = {
        getYearMonth: getYearMonth,
        ordinalSuffix:  ordinalSuffix,
        orderURL: orderURL
    };

    //return the number of the month
    function getMonthNumber(monthName){
        var monthNames = ["January", "February", "March", "April", "May", "June",
            "July", "August", "September", "October", "November", "December"
        ];
        return monthNames.indexOf(monthName) + 1
    }

    // build the url for the API call (yyyy/mm)
    function getYearMonth(year, month, isNextMonth){

        var currentMonthNumber = getMonthNumber(month)
        var nextMonth = currentMonthNumber + 1;

        // if current month is december, increment the year and set the month to 1 (january)
        if(currentMonthNumber == 12 && isNextMonth == true){
            nextMonth = 1;
            year = year + 1;
        }

        // if its previous month
        if(!isNextMonth){
            nextMonth = currentMonthNumber - 1;
        }

        // if current month is january, decrement the year and set month to 12 (december)
        if(currentMonthNumber == 1 && isNextMonth == false){
            nextMonth = 12;
            year = year - 1;
        }
        return nextMonth + "-" + year;
    }

    // add the ordinals for the UI
    function ordinalSuffix(dateString) {

        var parts = dateString.split('-');
        i = parts[0];

        var j = i % 10,
            k = i % 100;
        if (j == 1 && k != 11) {
            return i + "st";
        }
        if (j == 2 && k != 12) {
            return i + "nd";
        }
        if (j == 3 && k != 13) {
            return i + "rd";
        }
        return i + "th";
    }

    // build the order url
    function orderURL(dealProductId, checkInDate, quantity){

        checkInDate = checkInDate.replace(/-|\//g, "");// remove the slashes form the date
        return( '/' + dealProductId + '?qty=' + quantity + '&checkin=' + checkInDate);
    }

    // Set previous/next month functionality
    function setMonth(month){

        getMonthData(month, function (data) {
            _.extend(data, viewHelpers);
            $(el)
                .empty()
                .append(_.template(Templates.calendarDay)({allDaysData: data}));
        });
    };

    // Get the products for the selected date
    function getProducts(arrivalDate){
        getDayProductsData(arrivalDate, function (data) {
            $('#js-day-products')
                .empty()
                .append(_.template(Templates.dealProducts)({productsData: data}))
                .delegate(".reserve-checkout", "click", function() {
                    var dealProductId = $(this).attr("data-dealProductId");
                    var checkInDate = $(this).attr("data-checkInDate");
                    var productQuantityID = $(this).attr("data-dealProductId") + "-product-quantity";
                    var quantity = $("#" + productQuantityID).val();
                    var ourl = viewHelpers.orderURL(dealProductId, checkInDate, quantity);
                    //do stuff with the ourl
                })
                .delegate(".choose-product", "click", function() {
                    $('.product-container').removeClass('selected');
                    $(this).parents('.product-container').addClass('selected');
                })
                .delegate(".increment", "click", function() {
                    var quantity = $(this).siblings('.person-quantity');
                    if($(quantity).val() != $(quantity).attr('max')){
                        $(quantity).val(parseInt($(quantity).val())+1);
                    }
                })
                .delegate(".decrement", "click", function() {
                    var quantity = $(this).siblings('.person-quantity');
                    if($(quantity).val() != 1){
                        $(quantity).val(parseInt($(quantity).val())-1);
                    }
                });
        });
    }

    //api calls
    var getAllDaysData = function(callback) {
        $.getJSON('/calendar/' + settings.dealID, function (data) {
            callback(data);
        });
    };

    var getDayProductsData = function(productsDate, callback) {
        $.getJSON('/calendar/'+ settings.dealID + '/products/' + productsDate, function (data) {
            callback(data);
        });
    };

    var getMonthData = function(monthDay, callback) {
        $.getJSON('/calendar/' + settings.dealID +' /' + monthDay, function (data) {
            callback(data);
        });
    };


    return this.each( function() {

        // Get the calendar days on initial load
        getAllDaysData(function (data) {

            _.extend(data, viewHelpers);

            $(el)
                .append(_.template(Templates.calendarDay)({allDaysData: data}))
                .delegate(".change-month", "click", function() {
                    setMonth($(this).attr("data-change-month"));
                })
                .delegate("td.available", "click", function() {
                    getProducts($(this).attr("data-arrival-Date"));
                    $('td.available').removeClass('selected');
                    $(this).addClass('selected');
                })
                .delegate(".clear-selection", "click", function() {
                    $('td.available').removeClass('selected');
                    $('#js-day-products').empty();
                });
        })

    });
};

})(jQuery);

$(document).ready(function() {
$( "#js-calendar" ).Calendar({
    dealID : 157460
});
});
\$\endgroup\$
1
\$\begingroup\$
  1. How I am getting the data from the API with getJson and callbacks. I'm not sure if I am doing it efficiently.

I don't know what you mean with efficiency , the getJson is a built-in and you are using as the doc say.

The only improvemnt you can do is avoid 1 function, and do smething like:

   $.getJSON('/calendar/' + settings.dealID, callback);
  1. I am using the jQuery .delegate to attach events to future elements in the templates. Perhaps there is a better way of doing this or is this ok?

Yes, is ok.

  1. The structure of my jQuery plugin. As I mentioned its my first one and would like to hear how I could improve it.

Since you should have a dependency in your plug-in, you should add support for dependency resolver, or include a freeze version of them.

You have hardcode words in your template,you can't internationalizate your plugin.

Javascript is messy with the calculations of dates, check if you are using the correct calculus for next and previous months, days, numbers, etc. Considere use a 3party library to fix this (momentjs works nice).

Also, i don't want to disappoint you, but there are 300.000 billions of plugins for calendars dates and stuff. If you are doing something 4thelulz is ok. if not, try to use someone else.

\$\endgroup\$
  • \$\begingroup\$ Hey Vmariano, thanks for the answer. I made the change to the api calls and will look into a dependency resolver. I am aware of available plugins that can create calendars, but they did not suit the business requirements of this project. I agree with you about the dates. Itsthe part of the code I am most unhappy about. Its messy as you say. But the API formats are not the best so I had to do stuff like find the month number and add ordinals etc. I will take a look at momentjs today and see if it is something I can use. thanks! \$\endgroup\$ – Jose the hose Dec 11 '15 at 8:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.