90

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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Josh Stodola
  • 81,538
  • 47
  • 180
  • 227
  • 3
    Use Immediately Invoked Function Expression (IIFE) You can read more here: http://www.codearsenal.net/2014/11/immediately-invoked-function-expression.html –  Nov 27 '14 at 12:38

13 Answers13

76

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.

Nightfirecat
  • 11,432
  • 6
  • 35
  • 51
eyelidlessness
  • 62,413
  • 11
  • 90
  • 94
  • Or provide some sort of accessor. I agree. – Upperstage Dec 03 '09 at 18:35
  • 3
    It's helpful when people vote stuff down for them to explain their reasoning for voting down. – eyelidlessness Dec 03 '09 at 19:28
  • 7
    I did not down-vote. However, saying window['varName'] = varName is the same as making a global var declaration outside of the closure. `var foo = "bar"; (function() { alert(window['foo']) })();` – Josh Stodola Dec 03 '09 at 19:50
  • You answered the title, not the question. I didn't like that. Putting the closure idiom within the context of references from the inline event handler (as the meat of the question is getting at) would have been nicer. – Crescent Fresh Dec 03 '09 at 19:55
  • Josh Stodola, correct. That is because your inline event handler is calling a global function. With the closure, it is up to you to declare which variables (or functions) are global and which are not. It gives you full control over what (if anything) to make global. – eyelidlessness Dec 03 '09 at 20:25
  • 1
    Crescent Fresh, I answered the question. The question's assumptions would need to be redesigned in order to avoid global functions. This answer takes, as a guideline, the question's assumptions (an inline event handler, for instance) and allows the developer to choose only the necessary global access points rather than everything being in the global scope. – eyelidlessness Dec 03 '09 at 20:27
  • @eyelidlessness: Holy hell, good edit, +1. @Josh mentioned in a comment on another question: "The inline handler is required in this situation. When form submits to iframe, your onload handler set programmatically will not fire." Can someone confirm, `iframe.onload = ...` is not equivalent to ` – Crescent Fresh Dec 03 '09 at 20:50
  • I hate the "vote too old" bs too. I spent quite a bit of time yesterday assigning onload to the ifframe from the code-behind, and it simply would not work after the initial load. As soon as it displayed the response from the first upload, that was it. I also found it impossible to get the form to work properly with injecting the Iframe into the DOM. It has to be hard-coded in the markup. – Josh Stodola Dec 03 '09 at 20:57
  • Hold on, I'll build a quick test case of the `load` event. I really don't think it should be behaving the way you describe. – eyelidlessness Dec 03 '09 at 20:58
  • I've done some testing now, and yes you can assign the load event programmatically using your "addEvent" function. And I know what I was doing wrong yesterday. It's embarassing, really. I was doing `$(frames["postMe"]).load()` so I was the frame object instead of the element! – Josh Stodola Dec 03 '09 at 21:17
  • I guess I was just a little too late with my test case! Oh well, it's there for future reference anyhow. – eyelidlessness Dec 03 '09 at 21:20
  • I feel like such an idiot. I know I tried a bare-bones getElementById("postMe").onload = function() { } yesterday too, but I supposed that failed because I forgot to remove the logic required to bypass the very initial page load (of the parent) which seemed to always fire the inline onload. Thanks for your help, I've learned a lot today! – Josh Stodola Dec 03 '09 at 21:33
  • There, I can finally upvote now, Cripes. @eyelidlessness: nice work. – Crescent Fresh Dec 03 '09 at 23:56
  • I once saw something like this `(function(window, document) { ... })(window, document);`. What's the reason for it? – Iulian Onofrei Apr 04 '14 at 08:23
60

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
erenon
  • 18,838
  • 2
  • 61
  • 93
  • 3
    I will +1 because I understand this and it is extremely helpful, but I am still unclear as to how effective this would be in the situation where I only use one global variable. Correct me if I am wrong, but executing this function and returning it causes the object returned to be stored in `YAHOO.myProject.myModule`, which is a global variable. Right? – Josh Stodola Dec 03 '09 at 19:47
  • 11
    @Josh: global variable is not evil. Global variable_S_ are evil. Keep the count of globals as few as possible. – erenon Dec 03 '09 at 20:36
  • The whole anonymous function would be executed every time you wanted to access one of the 'modules' public properties/methods right? – UpTheCreek Sep 05 '11 at 14:27
  • @UpTheCreek: no, it won't. It's executed only once, when the program encounters the closing () on the last line, and the returned object will be assigned to the myModule property with the closure containing the myPrivateVar and myPrivateMethod. – erenon Sep 05 '11 at 15:37
  • Beautiful, exactly what I was searching for too. This allows me to separate my page logic from my jquery event handling. A question, with an XSS attack an attacker would still have access to YAHOO.myProject.myModule correct? Would it not be better to just leave the outer function nameless and put an (); at the end, around the $(document).ready as well? Maybe modification of the meta object of YAHOO.myProct.myModule properties? I've just invested quite a bit of time into js theory and am trying to fit it together now. – Dale Aug 03 '12 at 19:00
  • @Dale: There is no point to prevent a successful XSS attack this way. Client side javascript is not secure, period. – erenon Aug 04 '12 at 08:58
  • Broken link to module pattern? – Olle Härstedt Oct 20 '20 at 15:13
  • @OlleHärstedt: yes. a clone can be found here: http://www.yuuuuc.me/module_pattern/ – erenon Oct 21 '20 at 19:06
7

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).

BenMorel
  • 34,448
  • 50
  • 182
  • 322
Justin Johnson
  • 30,978
  • 7
  • 65
  • 89
  • 2
    You absolutely can avoid global scope. In your 'Closure' example, simply remove 'var startUpload = ' from the beginning, and that function will be completely enclosed, with no accessibility at the global level. In practice, many people prefer to expose a single variable, within which are contained references to everything else – derrylwc Jun 26 '13 at 04:58
  • 1
    @derrylwc In this case, `startUpload` is that single variable that you are referring to. Removing `var startUpload = ` from the closure example means that the inner function will never be able to be executed because there is no reference to it. The issue of avoiding global scope pollution is in regards to the internal counter variable, `uploadCount` which is used by `startUpload`. Moreover, I think that the OP is trying to avoid polluting any scope outside of this method with the internally used `uploadCount` variable. – Justin Johnson Jul 17 '13 at 22:53
  • If code within the anonymous closure adds the uploader as an event listener, of course it "will be able to be executed" whenever the appropriate event happens. – Damian Yerrick May 24 '18 at 15:32
6

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.

Nosredna
  • 83,000
  • 15
  • 95
  • 122
4
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);  
    }       
}
ChaosPandion
  • 77,506
  • 18
  • 119
  • 157
  • How do I increment the uploadCount inside of my `onload` handler on the iframe? This is crucial. – Josh Stodola Dec 03 '09 at 19:30
  • 1
    OK, I see what you did here. Unfortunately this is not the same. This fires all uploads seperately, but at the same time (technically, there is 100ms in between them). The current solution uploads them sequentially, meaning the second upload does not start until the first upload is completed. That's why the inline `onload` handler is required. Programmatically assigning the handler does not work, because it only fires the first time. The inline handler fires every time (for whatever reason). – Josh Stodola Dec 03 '09 at 19:41
  • I am still going to +1 though, because I do see that this is an effective method of hiding a global variable – Josh Stodola Dec 03 '09 at 19:43
  • You could create an iframe for each upload to maintain the individual callbacks. – Justin Johnson Dec 03 '09 at 20:31
  • 1
    I think this is ultimately a great answer, just slightly clouded by the particular example's requirements. Basically the idea is to create a template (object) and use 'new' to instantiate it. This is probably the best answer because it genuinely avoids a global variable ie without compromise – PandaWood May 09 '11 at 01:24
2

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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Jimmy
  • 89,068
  • 17
  • 119
  • 137
2

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.

Ajay Poshak
  • 726
  • 6
  • 14
1

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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
ben
  • 1,525
  • 2
  • 15
  • 15
1

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'.

Ashwin
  • 7,277
  • 1
  • 48
  • 70
0

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
})();
NilColor
  • 3,462
  • 3
  • 30
  • 44
  • My approach in the past has been to define a specific global variables object, and attach all global variables to that. How would I wrap this in a closure? Sadly, comment box limit doesn't allow me to embed typical code. – David Edwards Sep 18 '17 at 11:34
0

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)

Koenees
  • 1
  • 3
0

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
Lee Taylor
  • 7,761
  • 16
  • 33
  • 49
0

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
Ron Burk
  • 6,058
  • 1
  • 18
  • 20