I'm currently trying to make a HTML/JavaScript Windows 8 modern application in which I want to access a local XML file that is in the installation directory.
After reading many ideas and code snippets around the web, I came up with a convoluted asynchronous method of accessing the file, which works. However, is this the best/correct way to do something as simple as accessing a local XML file?
Additionally, I'd like to be able to have a function load the xml file, and save the XMLDocument object as a "global" variable, so that on button presses and other triggers, the XMLDocument object can be accessed and parsed. This is where all the problems start, since one method is async, and then the variables are undefined, etc....
(function () {
"use strict";
WinJS.UI.Pages.define("/pages/reader/reader.html", {
// This function is called whenever a user navigates to this page. It
// populates the page elements with the app's data.
ready: function (element, options) {
// TODO: Initialize the page here.
var button = document.getElementById("changeText");
button.addEventListener("click", this.buttonClickHandler, false);
var dropdown = document.getElementById("volumeDropdown");
dropdown.addEventListener("change", this.volumeChangeHandler, false);
var loadSettings = new Windows.Data.Xml.Dom.XmlLoadSettings;
loadSettings.prohibitDtd = false;
loadSettings.resolveExternals = false;
//previous attempt, also didn't work:
//this.xmlDoc = null;
//this.loadXMLdoc(this, this.testXML);
//also not working:
this.getXmlAsync().then(function (doc) {
var xmlDoc = doc;
});
//this never works also, xmlDoc always undefined, or an error:
//console.log(xmlDoc);
},
buttonClickHandler: function (eventInfo) {
// doesn't work, xmlDoc undefined or error:
console.log(xmlDoc);
},
volumeChangeHandler: function (eventInfo) {
var e = document.getElementById("volumeDropdown");
// of course doesn't work, since I can't save the XMLDocument object into a variable (works otherwise):
var nodelist2 = xmlDoc.selectNodes('//volume[#name="volumeName"]/chapter/#n'.replace('volumeName', list[0]));
var volumeLength = nodelist2.length;
for (var index = 0; index < volumeLength; index++) {
var option = document.createElement("option");
option.text = index + 1;
option.value = index + 1;
var volumeDropdown = document.getElementById("chapterDropdown");
volumeDropdown.appendChild(option);
}
},
getXmlAsync: function () {
return Windows.ApplicationModel.Package.current.installedLocation.getFolderAsync("books").then(function (externalDtdFolder) {
externalDtdFolder.getFileAsync("book.xml").done(function (file) {
return Windows.Data.Xml.Dom.XmlDocument.loadFromFileAsync(file);
})
})
},
loadXMLdoc: function (obj, callback) {
var loadSettings = new Windows.Data.Xml.Dom.XmlLoadSettings;
loadSettings.prohibitDtd = false;
loadSettings.resolveExternals = false;
Windows.ApplicationModel.Package.current.installedLocation.getFolderAsync("books").then(function (externalDtdFolder) {
externalDtdFolder.getFileAsync("book.xml").done(function (file) {
Windows.Data.Xml.Dom.XmlDocument.loadFromFileAsync(file, loadSettings).then(function (doc) {
var nodelist = doc.selectNodes("//volume/#name");
var list = [];
for (var index = 0; index < nodelist.length; index++) {
list.push(nodelist[index].innerText);
};
for (var index = 0; index < list.length; index++) {
var option = document.createElement("option");
option.text = list[index] + "new!";
option.value = list[index];
var volumeDropdown = document.getElementById("volumeDropdown");
volumeDropdown.appendChild(option);
};
var nodelist2 = doc.selectNodes('//volume[#name="volumeName"]/chapter/#n'.replace('volumeName', list[0]));
var volumeLength = nodelist2.length;
for (var index = 0; index < volumeLength; index++) {
var option = document.createElement("option");
option.text = index + 1;
option.value = index + 1;
var volumeDropdown = document.getElementById("chapterDropdown");
volumeDropdown.appendChild(option);
};
obj.xmlDoc = doc;
callback(obj);
})
})
});
},
initializeXML: function (doc, obj) {
console.log("WE ARE IN INITIALIZEXML NOW")
obj.xmlDoc = doc;
},
testXML: function (obj) {
console.log(obj.xmlDoc);
},
});
})();
In summary with all these complicated methods failing, how should I go about doing something as simple as loading an XML file, and then having it available as an object that can be used by other functions, etc.?
Thanks for your help!
PS:
I'm very new to JavaScript and Windows 8 Modern Apps/ WinAPIs.
Previous experience all in Python and Java (where doing this is trivial!).
There are a couple of things going on here that should help you out.
First, there are three different loading events for a PageControl, corresponding to methods in your page class. The ready method (which is the only one the VS project template includes) gets called only at the end of the process, and is thus somewhat late in the process for doing an async file load. It's more appropriate to do this work within the init method, which is called before any elements have been created on the page. (The processed method is called after WinJS.UI.processAll is complete but before the page has been added to the DOM. ready is called after everything is in the DOM.)
Second, your getXMLAsync method looks fine, but your completed handler is declaring another xmlDoc variable and then throwing it away:
this.getXmlAsync().then(function (doc) {
var xmlDoc = doc; //local variable gets discarded
});
The "var xmlDoc" declares a local variable in the handler, but it's discarded as soon as the handler returns. What you need to do is assign this.xmlDoc = doc, but the trick is then making sure that "this" is the object you want it to be rather than the global context, which is the default for an anonymous function. The pattern that people generally use is as follows:
var that = this;
this.getXmlAsync().then(function (doc) {
that.xmlDoc = doc;
});
Of course, it's only after that anonymous handler gets called that the xmlDoc member will be valid. That is, if you put a console.log at the end of the code above, after the });, the handler won't have been called yet from the async thread, so xmlDoc won't get be valid. If you put it inside the handler immediately after that.xmlDoc = doc, then it should be valid.
This is all just about getting used to how async works. :)
Now to simplify matters for you a little, there is the static method StorageFile.getFileFromApplicationUriAsync which you can use to get directly to in-package file with a single call, rather than navigating folders. With this you can load create the XmlDocument as follows:
getXmlAsync: function () {
return StorageFile.getFileFromApplicationUriAsync("ms-appx:///books/book.xml").then((function (file) {
return Windows.Data.Xml.Dom.XmlDocument.loadFromFileAsync(file);
}).then(function (xmlDoc) {
return xmlDoc;
});
}
Note that the three /// are necessary; ms-appx:/// is a URI scheme that goes to the app package contents.
Also notice how the promises are chained instead of nested. That's typically a better structure, and one that allows a function like this to return a promise that will be fulfilled with the last return value in the chain. This can then be used with the earlier bit of code that assigns that.xmlDoc, and you avoid passing in obj and a callback (promises are intended to avoid such callbacks).
Overall, if you have any other pages in your app to which you'll navigate, you'll really want to load this XML file and create the XmlDocument once for the app, not with the specific page. Otherwise you'd be reloading the file every time you navigate to the page. For this reason, you could choose to do the loading on app startup, not page load, and use WinJS.Namespace.define to create a namespace variable in which you store the xmlDoc. Because that code would load on startup while the splash screen is visible, everything should be ready when the first page comes up. Something to think about.
In any case, given that you're new to this space, I suggest you download my free ebook, Programming Windows Store Apps with HTML, CSS, and JavaScript, 2nd Edition, where Chapter 3 has all the details about app startup, page controls, and promises (after the broader introductions of Chapters 1 and 2 of course).
I am trying to use the Javascript Module pattern. I want my module to have a public method that can be called when a json file is loaded. In this simple example, the module loads the json on init and (should) load an image into a DOM element when the public method 'loadPic' is called. The source of the image is in the json file.) When I run the script the first time, I get the following error: Uncaught TypeError: Cannot read property 'src' of undefined. My interpretation is that the method 'loadPic' is called automatically, before the data is loaded... but I don't know how to prevent that. Here is the code (PS: I am using Zepto, the 'light' version of jQuery):
<!-- language: lang-js -->
/* in module file: DualG.js */
;(function ($) {
$.extend($.fn, {
DualG: function (element, options) {
var defaults = { // not used yet...
indexStart:0,
url: 'js/data.json'
},
my = {},
init,
loadPic,
plugin = this;
plugin.settings = {};
plugin.pics = [];
init = function () {
plugin.settings = $.extend{}, defaults, options);
$.getJSON(plugin.settings.url, function (data) {
var l = data.images.length, i;
for (i = 0; i < l; i = i + 1) {
plugin.pics[data.images[i].index] = data.images[i];
}
});
init();
my.loadPic = function (index, container) {
$(container).empty().append($("<img>").attr({"src":plugin.pics[index].src}));
};
return my;
}
});
}(Zepto));
My problem was that data is loaded a-synchronistically - so I was tying to use data that was not loaded yet. I added an event trigger in the init, after loading the data. Then, I listen to the event and call the loadPic method only then... Thanks all for your help.
I'm looking for a way to post variables in an external .js file.
Something like:
<script type="text/javascript" src="/js/script-starter.js?var=value"></script>
And then I'd like to call var (and thus, value). Is this possible in any way?
Thank you!
Yes, it is possible. You will have to look for all script tags in the DOM (make sure the DOM is loaded before attempting to look in it) using the document.getElementsByTagName function, loop through the resulting list, find the corresponding script and parse the src property in order to extract the value you are looking for.
Something along the lines of:
var scripts = document.getElementsByTagName('script');
for (var i = 0; i < scripts.length; i++) {
var src = scripts[i].src;
// Now that you know the src you could test whether this is the script
// that you are looking for (i.e. does it contain the script-starter.js string)
// and if it does parse it.
}
As far as the parsing of the src attribute is concerned and extracting the query string values from it you may use the following function.
it could be possible if that script is a serverside script that receives the variable in querystring and returns a javascript code as output
How about declaring the variable before script file loading:
<script type="text/javascript">var myVar = "Value"</script>
<script type="text/javascript" src="/js/script-starter.js"></script>
So you can use this variable in your script. Possibly this is one of the easiest ways.
Why don't you put the variables in a hidden a or p
<a id="myHiddenVar" style="display:none;">variables</a>
Then in your .js you access it with
var obj = document.getElementById("myHiddenVar").innerHTML;
or the jQuery style
var obj = $("#myHiddenVar").text();
function getJSPassedVars(script_name, varName) {
var scripts = $('script'),
res = null;
$.each(scripts, function() {
var src = this.src;
if(src.indexOf(script_name) >= 0) {
varName = varName.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]");
var string = new RegExp("[\\?&]"+ varName +"=([^&#]*)"),
args = string.exec(src);
if(!args.length) {
res = null;
} else {
res = args[1];
}
}
});
return res;
}
console.log(getJSPassedVars('script-starter.js', 'var'));
in my case my app folder structure is like following:
MyApp/
index.html
jquery.js
my.js
above function is within my.js with console.log and include it within index.html.
We all know that global variables are anything but best practice. But there are several instances when it is difficult to code without them. What techniques do you use to avoid the use of global variables?
For example, given the following scenario, how would you not use a global variable?
JavaScript code:
var uploadCount = 0;
window.onload = function() {
var frm = document.forms[0];
frm.target = "postMe";
frm.onsubmit = function() {
startUpload();
return false;
}
}
function startUpload() {
var fil = document.getElementById("FileUpload" + uploadCount);
if (!fil || fil.value.length == 0) {
alert("Finished!");
document.forms[0].reset();
return;
}
disableAllFileInputs();
fil.disabled = false;
alert("Uploading file " + uploadCount);
document.forms[0].submit();
}
Relevant markup:
<iframe src="test.htm" name="postHere" id="postHere"
onload="uploadCount++; if(uploadCount > 1) startUpload();"></iframe>
<!-- MUST use inline JavaScript here for onload event
to fire after each form submission. -->
This code comes from a web form with multiple <input type="file">. It uploads the files one at a time to prevent huge requests. It does this by POSTing to the iframe, waiting for the response which fires the iframe onload, and then triggers the next submission.
You don't have to answer this example specifically, I am just providing it for reference to a situation in which I am unable to think of a way to avoid global variables.
The easiest way is to wrap your code in a closure and manually expose only those variables you need globally to the global scope:
(function() {
// Your code here
// Expose to global
window['varName'] = varName;
})();
To address Crescent Fresh's comment: in order to remove global variables from the scenario entirely, the developer would need to change a number of things assumed in the question. It would look a lot more like this:
Javascript:
(function() {
var addEvent = function(element, type, method) {
if('addEventListener' in element) {
element.addEventListener(type, method, false);
} else if('attachEvent' in element) {
element.attachEvent('on' + type, method);
// If addEventListener and attachEvent are both unavailable,
// use inline events. This should never happen.
} else if('on' + type in element) {
// If a previous inline event exists, preserve it. This isn't
// tested, it may eat your baby
var oldMethod = element['on' + type],
newMethod = function(e) {
oldMethod(e);
newMethod(e);
};
} else {
element['on' + type] = method;
}
},
uploadCount = 0,
startUpload = function() {
var fil = document.getElementById("FileUpload" + uploadCount);
if(!fil || fil.value.length == 0) {
alert("Finished!");
document.forms[0].reset();
return;
}
disableAllFileInputs();
fil.disabled = false;
alert("Uploading file " + uploadCount);
document.forms[0].submit();
};
addEvent(window, 'load', function() {
var frm = document.forms[0];
frm.target = "postMe";
addEvent(frm, 'submit', function() {
startUpload();
return false;
});
});
var iframe = document.getElementById('postHere');
addEvent(iframe, 'load', function() {
uploadCount++;
if(uploadCount > 1) {
startUpload();
}
});
})();
HTML:
<iframe src="test.htm" name="postHere" id="postHere"></iframe>
You don't need an inline event handler on the <iframe>, it will still fire on each load with this code.
Regarding the load event
Here is a test case demonstrating that you don't need an inline onload event. This depends on referencing a file (/emptypage.php) on the same server, otherwise you should be able to just paste this into a page and run it.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>untitled</title>
</head>
<body>
<script type="text/javascript" charset="utf-8">
(function() {
var addEvent = function(element, type, method) {
if('addEventListener' in element) {
element.addEventListener(type, method, false);
} else if('attachEvent' in element) {
element.attachEvent('on' + type, method);
// If addEventListener and attachEvent are both unavailable,
// use inline events. This should never happen.
} else if('on' + type in element) {
// If a previous inline event exists, preserve it. This isn't
// tested, it may eat your baby
var oldMethod = element['on' + type],
newMethod = function(e) {
oldMethod(e);
newMethod(e);
};
} else {
element['on' + type] = method;
}
};
// Work around IE 6/7 bug where form submission targets
// a new window instead of the iframe. SO suggestion here:
// http://stackoverflow.com/q/875650
var iframe;
try {
iframe = document.createElement('<iframe name="postHere">');
} catch (e) {
iframe = document.createElement('iframe');
iframe.name = 'postHere';
}
iframe.name = 'postHere';
iframe.id = 'postHere';
iframe.src = '/emptypage.php';
addEvent(iframe, 'load', function() {
alert('iframe load');
});
document.body.appendChild(iframe);
var form = document.createElement('form');
form.target = 'postHere';
form.action = '/emptypage.php';
var submit = document.createElement('input');
submit.type = 'submit';
submit.value = 'Submit';
form.appendChild(submit);
document.body.appendChild(form);
})();
</script>
</body>
</html>
The alert fires every time I click the submit button in Safari, Firefox, IE 6, 7 and 8.
I suggest the module pattern.
YAHOO.myProject.myModule = function () {
//"private" variables:
var myPrivateVar = "I can be accessed only from within YAHOO.myProject.myModule.";
//"private" method:
var myPrivateMethod = function () {
YAHOO.log("I can be accessed only from within YAHOO.myProject.myModule");
}
return {
myPublicProperty: "I'm accessible as YAHOO.myProject.myModule.myPublicProperty."
myPublicMethod: function () {
YAHOO.log("I'm accessible as YAHOO.myProject.myModule.myPublicMethod.");
//Within myProject, I can access "private" vars and methods:
YAHOO.log(myPrivateVar);
YAHOO.log(myPrivateMethod());
//The native scope of myPublicMethod is myProject; we can
//access public members using "this":
YAHOO.log(this.myPublicProperty);
}
};
}(); // the parens here cause the anonymous function to execute and return
First off, it is impossible to avoid global JavaScript, something will always be dangling the global scope. Even if you create a namespace, which is still a good idea, that namespace will be global.
There are many approaches, however, to not abuse the global scope. Two of the simplest are to either use closure, or since you only have one variable you need to keep track of, just set it as a property of the function itself (which can then be treated as a static variable).
Closure
var startUpload = (function() {
var uploadCount = 1; // <----
return function() {
var fil = document.getElementById("FileUpload" + uploadCount++); // <----
if(!fil || fil.value.length == 0) {
alert("Finished!");
document.forms[0].reset();
uploadCount = 1; // <----
return;
}
disableAllFileInputs();
fil.disabled = false;
alert("Uploading file " + uploadCount);
document.forms[0].submit();
};
})();
* Note that incrementing of uploadCount is happening internally here
Function Property
var startUpload = function() {
startUpload.uploadCount = startUpload.count || 1; // <----
var fil = document.getElementById("FileUpload" + startUpload.count++);
if(!fil || fil.value.length == 0) {
alert("Finished!");
document.forms[0].reset();
startUpload.count = 1; // <----
return;
}
disableAllFileInputs();
fil.disabled = false;
alert("Uploading file " + startUpload.count);
document.forms[0].submit();
};
I'm not sure why uploadCount++; if(uploadCount > 1) ... is necessary, as it looks like the condition will always be true. But if you do need global access to the variable, then the function property method I described above will allow you to do so without the variable actually being global.
<iframe src="test.htm" name="postHere" id="postHere"
onload="startUpload.count++; if (startUpload.count > 1) startUpload();"></iframe>
However, if that's the case, then you should probably use an object literal or instantiated object and go about this in the normal OO way (where you can use the module pattern if it strikes your fancy).
Sometimes it makes sense to have global variables in JavaScript. But don't leave them hanging directly off window like that.
Instead, create a single "namespace" object to contain your globals. For bonus points, put everything in there, including your methods.
window.onload = function() {
var frm = document.forms[0];
frm.target = "postMe";
frm.onsubmit = function() {
frm.onsubmit = null;
var uploader = new LazyFileUploader();
uploader.startUpload();
return false;
}
}
function LazyFileUploader() {
var uploadCount = 0;
var total = 10;
var prefix = "FileUpload";
var upload = function() {
var fil = document.getElementById(prefix + uploadCount);
if(!fil || fil.value.length == 0) {
alert("Finished!");
document.forms[0].reset();
return;
}
disableAllFileInputs();
fil.disabled = false;
alert("Uploading file " + uploadCount);
document.forms[0].submit();
uploadCount++;
if (uploadCount < total) {
setTimeout(function() {
upload();
}, 100);
}
}
this.startUpload = function() {
setTimeout(function() {
upload();
}, 100);
}
}
Some things are going to be in the global namespace -- namely, whatever function you're calling from your inline JavaScript code.
In general, the solution is to wrap everything in a closure:
(function() {
var uploadCount = 0;
function startupload() { ... }
document.getElementById('postHere').onload = function() {
uploadCount ++;
if (uploadCount > 1) startUpload();
};
})();
and avoid the inline handler.
Other way to do this is to create an object and then add methods to it.
var object = {
a = 21,
b = 51
};
object.displayA = function() {
console.log(object.a);
};
object.displayB = function() {
console.log(object.b);
};
In this way, only object 'obj' is exposed and methods attached to it. It is equivalent to adding it in namespace.
Using closures might be OK for small to medium projects. However, for big projects, you might want to split your code into modules and save them in different files.
Therefore I wrote jQuery Secret plugin to solve the problem.
In your case with this plugin the code would look something like the following.
JavaScript:
// Initialize uploadCount.
$.secret( 'in', 'uploadCount', 0 ).
// Store function disableAllFileInputs.
secret( 'in', 'disableAllFileInputs', function(){
// Code for 'disable all file inputs' goes here.
// Store function startUpload
}).secret( 'in', 'startUpload', function(){
// 'this' points to the private object in $.secret
// where stores all the variables and functions
// ex. uploadCount, disableAllFileInputs, startUpload.
var fil = document.getElementById( 'FileUpload' + uploadCount);
if(!fil || fil.value.length == 0) {
alert( 'Finished!' );
document.forms[0].reset();
return;
}
// Use the stored disableAllFileInputs function
// or you can use $.secret( 'call', 'disableAllFileInputs' );
// it's the same thing.
this.disableAllFileInputs();
fil.disabled = false;
// this.uploadCount is equal to $.secret( 'out', 'uploadCount' );
alert( 'Uploading file ' + this.uploadCount );
document.forms[0].submit();
// Store function iframeOnload
}).secret( 'in', 'iframeOnload', function(){
this.uploadCount++;
if( this.uploadCount > 1 ) this.startUpload();
});
window.onload = function() {
var frm = document.forms[0];
frm.target = "postMe";
frm.onsubmit = function() {
// Call out startUpload function onsubmit
$.secret( 'call', 'startUpload' );
return false;
}
}
Relevant markup:
<iframe src="test.htm" name="postHere" id="postHere" onload="$.secret( 'call', 'iframeOnload' );"></iframe>
Open your Firebug, you will find no globals at all, not even the funciton :)
For full documentation, please see here.
For a demo page, please see this.
Source code on GitHub.
I use it this way:
{
var globalA = 100;
var globalB = 200;
var globalFunc = function() { ... }
let localA = 10;
let localB = 20;
let localFunc = function() { ... }
localFunc();
}
For all global scopes use 'var', and for local scopes use 'let'.
Use closures. Something like this gives you a scope other than global.
(function() {
// Your code here
var var1;
function f1() {
if(var1){...}
}
window.var_name = something; //<- if you have to have global var
window.glob_func = function(){...} //<- ...or global function
})();
For "securing" induvidual global variables:
function gInitUploadCount() {
var uploadCount = 0;
gGetUploadCount = function () {
return uploadCount;
}
gAddUploadCount= function () {
uploadCount +=1;
}
}
gInitUploadCount();
gAddUploadCount();
console.log("Upload counter = "+gGetUploadCount());
I'm a novice to JS, currently using this in one project.
(i apreciate any comment and criticism)
What happens is that within the function makeCounter:
function makeCounter() {
var i = 0;
return function() {
console.log( ++i );
};
}
You are returning a function, then to use it is the following:
const counter = makeCounter(); // execute function and return other function
counter(); // This executes the function that you returned
If for example you did not return a function, it would work as expected:
function makeCounter() {
var i = 0;
console.log( ++i );
}
makeCounter(); // this execute logs
One fun (YDOFMV1) way to avoid scope clutter for not just JavaScript but also HTML and even CSS(!) is with HTML templates. You do have to whip up a little architectural code (which of course doesn't have to be in global scope). Consider this HTML:
<html lang="en">
<head>
<title>Templates Are Fun!</title>
<script> // stuff!
</script>
</head>
<body>
<template>
<style> these style directives are local!</style>
<div> just about any html stuff! </div>
<script>
doStuff(); // this code ignored unless you eval it!
</script>
</template>
</body>
</html>
All the stuff in the template has no effect on your HTML page, so what good is it? First, the style element only affects HTML elements inside the template. Second, the script element is not actually evaluated, which means you can do that yourself and put the code inside a scope of your own choosing, with whatever environmental features you care to give it (all without messing about with "components" and whatnot).
On to the minor architecture you have to (get to!) do yourself:
For Each Template
Obviously, if you were doing a little single-page app, you could put each page in a separate template, then you swap in the rendered template as needed. Left as an exercise for the reader, but works great.
Also, this concept works great for recursive templates. I.e., the whole page is one big template, but it contains smaller templates (navigation bar, footer, etc.). Again, left as an exercise, but can't be that hard if I did it.
Whatever your scheme, you're going to need to get hold of that template node, set aside the script sub-node (so you can control its execution), and then replace the template node with the actual contents of the template. Your code may look at least vaguely like:
const template = document.querySelector('template');
const templateRoot = template.content;
const scriptNode = templateRoot.querySelector('script');
scriptNode.remove(); // take it out of the template
template.replaceWith(templateRoot); // replace template with its contents
After that code runs, the template contents now exist in your document and will no longer be ignored by the DOM parser. The script element still hasn't been executed but you have a reference to it in scriptNode.
Control That Script
You could eval the script, of course, but we're talking about limiting scope to avoid
problems
right? Use the Function constructor instead for greater control:
const code = '"use strict";' + scriptNode.innerHTML;
const templateFunc = new Function(code);
// now run templateFunc when you want
Note the injection of "use strict", because the code being evaluated does not inherit that setting from the calling code.
The code from the template script tag doesn't have access to much of anything other than the global scope, so you don't have to worry much about it colliding with any of your other code. Of course, that leads immediately to the problem that you may need that code have some interaction with the rest of your code.
Letting The Outside Call In
The "module" pattern works fine here. Since the template script will be wrapped in an anonymous function by the Function constructor, it can just return some interface that suits you:
// from inside template <script>
function init() { outside calls me to let me initialize };
function render() { outside calls me to tell me to render };
return {init:init, render:render};
Your world outside the template(s) then keeps that return value around to call into the template script code:
const code = '"use strict";' + scriptNode.innerHTML;
const templateFunc = new Function(code);
const templateInterface = templateFunc();
// let's tell the template script to initialize
templateInterface.init();
Letting the Inside Call Out
Besides the need to tell the template script what to do,
the template script probably needs some limited access to the outside
world. Again, we want to exercise total control over that access.
Once again, some flavor of "module" pattern works fine.
Suppose that we have an interface called "model" that contains data
some template scripts may need in order to render. Let's roll our own
require function, which we inject into the script code by making
require an argument to the anonymous function that the Function constructor creates:
// revised to give <script> code access to a 'require' function
// model defined elsewhere in this scope
function require(name){
switch(name){
case 'model' : return model;
// etc.
}
}
const code = '"use strict";' + scriptNode.innerHTML;
// anonymous function will have arg named "require"
const templateFunc = new Function("require", code);
const templateInterface = templateFunc(require);
// let's tell the template script to initialize
templateInterface.init();
Now the code inside the template script has access to
a variable (first argument of the anonymous function that encloses it)
named require that it can use in the standard ways:
<script>
const model = require('model');
// remaining code has access to model forever more
//...
<script>
Enriching the Environment
Of course, you can enrich the environment of the script code far beyond
just giving it a require function. This is why there are a billiondy frameworks for JavaScript. One I like is generating on the fly an object for each template that gives access to DOM elements it is interested in inside the template. This addresses the annoying problem that you often want to locate DOM elements by id, but then you have that nagging feeling that you're supposed to make all ids unique across the entire document, so id effectively becomes a global variable that you can't modularize.
Suppose that inside the template, you identify elements your script code cares about with a data-id tag:
<template>
<dialog id="PickOrCreateProject">
<center>
Project name: <input type="text" data-id="textProject" />
<input type="button" value="Create" data-id="createButton">
<br/>
<div data-id="div">
<select data-id="select"></select>
<input type="button" value="Select" data-id="selectButton">
</div>
</center>
</dialog>
<script> etc.</script>
</template>
And suppose that we want to give our script code easy
access to an object (call it viewNodes) that contains references to each node inside the template marked with a data-id attribute,
such that the script can do this:
const {textProject,createButton,selectButton} = viewNodes;
You can whip that up as easy as:
let viewNodes = {};
for(const element of templateRoot.querySelectorAll("[data-ID]"))
viewNodes[element.dataset.id] = element;
And revise the Function constructor to supply it to the
script code:
const templateFunc = new Function("require", "viewNodes", code);
const templateInterface = templateFunc(require, viewNodes);
Doesn't matter if two different templates use conflicting
data-id attributes, since no template ever looks outside
itself for the identifiers it wants.
Conclusion
In conclusion, templates are fun for modularity, and I must have some work I am
procrastinating hard on to waste time spewing this all out :-).
1Your Definition Of Fun May Vary