8. Script Block - Introduction

In this chapter, we will create a Note widget. It is a relatively simple widget to build, but we need to address a few challenges to ensure it functions correctly as a WEM widget.

As mentioned previously, adding widgets will introduce CSS and JavaScript into a global space. This can be quite challenging for more advanced widgets, as we want to avoid overriding and breaking existing CSS and JavaScript. Therefore, we will start with some scaffolding to see how we can prevent these issues.

We will learn about invariant culture, naming collisions, cleaning up the widget, and a few other concepts along the way. Let's get started!

  • Create a new widget and name it "Note."

  • Copy and paste the following CSS into a new file called "note.css."

  • Add it as a resource named "NoteCss."

.wem-note {
	background-color: lightgoldenrodyellow;
	display: flex;
	flex-direction: column;
	position: fixed;
	left: calc(var(--x) * 1px);
	top: calc(var(--y) * 1px);
	width: 250px;
	height: 250px;
}

.wem-note > .handle-bar {
	background-color: tomato;
	cursor: pointer;
	flex: 0 0 20px;
}

.wem-note > .message {
	flex: 1 0;
	outline: none;
	padding: 8px;
	resize: none;
}

Notice that we use a more specific class name for our Note widget, .wem-note.

  • Copy and paste the JavaScript code below into a new file called "note.js."

  • Add it as a resource named "NoteJs."

class WemNote {
    messageEl;
    rootEl;
    x; 
    y; 
    previousX; 
    previousY;
    
    constructor({ x = 100, y = 100, message = "New note" }) {
        this.setThisContext();

        const handleBarEl = document.createElement("div");
        handleBarEl.classList.add("handle-bar");
        handleBarEl.addEventListener("mousedown", this.startDragHandler);

        this.messageEl = document.createElement("textarea");
        this.messageEl.classList.add("message");
        this.messageEl.value = message;

        this.rootEl = document.createElement("div");
        this.rootEl.classList.add("wem-note");
        this.rootEl.append(handleBarEl, this.messageEl);
        
        this.updatePosition(x, y);
    }

    dispose() {
        this.removeWindowEventListeners();
    }

    getData() {
        return {
            x: this.x,
            y: this.y,
            message: this.messageEl.value,
        };
    }

    moveHandler(event) {
        const deltaX = event.screenX - this.previousX;
        const deltaY = event.screenY - this.previousY;

        this.previousX = event.screenX;
        this.previousY = event.screenY;

        this.updatePosition(this.x + deltaX, this.y + deltaY);

        // Disable browser selection.
        event.preventDefault();
        event.stopImmediatePropagation();
    }

    removeWindowEventListeners() {
        window.removeEventListener("mousemove", this.moveHandler, { capture: true });
        window.removeEventListener("mouseup", this.removeWindowEventListeners);
    }

    setThisContext() {
        this.moveHandler = this.moveHandler.bind(this);
        this.removeWindowEventListeners = this.removeWindowEventListeners.bind(this);
        this.startDragHandler = this.startDragHandler.bind(this);
    }

    startDragHandler(event) {
        this.previousX = event.screenX;
        this.previousY = event.screenY;
        
        window.addEventListener("mousemove", this.moveHandler, { capture: true });
        window.addEventListener("mouseup", this.removeWindowEventListeners);
    }

    updatePosition(x, y) {
        this.x = x;
        this.y = y;
        this.rootEl.style.setProperty("--x", this.x);
        this.rootEl.style.setProperty("--y", this.y);
    }
}

The above CSS and JavaScript code are standalone and can even be used in a non-WEM application if desired. The code below is necessary to make it work as a WEM widget. There are a few interesting aspects that we will dissect later.

<? 
	scriptreference "wem-note" FileUrl(@NoteJs)

	register input @Message 
	register input @X invariant culture
	register input @Y invariant culture
?>
<input type="hidden" id="<?attr OutputId(@Message) ?>" name="<?attr OutputId(@Message) ?>">
<input type="hidden" id="<?attr OutputId(@X) ?>" name="<?attr OutputId(@X) ?>">
<input type="hidden" id="<?attr OutputId(@Y) ?>" name="<?attr OutputId(@Y) ?>">
<? startupscript ?>

	const note = new WemNote({
		message: <?js @Message ?>,
		x: <?js @X ?>,
		y: <?js @Y ?>,
	});

	document.documentElement.append(note.rootEl);

	window[Symbol.for(<?js OutputId() ?>)] = note;

