9. Script Block - Global Space

In the previous chapter, we were introduced to the specific types of script blocks and when to use them. Within these blocks, we use window[Symbol.for(<?js OutputId() ?>)] to assign it to the instance of the Note widget. But what is the purpose of this? Let's examine what is happening under the hood to understand it better.

Let’s take the following widget as an example and see how the final rendered page output will look:

<h2>Script Block Example Widget</h2>
<script>
	console.log("Regular JavaScript script block");
</script>
<? scriptmodule "script-block-example-widget" ?>
	console.log("Script module");
<? end ?>
<? scriptreference "example-website-script" "https://example.website/script.js" ?>
<? startupscript ?>
	console.log("Startup script");
<? end ?>
<? submitscript ?>
	console.log("Submit script");
<? end ?>
<? unloadscript ?>
	console.log("Unload script");
<? end ?>

When inspecting the code output of the page and observing how this widget has been rendered, it roughly translates to the following stripped-down output:

<html>
	<head>
		...
		<script>
			console.log("Script module");
		</script>
		<script src="https://example.website/script.js"></script>
		...
	</head>
	<body>
		...
		<!-- Page content -->
		...
		<h2>Script Block Example Widget</h2>
		<script>
			console.log("Regular JavaScript script block");
		</script>
		...
		<!-- Other page content -->
		...
		<script> 
			Runtime.scripts.addStartupScript(function() {
				console.log("Startup script");
			});
			
			Runtime.scripts.addSubmitScript(function() {
				console.log("Submit script");
			});
			
			Runtime.scripts.addUnloadScript(function() {
				console.log("Unload script");
			});
		</script>
	</body>
</html>

A few interesting things are happening here. The scriptmodule is JavaScript code inside a <script> tag that has been added to the <head> section. The scriptreference behaves similarly. The regular <script> blocks are rendered as part of the page body content. However, you will notice that the code inside the other script blocks is registered at the end of the <body> in anonymous functions.

It is therefore crucial that your function and class names are unique when used in a scriptmodule or scriptreference. To clarify, consider these two widgets created by Bob and Jane:

<? scriptmodule "bobs-button" ?>
	class Button {/* Bob's button */}
<? end ?>
<? scriptmodule "janes-button" ?>
	class Button {/* Jane's button */}
<? end ?>

This will output roughly to:

<head>
	<script>
		class Button {/* Bob's button */}
	</script>
	<script>
		class Button {/* Jane's button */}
	</script>
</head>

In this case, Bob's button is overwritten by Jane's, which is problematic. The scriptreference faces the same issue, as the script is loaded externally instead of being embedded within the page itself.

We resolved this issue in the Note widget by using class WemAcademyNote instead of class Note. While this is not a perfect solution, it helps minimize naming collisions.

There are other ways to address this naming collision, but they come with trade-offs. I will explain those later. First, let’s take a closer look at window[Symbol.for(<?js OutputId() ?>)]. Notice that the startupscript, submitscript, and unloadscript are anonymous functions. But what if we need access to context outside their local scope? This was the problem we encountered in the Note widget. To achieve this, we need a way to share state between those functions, which requires a global context. However, this can be very risky if not handled carefully.

Let's examine a stripped-down version of initializing and disposing of the Note widget from the previous chapter in a potentially dangerous way:

<? startupscript ?>
	const note = new Note();
	document.documentElement.append(note.rootEl);
	window.note = note;
<? end ?>
<? unloadscript ?>
	window.note.dispose();
<? end ?>

This will output roughly to:

<body>
	...
	<script> 
		Runtime.scripts.addStartupScript(function() {
			const note = new Note();
			document.documentElement.append(note.rootEl);
			window.note = note;
		});
		
		Runtime.scripts.addUnloadScript(function() {
			window.note.dispose();
		});
	</script>
</body>

In this example, we declared a global variable note, which can be risky. If another piece of JavaScript code uses a global variable with the same name, we may encounter undefined behavior. In this case, we could inadvertently overwrite someone else's note variable. This is a classic pitfall of using global variables!

However, there is a way to make this global variable "hidden." Let's dissect window[Symbol.for(<?js OutputId() ?>)] using the following code:

<? startupscript ?>
	const note = new Note();
	document.documentElement.append(note.rootEl);
	window[Symbol.for(<?js OutputId() ?>)] = note;
<? end ?>
<? unloadscript ?>
	window[Symbol.for(<?js OutputId() ?>)].dispose();
<? end ?>

This will output to the following:

<body>
	...
	<script> 
		Runtime.scripts.addStartupScript(function() {
			const note = new Note();
			document.documentElement.append(note.rootEl);
			window[Symbol.for("cc1")] = note;
		});
		
		Runtime.scripts.addUnloadScript(function() {
			window[Symbol.for("cc1")].dispose();
		});
	</script>
</body>

Here, we see that <? OutputId() ?> has printed "cc1". As we learned in previous chapters, OutputId() creates a unique identifier for each instance of the widget on the page. This means that if we add multiple instances of this widget, the identifiers will be "cc2", "cc3", and so on. Note that we use "cc1" as an example, and it is bound to the implementation of the WEM Runtime, so it can be anything.

The window object is the global object in JavaScript, and the Symbol class is particularly interesting here. It creates symbols that are guaranteed to be unique. In our case, we use it to create a unique key that we store in window, effectively creating our "hidden" global variable. In other words, this approach provides a form of weak encapsulation or weak information hiding.

You might wonder, "What if we navigate to the next page? Wouldn't we potentially have another instance of a widget called cc1?" Yes, that is true, but it does not matter because we disposed of the instance of the note in the previous unloadscript. Therefore, it is acceptable to overwrite the contents of window[Symbol.for("cc1")].

In our Note widget, we implemented the following:

<? unloadscript ?>
	const noteSymbol = Symbol.for(<?js OutputId() ?>);
	window[noteSymbol].dispose();
	delete window[noteSymbol];
<? end ?>

To ensure that we clean everything up, we also delete our "hidden" global variable, as shown above. This helps the JavaScript garbage collector free up memory space. More importantly, it ensures that we do not have access to the variable after it has been used.

Last updated

Was this helpful?