Reputation:
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(/"/g, '"').replace(/</, ''));
viewData.CONFIG = JSON.parse('{{json CONFIG}}'.replace(/"/g, '"').replace(/</, ''));
</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
.
Upvotes: 3
Views: 947
Reputation: 23294
I prefer the Original Module pattern above the Revealing Module pattern, mainly because of the benefits of loose augmentation; In short, it allows to break up a module into parts which can be loaded asynchronously, read more here.
Via window.NAME = window.NAME || {}
in the code below a custom namespace with name NAME gets declared if it doesn't exist yet. Module 1 declares a variable my
into it and module 2 a variable other
; whether module 1 runs before or after module 2 doesn't matter.
When the main module gets executed (after module 1 and 2) it can access the variables defined in both. Notice that these can be accessed in 3 different ways.
// Module1.js.
(function(NAME) {
NAME.my = "foo" // Replace with your map instance.
})(window.NAME = window.NAME || {});
// Module2.js.
(function(namespace) {
namespace.other = "bar"
})(window.NAME = window.NAME || {});
// Main module using what is defined in the 2 modules above.
(function(namespace) {
console.log(NAME.my);
console.log(namespace.my)
console.log(window.NAME.my);
console.log(NAME.other);
console.log(namespace.other)
console.log(window.NAME.other)
})(window.NAME = window.NAME || {});
Upvotes: 0
Reputation: 10886
There are a few things going on:
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
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
.
Upvotes: 2
Reputation: 114569
You need to set explicit fields... for example:
let x = (function(){
let obj = {}; // the "namespace"
let private_var = 0;
function foo() {
return private_var++; // Access private vars freely
};
obj.foo = foo; // "publish" the function
console.log(foo()); // you can use unqualified foo here
return obj;
})();
// outputs 0 from the console log call inside the "constructor"
console.log(x.private_var); // undefined, it's not published
console.log(x.foo()); // outputs 1, the function was published
console.log(x.foo()); // 2
console.log(x.foo()); // 3
Local variables or local functions of a Javascript function are not implicitly published anywhere. If you want to access them then you need to set up an object field.
Upvotes: 0