Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use an IIFE approach and pass a variable from one file to another

One of my previous questions was how to organize the code between multiple .js files. Now I have a problem.

I have a map in d3.js divided by countries. When the user double-clicks on a country, I would like to pass a variable to another js file.

This is my html file, index.hbs:

<html lang='en'>
    <head>
        <meta charset='utf-8'>
        <script src='https://d3js.org/d3.v5.js' charset='utf-8'></script>
        <script src='https://d3js.org/topojson.v2.min.js'></script>
        <script src='https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js'></script>

        <link href='/css/all.css' rel='stylesheet'/>
    </head>

    <body>
        <div id='map'></div>

        <script> 
            var viewData = {};  
            viewData.nuts0 = JSON.parse('{{json nuts0}}'.replace(/&quot;/g, '"').replace(/&lt;/, ''));
            viewData.CONFIG = JSON.parse('{{json CONFIG}}'.replace(/&quot;/g, '"').replace(/&lt;/, '')); 
        </script>

        <script src='/script/map.js' rel='script'/><{{!}}/script>
        <script src='/script/other.js' rel='script'/><{{!}}/script>
    </body>
</html>

map.js:

var NAME=(function map() {

    var my = {};

    var CONFIG = viewData.CONFIG;
    var nuts0 = viewData.nuts0;

    // paths
    var countries;

    // width and height of svg map container
    var width = CONFIG.bubbleMap.width;
    var height = CONFIG.bubbleMap.height;

    // to check if user clicks or double click
    var dblclick_timer = false;

    // create Hammer projection
    var projectionCurrent = d3.geoHammer() 
        .scale(1) 
        .translate([width/2, height/2]); 

    var projectionBase = d3.geoHammer()
        .scale(1) 
        .translate([width/2, height/2]);

    // creates a new geographic path generator with the default settings. If projection is specified, sets the current projection
    var path = d3.geoPath().projection(projectionCurrent);

    // creates the svg element that contains the map
    var map = d3.select('#map');

    var mapSvg = map.append('svg')
        .attr('id', 'map-svg')
        .attr('width', width)
        .attr('height', height);

    var mapSvgGCountry = mapSvg.append('g').attr('id', 'nuts0');

    countries = topojson.feature(nuts0, nuts0.objects.nuts0);
    projectionCurrent.fitSize([width, height], countries);

    var mapSvgGCountryPath = mapSvgGCountry.selectAll('path')
        .data(countries.features)
        .enter()
        .append('path');

    mapSvgGCountryPath.attr('class', 'country')
        .attr('fill', 'tomato')
        .style('stroke', 'white')
        .style('stroke-width', 1) 
        .attr('d', path)
        .attr('id', function(c) {
            return 'country' + c.properties.nuts_id;
        })
        .on('click', clickOrDoubleCountry);

    function clickOrDoubleCountry(d, i) {
        if(dblclick_timer) { // double click
            clearTimeout(dblclick_timer);
            dblclick_timer = false;
            my.countryDoubleClicked = d.country; // <-- variable to pass
        }
        else { // single click
            dblclick_timer = setTimeout(function() {
                dblclick_timer = false;
            }, 250)
        } 
    } 

    return my;

}());

other.js:

(function other(NAME) {
    console.log('my:', NAME.my); // undefined
    console.log('my:', NAME.countryDoubleClicked); // undefined
})(NAME);

I would like to be able to read the my object created in map.js in the other.js file and then be able to access the my.countryDoubleClicked variable from other.js.

This code doesn't work, I get TypeError: NAME.my is undefined.


1 Answers

There are a few things going on:

Revealing Variables

First you're not revealing the my variable to show up as NAME.my in map.js:

var NAME = (function map() {
    var my = {};
    //...
    return my;
}());

This sets NAME to my, instead of setting NAME.my to my. If you do want to do this, you can do something like this:

var NAME = (function map() {
    var my = {};
    //...
    return {
      my: my
    };
}());

