« Back to Index

The model only triggers the change event when the value being set is unique (i.e. not set before)

View original Gist on GitHub

Model.js

define(['Backbone'], function(){

    var min = 50;
    var max = 400;

    return Backbone.Model.extend({
        defaults: {
            amount: 0,
            code: null,
            date: (new Date())
        },

        initialize: function(){
            this.on('error', this.handle_errors);
            this.on('change:amount', this.amount_change);
        },  
                      
        validate: function (attributes) {
            var errors = [];
            var amount = parseInt(attributes.amount, 10);

            /*
                We first parse the amount (as it comes through as a String)
                Once it's converted to a number we check that the value is NaN.
                We also check if the amount entered fits within the allowed range.
             */
            if (_.isNaN(amount) || amount < min || amount > max) {
                errors.push({
                    field: 'amount',
                    error: 'The amount was invalid'
                });
            }

            if (errors.length) {
                return errors
            }
        },

        handle_errors: function (model, error) {
            // Context of 'this' gets lost within _.each()
            var self = this;
            
            _.each(error, function (item, iterator) {
                self.trigger('item:invalid', item);
            });
        },

        amount_change: function(){
            this.trigger('amount:changed');
        }
    });

});

View.js

define(['../Utils/Templating/hogan', '../Utils/Datepicker/kalendae', '../Models/LoanApplication', '../Utils/DOM/getEl', 'Backbone'], function (hogan, Kalendae, LoanApplication, getElement) {

    // ES3, ES5 non strict
    var global = (function(){return this})();

    return Backbone.View.extend({
        initialize: function(){
            // Store other elements that will be interacted with.
            // Any element that will potentially utilise jQuery we pre-wrap in a single jQuery instance.
            this.promocode = getElement('js-promocode');
            this.amount = getElement('js-amount');
            this.error_amount = $('#js-amounterror');
            this.error_amount_popup = $('#js-amounterror-popup');
            this.popup = $('#js-loanpopup');
            this.popup_amount = getElement('js-popupamount');

            // We use this to tell whether the calendar widget has already been rendered,
            // as there is no point re-rendering it every time the popup is closed then opened again.
            this.is_calendar_rendered = false;

            // Store the Model object for easy reference
            this.model = new LoanApplication();

            // The Model triggers custom events when certain actions happen which the View should ideally handle
            this.model.on('item:invalid', this.process_errors, this);
            this.model.on('amount:changed', this.remove_error, this);
        },

        // The containing element
        el: getElement('js-loanapplication'),

        // Selectors are scoped to the parent element
        events: {
            'focus #js-pickdate': 'validate_amount',
            'click #js-calendarclose': 'close_popup',
            'click #js-applynow': 'validate_amount'
        },

        validate_amount: function(){
            // We validate a different field depending on whether the popup is open (the popup has its own copy of the application fields)
            // Note: Model's "set" method calls Backbone validation by default (see Model for validation rules)
                    
            // If the popup is NOT visible
            if (this.popup.hasClass('hide')) {
                this.model.set({
                    amount: this.amount.value
                });
            } else {
                this.model.set({
                    amount: this.popup_amount.value
                });
            }
        },

        process_errors: function (item) {
            // Check what field was invalid and display corresponding error message
            switch (item.field) {
                case 'amount':
                    // We display the error message in a different place depending on whether the popup is open
                    
                    // If the popup is NOT visible
                    if (this.popup.hasClass('hide')) {
                        this.error_amount.removeClass('invisible');
                    } else {
                        this.error_amount_popup.removeClass('invisible');
                    }
                    
                    break;
            }
        },

        remove_error: function(){
            // We remove the error message from different places depending on whether the popup is open

            // If the popup is NOT visible
            if (this.popup.hasClass('hide')) {
                this.error_amount.addClass('invisible');
                this.display_calendar();
            } else {
                this.error_amount_popup.addClass('invisible');

                // NOW PROCESS THE APPLICATION!
            }
        },

        display_calendar: function(){
            // We only load the calendar on screens large enough to display it
            // And we make sure to only render it once by check "is_calendar_rendered" is false
            if (document.documentElement.clientWidth >= 585 && !this.is_calendar_rendered) {
                this.render_calendar();
            }

            this.popup_amount.value = this.amount.value; // pass through the value into this new popup view
            this.popup.removeClass('hide');
        },

        render_calendar: function(){
            this.is_calendar_rendered = true;

            // the following variables are used for calculating the difference between 
            // today's date and the selected date to pay back the loan
            var calendar_container = getElement('js-calendar');
            var curent_date = new Date();
            var current_day = curent_date.getDate();
            var current_month = curent_date.getMonth();
            var current_year = curent_date.getFullYear();
            var today, calendar;
            
            // we correct current_month to include a zero prefix if the number is less than 10
            current_month = (current_month < 10) ? ('0' + current_month) : current_month;
            
            // construct a date for today which is used for calculating diff
            today = new Date(current_year, current_month, current_day);
            
            calendar = new Kalendae({
                // element to attach the calendar to
                attachTo: calendar_container,
                
                // blackout days after 45 days from current date
                blackout: function (date) {
                    return Kalendae.moment().yearDay() + 45 < date.yearDay(); // yearDay() is an extension Kalendae adds to moment.js to calculate the total number of days since epoch.
                },
                
                // how many characters from the week day name to display (e.g. we've gone with 3 = Mon, Tue, Wed, Thu, Fri, Sat, Sun)
                columnHeaderLength: 3,
                
                // restricts date selectability to past or future ('future' blacks out all days previous to current date)
                direction: 'future',
                
                // only allows selection of one day
                mode: 'single',
                
                // determines the number of months to display
                months: 2,
                
                // determines when the week should start (Sunday = 0 [default] or Monday = 1 etc)
                weekStart: 1,
                
                // causes the <input> to update to the selected date
                subscribe: {
                    'change': function(){
                        var selected_date = this.getSelected();
                        var temp_integer_month;
                        var one_day;
                        var payback_date;
                        
                        days_to_pay = selected_date.split('-');
                        
                        // the date is returned as non-zero index format, so put it back to be zero-indexed
                        temp_integer_month = parseInt(days_to_pay[1], 10);
                        days_to_pay[1] = '0' + --temp_integer_month;
                        
                        one_day = 24*60*60*1000; // hours * minutes * seconds * milliseconds
                        payback_date = new Date(days_to_pay[0], days_to_pay[1], days_to_pay[2]);
                        days_to_pay = Math.abs((today.getTime() - payback_date.getTime()) / (one_day));
                        
                        // update the <input> #js-choosepaydate (currently sitting behind the popup) to display the date selected by the user
                        //paydate.value = selected_date;
                        
                        // call function which will pull in the relevant template and populate with relevant costs
                        //calculate();
                        console.log(selected_date);
                    }
                }
            });
        },

        close_popup: function(){
            this.popup.addClass('hide');
            this.amount.value = ''; // we reset the value so the Model's "change" event can be fired (which is what we rely upon to trigger the popup)
        }

    });

});