0

I'm trying to use the Revealing Module pattern together with the Constructor pattern to create somewhat beautiful JS! I want to be able to create multiple instances if "Countdown" like this:

var c1 = new Countdown();
c1.init();
var c2 = new Countdown();
c2.init();

These should be independent. So instead of declaring variables with "var", I use "this" on the prototype. I reference "this" inside a function, but it's undefined. How can I access "this"?

var Countdown = function() {};

Countdown.prototype = (function(doc) {

    return {
        init: init
    };

    function init() {
        // day
        this.d1 = doc.createElement('div');
        this.d1.setAttribute('class', 'day-1 elem');

        // ... more elements left out here
    }

    function updateView(time) {
        this.d1.textContent = getDays(secs)[0];
    }

    function getDays(secs) {
        var result = 0;
        if (secs > 0) {
            result = secs / (60 * 60 * 24);
        }

        return Math.floor(result);
    }

})(document);

--- EDIT ---

Here's my complete code:

"use strict";

var Countdown = function() {
    //this.self = this;
};

Countdown.prototype = (function(doc) {

    return {
        initialize: initialize
    };

    function createElements() {
        var countdown = doc.createElement('div');
        countdown.setAttribute('class', 'countdown');

        var heading = doc.createElement('h2');
        heading.textContent = 'Countdown';
        countdown.appendChild(heading);
        document.body.appendChild(countdown);



        // day
        var wrapDay = doc.createElement('div');
        wrapDay.setAttribute('class', 'wrap-day');
        countdown.appendChild(wrapDay);

        this.d1 = doc.createElement('div');
        this.d1.setAttribute('class', 'day-1 elem');
        wrapDay.appendChild(this.d1);

        this.d2 = doc.createElement('div');
        this.d2.setAttribute('class', 'day-2 elem');
        wrapDay.appendChild(this.d2);

        var lblDay = doc.createTextNode('dage');
        wrapDay.appendChild(lblDay);

        var sepDay = doc.createElement('div');
        sepDay.setAttribute('class', 'separator');
        sepDay.textContent = ':';
        countdown.appendChild(sepDay);



        // hour
        var wrapHour = doc.createElement('div');
        wrapHour.setAttribute('class', 'wrap-hour');
        countdown.appendChild(wrapHour);

        this.h1 = doc.createElement('div');
        this.h1.setAttribute('class', 'hour-1 elem');
        wrapHour.appendChild(this.h1);

        this.h2 = doc.createElement('div');
        this.h2.setAttribute('class', 'hour-2 elem');
        wrapHour.appendChild(this.h2);

        var lblHour = doc.createTextNode('timer');
        wrapHour.appendChild(lblHour);

        var sepHour = doc.createElement('div');
        sepHour.setAttribute('class', 'separator');
        sepHour.textContent = ':';
        countdown.appendChild(sepHour);



        // min
        var wrapMin = doc.createElement('div');
        wrapMin.setAttribute('class', 'wrap-min');
        countdown.appendChild(wrapMin);

        this.m1 = doc.createElement('div');
        this.m1.setAttribute('class', 'min-1 elem');
        wrapMin.appendChild(this.m1);

        this.m2 = doc.createElement('div');
        this.m2.setAttribute('class', 'min-2 elem');
        wrapMin.appendChild(this.m2);

        var lblMin = doc.createTextNode('minutter');
        wrapMin.appendChild(lblMin);

        var sepMin = doc.createElement('div');
        sepMin.setAttribute('class', 'separator');
        sepMin.textContent = ':';
        countdown.appendChild(sepMin);



        // sec
        var wrapSec = doc.createElement('div');
        wrapSec.setAttribute('class', 'wrap-sec');
        countdown.appendChild(wrapSec);

        this.s1 = doc.createElement('div');
        this.s1.setAttribute('class', 'sec-1 elem');
        wrapSec.appendChild(this.s1);

        this.s2 = doc.createElement('div');
        this.s2.setAttribute('class', 'sec-2 elem');
        wrapSec.appendChild(this.s2);

        var lblSec = doc.createTextNode('sekunder');
        wrapSec.appendChild(lblSec);
    }

    function initialize() {
        domReady(function() {

            // create DOM
            createElements();

            // get time
            var now = new Date();
            var year = now.getFullYear();
            var month = now.getMonth();
            var dateFinal = new Date(year, month + 1, 1, 0, 0, 0); // first day next month
            var seconds = getSecsLeft(dateFinal);
            var time = getTimeLeftObject(seconds);

            // update view every second
            setInterval(function() {
                seconds = getSecsLeft(dateFinal);
                time = getTimeLeftObject(seconds);
                updateView(time);
            }, 1000);

            // first time
            updateView(time);
        });
    }

    function updateView(time) {
        var days = zeroPadding(time.days);
        var hours = zeroPadding(time.hours);
        var mins = zeroPadding(time.mins);
        var secs = zeroPadding(time.secs);

        this.d1.textContent = days[0];
        this.d2.textContent = days[1];

        this.h1.textContent = hours[0];
        this.h2.textContent = hours[1];

        this.m1.textContent = mins[0];
        this.m2.textContent = mins[1];

        this.s1.textContent = secs[0];
        this.s2.textContent = secs[1];
    }

    function getDays(secs) {
        var result = 0;
        if (secs > 0) {
            result = secs / (60 * 60 * 24);
        }

        return Math.floor(result);
    }
    function getHours(secs) {
        var result = 0;
        if (secs > 0) {
            result = (secs / (60*60)) % 24;
            result = result === 24 ? 0 : result;
        }

        return Math.floor(result);
    }
    function getMins(secs) {
        var result = 0;
        if (secs > 0) {
            result = (secs / 60) % 60;
            result = result === 60 ? 0 : result;
        }

        return Math.floor(result);
    }
    function getSecs(secs) {
        var result = 0;
        if (secs > 0) {
            result = secs % 60;
            result = result === 60 ? 0 : result;
        }

        return Math.floor(result);
    }

    function zeroPadding(num) {
        var result;
        result = num < 10 ? "0" + num : num;

        return new String(result);
    }

    function getTimeLeftObject(seconds) {
        var secs = getSecs(seconds);
        var mins = getMins(seconds);
        var hours = getHours(seconds);
        var days = getDays(seconds);

        return {
            days: days,
            hours: hours,
            mins: mins,
            secs: secs
        };
    }

    function getSecsLeft(dateFinal) {
        var result = (dateFinal - new Date()) / 1000;
        return result < 0 ? 0 : result;
    }

    function domReady(callback) {
        document.readyState === "interactive" || document.readyState === "complete" ? callback() : document.addEventListener("DOMContentLoaded", callback);
    }

})(document);
olefrank
  • 4,204
  • 7
  • 45
  • 75
  • Do you mean you want to call `updateView` from within `init` function? If yes use `updateView.call(this, 100)` – Yury Tarabanko Jul 26 '16 at 09:17
  • I update the text every second with a interval timer (code omitted) – olefrank Jul 26 '16 at 09:18
  • I think you just need to call this.init() in the Countdown constructor ? I'm not sure about what you're trying to achieve... Because for me this should be defined, I tested it and it works... – R. Foubert Jul 26 '16 at 09:19
  • I dont see the problem with your code - logging `d1 ` inside init` appears to look as expected. https://jsfiddle.net/bxxsu6qh/ I suspect you've omitted the bit you're having problems with!! Also, logging `this` is not undefined, it is a reference to the `Countdown` instance. – Jamiec Jul 26 '16 at 09:20
  • I updated my question to include all code, so you can see what I'm trying to achieve. – olefrank Jul 26 '16 at 09:29
  • See this answer for an explanation of how `this` work: http://stackoverflow.com/questions/13441307/how-does-the-this-keyword-in-javascript-act-within-an-object-literal/13441628#13441628 – slebetman Jul 26 '16 at 09:39

1 Answers1

4

It seems you are calling updateView within setInterval

function init() {
    // day
    this.d1 = doc.createElement('div');
    this.d1.setAttribute('class', 'day-1 elem');

    var update = updateView.bind(this); // you need to bind updateView context
    setInterval(function() {
       var time = ???
       update(time); // `this` will be current instance
    }, 1000)
}

UPD Since you are setting interval from within domReady callback you need either bind the callback itself or bind updateView before hand.

function initialize() {
    domReady(function() {

        // create DOM
        createElements.call(this); // call with current context

        // get time
        var now = new Date();
        var year = now.getFullYear();
        var month = now.getMonth();
        var dateFinal = new Date(year, month + 1, 1, 0, 0, 0); // first day next month
        var seconds = getSecsLeft(dateFinal);
        var time = getTimeLeftObject(seconds);

        var update = updateView.bind(this); 

        // update view every second
        setInterval(function() {
            seconds = getSecsLeft(dateFinal);
            time = getTimeLeftObject(seconds);
            update(time);
        }, 1000);

        // first time
        update(time);
    }.bind(this)); //bind callback
}

Or

function initialize() {
    var update = updateView.bind(this),
        create = createElements.bind(this); // prebind functions that use this
    domReady(function() {

        // create DOM
        create(); //call prebinded function

        // get time
        var now = new Date();
        var year = now.getFullYear();
        var month = now.getMonth();
        var dateFinal = new Date(year, month + 1, 1, 0, 0, 0); // first day next month
        var seconds = getSecsLeft(dateFinal);
        var time = getTimeLeftObject(seconds);

        // update view every second
        setInterval(function() {
            seconds = getSecsLeft(dateFinal);
            time = getTimeLeftObject(seconds);
            update(time);
        }, 1000);

        // first time
        update(time);
    });
}
Yury Tarabanko
  • 39,619
  • 8
  • 73
  • 90
  • well spotted (well, understood - it wasnt in the question, just the comments!) – Jamiec Jul 26 '16 at 09:26
  • ohhh ok now I understand the question ! Nice ;) – R. Foubert Jul 26 '16 at 09:35
  • Just for my own interest, why do you need to bind `this` to the `updateView` function? – LeDoc Jul 26 '16 at 09:41
  • @LeDoc It was the only function that was using `this` in OP. – Yury Tarabanko Jul 26 '16 at 09:44
  • And if you directly call updateView(time) without binding 'this', 'this' will not be your counter instance in the updateView function ! – R. Foubert Jul 26 '16 at 09:47
  • So where does `this` point to in the `updateView(time)` function without the binding? (Something I'm trying to get my head round, so appreciate any explanations! :) ) – LeDoc Jul 26 '16 at 09:51
  • Javascript functions have dynamic context. If you simply call `updateView()` context will be either `undefined` or `window` depending on if you are in `strict mode` or not. – Yury Tarabanko Jul 26 '16 at 09:55
  • OK thanks! But it does not solve my problem... If you create multiple instances of Countdown, only the last one is updated. So the elements ('d1', 'd2' etc...) doesn't seem to be contained inside a Countdown instance. Why is this? – olefrank Jul 26 '16 at 10:20
  • Since you are calling `setInterval` from within `domReady` callback you need to either bind the callback or bind functions inside `initialize`. See updated answer. – Yury Tarabanko Jul 26 '16 at 10:31
  • Oh now I understand :) Thank you very much for the help and explanation. BTW I like your second solution the most (bind beforehand) – olefrank Jul 26 '16 at 10:49