Still not getting closures right

Arg, I’m losing hair.

I’m having trouble understanding something extremely basic and important. I have functions who call functions who call functions… but I’m having trouble doing anything useful with their results. I can’t seem to “grab” them. They just get garbage collected. Scope is becoming my enemy.

This is also hard to explain because the code is modular, so stuff is calling stuff is calling stuff.

Everything happens inside a large Object.

In Object.init, I set event handlers and listeners.
when a form control is changed, the listener “selectlistener” calls a function


    selectListener: function(event) {
        Object.findValue(this);
        //and more
    },

findValue() needs to find the values inside the input who just got the change event, and it does for most inputs. But one input needs to grab values from a script.


findValue: function(input) {
        var div = input.parentNode;
        var p = div.lastChild;
        var formKey = input.id,
            formVal = input.value.toLowerCase();
...

        if (formKey == 'Postcode') {
           [b] Object.getPrice(input);
            //getPrice() calls Object.ajax() who calls Object.gotPrice()
            p.firstChild.nodeValue = the results from getPrice[/b]
        }
    ...stuff for other inputs
    },

This is the most recent try at this. Object.getPrice is starting a whole ajax thing which is retrieving a value I need. I want to assign that value to p.firstChild.nodeValue (which no outside function can find cause it’s declared inside the findvalue() function).
If I do
p.firstChild.nodeValue = Object.gotPrice;
then I’m just just renaming gotPrice. I want something more like this
p.firstChild.nodeValue = Object.gotPrice(); in other words, I want p.firstChild.nodeValue to = the results of gotPrice. Except, it can’t actually execute it here. gotPrice gets executed by someone else (who is executed by someone else who is ultimately called by p.firstChild.nodeValue’s function).

If the input selected is “PostCode”, I need to send its value to an ajax snippet, and I do get a value, but I can’t do anything with it. I can’t return it out into the open. This other one works:


   [b] setBasiskost: function(input) {
        return (input.value <= 1999) ? input.value = 2000 : input.value;
    },[/b]

