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 script
s 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 id
s 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