You can read more about this technique, called the "Revealing Module Pattern", from articles like this one: http://jargon.js.org/_glossary/REVEALING_MODULE_PATTERN.md

Using functions to run code later

Second, as others have mentioned and as you've realized, since the code in other.js runs immediately, it'll run that code before the user has any chance to click on a country. Instead, you want code that can run on demand, (in this case when the user double clicks on something). In JavaScript, this is traditionally done by assigning or passing a function. For simplicity, we can assign something to my.doubleClickHandler and then invoke that function in clickOrDoubleCountry. For this I've made the country an argument passed to the handler, in addition to assigning it to NAME.my.countryDoubleClicked, but you'll probably only need to use one of them.

function clickOrDoubleCountry(d, i) {
    if(dblclick_timer) { // double click
        clearTimeout(dblclick_timer);
        dblclick_timer = false;
        my.countryDoubleClicked = d.country; // <-- variable to pass
        if (my.doubleClickHandler) {
            my.doubleClickHandler(d.country);
        }
    }
    // ...
} 

Then in other.js, you'd assign the function you want to run to NAME.my.doubleClickHandler:

(function other(NAME) {
    NAME.my.doubleClickHandler = function (country) {
        // now this code runs whenever the user double clicks on something
        console.log('exposed variable', NAME.my.countryDoubleClicked); // should be the country
        console.log('argument', country); // should be the same country
    });
})(NAME);

So in addition to the modified other.js above, this is complete modified map.js:

var NAME=(function map() {

    var my = {};

    var CONFIG = viewData.CONFIG;
    var nuts0 = viewData.nuts0;

    // paths
    var countries;

    // width and height of svg map container
    var width = CONFIG.bubbleMap.width;
    var height = CONFIG.bubbleMap.height;

    // to check if user clicks or double click
    var dblclick_timer = false;

    // create Hammer projection
    var projectionCurrent = d3.geoHammer() 
        .scale(1) 
        .translate([width/2, height/2]); 

    var projectionBase = d3.geoHammer()
        .scale(1) 
        .translate([width/2, height/2]);

    // creates a new geographic path generator with the default settings. If projection is specified, sets the current projection
    var path = d3.geoPath().projection(projectionCurrent);

    // creates the svg element that contains the map
    var map = d3.select('#map');

    var mapSvg = map.append('svg')
        .attr('id', 'map-svg')
        .attr('width', width)
        .attr('height', height);

    var mapSvgGCountry = mapSvg.append('g').attr('id', 'nuts0');

    countries = topojson.feature(nuts0, nuts0.objects.nuts0);
    projectionCurrent.fitSize([width, height], countries);

    var mapSvgGCountryPath = mapSvgGCountry.selectAll('path')
        .data(countries.features)
        .enter()
        .append('path');

    mapSvgGCountryPath.attr('class', 'country')
        .attr('fill', 'tomato')
        .style('stroke', 'white')
        .style('stroke-width', 1) 
        .attr('d', path)
        .attr('id', function(c) {
            return 'country' + c.properties.nuts_id;
        })
        .on('click', clickOrDoubleCountry);

    function clickOrDoubleCountry(d, i) {
        if(dblclick_timer) { // double click
            clearTimeout(dblclick_timer);
            dblclick_timer = false;
            my.countryDoubleClicked = d.country; // <-- variable to pass
            if (my.doubleClickHandler) {
                my.doubleClickHandler(d.country);
            }
        }
        else { // single click
            dblclick_timer = setTimeout(function() {
                dblclick_timer = false;
            }, 250)
        } 
    } 

    return {
        my: my
    };

}());

If you don't want to use NAME.my for everything and want methods and variables accessible directly from NAME (e.g. NAME.countryDoubleClicked instead of NAME.my.countryDoubleClicked), you can use the original return statement return my;, just bear in mind that there will be no variable named NAME.my.

like image 76
Steve Avatar answered Jan 21 '26 05:01

Steve



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!