...
 findValue: function(input) {
        var div = input.parentNode;
        var p = div.lastChild;
        var formKey = input.id,
            formVal = input.value.toLowerCase();
      ...          
        if (formKey == 'Basiskosten') {
           [b] p.firstChild.nodeValue = Begrafenis.setBasiskost(input);[/b] 
        }
...

The results of “setBasisKost” are given back to p.firstChild.nodeValue of “Basiskosten”. I tried something similar to Postcode but because ultimately that function didn’t actually return a value (I just can’t get it to), it cannot be assigned. I tried with just one big function instead of the separated ones I have, but I can’t even get the value out of the onreadystatechange() of the ajax call… not without sending it to some other function. So I can keep sending this value to a million other functions but I can’t give it back to the element who’s calling it.

I also tried assigning the returned value to some global variable (by prefacing the variable with the object name and not using “var”) but while that doesn’t work, I would rather not do that anyway. The only one who needs to see this result is PostCode.

When you have functions calling functions calling functions, how do you get the final result back to the original caller?? I’ve been going back and back over closures but they always seem to be doing the opposite of what I’m doing: their assigning the results of inner functions as variables to outer functions.

The whole thing, which has been mutilated in attempts to get this working, and won’t work for anyone because the server the script is on is a dev server:

The parts I’m looking at are in bold. So all the other stuff I think can be ignored but it’s there just in case (oh and IE pukes on the “kosten” object so absolutely nothing works in IE).


var Begrafenis = {
    init: function() {

        var pintro = document.getElementById('pintro'),
            pkost = document.getElementById('pkost'),
            divs = document.forms['formKost'].getElementsByTagName('div'),
            basiskostDiv = document.getElementById('basiskost'),
            basiskostInput = document.getElementById('Basiskosten'),
            typekiezenInput = document.getElementById('TypeKiezen'),,
            inputs = document.forms['formKost'].elements;

        /*onload change form action and change submit text*/
        var submit = inputs[inputs.length-1];
        submit.setAttribute("aria-describedby", 'TotaalKost'); 
        submit.value = 'Volgende »';
        document.forms['formKost'].action = 'berekenpremie_berekenen';

        /*onload hide intro text and show basikost element*/
        Basis.addClass(pintro, 'hidden');
        Basis.addClass(pkost, 'hidden');
        Basis.removeClass(basiskostDiv, 'hidden'); 
   
        /*onload run functions and set event listeners*/
        Begrafenis.chooseType(typekiezenInput);
        Begrafenis.setBasiskost(basiskostInput);
        Begrafenis.createParagraphs(divs);
        Begrafenis.loadValues(divs);
        Basis.addEventListener(basiskostInput, 'blur', Begrafenis.basiskostListener);
        Basis.addEventListener(typekiezenInput, 'change', Begrafenis.chooseListener);

        var formSelects = inputs;
        for (var s=0, ss=formSelects.length; s<ss; s++) {
            var typePattern = /^(input|text|select-one)$/i;
            if (typePattern.test(formSelects[s].type)) {
                Begrafenis.removeText(formSelects[s]);
                Begrafenis.findValue(formSelects[s]);
                Begrafenis.getTotal(Begrafenis.values);
              [b]  Basis.addEventListener(formSelects[s], 'change', Begrafenis.selectListener);[/b]  
            }
        }
    },

    kosten: {
        'BasisKosten': {
             'value': {
                 '2000': 2000
             }    
        },
        'TypeKiezen': {
            'value': {
                'begraven': 'zie onder',

                'cremeren': 1500

            },
            'begraven': {
                'show': ['Postal', 'Box']
            },
            'cremeren': {
                'hide': ['Postal', 'Box']
            }
        },
        'Kist': {
            'value': {
                'geen': 0,
                'eenvoudig': 400,
                'standaard': 750,
                'luxe': 1000
            }
        },
        'Herdenkingsdienst': {
            'value': {
                'geen': 0,
                'rouwcentrum': 250,
                'kerk': 500   
            }
        },
        'Volgautos' : {
            'value': {
                'geen': 0,
                'een': 200,
                'twee': 400,
                'drie': 600
            }
        },
        'Koffietafel' : {
            'value': {
                'geen': 0,
                '50': 250,
                '100': 500,
                '150': 750 
            }
        },
        'Rouwbrieven' : {
            'value': {
                'geen': 0,
                '50': 150,
                '100': 200,
                '150': 450
            }
        },
        'DankBid' : {
            'value': {
                'geen': 0,
                '100': 175,
                '200': 250,
                '300': 325
            }
        },
        'Advertenties' : {
            'value': {
                'geen': 0,
                'regionaal': 650,
                'landelijk': 1000,
                'beide': 1650
            }
        },
        'Bloemen' : {
            'value': {
                'geen': 0,
                'eenvoudig': 100,
                'standaard': 150,
                'luxe': 200
            }
        },
        'Grafsteen' : {
            'value': {
                'geen': 0,
                'staande': 1500,
                'liggende': 2000,
                'grafmonument': 2500
            }
        }
    },

    removeText: function(input) {
        if (input.type == 'select-one') {
            var options = input.options;
            var optL = options.length;
            for(var i=0; i<optL; i++) {
              if(options[i].text.indexOf(':') != -1) {
                var optText = options[i].text.split(':');
                options[i].text = optText[0];
              }
            }
        }
    },

    createParagraphs: function(divs) {
        /*prijs header p*/
        var priceHeader = document.createElement('p');
        Basis.addClass(priceHeader, 'infos');
        var pText1 = document.createTextNode('Kosten in €');
        priceHeader.appendChild(pText1);
        divs[0].parentNode.insertBefore(priceHeader,divs[0]);

        /*totaalkost p*/
        var lastDiv = divs[divs.length-1];
        var totaalP = document.createElement('p');
        totaalP.id = 'TotaalKost';
        Basis.addClass(totaalP, 'infos');
        var pText2 = document.createTextNode(' ');
        totaalP.appendChild(pText2);
        Basis.insertAfter(totaalP, lastDiv);
    },

    chooseType: function(input) {

        var val = input.value.toLowerCase(),
             value;

        for (result in Begrafenis.kosten[input.id][val].show) {
            value = document.getElementById(Begrafenis.kosten[input.id][val].show[result]);

            Basis.removeClass(value, 'none');

        }



        for (result in Begrafenis.kosten[input.id][val].hide) {

            value = document.getElementById(Begrafenis.kosten[input.id][val].hide[result]);

            Basis.addClass(value, 'none');

        }
        Begrafenis.setSelectedValue(value);
    },

    setSelectedValue: function(value) {
        var s = document.getElementById('Kist');
        var postcode = document.getElementById('Postcode'),
             opt;
        //if "cremeren" selected, values for postcode en kist moet 0 zijn*/
        if (Basis.hasClass(value, 'none')) { 
            for (var i=0; i<s.options.length; i++) {
                opt = s.options[i];
                opt.selected = opt.value == 'geen' ? true : false;
            }
            postcode.value = '';
        }
        //anders, "eenvoudig" geselecteerd
        else {
            for (var i=0; i<s.options.length; i++) {
                opt = s.options[i];
                opt.selected = opt.value == 'eenvoudig' ? true : false;
            } 
        }
        //als er een "p" is, is dit niet "onload" maar na "onchange"...
        //dus, update de p value
        var p = s.parentNode.lastChild;
        if (p.nodeType == 1 && p.nodeName.toLowerCase() == 'p') {
            Begrafenis.findValue(s);
        }
    },

    loadValues: function(divs) {
        for (var i=0, l=divs.length, p, div; i<l; i++) {
            div = divs[i];
            p = document.createElement('p');
            Basis.addClass(p, 'value');
            var pText = document.createTextNode('0');
            p.appendChild(pText);
            div.appendChild(p);
        }
    },

    setBasiskost: function(input) {
        return (input.value <= 1999) ? input.value = 2000 : input.value;
    },

   [b] getPrice: function(input) {
        var pc = input.value;
        if(pc.length >= 4) {
            var pcnum = pc.replace(/[^\\d]/g, '');

            if(pcnum.length >= 4) {
                pcnum = pcnum.substr(0,4);
                Begrafenis.ajaxZonderX(pcnum);  //if the user put in anything useful, do teh Ajax
            } 
            else if(pcnum.length < 4) {  //otherwise set stuff to 0
                pcnum = 0;
                pc = pcnum;
                return pc; 
            } 
        }
        else { 
            return;
        }
    },

    gotPrice: function(result) {
        var postcodePrice;
        if(isNaN(result)) {
            postcodePrice = 'onbekend';
        }
        else {
            postcodePrice = result;
        }
        alert(postcodePrice);  //this works fine, I can alert anything
        return postcodePrice;  //does not return the value to anyone
    },

    ajaxZonderX: function(data) {
        var xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP"),
            url = '/verzekerjebegrafenis.nl/grafkosten.php?postcode=' + data; 
        if (xhr !==null) {
            xhr.open('GET', url, true);
            xhr.send(null);
            xhr.onreadystatechange = function() {
                if (this.readyState == 4) {
                    if(this.status == 200 || this.status == 304) {
                        if (this.responseText) {
                            Begrafenis.gotPrice(this.responseText);
                        } 
                        else {
                            alert('Geen response tekst');
                        }
                    } 
                    else {
                        alert('Error: ' + this.status);
                    }
                }
            };
        }
    },[/b]

   [b] findValue: function(input) {
        var div = input.parentNode;
        var p = div.lastChild;
        var formKey = input.id,
            formVal = input.value.toLowerCase(),[/b]
            values = [];
                
        if (formKey == 'Basiskosten') {
            p.firstChild.nodeValue = Begrafenis.setBasiskost(input); 
        }
      [b]if (formKey == 'Postcode') {
            Begrafenis.getPrice(input);
            //p.firstChild.nodeValue = Begrafenis.gotPrice;
        }[/b]
 
        for (result in Begrafenis.kosten)  {
            if (result == formKey) {
                p.firstChild.nodeValue = Begrafenis.kosten[formKey].value[formVal];
            }
        }
        var ps = Basis.getElementsByClass('value');
        for (var i=0, l=ps.length; i<l; i++) {
            var para = ps[i];
            values.push(para.firstChild.nodeValue);
        }

        Begrafenis.values = values;
    },
    
    getTotal: function(values) {
        var totaal = document.getElementById('TotaalKost'),
            totaalValue = 0;
        
        for (var i=0, l=values.length; i<l; i++) {
            var pVal = Begrafenis.values[i];
            pVal = parseInt(pVal, 10);
            pVal = isNaN(pVal) ? 0 : pVal;
            totaalValue += pVal; 
        }
        totaal.firstChild.nodeValue = 'Uw totaal begrafeniskosten:  \\u20AC' + totaalValue;
    },

  [b]selectListener: function(event) {
        Begrafenis.findValue(this);
        Begrafenis.getTotal(Begrafenis.values);
    },[/b]

    basiskostListener: function(event) {
        Begrafenis.setBasiskost(this);   
    },

    chooseListener: function(event) {
        Begrafenis.chooseType(this);
    }
};

Basis.begin(Begrafenis.init);

Where do I have to go to understand basic Javascript things like getting values back out of a function? It’s always the same problem I hit every time I use functions to figure out some value. It’s always locked away.

If anyone happens to know of some place where one can practice with these things, that would be nice. Closures for morons? Functions for dummies? Something like that. I’ve got bookmarks of pages explaining functions and values and closures but I can’t seem to take that over to what I want to do with them.

Isn’t your problem that Object.gotPrice(); fires of a Ajax request. It does not wait for the return.

When the return happens the onreadystatechange event is fired and that function is executed so it is in there that you have the gotPrice.

getPrice calls the ajax (after sanitising and determining if it can be used),
the ajax sends the results to gotPrice or fails
gotPrice uses the value to set one of two real values… can alert this new value, but can’t give it to Postcode’s p.

Though I may still have a wait problem, but I don’t want to wait based on time, but based on whether I get a result or not.

Are you saying if there wasn’t this time issue with ajax, that if I had
p.firstChild.nodeValue = callOneBigFunctionThatDoesItAll(input); (what I had earlier)
then it would be doing the same as my other, smaller function (basiskost)?

Sorry cannot get my head around all your code but the way I view it there are two different and separate stages. There is first firing the ajax request and the second receiving the ajax reply.

The two steps are completely separate. The first step completes before the second step starts. What you want to happen is for the second step to complete the first step.

One way of doing that would be to assign a specific function to the onreadystatechange event which competes the first step.

One way of doing that would be to assign a specific function to the onreadystatechange event which competes the first step.

At the suggestion of another on the forums, I had such a thing. But because that new function had (), it ran right away-- before the .send(null) statement. (because for some unknown and unexplained reason, all my books and the tutorials I find online set the .send(null) line before the onreadystatechange function) which meant I never got a readystate4, it stayed at one.

So later I switched the orders and then I had a similar problem: my value was stuck in the onreadystatechange function. I don’t know how to get it out. It’s stuck in there, and I can alert it but I can’t plug it back into my page. Not sure how to get around that.

event which competes the first step.

Other than making an alert box, I can’t seem to complete the first step anyway.

Since I don’t actually NEED anything to be asynchronous (really the user ought to be stopped until they get their value as everything else depends on it), I tried setting the .open statement to false. But, apparently that means onreadystatechange is skipped entirely and nothing happens at all. : (

It strikes me that the important piece of information from the firing function is the argument of the function, namely “input”. If this was known to the return function from the ajax method then you could complete the task.

One solution would be to store “input” as a property of the xhr it would then be available to the onreadystatechange function.

One solution would be to store “input” as a property of the xhr it would then be available to the onreadystatechange function.

I can do that?? That gives me something to try.

I ended up “fixing” my page by having the last function (gotPrice) basically recreate the entire setup from the original calling function and then assigning the value there. Meaning because I couldn’t pass the value back to the original calling function, I copied a bunch of code and made the script work twice as hard instead. Which doesn’t make me happy.


    gotPrice: function(result) {
        var postcodePrice;
        if(isNaN(result)) {
            postcodePrice = 'onbekend';
        }
        else {
            postcodePrice = result;
        }
        var Postcode = document.getElementById('Postcode');
        var div = Postcode.parentNode;
        var p = div.lastChild;
        p.firstChild.nodeValue = postcodePrice;   
    },

Of course this isn’t always possible. I still need to figure out how to get values back to original calling functions. I’ll see if I can keep that input var, but I seem to lose it after I’ve sanitised user input and renamed, here in getPrice:


            if(pcnum.length >= 4) {
                pcnum = pcnum.substr(0,4);
                Begrafenis.ajaxZonderX(pcnum);<-- at this point, there is no "input"... can I pass it as an argument?
            }