Reputation:
I'm trying to organize JS code from different files. I'm using D3.js and I have created 3 visualizations with on the user can interact. For example, if you hover over an element of the first visualization, I would like to change the second and third accordingly.
This procedure works using this code.
index.html:
<body>
<span id='elem1' class='el'></span>
<span id='elem2' class='el'></span>
<span id='elem3' class='el'></span>
<script src='page1.js' rel='script'></script>
<script src='page2.js' rel='script'></script>
<script src='page3.js' rel='script'></script>
</body>
page1.js:
var PAGE1 = (function page1() {
// object to export
var moduleObj = {};
var elem1 = d3.select('#elem1')
.append('svg')
.append('g')
.append('rect')
.attr('id', 'elem1rect')
.attr('width', 50)
.attr('height', 50)
.attr('fill', '#FF5733');
elem1.on('click', function() {
var elemClicked = 'elem1';
console.log('PAGE1 // Click on:', elemClicked);
if(moduleObj.clickElem1ToPage2Handler) {
moduleObj.clickElem1ToPage2Handler(elemClicked);
}
if(moduleObj.clickElem1ToPage3Handler) {
moduleObj.clickElem1ToPage3Handler(elemClicked);
}
});
return moduleObj;
}());
page2.js:
(function page2() {
var elem2 = d3.select('#elem2')
.append('svg')
.append('g')
.append('rect')
.attr('id', 'elem2rect')
.attr('width', 50)
.attr('height', 50)
.attr('fill', '#FFC300');
elem2.on('click', function() {
var elemClicked = 'elem2';
console.log('PAGE2 // Click on:', elemClicked);
});
PAGE1.clickElem1ToPage2Handler = function(clickFrom) {
console.log('PAGE2 // Page2 received click from:', clickFrom);
};
})();
page3.js:
(function page3() {
var elem3 = d3.select('#elem3')
.append('svg')
.append('g')
.append('rect')
.attr('id', 'elem3rect')
.attr('width', 50)
.attr('height', 50)
.attr('fill', '#DAF7A6');
elem3.on('click', function() {
var elemClicked = 'elem3';
console.log('PAGE3 // Click on:', elemClicked);
});
PAGE1.clickElem1ToPage3Handler = function(clickFrom) {
console.log('PAGE3 // Page3 received click from:', clickFrom);
};
})();
Now, however, I would like to be able to do the inverse, that is, by passing the mouse over an element of the second visualization, the first and third visualizations are modified. Likewise, if you hover over an element of the third visualization, the first and second are changed.
In these two cases I have a problem: using the same strategy used earlier does not work anymore. I modify page1.js and page2.js.
page1.js:
var PAGE1 = (function page1() {
// object to export
var moduleObj = {};
var elem1 = d3.select('#elem1')
.append('svg')
.append('g')
.append('rect')
.attr('id', 'elem1rect')
.attr('width', 50)
.attr('height', 50)
.attr('fill', '#FF5733');
elem1.on('click', function() {
var elemClicked = 'elem1';
console.log('PAGE1 // Click on:', elemClicked);
if(moduleObj.clickElem1ToPage2Handler) {
moduleObj.clickElem1ToPage2Handler(elemClicked);
}
if(moduleObj.clickElem1ToPage3Handler) {
moduleObj.clickElem1ToPage3Handler(elemClicked);
}
});
PAGE2.clickElem2ToPage1Handler = function(clickFrom) {
console.log('PAGE1 // Page1 received click from:', clickFrom);
};
return moduleObj;
}());
page2.js:
var PAGE2 = (function page2() {
// object to export
var moduleObj = {};
var elem2 = d3.select('#elem2')
.append('svg')
.append('g')
.append('rect')
.attr('id', 'elem2rect')
.attr('width', 50)
.attr('height', 50)
.attr('fill', '#FFC300');
elem2.on('click', function() {
var elemClicked = 'elem2';
console.log('PAGE2 // Click on:', elemClicked);
if(moduleObj.clickElem2ToPage1Handler) {
moduleObj.clickElem2ToPage1Handler(elemClicked);
}
});
PAGE1.clickElem1ToPage2Handler = function(clickFrom) {
console.log('PAGE2 // Page2 received click from:', clickFrom);
};
return moduleObj;
})();
The error that is generated is:
ReferenceError: PAGE2 is not defined (page1.js:27:2)
TypeError: PAGE1 is undefined (page2.js:24:2)
TypeError: PAGE1 is undefined (page3.js:17:2)
Here is the whole (working) code.
Is there a way to handle all cases (possibly without having to change the entire structure)? Thank you
Upvotes: 3
Views: 901
Reputation: 1098
The easiest way of doing this would be using global object that way even if are trying to access your varible outside of the function you are operating on. A simple snipet can be.
window.var1= () => {
//var1 implemntation here
}
window.var2= () => {
//var2 implemntation here
}
window.var3= () => {
//var3 implemntation here
}
//If you want to access these even in some other scope you can do this
let test =() => {
//want var1 here
window.var1();
}
Upvotes: 0
Reputation: 593
I would recommend creating a service module that is invoked before all other modules you have there and has getters and setters for the information you want to pass around.
It would serve as an intermediary i.e. a data storage. All of your other files could use it as an information delegator.
Upvotes: 0
Reputation: 1305
One thing you can do is declare elem1
, elem2
, and elem3
outside of the immediately invoked functions, and then create a separate eventHandlers.js file. Include this file after all the others so you have access to the variables.
For example:
index.html
<html lang='en'>
<head>
<meta charset='utf-8'>
<script src='https://d3js.org/d3.v5.js' charset='utf-8'></script>
<style>
.el {
margin-bottom: 0px;
padding-bottom: 0px;
}
</style>
</head>
<body>
<span id='elem1' class='el'></span>
<span id='elem2' class='el'></span>
<span id='elem3' class='el'></span>
<script src='page1.js' rel='script'></script>
<script src='page2.js' rel='script'></script>
<script src='page3.js' rel='script'></script>
<script src='eventHandlers.js' rel='script'></script>
</body>
</html>
page1.js
var elem1 = d3.select('#elem1')
.append('svg')
.append('g')
.append('rect')
.attr('id', 'elem1rect')
.attr('width', 50)
.attr('height', 50)
.attr('fill', '#FF5733');
var PAGE1 = (function page1() {
// object to export
var moduleObj = {};
elem1.on('click', function() {
var elemClicked = 'elem1';
console.log('PAGE1 // Click on:', elemClicked);
moduleObj.clickHandler(elemClicked);
});
return moduleObj;
}());
page2.js
var elem2 = d3.select('#elem2')
.append('svg')
.append('g')
.append('rect')
.attr('id', 'elem2rect')
.attr('width', 50)
.attr('height', 50)
.attr('fill', '#FFC300');
var PAGE2 = (function page2() {
// object to export
var moduleObj = {};
elem2.on('click', function() {
var elemClicked = 'elem2';
console.log('PAGE2 // Click on:', elemClicked);
moduleObj.clickHandler(elemClicked);
});
return moduleObj;
})();
page3.js
var elem3 = d3.select('#elem3')
.append('svg')
.append('g')
.append('rect')
.attr('id', 'elem3rect')
.attr('width', 50)
.attr('height', 50)
.attr('fill', '#DAF7A6');
var PAGE3 = (function page3() {
var moduleObj = {};
elem3.on('click', function() {
var elemClicked = 'elem3';
console.log('PAGE3 // Click on:', elemClicked);
moduleObj.clickHandler(elemClicked);
});
return moduleObj;
})();
eventHandlers.js
//can access elem1, elem2, and elem3 from here
PAGE1.clickHandler = function(clickFrom) {
console.log('Received click from', clickFrom);
};
PAGE2.clickHandler = function(clickFrom) {
console.log('Received click from', clickFrom);
};
PAGE3.clickHandler = function(clickFrom) {
console.log('Received click from', clickFrom);
};
Using this method, you would edit elem2
and elem3
in PAGE1.clickHandler
, etc.
Upvotes: 2
Reputation: 1327
Per you request not to make major changes, here is the minimal code change to get around the error. The pieces you need to have the different pages talk together are there, just not quite complete:
index.js This is the key point. To have better encapsulation and avoid the scope issues, all code connecting the various pages should be in the file that executes the other scripts (in your case, that is index.js).
<body>
<span id='elem1' class='el'></span>
<span id='elem2' class='el'></span>
<span id='elem3' class='el'></span>
<script src='page1.js' rel='script'></script>
<script src='page2.js' rel='script'></script>
<script src='page3.js' rel='script'></script>
<script>
PAGE2.clickElem2ToPage1Handler = PAGE1.clickHandler;
</script>
</body>
page1.js Your page1.js modules are already returning an object which can I extended to have a click handler. Page2+3 should do the same--although I did not make these changes.
var PAGE1 = (function page1() {
// object to export
var moduleObj = {
clickHandler: function (clickedOn) {
console.log('PAGE1 // Page1 clicked on:', clickedOn);
}
};
var elem1 = d3.select('#elem1')
.append('svg')
.append('g')
.append('rect')
.attr('id', 'elem1rect')
.attr('width', 50)
.attr('height', 50)
.attr('fill', '#FF5733');
elem1.on('click', function () {
var elemClicked = 'elem1';
console.log('PAGE1 // Click on:', elemClicked);
if (moduleObj.clickElem1ToPage2Handler) {
moduleObj.clickElem1ToPage2Handler(elemClicked);
}
if (moduleObj.clickElem1ToPage3Handler) {
moduleObj.clickElem1ToPage3Handler(elemClicked);
}
});
return moduleObj;
}());
I did have to change page2.js in the plunker to match what you posted in the answer--it needs to return the moduleObj to the PAGE2 variable. I made a fork in the Plunker that has these changes (https://plnkr.co/edit/Uo6SJCZRQlyrKcVcjWEr -- make sure you select the for I made, I don't see a way to link to it directly).
As the previous answer stated, this architecture would not scale well to an app of any complexity at all. Frameworks like Angular and React solve these sorts of issue for you.
In case you are wondering about the error message you were getting, ReferenceError: PAGE2 is not defined (page1.js:27:2)
is because PAGE2 will not in scope until the function in page2.js executes. You might be used to functions being is scope in a file previous to their declaration, but that is not the case with PAGE2, as it is a variable (even if a variable points to a function, which is not the case here, the variable would not be in scope until after it had executed). The PAGE1 errors are because the code in page1.js errors before the return statement is reached, so the PAGE1 variable assignment in index.js does not happen.
(final note: the console.log messages in your handlers do not correctly describe what is happening in all cases. I did not correct this.)
Upvotes: 4
Reputation: 1207
In order to follow your requirement of not drastically changing the entire structure, I think the easiest fix is to adopt an event-driven communication architecture.
You can create a custom event like this:
d3Event = document.createEvent('Event');
d3Event.initEvent('d3TestClick', true, true);
And then you can dispatch this custom event when one of your elements is interacted with:
elem1.on('click', function() {
var elemClicked = 'elem1';
console.log('PAGE1 // Click on:', elemClicked);
document.getElementById("elem1").dispatchEvent(d3Event);
});
And also add event handlers for each component for when another component fires the event:
document.getElementById("elem2").addEventListener('d3TestClick', function (e) {
console.log('PAGE1 // Page1 received click from: Page2', e);
}, false);
document.getElementById("elem3").addEventListener('d3TestClick', function (e) {
console.log('PAGE1 // Page1 received click from: Page3', e);
}, false);
P.S. I believe that nor your approach or my suggestion follow best practices, because it's tightly coupling the components between each other and hinders scalability.
I believe the best approach to solve this would be one where the components don't know of the existence of any other components and are controlled by a global filter. This way you could have infinite components and they could all react in their own way to whatever the global filter is saying.
Achieving this in Vanilla JS feels like quite an undertaking. May I suggest using a framework, such as AngularJS for this?
Hope this helps :)
Upvotes: 5