<? end ?>
<? submitscript ?>
	
	const note = window[Symbol.for(<?js OutputId() ?>)];
	const { message, x, y } = note.getData();
	
	document.getElementById(<?js OutputId(@Message) ?>).value = message;
	document.getElementById(<?js OutputId(@X) ?>).value = x;
	document.getElementById(<?js OutputId(@Y) ?>).value = y;
	
<? end ?>
<? unloadscript ?>

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

<? end ?>

Invariant Culture

An extra argument, invariant culture, has been added to the register input statements in the following lines of code:

register input @X invariant culture
register input @Y invariant culture

In the previous encoding section, I explained the different writing formats used by various cultures (for example, in English, a "." is used as a decimal separator, while in Dutch, a "," is used). This discrepancy can make it challenging for programmers to read and write numbers consistently between systems. An "invariant culture" is essentially an artificial culture where the format specifications remain constant. In the case of numbers, the "invariant culture" specifies that the decimal separator will be a ".".

ID Attribute

In addition to the name attribute we encountered in our Simple Form widget, we have now added an id attribute to the <input> elements in the following lines of code:

<input type="hidden" id="<?attr OutputId(@Message) ?>" name="<?attr OutputId(@Message) ?>">
<input type="hidden" id="<?attr OutputId(@X) ?>" name="<?attr OutputId(@X) ?>">
<input type="hidden" id="<?attr OutputId(@Y) ?>" name="<?attr OutputId(@Y) ?>">

The reason we need the id attribute here is that we have an additional step in the submitscript to retrieve the new values from the note instance, and we want to write those values back to these inputs. The name attributes are still mandatory on the <input> fields for sending the data back with the request to the WEM runtime.

Special Script Blocks

You may have noticed that we haven't written any JavaScript inside a <script> tag yet. This is not incorrect by any means, and there are situations where it is even necessary when using third-party libraries. However, when building a widget, you want to have more control over when to call functionality in response to specific events. Each piece of JavaScript code in these special JavaScript blocks can be thought of as handlers, similar to a click handler on a button. We have three special JavaScript blocks; let's walk through them.

Startup Script

<? startupscript ?>

	const note = new WemNote({
		message: <?js @Message ?>,
		x: <?js @X ?>,
		y: <?js @Y ?>,
	});

	document.documentElement.append(note.rootEl);

	window[Symbol.for(<?js OutputId() ?>)] = note;

<? end ?>

This code will run after the page has been fully replaced, making it ideal for initializing JavaScript instances since you can be sure that all elements have been rendered. As shown in the example above, we initialize our note here. You may be wondering about the Symbol.for statement; I will explain that later.

Submit Script

<? submitscript ?>

const note = window[Symbol.for(<?js OutputId() ?>)];
const { message, x, y } = note.getData();

document.getElementById(<?js OutputId(@Message) ?>).value = message;
document.getElementById(<?js OutputId(@X) ?>).value = x;
document.getElementById(<?js OutputId(@Y) ?>).value = y;

<? end ?>

This code executes after an event has been triggered, such as a button click, but before the request is made. It is ideal for sanitizing and serializing data, as well as updating the data just before sending it with the request. This is why we need the id attribute on the input fields.

Unload Script

<? unloadscript ?>

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

delete window[noteSymbol];

<? end ?>

The code above runs after receiving a new response from the server but before the new page is rendered. It is perfect for cleaning up a widget instance. In this case, we call the note.dispose() method to clean up our Note widget. It is always important to perform cleanup, especially when there may still be a callback from setTimeout() that needs to be cleared with clearTimeout(). Additionally, our Note widget could still have "mouseup" and "mousemove" event listeners attached to the window object. Although optional, deleting the note ensures that there are no lingering references in the JavaScript runtime, which can assist the garbage collector.

Conclusion

We have built the foundational components to initialize our widget via the startupscript, update our form data using the submitscript, and clean up our widget in the unloadscript. To reduce the risk of naming collisions, we prefixed our class names to make them less generic. In the next chapters, we will explore additional details, such as the purpose of window[Symbol.for(<?js OutputId() ?>)] in the code above.

Last updated

Was this helpful?