Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
In the previous chapters, you learned how to create basic widgets. In this chapter, we will delve deeper into the topics we previously covered. However, we will not go in-depth into certain aspects of WEMscript, as that requires a separate module of its own. By the end of this chapter, you will have a better understanding of Properties, View State, Styling, Events, and development flow of a widget. Once you have a detailed knowledge of these concepts, we will be ready to start writing advanced widgets in the following module.
A WEM widget can be built for different platforms, including iOS, Android, or the web. However, it is not mandatory to support all of them.
In the General tab, you will find the "Availability Settings" panel. Here, you can toggle whether the widget is supported on any of these platforms.
If a widget is not supported on a specific platform, it will be hidden in the modeler for that platform.
Great! You’ve made your first widget. Now, let’s take a step back to the General tab.
Here, you will find three sections: General Settings, Availability Settings, and Content Security Policy.
This section is straightforward; here, you can rename the widget, but you can only rename it.
This section indicates whether the widget is available for any of the following three WEM applications: the WEM web application, the WEM Android application, and the WEM iOS application.
For example, if you have a WEM web application and a widget that only supports Android, that widget will not be available for that project, even if you have imported the library it resides in.
You can always return to a widget to make it compatible with other platforms.
CSP (Content Security Policy) is a security feature implemented in WEM and web applications in general to help prevent various types of attacks, such as Cross-Site Scripting (XSS) and data injection attacks.
Any external sources that the widget uses should be included in the CSP. For instance, if you have the following HTML element:
That image will not load when CSP is enabled in your WEM portal unless you add the hostname https://some-website.com as an img-src directive in the CSP settings overlay behind the "Edit settings" button.
CSP allows web developers to specify which content sources are trusted and can be loaded by the browser when rendering a web page, thereby making the WEM application safer.
As of WEM 4.2, Content Security Policy features have been added. The widget documentation was written before this update, and some example code may not be compatible. This is one of the unfortunate side effects of enhancing application security. For now, to keep things simple, we will not use CSP in our examples. Just ensure that your WEM portal has CSP disabled at the moment for the example code to work.
We will delve deeper into these CSP settings later on.
I believe many programmers are familiar with the classic "Hello, World!" program.
When I was around 13 years old, I wrote my own "Hello, World!" program in QBasic 1.0, which came installed with MS-DOS on our computer. I remember changing the text "World" to my own name, "Vincent." After pressing F5 to run the program, I was thrilled to see my computer greeting me:
I was amazed! I felt the power in my fingertips and wanted to learn more. I used a dial-up modem to connect to various bulletin board systems (BBS) in search of QBasic source codes to study. My parents were not always pleased, as calling on a phone line was expensive back in those days. Not to mention, no one could use the phone while the modem was connected. And let's not forget the times when I wanted to download the latest shareware demo of a game that was just a few megabytes! If someone accidentally picked up the phone, I would have to start the download all over again. Good times!
Although WEM is a no-code platform, I love writing code. Creating widgets is for those who enjoy programming. If you are making widgets, you are engaging in traditional programming. You could even say that if you create widgets, you can call yourself a web developer, as building advanced widgets requires extensive knowledge of HTML, CSS, and JavaScript.
In this module, we will teach you the basics of building a functional widget. We will start simply by creating a "Hello, World!" widget, then extend it to make it more interactive, and eventually conclude with a fully functional widget.
Creating widgets opens up an unlimited array of possibilities, but it can also be quite challenging. I hope that by building widgets, you will feel as excited as I was when I wrote my first programs.
When learning a new language, it is almost traditional to create a "Hello, World!" program as your first project. Let's do that too! Along the way, I will skip a few details here and there, but don't worry; I will explain them in detail in the upcoming chapters. So, let's create our first widget!
To create a widget, we need to open the Widget Editor and create a widget library first.
Open or create a new WEM web project.
The development workflow for building a WEM project differs from that of building widgets. When developing a WEM project, you follow a cycle of developing, testing in preview, staging, and finally publishing it to live. This process is repeated whenever there is a new feature to implement or a bug to fix. In contrast, when building widgets, you have a development and testing stage, but once the widget is made available to the public, every change—whether it’s a bug fix or a new feature—is done live. While this approach is not uncommon, it requires caution.
When you create a new widget, its status is "in development." This status will appear as a prefix at the end of the widget name, for example, "My Widget (in Development)." During this stage, the widget can be developed and tested in preview.
If you attempt to view this widget in a staging or live environment, an error will indicate that the widget is not available.
Do you have some spare time to enhance your widget by adding some JavaScript? While this is not mandatory for a functional widget, it can elevate the professionalism of your widget and, in some cases, improve the user experience (UX) in the Template Editor by providing visual feedback to users.
Please note that this code runs only within the Template Editor in the Modeler, not on a page in a previewed or published WEM runtime portal.
Let's update our "Information Card" widget with a Template Editor script.
In previous chapters, we used events to refresh the page without writing any code for it, and to toggle between the collapsed and expanded states of our message box widget. In this chapter, we will create our own button and add different types of actions to it.
First, create a new widget and name it "Button." Use the following script:
Next, add a dropdown property called "Action" with the following options:
WEMscript is a statically strongly typed language. Before you can use a variable, it must be declared using the var statement, with a specific type either explicitly set or inferred from its value. Every variable is prefixed with an @ symbol. A variable's value is unknown when no value has been assigned yet.
The following types are available in WEMscript: boolean, concept, conceptset, datetime, duration, file, list,
Events also allow for the manipulation of the row position within a list. This functionality is similar to using the List node in the flowchart editor; however, not all features available in the List node are supported.
The basic syntax is as follows:
The syntax is straightforward, so we won't go into detail about what each command does. However, as mentioned earlier, not every feature is available out of the box. For instance, if we want to create an event that navigates to a specific row, we can program it as follows: we create a numeric view state called "RowIndex" to store the current row index. Note that in this example, the indices start at 1, with the first row being index 1. Fortunately, the code is quite simple:
One important aspect of a widget property is that it is read-only in WEMscript. To assign a new value to a property, you need to perform a postback. Let's look at some example code, assuming we have created a literal text widget property called @Name.
To assign a new value to the property, you need an HTML form. After a WEM event, the property is set to its new value if it has been correctly validated. Here is a short example of how this looks:
In the next chapters, we will provide a more in-depth explanation of what the statement register input and the function OutputId() do.
Once you are absolutely sure that the widget is complete, you can make it available. To do this, right-click on the widget in the tree to open the context menu, and select the option "Make Available." A message box will pop up to confirm your decision.
If you publish to staging or live now, you will see that the widget is displayed.
Once a widget is available, every portal that uses this widget will be published with the latest version. This can make updating the widget challenging. Non-breaking changes, such as bug fixes, are generally manageable. However, implementing new features can be difficult. In such cases, it may be wise to create a new widget.
If a significant update is needed for a publicly available widget, I recommend making a copy of the widget by exporting it and then importing it with a new name. You can then develop and test this new widget. Once it is finished, you can carefully update the original widget.
Once a widget is available to the public, it can be flagged as deprecated if necessary. You can deprecate a widget by right-clicking on its name in the left tree view to open the context menu and selecting the "Deprecate" option.
The already published portals that are using this widget are not immediately affected by its deprecation. However, if a portal is published using a deprecated widget, it will display an error message indicating that the widget is not available.
Finally, we will take a closer look at WEMscript. Although you have encountered some code previously, this time we will explore it in greater detail. WEMscript is a standalone module that acts as the glue connecting all the other components of a widget, and we will utilize these components throughout this module.
You have learned the basics of the concepts involved in building a widget. While we haven't gone in-depth yet, we have only scratched the surface. However, I hope you now have a foundational understanding of what it takes to create basic widgets. I also hope I didn’t scare you off with the last chapter. With almost daily news about data breaches and computers being compromised by ransomware, it is crucial to prioritize security in programming. While programming with security in mind has always been important, it is even more essential in today’s world.
Select the "Templates, widgets, files, and hyperlinks" tab in the project tree.
Right-click on "Widget libraries" to display the context menu.
Click on "Open widget editor."
To create a widget, we first need to create a widget library:
Navigate to your workspace in the widget editor tree.
Right-click on your workspace to display the context menu.
Click on "Create library."
Let's name the library "Widget Academy."
Now let's create the widget:
Right-click on "Widget Academy" to display the context menu.
Click on "Create widget."
Name the widget "Hello, World!"
Double-click on the newly created widget to open it.
Click on the Script tab.
Write the following HTML code in the code editor:
Press Save.
Congratulations! You have created your first widget! But how do you see it in action? To view it, we need to add the widget library to our project. Once we do that, we can add the widget to the template in the template editor.
Select the "Templates, widgets, files, and hyperlinks" tab in the project tree.
Right-click on "Widget libraries" to display the context menu.
Click on "Add widget library."
Select the "Widget Academy" library.
Press OK.
Now, let's place the widget on a template:
Create a new flowchart.
Create a new interaction node.
Drag and drop the "Custom" ribbon item (the one with the rocket) at the top of the template.
Expand the "Widget Academy" library.
Select the "Hello, World!" widget.
Press Select.
Save the template.
Now, preview this interaction node, and you will be greeted with the words:
"Hello, World!"
This widget may not be anything special. We didn't even write any WEMscript, nor did we discuss binding with the WEM data model. I skipped a lot of details. But if you’ve made it this far, then let the fun begin!
<img src="https://some-website.com/images/funny-cat.jpg">10 PRINT "Hello, World!"
20 GOTO 10Hello, Vincent!
Hello, Vincent!
Hello, Vincent!
Hello, Vincent!
Hello, Vincent!
Hello, Vincent!
Hello, Vincent!
...numberrichtexttextarray<T>Tarray/* Declare a boolean variable @a with an initial value of `unknownboolean` */
var @a: boolean
/* Declare a number variable @b with an initial value of `10` */
var @b := 10
/* Assign the variable @c the text "Hello" */
@c := "Hello"goto first row of @List
goto next row of @List
goto previous row of @List
goto last row of @Listgoto first row of @List
var @i := 1
while @i < @RowIndex
goto next row of @List
@i := @i + 1
end/* Reading the value from the property is fine */
var @uppercasedName = ToUpper(@Name)
/* However, assigning a new value will throw an error */
@Name := "Jane Doe"<? register input @Name ?>
<input type="text" name="<?attr OutputId(@Name) ?>" value="<?attr @Name ?>">Now that we have created a simple form, we can make the fields required. How does that work? It’s actually quite simple. We add the required property to a property when using register input. Now, when a WEM runtime event is triggered—such as a button click—and a property does not have a value, that event is ignored, and the WEM runtime remains on the same page.
Let's update our previous form script:
<?
register input @FirstName required = true
register input @LastName required = true
?>
<? if @ShowErrors and IsEmpty(@FirstName) ?>
<div class="alert alert-warning">Please fill in your first name.</div>
<? end ?>
<label>
First name:
<input type="text" name="<?attr OutputId(@FirstName) ?>" value="<?attr @FirstName ?>">
</label>
<? if @ShowErrors and IsEmpty(@LastName) ?>
<div class="alert alert-warning">Please fill in your last name.</div>
<? end ?>
<label>
Last name:
<input type="text" name="<?attr OutputId(@LastName) ?>" value="<?attr @LastName ?>">
</label>
<?
@ShowErrors := true
?>Although it is not part of this chapter, we have added a @ShowErrors boolean view state field. This is to prevent errors from being displayed when the widget is first rendered, enhancing the user experience. We don’t want to show errors indicating that specific fields need to be filled in upon first viewing. We take advantage of the fact that, during the initialization of the widget, the view state properties are set to the value unknown.
Other than adding the required option to the register input, we didn’t do anything special. Fortunately, some features don’t have to be difficult to implement.
It is important to inform the WEM runtime that we want to run a specific event by registering it. The register event statement does exactly that, and it is mandatory for an event to function properly. Based on the previous chapters, the second line should now be clear regarding its purpose. The third line is interesting and requires some explanation.
Before I explain that, let's create the refresh screen event first:
Select the Events tab.
Click on "New event."
Name it "RefreshScreen."
And that's it! There is no need to write any event code for a refresh screen event. We will delve deeper into this in later chapters. Now, let's continue with the interesting part on line 3:
A veteran web developer will recognize the onclick attribute. What is interesting is the <?attr, which I briefly explained in previous chapters; it is a shorthand for HTML attribute encoding the output. This is important because, without it, we could potentially create invalid HTML. The @RefreshScreen is another interesting aspect; it is not a number variable, a boolean, or text, but rather an event. You may wonder how you can output an event. What gets output is JavaScript code that will trigger the @RefreshScreen event, and it will look something like this:
Don't worry; you never have to call the WEM Runtime API directly, as it is abstracted from the widget developer. This is just to illustrate what is happening under the hood. Note that it does not matter where you trigger an event; it could also be in an ondblclick attribute, an onblur event, or even manually called in JavaScript.
Give it a try! Add the widget to any template and see if luck is on your side!
We've only just begun to explore what you can do with events. In the following chapters, we will delve deeper into the topic.
<? register event @RefreshScreen ?>
<div>The six-sided die rolled: <?= Random(1, 6) ?></div>
<div><button onclick="<?attr @RefreshScreen ?>">Roll again</button></div>Copy and paste the following code:
Don't forget to save your changes!
This code should be quite familiar to JavaScript veterans. Notice that we use utility.createElement instead of document.createElement to create our HTMLElement. This is because we provide only a subset of the JavaScript API, just enough to create a preview. We will discuss this in more detail in later chapters.
If you still have the template open where this widget was placed, you should already see the changes from the default appearance to our custom preview. If not, please open the template to view the updated design.
const placeholderElement = utility.createPlaceholderElement(placeholders.Contents.name);
const cardElement = utility.createElement("div");
cardElement.style.backgroundColor = "#d1ecf1";
cardElement.style.border = "1px solid #aadce5";
cardElement.style.borderRadius = "4px";
cardElement.style.padding = "8px";
cardElement.append(placeholderElement);
return cardElement;Follow button exit
follow
Then, add three additional properties called "Flowchart," "NavigationItem," and "ButtonExit," using the same property type as their names. Do you remember the "Visible when" property explained previously? Click on the "ButtonExit" property and change the "Visible when" setting from "Always visible" to "Action." Note that "Action" refers to the dropdown property we created with selectable options. For "ButtonExit," check the "follow" checkbox. Repeat this process for the other two properties. You will see this functionality in action once we display this widget in the template editor.
Now, create an event called "OnClick" and use the following example code:
Three new statements are introduced here: execute, navigate, and follow. By now, you should be familiar with WEM terminology. If you read the code, you will find that these statements do exactly what you expect, and they should be self-explanatory.
Place the widget to a template and test it out.
<? register event @OnClick ?>
<button class="btn btn-default" onclick="<?attr @OnClick ?>">Click me</button>Refresh screen
refresh
Execute flowchart
execute
Navigate to
navigate
if @Action = "execute"
execute @Flowchart
elseif @Action = "navigate"
navigate to @NavigationItem
elseif @Action = "follow"
follow @ButtonExit
end<h2>Hello, World!</h2>onclick="<?attr @RefreshScreen ?>"<button onclick="Runtime.raiseEvent(...)">Roll again</button>The teaching of Less and CSS is outside the scope of this academy. However, there are a few important points to keep in mind. Most of the concepts I explain here apply not only to WEM widgets but also to web development in general.
When you publish or preview a WEM application, the Less styles from all the widgets are combined into a single CSS file. This CSS file is then included in the design template. This presents a significant issue: all the CSS exists in a global space. I briefly discussed this in previous chapters. As we learned, if you are not careful, you could inadvertently overwrite the styles of other widgets or any HTML element, for that matter! For example:
/* Bob's card widget */
.card {
background: tomato;
}In this case, both Bob and Jane have created a card widget using the class name "card." Even if Jane's widget is not used on a page, if Bob's widget is included, it may appear to have a strange yellow background. This can lead to confusion and blame for issues that are not actually related to Bob's widget. It is advisable to make class names more unique. You can add prefixes to the class names, such as your initials, your company name, or both, like this:
When WEM supported widgets, it included Less by default. Less is powerful because it supports variables, which makes styling more organized and easier to maintain. Nowadays, CSS variables are supported in all major browsers and offer even greater capabilities. You can alter the value of a CSS variable in real time, creating new opportunities for styling your widgets. You can still use Less variables as a base. For example, consider the following Less code:
However, if you save this code, you will encounter an error message indicating a syntax error on the line setting the border property.
What is happening here? This is because Less has its own syntax and attempts to parse the CSS value as a Less expression. We can resolve this issue by using the escape syntax ~"value". If we modify the line as follows:
This change makes the code valid. While it may not be ideal, it is better than nothing.
You may be wondering how to embed a resource in Less. The short answer is that you cannot do it directly. The following code, which uses a made-up WEM style syntax, will not work:
To achieve this, you need to revert to using the <style> tag or apply it via JavaScript.
If you are familiar with the Modeler and creating page templates, then you are probably acquainted with components such as Alert, Panel, Conditional, and others. To create a similar widget, we need an additional feature called Placeholders. This feature allows you to reserve a section in your widget for content that you can add in the Template Editor within the Modeler. Let's create a simplified Alert component to explain the concept of placeholders.
Create a new widget called "Information Card."
Select the Placeholders tab.
Click on "New placeholder."
Name it "Contents."
You can provide a label and a description if you wish; these will be displayed in the Template Editor.
Copy and paste the following script:
To keep this chapter simple, we are reusing the Alert Bootstrap classes. Other than that, there is one new statement here called render. As the name implies, this statement will render the contents of the @Contents placeholder. And that's it!
Now, place this widget on a template and notice that there is an additional container that was not previously shown in the other widgets we created. Try dragging some components, adding some text, or including other widgets in this container.
As you can see, placeholders are very easy to implement and incredibly powerful.
A veteran web developer knows that there are multiple ways to style HTML elements. For example, this can be done statically by including a <link rel="stylesheet" href="style.css">, or dynamically via JavaScript with element.style.backgroundColor = "tomato", to name a few. Yes! "Tomato" is, unironically, the best red color there is, in my personal opinion!
One way to style widgets is by using the Less editor under the Styles tab. Less is a CSS preprocessor that provides additional functionality on top of CSS. If you prefer not to write in Less, you can still write standard CSS without using any Less features.
If you do want to write in Less, you will also have access to built-in variables from Bootstrap 3, which can be very handy if you want to customize some of their components in a widget. You can find these variables in the right-side panel of the screen.
In this chapter, we are going to write a simple message box. If you are familiar with Bootstrap 3, you know they have an Alert component available out of the box, and we provide that as one of our base components. However, for the sake of this exercise, we will write one from scratch.
Create a new widget called "Message" in the "Widget Academy" library.
Copy and paste the following script:
Create a Literal Text Property called "Text" and place it in the General section.
Create a Dropdown Property called "Style" and place it in the Appearance section, with the following labels: "Info," "Success," and "Danger," and the corresponding values: "info," "success," and "danger." Note that the values are case-sensitive and should be in lowercase!
Finally, click on the Styles tab and copy and paste the following Less code:
Note that we are using the predefined Bootstrap branding color variables to ensure this widget adopts the color styles of the selected design template in a portal, making it look nice across various design templates. Additionally, we use two Less color functions: lighten() and darken() to differentiate the branding colors from the background, border, and text color of the message box.
My guess is that you are eager to build a widget on your own, and your fingers are itching to write some code! But as the great Uncle Ben from Peter Parker famously said, "With great power comes great responsibility." While this quote was ironically popularized by the movie Spider-Man, it holds true nonetheless. So, sit tight, and let’s have a candid discussion about the risks and responsibilities involved.
WEM is a no-code platform that allows you to create applications ten times faster than traditional methods. We are proud to share customer stories that prove this. With the development of AI, it’s not unreasonable to think that we will be able to build apps even faster. However, what people often overlook is that WEM implements numerous safety precautions, enabling you to build without worry while it handles the hard work in the background, allowing you to sleep soundly at night.
I’m referring to safety on a technical level. I’m not suggesting that WEM guarantees you will never create unsafe applications. For instance, someone might forget to add authorization to a sensitive page that displays client information openly. This is a user error that WEM cannot prevent. WEM provides you with tools built on universal standards to implement authorization, but it cannot ensure that you will implement it correctly. Think of WEM as a car equipped with bulletproof glass; it offers protection, but if a reckless driver looks too deeply into the glass, don’t be surprised if the car comes home with bumps and scratches.
"What does this have to do with widgets?" With widgets, you gain more control—essentially, more power. And you know what Uncle Ben said? Building widgets requires not only extensive knowledge of HTML, CSS, and JavaScript but also an understanding of how the WEM Runtime works. It’s a lot to take in, and if you’re not careful, you might inadvertently introduce XSS (cross-site scripting) vulnerabilities into your widget without even realizing it.
I take my job as a programmer very seriously, and you should too. I see too many people copying code from the internet and pasting it without understanding what it does. "It works, so it must be fine, right?" So, I want to ask you a question: Are you a good programmer, or a good Googler?
Do you know the dangers of using innerHTML? It seems safe to use, right? After all, any <script> tags are ignored, so it should be perfectly safe. Well, not quite.
Here’s something interesting. Do you know what the following CSS does?
That’s right! This is a stripped-down version of a keylogger written in CSS! What about this one?
Yes, if you are creative enough, you can even execute scripts without using <script> blocks. In the example above, because the image "x" does not exist, the onerror event will be triggered. So even if innerHTML ignores <script> blocks, there are still ways to execute all kinds of malicious actions.
From this point forward, a strong knowledge of HTML, CSS, and JavaScript is essential. I’m going to be more strict this time. If you don’t understand the dangers of using innerHTML, you should. If you don’t know why it’s important to encode output, you should. We are going to delve deeper and get more technical, so strap yourself in a little tighter, and let’s go!
In the previous module of the view state chapter on building the Message Widget, we declared a field and created an event to toggle that field, allowing the message box to expand and collapse. Another way to change the view state is through JavaScript. Let's update our previous implementation of the message box to utilize JavaScript instead.
First, we need to add the Text text property and the Style dropdown property, which should include the values "info," "success," and "danger." You can refer to the previous view state chapter for guidance on how we created these properties. Next, we will create a boolean view state field named Collapsed.
Use the following script:
What has changed is that there is no register event because we don't have an event in this widget, nor do we need @Collapsed := @Collapsed ? false to initialize that field to false when it is unknown. This works in this case because when we check with if @Collapsed then, it does not matter whether the value is unknown or false.
We introduce a new function, OutputId(). Every widget has its own Output ID, which serves as a unique identifier on the page. This is perfect for use in conjunction with the id attribute, which should also be unique! We will discuss this function in more detail in later chapters.
We changed the onclick attribute to execute the toggleMessageBox() function instead of triggering an event, as we did in the previous chapter. A new feature is that this function is declared inside a scriptmodule, which is another way to add JavaScript functionality to the widget, aside from including it via a file. We will cover this topic in more detail in later chapters.
The toggleMessageBox(element: HTMLElement) function is straightforward. Depending on the state, we toggle between the two classes. We retrieve the Output ID from element.id and check if it is collapsed by verifying if the element's class list contains the "collapsed" class. The Runtime.viewState(outputId: string) function returns the view state of the widget with that Output ID. We use the set(key: string, value: any) function, where the key is the name of the view state field, to set a value for that field. There are two additional functions besides set(): get(key: string) and clear(key?: string). These functions are self-explanatory, but you can find more information about them in the WEM widget reference.
The styles remain unchanged and are the same as before:
Now, place this widget on a template and add a refresh button to the page. Preview the widget. Notice that when you click the widget, there is no refresh, unlike in the previous implementation. This creates a better user experience. However, there is one downside: the view state is only persisted when we perform a postback to the WEM Runtime, such as when clicking the refresh button.
Try toggling the state of the widget, and then press F5 to refresh the page in your browser. You will notice that the widget retains its previous state. Now, toggle the state of the widget again and click the refresh button we added to the page. This time, the state is saved.
In the next chapter, we will explore a more modern approach to storing view state using localStorage and sessionStorage. This method addresses the refresh issue but comes with its own set of challenges.
On this page, you will find example widgets that are created as you follow the chapters. You can download the widgets and then import them into your own library to take a look. You can even use and modify them to your liking if you wish.
Please note that the widgets created in the widget chapters are being updated to comply with CSP (Content Security Policy) standards. They are also being modified to prevent any collisions in the global scope.
Below are the widgets that you will create as you follow the basics module.
A simple widget that demonstrates the use of the text property, HTML-encoded to output a name.
A widget that simulates a six-sided die. To ensure that this widget is CSP (Content Security Policy) compliant, it utilizes the startupscript to add a click listener to the "Roll Again" button, which refreshes the page.
This widget is similar to the Alert component in Bootstrap. You can expand or collapse the message by clicking on it, demonstrating how the WEM view state fields functions.
This widget functions similarly to Example 1, but it utilizes the WEM Runtime.viewState JavaScript API instead.
This message widget uses sessionStorage to store the view state and employs an ES module that utilizes Shadow DOM to prevent global scope collisions.
This widget demonstrates how to use the import() function to dynamically load script modules, preventing collisions in the global scope. It also utilizes the Shadow DOM to style the widget, further avoiding CSS conflicts.
This widget demonstrates how easy it is to implement placeholders and highlights their powerful capabilities. It also provides an example of how to create a Template Editor Script.
More coming soon.
Manipulating the view state via JavaScript can be quite overwhelming. The following table illustrates the differences between various scenarios.
The table below assumes that some view state has been set via JavaScript after a page load to a view state field.
In our previous chapters, we created a simple static widget. Now, let's make it a little more dynamic by adding properties to the widget. These properties are similar to any other property you see in the user interface of the modeler, such as the Name of an interaction node, the Validation of a text data field, or an Action on an assignment node.
There is one important aspect to address before concluding this chapter. When you use widgets in a WEM project, the WEM runtime will render these widgets within the page, and the styles and JavaScript associated with the widget will be added in a global context. Therefore, you must be careful when naming your CSS selectors and JavaScript identifiers, just as you would when building a standard website.
While there are modern solutions to this issue, WEM does not yet support all of them. In later chapters, we will explore these solutions in greater detail. For now, let's take a brief look at the potential issues.
In CSS, naming collisions occur when two or more styles are defined with the same selector. This can lead to unexpected results, potentially overriding earlier styles. For example, consider the following:
Design systems like Bootstrap often use simple class names such as .btn
A richtext type variable is used to represent and manipulate a sequence of characters that holds rich content.
When you concatenate a text value to a richtext variable, implicit encoding is applied to the text value. This is not always desired; therefore, the ToRichText() function can be used. Let’s dissect the example above.
This line declares the variable @rt as richtext
The concept is a special kind of type. The values of a concept are not known within the context of a widget. However, if you create a generic widget, you don't need to know them. Let’s take a look at the following WEMscript to illustrate this.
Throughout the examples, we will assume we have a concept data model property called ConceptProperty.
Notice that everything works except for the assignment of the 'Colors'.'Red' concept literal, which is unknown in the context of a widget and only recognized within a project. To assign a concept data model property a new value, we use arbitrary IDs by calling the ConceptId() function. Fortunately, these IDs are just integers and can be easily passed around in the code.
Now, let’s look at the following dropdown example widget that uses a range of concepts to set the @ConceptProperty:
A text type variable is used to represent and manipulate a sequence of characters.
Writing a widget that uses a text property is quite straightforward and is likely the easiest type to implement.
You don't need to do anything special when using the text type in JavaScript, as it is similar to the String type in JavaScript.
Finally, we no longer have to worry about naming collisions and overwriting other code when using ES Modules. We can use generic class names like Button or Note without adding extra noise to our code, making it much more enjoyable to read and write. However, is there a similar solution for CSS, like CSS Modules? Unfortunately, no. Nevertheless, the W3C seems to be working on it.
This is unfortunate, as we have found a way to encapsulate JavaScript code from other widgets, but our CSS still lacks this capability. However, there is a solution: we can style elements programmatically within JavaScript. While you can style elements using element.style.propertyName, a better approach is to use Web Components with the Shadow DOM.
If we modify the code of our Note widget to utilize the Shadow DOM, it would look something like this:
In this code, we use CSSStyleSheet and call the replaceSync() method to set the CSS. As you can see, we use very generic class names like .message
/* Jane's card widget */
.card {
background: lightgoldenrodyellow;
}Yes
Yes
Removed when the widget is unloaded
Yes
Depends
Depends
Removed when the browser tab is closed
Yes
Yes
No
Removed when the browser is closed
Yes
Yes
No
Removed when your computer is hit by a meteor
Yes
Yes
Yes
In the case of a widget being unloaded, there are two scenarios where the outcome depends on specific conditions. As discussed in the previous chapter, we have the option to manually clear the state of localStorage and sessionStorage in the unloadscript block.
Persisted on browser refresh (F5)
No
Yes
Yes
Persisted on WEM application event
Yes
/* Bob's card widget */
.bobs-company-name-card {
background: tomato;
}<div class="alert alert-info">
<? render @Contents ?>
</div><div class="message <?attr @Style ?>"><?= @Text ?></div><style>
input[type="password"][value$="a"] { background: url("http://website.com/a"); }
input[type="password"][value$="b"] { background: url("http://website.com/b"); }
input[type="password"][value$="c"] { background: url("http://website.com/c"); }
/* ... */
input[type="password"][value$="z"] { background: url("http://website.com/z"); }
input[type="password"][value$="{"] { background: url("http://website.com/%7B"); }
input[type="password"][value$="|"] { background: url("http://website.com/%7C"); }
/* etc */
</style><img src="x" onerror="console.log('Hey look mom! No <script> block.')">.wem-academy-note > .messageThe final step is to create the Shadow DOM and append our CSSStyleSheet instance to it. We accomplish this with the attachShadow() method available on an HTMLElement. This method returns a ShadowRoot, which is a special type of DOM node but provides the familiar DOM manipulation methods, such as shadow.append(handleBarEl, this.messageEl), which we use to append the handle bar and message box. Next, we use shadow.adoptedStyleSheets.push(sheet) to attach the CSS to this Shadow DOM.
That's about it! You can see that there are several ways to style elements, each with its pros and cons. It is up to you to choose the one that best fits your needs. Hopefully, the W3C will expedite the development of CSS Modules, as that would seem like the optimal solution.
Let's create a widget that provides a more personal greeting:
Inside the "Widget Academy" library, create a new widget and name it "Hello, You!"
Select the Scripts tab and paste the following code:
There are a few new things happening here, so let’s unpack them.
The <? ?> symbols denote code blocks, where you write WEMscript. The code within these blocks is executed server-side and is not included in the output. In the first line, we declare a variable using the var statement. For those familiar with WEM expressions, you’ll notice that we convert the name to uppercase using ToUpper(@Name). Yes! In WEMscript, you can also write WEM expressions! In the second line, we use the print statement to output the contents of the variable @uppercasedName. Notice the html argument provided to the print statement; this will HTML encode the output. For example, < will be output as <. If we don't HTML encode it, we risk breaking the HTML or, even worse, allowing a hacker the opportunity to inject malicious code, something we will discuss in depth in later chapters.
You may be wondering if writing <? print html ... ?> will become tedious over time. Fortunately, WEMscript has a shorthand for that: <?= @uppercasedName ?>. Our updated code now looks like this:
This is much shorter and more convenient! There are also other shorthand notations, such as <?js for JavaScript and <?attr for HTML attribute encoding, which will be discussed in later chapters.
Now, you may notice that when you validate the widget (try it now by pressing the validate button next to the save button), an error will appear: "An unknown variable is used." This is because we have not yet added @Name as a property.
To add the property:
Select the Properties tab.
Click on "New property" in the toolbar.
Name the property "Name."
Set the property type to "Text."
Set the input type to "Literal."
Set the property group to "General."
We will go into much more detail about the different property types and settings in later chapters. For now, we are ready to test our new widget. Place this widget on a template, select it, and you will notice that a Name property is visible in the right panel. Fill in the property with a value, such as your name, save the template, and preview it.
.alertWhen using JavaScript in your widget, you must be cautious when naming your functions, classes, and variables. If you are not careful, you may inadvertently override them, leading to undefined behavior in the widget's functionality. For instance:
In this example, clicking Bob's button will log "Jane here!" to the console. While this may seem like a harmless example, it is not the expected behavior. There are multiple ways to address this issue, but they are beyond the scope of this chapter.
The scriptreference statement is also susceptible to naming collisions. Consider the following example:
It is important to note that it does not matter that both use @ButtonJs as the same reference name, as this is a local variable of the widget. However, using "button" as a namespace is too generic. In this case, it will not override other functionalities but will instead use the script that has already been registered under the name "button". Another statement that has the same issue is called scriptmodule, which we will discuss in later chapters.
There are various ways to address these issues, with some solutions being better than others. One simple approach is to avoid using generic names like button. Instead, use a format like yourcompanyname-componentname to make it less generic. In CSS, you would have something like:
In a scriptreference, you would write:
This is just one of the possible solutions, and there are many more depending on the context. I have also omitted a solution for JavaScript, as that topic requires a dedicated section of its own, which we will explore in later chapters.
For now, it is essential to remember that you should avoid using generic names unless you are absolutely certain of what you are doing.
Now that you have learned the basics of building a widget, we also need to discuss the risks and responsibilities involved in this process. The next chapter will be serious but very important!
The input type determines how we set the value of the property. There are three methods to do this. Note that there is only one way to write to a property, which is via the Data Model.
Literal
A value that is set statically.
No
Data Model
A value that is set by binding to a data model item, which can be either a list or a field, depending on the property type that has been set. The field must also be writable.
Depends
Expression
A value that is set by a WEM expression.
No
This property indicates which group the property should be rendered in within the template editor. It does not affect the functionality of the property.
The file property has an additional attribute for validation. You can leave it blank to accept any file type.
The dropdown has an extra property called Dropdown Options. The options consist of a tuple of a label and a value. The labels are used to display the options in the template editor, while the values are used in WEMscript and are of the text type. In technical terms, a dropdown is a static set of text enums.
Let’s assume a dropdown property called @StyleContext with the following set of options for UI context styles:
Success
success
Info
info
Warning
warning
Danger
danger
The value of the dropdown is simply a text value. Here’s an example of how to use it:
The navigation types include Button Exit, Flowchart, Hyperlink, and Navigation Item. These are always set statically.
This is the initial value of the property. If it is not set, the initial value will be unknown.
This property is only available when the input type is Data Model. If you want to write values back to the data model, this should be set to true. The property becomes stricter when it is writable; you cannot use calculated fields.
This property determines when the property should be visible in the template editor. It works in conjunction with literal boolean and dropdown properties. You will notice that if you have any of those as properties for a widget, the "Visible When" dropdown is populated with those properties. The "Visible When" feature is useful for hiding properties that are not required in certain contexts.
Since the left-hand side @rt is of type richtext and the expression on the right-hand side is a literal text, the text is treated as richtext. The value of the variable @rt is now <strong>This text is very.
Once again, the left-hand side @rt is of type richtext, but the expression on the right-hand side is a concatenation of richtext + richtext, resulting in the value <strong>This text is very strong</strong>.
What happens if we don't use the ToRichText() function?
In this case, the expression is of richtext + text. When evaluating the right text operand, it will be coerced into richtext. During this conversion, the text value will be HTML encoded, resulting in <strong>This text is very strong</strong>. This is not what we wanted in this example, as it generates malformed HTML.
So, be cautious when concatenating with richtext.
Fortunately, performing a postback with richtext is quite straightforward. You don't have to deal with localization, and updating the property with a new value is simply a matter of sending it back as is.
Since we are dealing with richtext, the encoding is mostly ignored, except for JavaScript encoding. Let’s assume we have a variable var @rt: richtext that holds the value <p>I am a paragraph.</p>. The following encoding (or lack thereof) will produce the corresponding outputs:
<?= @rt ?>
<p>I am a paragraph.</p>
<?attr @rt ?>
<p>I am a paragraph.</p>
<?js @rt ?>
"<p>I am a paragraph.</p>"
<?raw @rt ?>
<p>I am a paragraph.</p>
In the above example, we have created a writable concept data model property called @ConceptProperty. The context of this concept is unknown to us; it could represent a range of colors, animals, or the status of a ticket. However, in this case, we do not need to know the specific context. We obtain the range of @ConceptProperty using the range of keyword and enumerate through it with the loop. We create the <option> elements where the values are generated using the ConceptId(concept) function, with concept being a special keyword that refers to the current enumerated concept within the range of @ConceptProperty. By comparing concept with @ConceptProperty, we can determine if the latter holds the value of concept, allowing us to select the appropriate option.
var @c: concept
/* Assign @c with the concept value that is inside the property @ConceptProperty */
@c := @ConceptProperty
/* This will throw an error: 'Colors'.'Red' is an unknown concept in the context of a widget */
@c := 'Colors'.'Red'
/* Executing concept-specific functions is allowed */
var @d := Description(@Color)
var @s := ToString(@Color)
var @i := ConceptId(@ConceptProperty)Depending on the encoding format, different outputs are generated. Let's assume a variable var @s: text holds the value ">>> Stan Laurel & Oliver Hardy <<<". The following outputs will be generated:
<?= @s ?>
>>> Stan Laurel & Oliver Hardy <<<
<?attr @s ?>
>>> Stan Laurel & Oliver Hardy <<<
<?js @s ?>
">>> Stan Laurel & Oliver Hardy <<<"
<?raw @s ?>
>>> Stan Laurel & Oliver Hardy <<<
/* Jane's card widget */
.janes-company-name-card {
background: lightgoldenrodyellow;
}body {
--card-color: @brand-danger;
}
.janes-company-name-card {
background: var(--card-color);
border: 1px solid hsl(from var(--card-color) h s calc(l - 10));
color: white;
padding: 8px;
}.janes-company-name-card {
border: ~"1px solid hsl(from var(--card-color) h s calc(l - 10))";
}.company-logo {
/* This does not work. */
background: <?less FileUrl(@CompanyLogo) ?>;
}.message {
border: 1px solid transparent;
border-radius: 4px;
padding: 15px;
&.danger {
background: lighten(@brand-danger, 25%);
border-color: lighten(@brand-danger, 10%);
color: darken(@brand-danger, 23%);
}
&.info {
background: lighten(@brand-info, 25%);
border-color: lighten(@brand-info, 10%);
color: darken(@brand-info, 23%);
}
&.success {
background: lighten(@brand-success, 25%);
border-color: lighten(@brand-success, 10%);
color: darken(@brand-success, 23%);
}
}<div id="<?attr OutputId() ?>" class="message <?attr @Style ?> <?attr if @Collapsed then "collapsed" else "expanded" ?>" onclick="toggleMessageBox(this)">
<span class="box ellipses">
<span class="glyphicon glyphicon-comment"></span>
...
</span>
<span class="box text">
<span class="glyphicon glyphicon-heart-empty"></span>
<?= @Text ?>
</span>
</div>
<? scriptmodule "message-box" ?>
function toggleMessageBox(element) {
element.classList.toggle("collapsed");
element.classList.toggle("expanded");
const outputId = element.id;
const collapsed = element.classList.contains("collapsed");
Runtime.viewState(outputId).set("Collapsed", collapsed);
}
<? end ?>.message {
display: flex;
> .box {
border: 1px solid transparent;
border-radius: 4px;
cursor: pointer;
}
&.danger > .box {
background: lighten(@brand-danger, 25%);
border-color: lighten(@brand-danger, 10%);
color: darken(@brand-danger, 23%);
}
&.info > .box {
background: lighten(@brand-info, 25%);
border-color: lighten(@brand-info, 10%);
color: darken(@brand-info, 23%);
}
&.success > .box {
background: lighten(@brand-success, 25%);
border-color: lighten(@brand-success, 10%);
color: darken(@brand-success, 23%);
}
&.collapsed > .box {
padding: 5px;
&.text {
display: none;
}
}
&.expanded > .box {
padding: 15px;
&.ellipses {
display: none;
}
}
}class Note {
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;
const sheet = new CSSStyleSheet();
sheet.replaceSync(`
:host {
background-color: lightgoldenrodyellow;
display: flex;
flex-direction: column;
position: fixed;
left: calc(var(--x) * 1px);
top: calc(var(--y) * 1px);
width: 250px;
height: 250px;
}
.handle-bar {
background-color: var(--handle-bar-color, tomato);
cursor: pointer;
flex: 0 0 20px;
}
.message {
flex: 1 0;
outline: none;
padding: 8px;
resize: none;
}
`);
this.rootEl = document.createElement("div");
const shadow = this.rootEl.attachShadow({ mode: "open" });
shadow.append(handleBarEl, this.messageEl);
shadow.adoptedStyleSheets.push(sheet);
this.updatePosition(x, y);
}
}<? var @uppercasedName := ToUpper(@Name) ?>
<h2>Hello, <? print html @uppercasedName ?>!</h2><? var @uppercasedName := ToUpper(@Name) ?>
<h2>Hello, <?= @uppercasedName ?>!</h2>// Bob's button widget
.button {
background-color: tomato;
}// Jane's button widget
.button {
background-color: lightgoldenrodyellow;
}// Bob's button widget
function clickHandler() {
console.log("My name is Bob.");
}// Jane's button widget
function clickHandler() {
console.log("Jane here!");
}<?
/* In Bob's button widget */
scriptreference "button" FileUrl(@ButtonJs)
?><?
/* In Jane's button widget */
scriptreference "button" FileUrl(@ButtonJs)
?>.janescompany-button {
background-color: tomato;
}<? scriptreference "janescompany-button" FileUrl(@ButtonJs) ?>if @StyleContext = "success"
/* Render success style */
elseif @StyleContext = "info"
/* Render info style */
elseif @StyleContext = "warning"
/* Render warning style */
else
/* Render danger style */
end<?
var @rt: richtext
@rt := "<strong>This text is very"
@rt := @rt + ToRichText(" strong</strong>")
?>var @rt: richtext@rt := "<strong>This text is very"@rt := @rt + ToRichText(" strong</strong>")@rt := @rt + " strong</strong>"<? register input @RichTextProperty ?>
<textarea name="<?attr OutputId(@RichTextProperty) ?>"><?= @RichTextProperty ?></textarea><? register input @ConceptProperty ?>
<select name="<?attr OutputId(@ConceptProperty) ?>">
<? loop range of @ConceptProperty ?>
<option value="<?attr ConceptId(concept) ?>" <? if concept = @ConceptProperty ?>selected<? end ?>>
<?= concept ?>
</option>
<? end ?>
</select><?
var @t: text
@t := "Hello"
@t := @t + " there."
@t := ToUpper(@t)
?><? register input @TextProperty ?>
<input type="text" name="<?attr OutputId(@TextProperty) ?>" value="<?attr @TextProperty ?>"><? register input @TextProperty ?>
<input type="hidden" id="<?attr OutputId(@TextProperty) ?>" name="<?attr OutputId(@TextProperty) ?>">
<? startupscript ?>
// It is important to use 'js' encoding when outputting the property value in JavaScript blocks.
let value = <?js @TextProperty ?>;
value = value.toUpperCase();
console.log("Result in uppercase:", value);
<? end ?>
<? submitscript ?>
const el = document.getElementById(<?js OutputId(@TextProperty) ?>);
// Performing a postback with the text property is straightforward.
el.value = "Hi, can you post me back please? ♡";
<? end ?>To maintain the state of the scroll position of an HTML element, the zoom position of a chart, or any state that is specific to the view when the widget needs to be redrawn, we can use the view state. This state is accessible when the widget is rendered on a template but is destroyed when the widget is no longer rendered. We will discuss this in more detail in later chapters. For now, let's extend our previous "Message" widget to include the ability to collapse and expand.
Copy and paste the following script:
Notice that we are registering an event again using register event @ToggleCollapseState. We will create an event later that will change the view state. In the second line, you will notice the @Collapsed variable. This is a view state variable (also called a view state field). We assign the @Collapsed variable to its original value, but when it is unknownboolean, we assign it the value false. This ensures that the toggle behavior functions correctly. More on that in the event we will create shortly.
To create the view state field:
Click the "View state" tab.
Click on "New field" in the toolbar.
Name it "Collapsed."
Select "Boolean" as the type.
Now let's create the event:
Create the "ToggleCollapseState" event.
Copy and paste the following event code:
Notice that if we did not assign @Collapsed to false when it was unknownboolean, this event would not work, because assigning a variable with not unknownboolean still results in unknownboolean.
The Less style of the widget has also been updated. Replace the existing style with the following:
Now, preview the widget, and you will see that it is clickable and toggles between an expanded and a collapsed state. Notice that if you refresh the page, the view state still exists. Maintaining the view state is essential for creating a good user experience (UX).
These are just the basics of view state. For instance, you can also manage view state via JavaScript using the WEM Runtime.viewState, or the native localStorage, and sessionStorage APIs, which we will explore in more detail in later chapters.
This function has multiple use cases and is essential for ensuring that the widget functions correctly. We previously used this function to create a unique identifier value for the id HTML attribute. Another important role of this function is to generate identifiers for HTML input fields, enabling two-way binding with writable data field properties of the widget. In other words, it facilitates sending data back.
Let's create a very simple form item to see it in action:
Create a new widget and name it "Simple Form Example."
Create two new data model text properties called "FirstName" and "LastName."
Make both properties writable and required.
Use the script below:
For those familiar with sending form data over the internet, there is nothing new here. Just like in regular HTML forms, we use the <input> element to edit and submit the data. One interesting aspect is the use of OutputId(@PropertyName) in the name attribute. We cannot simply use name="PropertyName" here, as that would lead to name collisions if there are multiple instances of this widget on the same page. In other words, OutputId(@PropertyName) ensures that we get a unique name for this property specific to each widget instance.
Next, we will write a more exciting widget: a note widget that you can drag around the screen. This will explain many concepts, especially those related to creating WEM widgets.
A boolean variable can be set using the keywords true, false, unknownboolean, or a WEM expression that returns a boolean type.
When performing a postback, it is essential to send the text "true", "false", or an empty string "" to update the property to the respective values of true, false, and unknownboolean. This requires careful attention when sending data back.
Let’s examine some examples and analyze them.
All examples feature a writable boolean data model property called BooleanProperty.
Aside from the poor user experience, this approach does not work because when the portal language is set to English, <?attr @BooleanProperty ?> will output "yes" when @BooleanProperty is true and "no" when it is false. These outputs are invalid boolean values for a postback. As a workaround, you could use <?js @BooleanProperty ?> instead, as it will output true, false, and unknownboolean as "true", "false", and "null" respectively. However, "null" is an invalid value, so it will be interpreted as unknownboolean. But please keep this workaround confidential.
Since we typically do not create a boolean text field, let’s explore more common boolean input fields.
In this example, we register the input @BooleanProperty and create three radio buttons with the text values "true", "false", and an empty string "" to set the value to unknownboolean. We check the current state of @BooleanProperty and add the checked attribute if it corresponds to the option value. Note that if @BooleanProperty is unknown, checking whether it is true will always return false, so we do not need to use IsKnown(@BooleanProperty) for the "true" and "false" options.
This checkbox will set @BooleanProperty to either true or unknownboolean. This behavior is due to how the browser sends post data. If the checkbox is not checked, the browser will ignore it, resulting in the property assigned to this checkbox being set to unknownboolean. This may not always be the desired outcome, so let’s look at an improved version to address this issue.
In this example, we introduce a hidden input field that will be used to send the data back, along with an intermediate checkbox to read the current checked state in the submitscript. Depending on whether the checkbox is checked, we set the hidden input field to "true" or "false".
Regardless of the type of widget you are building, even a simple boolean can become complicated if not carefully considered.
A datetime type represents a single moment in time in a platform-independent format.
When performing a postback, the WEM runtime expects the datetime to be in ISO date format. You can use the FormatDate() function with the Iso8601 constant as the second argument to convert the datetime to this format.
You can also utilize sessionStorage and localStorage, which are available in the JavaScript standard library. These solutions are purely JavaScript-based and do not rely on WEM view state internals.
Keep in mind that storing state in sessionStorage or localStorage is permanent throughout the browser session or on your local machine. This can be a useful feature. However, if you want to clean up the view state, you need to do that manually, which can be quite tricky to implement.
Creating a widget to upload a file is relatively straightforward. It can be as simple as the following two lines of code, along with including a @File data model property field:
However, it becomes a bit trickier when working with dynamically created binary data instead of a static file on the desktop. Nowadays, in JavaScript, you can only create a binary file programmatically because browsers have become more restricted due to security measures. Additionally, you can only assign a file to a file input through user interaction, such as a button click.
Let’s first look at how to do it the traditional way, and then follow that up with a more modern approach.
If you examine the code below, you may wonder where the base64 option in the register input comes into play. The reason for this is legacy. Previously, JavaScript did not have an out-of-the-box solution for creating native files; it only offered emulation using a string or an array of numbers. This is how WEM could provide a way to upload files in base64 format. Let’s examine our script below using base64 as an option:
Looks pretty cool, right? You simply encode the "Hello" text, and you're good to go. However, be aware that several issues can arise if you're not careful; we are using a simple "Hello" text as an example. Let’s walk through the process.
When we refer to a resource, we mean any type of file that will be embedded with the widget. This can include images, sounds, PDF documents, JavaScript, CSS, XML files, and more.
In this chapter, we are going to create a CSS file, a JavaScript file, embed a video, and add them as resources to our widget. Let's get started!
We have seen that if you are not careful, it can be very easy to overwrite classes and functions. Is there really no way to solve this? Yes! The solution lies in ES Modules. Unfortunately, WEM widgets were built before ES Modules became widely adopted. However, while it may not be perfect, using the building blocks that WEM provides can get you close enough if you know what you are doing. ES Modules is a feature in JavaScript that helps encapsulate code, making it possible to use imports and exports to manage sections of code. Let’s see how this can help us fix naming collisions once and for all!
Let’s imagine we have two separate JavaScript modules, each containing a function with the same name.
Normally, we would encounter a problem if we embedded these files using scriptreference. The first issue is naming collisions, and the second is that the export keyword is reserved for use in a module. To use both functions, we need to import them. There are two ways to do this: one is with <script type="module">, and the other is with the import() function. Unfortunately, while using the <script> tag is the easiest method, we cannot use it due to the way the WEM runtime handles loading dynamic scripts. Therefore, we are limited to using
A number type variable holds a numeric value in literal forms such as 255 or 3.14159.
To create a widget that performs a simple postback, you would write something like this:
In the example above, we created a simple text field that outputs the value of @NumberProperty. This example is culture-independent, and the WEM runtime checks the culture settings of the portal, using the appropriate number format for validation. For instance, if you set the culture settings to Dutch, the number will be formatted as
var @a: boolean
@a := true
@a := false
@a := unknownboolean
@a := 3.1415 <> 2.7182<?
register input @FirstName
register input @LastName
?>
<label>
First name:
<input type="text" name="<?attr OutputId(@FirstName) ?>" value="<?attr @FirstName ?>">
</label>
<label>
Last name:
<input type="text" name="<?attr OutputId(@LastName) ?>" value="<?attr @LastName ?>">
</label>2025-12-31
2025-12-31 23:59
2025-12-31 23:59:59
2025-12-31 23:59:59.999999
An optional "T" can be included between the date and time parts, as in "2025-12-31T23:59". Additionally, you can add timezone-specific parts such as "Z", "+2:00", or "-5:00", as in "2025-12-31T23:59-5:00". Be aware that if you include a timezone part, the WEM runtime will convert the time to local time. For example, a datetime of "2025-01-01T01:00:00-2:00" will be converted to "2025-01-01T03:00:00". You can test this with the text datetime input field widget below.
This is straightforward. The only requirement is to convert the datetime value of @DateTimeProperty into the correct format using FormatDate(@DateTimeProperty, Iso8601).
This example is almost identical to the text datetime input field example. The only difference is that we set type="datetime-local" on the input field. Since the browser adheres to the same datetime specifications, we can again use FormatDate(@DateTimeProperty, Iso8601).
Unfortunately, the browser is a bit stricter in this case. Since we do not need to consider time, we set type="date" on the input field. However, we cannot use Iso8601 as the second argument for FormatDate(). The reason is that the browser we tested does not accept a value that includes time. For instance, if you have value="2025-01-01T23:59:59", the entire value will be ignored. Fortunately, we can provide our own format in the FormatDate() function, as demonstrated in the widget example above: FormatDate(@DateProperty, "yyyy-MM-dd").
Using a datetime type within a JavaScript context is quite simple. By printing it with the JavaScript encode <?js, you will receive a new Date() object in return. For example, if we have a @DateTimeProperty and we want to log the year from that datetime in JavaScript:
This will result in:
Update the script with the following:
In this update, we introduce startupscript, which is a JavaScript code block that runs after the page has been initialized and all static HTML elements are rendered. There are others like these and we will discuss these script blocks in more detail in later chapters.
Comparing the previous code with the new version, you will notice that the initializeMessageBox() function is used to initialize the message box, as the name suggests. Here, we set the class names and retrieve the view state using sessionStorage through JavaScript. Additionally, we updated the code to set the click handler here instead of using an onclick attribute. While functionally there is no difference, this approach promotes better organization, a principle known in computer science as "separation of concerns."
Looking at the function getKeyMessageBox(outputId, nodeId) and the line const key = `${element.id}:${nodeId}:collapsed`; , we see that the key is created by combining the Output ID, a node ID, and an arbitrary identifier, separated by a colon. Since sessionStorage keys are stored globally, we need to ensure that the keys for this widget are globally unique. If we had simply used "collapsed" as the key and had two identical widgets on the same page, they would overwrite each other's state. The node ID ensures that the view state of this widget is unique per interaction node. While we use a colon here, any character that is not alphanumeric can be used, as those characters are reserved for the Output ID and node ID.
First, we add base64 = true to our register input, which is straightforward. However, note that we changed our input to a text field using <input type="text">. We are no longer working with files but rather with text that simulates a file. This can become cumbersome when dealing with large files, especially when using the btoa() function to encode the string into base64 format. Additionally, encoding data into base64 increases the size of the contents by approximately 35%.
Another important point is that we are using a string to hold our "Hello" data. In JavaScript, this string is in Unicode. However, Base64, by design, expects binary data as its input. In terms of JavaScript strings, this means that each character's code point should occupy only one byte. If you pass a string like "a Ā 𐀀 文 🦄" into btoa(), which contains characters that occupy more than one byte, you will encounter an error.
Finally, what happens when we call clickHandler() directly without user interaction? Since we are using a regular text field to set its contents with a string value, there is no error in that case.
What does a modern version look like without using the base64 option?
In this version, we have removed the base64 option from our register input, and we now have a <input type="file"> again. However, our JavaScript code now utilizes several new classes that we didn't have before, such as Uint8Array, Blob, File, and DataTransfer, each serving its own purpose. By reading the comments in the code above, it becomes clear how to programmatically create a file containing binary data and how to set the file input with that file.
<? register input @File ?>
<input type="file" name="<?attr OutputId(@File) ?>">Copy the following CSS content:
Paste it into your favorite text editor.
Save the file as "tickle.css" on your computer.
Copy the following JavaScript content:
Paste it into your text editor.
Save the file as "tickle.js."
Download the video "tickle.mp4" that we are going to embed here.
Now that we have all the resources locally on our computer, let's create the widget.
Create a new widget named "Tickle."
Select the Resources tab.
Click on "New resource."
Name it "TickleCss."
A resource has been created, but without any file attached to it. In this case, we need to upload our newly created "tickle.css" file.
Select the "TickleCss" resource if it is not already selected.
Click the "Browse" button in the details panel on the right.
Select the newly created "tickle.css" file on your computer.
Click "OK."
The last three steps may vary slightly depending on the browser you are using.
The file should now be uploaded, and under the "File" column in the resources overview, the cell should display "tickle.css."
Now, repeat the process to create two more resources for "tickle.js" and "tickle.mp4," naming them "TickleJs" and "TickleVideo," respectively.
Almost there!
Copy the HTML code below and paste it as a Script in the Widget Editor:
We introduce a new statement here called scriptreference, which requires two arguments. The first one should be a unique key (more on that in later chapters), and the second one is the URL of the resource. We also introduce the FileUrl function to retrieve the file URLs of all the resources.
Important Note: Every CSS and JavaScript file that you include in the widget will be globally interpreted in the browser. This means that if you are not careful, you could create CSS collisions or, even worse, overwrite JavaScript variables, functions, and classes! We will discuss this in more depth in later chapters.
For now, preview this widget, and hover over the video! :)
@idlooprange of @ConceptSetProperty@idConceptId(concept)@ConceptSetProperty contains concept<?= concept ?>In the background, during a postback, we send back a list of IDs that will update the data model bound to @ConceptSetProperty.
The following example demonstrates a less user-friendly UX for updating a concept set. However, it illustrates the concept of updating a concept data model with a postback using a single input field—in this case, a text field.
This widget renders all the concepts along with their concept IDs from the range of @ConceptSetProperty. When @ConceptSetProperty contains concept, that concept will be included in the value of the text field. The result will be a comma-separated list of IDs, e.g., 1,23,400. To update the concept set, you simply modify the text value, which holds the IDs that are part of this concept set. Note that the format is very strict; for instance, the value 3,70, with an additional trailing comma at the end is not accepted by the WEM runtime and will result in an empty concept set.
<? register input @ConceptSetProperty ?>
<? var @id := OutputId(@ConceptSetProperty) ?>
<? loop range of @ConceptSetProperty ?>
<label>
<input
type="checkbox"
name="<?attr @id ?>" <? if @ConceptSetProperty contains concept ?>checked<? end ?>
value="<?attr ConceptId(concept) ?>"
>
<?= concept ?>
</label>
<? end ?>import()We import our JavaScript module dynamically using the import() function. When the script is loaded, the .then() function is called, with the first argument being the loaded module. We use destructuring syntax { setHelloText } to extract our function from the module.
Since calling import() is asynchronous, there is a possibility that the widget may have already been removed by the time this function finishes loading the module. Therefore, we perform a simple check with if (enEl.parentNode), which verifies that the parentNode is not null. This ensures that the element is still attached to the DOM and that the widget has not been removed.
If we update our Note widget to use ES modules, we can revert to using class Note instead of class WemAcademyNote, without worrying about another widget using the same class name. Our updated initialization code now looks like this:
Once again, due to the asynchronous nature of the import() function, we need to handle some race conditions. In the startupscript, within the then() code block, we check if (window[symbol] !== "loading"). If this condition is true, we know that unloadscript has been called before the module has finished loading. This is because, at the end of unloadscript, we delete window[symbol], which sets its value to undefined. A similar check in the unloadscript is if (note !== "loading"). If this condition is true, we know that the module has been loaded before unloadscript was called, and the value of window[symbol] has been set to a new Note(), allowing us to safely call note.dispose().
<?
register event @ToggleCollapseState
@Collapsed := @Collapsed ? false
?>
<div class="message <?attr @Style ?> <?attr if @Collapsed then "collapsed" else "expanded" ?>" onclick="<?attr @ToggleCollapseState ?>">
<span class="box ellipses">
<span class="glyphicon glyphicon-comment"></span>
...
</span>
<span class="box text">
<span class="glyphicon glyphicon-heart-empty"></span>
<?= @Text ?>
</span>
</div>@Collapsed := not @Collapsed.message {
display: flex;
> .box {
border: 1px solid transparent;
border-radius: 4px;
cursor: pointer;
}
&.danger > .box {
background: lighten(@brand-danger, 25%);
border-color: lighten(@brand-danger, 10%);
color: darken(@brand-danger, 23%);
}
&.info > .box {
background: lighten(@brand-info, 25%);
border-color: lighten(@brand-info, 10%);
color: darken(@brand-info, 23%);
}
&.success > .box {
background: lighten(@brand-success, 25%);
border-color: lighten(@brand-success, 10%);
color: darken(@brand-success, 23%);
}
&.collapsed > .box {
padding: 5px;
&.text {
display: none;
}
}
&.expanded > .box {
padding: 15px;
&.ellipses {
display: none;
}
}
}<? register input @BooleanProperty ?>
<input type="text" name="<?attr OutputId(@BooleanProperty) ?>" value="<?attr @BooleanProperty ?>"><? register input @BooleanProperty ?>
<span>
This statement is?
</span>
<label>
True:
<input type="radio" name="<?attr OutputId(@BooleanProperty) ?>" value="true" <? if @BooleanProperty ?>checked<? end ?>>
</label>
<label>
False:
<input type="radio" name="<?attr OutputId(@BooleanProperty) ?>" value="false" <? if not @BooleanProperty ?>checked<? end ?>>
</label>
<label>
Unknown:
<input type="radio" name="<?attr OutputId(@BooleanProperty) ?>" value="" <? if IsUnknown(@BooleanProperty) ?>checked<? end ?>>
</label><? register input @BooleanProperty ?>
<label>
<input type="checkbox" name="<?attr OutputId(@BooleanProperty) ?>" value="true" <? if @BooleanProperty ?>checked<? end ?>>
Is the checkbox not checked?
</label><? register input @BooleanProperty ?>
<label>
<input type="hidden" id="<?attr OutputId(@BooleanProperty) ?>-hidden" name="<?attr OutputId(@BooleanProperty) ?>">
<input type="checkbox" id="<?attr OutputId(@BooleanProperty) ?>-checkbox" <? if @BooleanProperty ?>checked<? end ?>>
Is the checkbox not checked?
</label>
<? submitscript ?>
const checkboxEl = document.getElementById(<?js OutputId(@BooleanProperty) ?> + "-checkbox");
const hiddenEl = document.getElementById(<?js OutputId(@BooleanProperty) ?> + "-hidden");
hiddenEl.value = checkboxEl.checked ? "true" : "false";
<? end ?><?
var @d: datetime
@d := Date(1643, 1, 4)
@d := '1643-01-04'
@d := '1643-01-04T13:03:34'
?><? register input @DateTimeProperty ?>
<label>
Current date: <?= @DateTimeProperty ?>
<input type="text" name="<?attr OutputId(@DateTimeProperty) ?>" value="<?attr FormatDate(@DateTimeProperty, Iso8601) ?>">
</label><? register input @DateTimeProperty ?>
<label>
Select date and time:
<input type="datetime-local" name="<?attr OutputId(@DateTimeProperty) ?>" value="<?attr FormatDate(@DateTimeProperty, Iso8601) ?>">
</label><? register input @DateProperty ?>
<label>
Select date:
<input type="date" name="<?attr OutputId(@DateProperty) ?>" value="<?attr FormatDate(@DateProperty, "yyyy-MM-dd") ?>">
</label><? startupscript ?>
const d = <?js @DateTimeProperty ?>;
console.log(d.getFullYear());
<? end ?><? startupscript ?>
const d = new Date(2025, 2, 24, 16, 56, 37, 42);
console.log(d.getFullYear());
<? end ?><div id="<?attr OutputId() ?>" class="message">
<span class="box ellipses">
<span class="glyphicon glyphicon-comment"></span>
...
</span>
<span class="box text">
<span class="glyphicon glyphicon-heart-empty"></span>
<?= @Text ?>
</span>
</div>
<? scriptmodule "wem-message-box" ?>
function clearStateMessageBox(outputId, nodeId) {
const key = getKeyMessageBox(outputId, nodeId);
sessionStorage.removeItem(key);
}
function getKeyMessageBox(outputId, nodeId) {
return `${outputId}:${nodeId}:collapsed`;
}
function initializeMessageBox(outputId, nodeId, style) {
const element = document.getElementById(outputId);
const key = getKeyMessageBox(outputId, nodeId);
const collapsed = sessionStorage.getItem(key) === "yes";
element.classList.add(style);
element.classList.toggle("collapsed", collapsed);
element.classList.toggle("expanded", !collapsed);
element.addEventListener("click", () => toggleMessageBox(element, key));
}
function toggleMessageBox(element, key) {
element.classList.toggle("collapsed");
element.classList.toggle("expanded");
const collapsed = element.classList.contains("collapsed");
sessionStorage.setItem(key, collapsed ? "yes" : "no");
}
<? end ?>
<?
/* Unfortunately, WEM does not yet have a NodeId() function. */
var @currentNodeId := Last(Split(NodeTrail(), ","))
?>
<? startupscript ?>
initializeMessageBox(<?js OutputId() ?>, <?js @currentNodeId ?>, <?js @Style ?>);
<? end ?><? register input @File base64 = true ?>
<input type="text" id="<?attr OutputId(@File) ?>" name="<?attr OutputId(@File) ?>">
<button id="<?attr OutputId(@File) ?>-btn">Create binary data</button>
<? startupscript ?>
function clickHandler() {
const helloFile = createHelloFile("hello.txt");
// Assign the text field with the formatted contents of `helloFile`.
fileInputEl.value = helloFile;
}
function createHelloFile(fileName) {
if (fileName.includes(";"))
throw new Error("Filename cannot contain a ';'.");
// Create Base64 representation of the text "Hello".
const encoded = btoa("Hello");
// Return the emulated file in the format "filename;base64 data".
return `${fileName};${encoded}`;
}
const fileInputEl = document.getElementById(<?js OutputId(@File) ?>);
const btnEl = document.getElementById(<?js OutputId(@File) ?> + "-btn");
btnEl.addEventListener("click", () => clickHandler());
<? end ?><? register input @File ?>
<input type="file" id="<?attr OutputId(@File) ?>" name="<?attr OutputId(@File) ?>">
<button id="<?attr OutputId(@File) ?>-btn">Create binary data</button>
<? startupscript ?>
function clickHandler() {
const helloFile = createHelloFile("hello.txt");
// Use the DataTransfer to temporarily create a file list for the file input.
const dataTransfer = new DataTransfer();
dataTransfer.items.add(helloFile);
// Assign the file input with the file list from the data transfer.
fileInputEl.files = dataTransfer.files;
}
function createHelloFile(fileName) {
// Binary data that represents "Hello" in ASCII.
const binaryData = new Uint8Array([72, 101, 108, 108, 111]);
// We need a Blob to hold our binary data.
const blob = new Blob([binaryData], { type: "application/octet-stream" });
// Finally, return a plain text file with a custom file name.
return new File([blob], fileName, { type: "text/plain" });
}
const fileInputEl = document.getElementById(<?js OutputId(@File) ?>);
const btnEl = document.getElementById(<?js OutputId(@File) ?> + "-btn");
btnEl.addEventListener("click", () => clickHandler());
<? end ?>.wem-academy-tickle {
animation: shake infinite 0.4s;
}
@keyframes shake {
0% { transform: translate(0, 0); }
10% { transform: translate(4px, -4px); }
20% { transform: translate(-2px, 4px); }
30% { transform: translate(1px, 2px); }
40% { transform: translate(-4px, -4px); }
50% { transform: translate(0, -1px); }
60% { transform: translate(4px, -4px); }
70% { transform: translate(-4px, 2px); }
80% { transform: translate(-1px, -4px); }
90% { transform: translate(4px, -4px); }
}function startTickle(videoElement) {
videoElement.classList.add("wem-academy-tickle");
videoElement.play();
}
function pauseTickle(videoElement) {
videoElement.classList.remove("wem-academy-tickle");
videoElement.pause();
}<? scriptreference "wem-academy-tickle" FileUrl(@TickleJs) ?>
<link rel="stylesheet" href="<?attr FileUrl(@TickleCss) ?>">
<video class="tickle" loop src="<?attr FileUrl(@TickleVideo) ?>" onmousemove="startTickle(this)" onmouseout="pauseTickle(this)"></video><?
var @cs: conceptset
/* Assign the concept set with a single concept from @ConceptProperty1 */
@cs := @ConceptProperty1
/* Loop through the range of the concept set data field */
loop range of @ConceptSetProperty
/* Add the current concept from the range to the concept set @cs */
@cs := @cs + concept
end
/* Remove @ConceptProperty2 from the concept set @cs */
@cs := @cs - @ConceptProperty2
/* Loop through the concepts included in the concept set of @ConceptSetProperty */
loop @ConceptSetProperty
/* ... */
end
?><?
register input @ConceptSetProperty
var @ids: text
loop range of @ConceptSetProperty
?>
<div>
<?
var @included := @ConceptSetProperty contains concept
if @included
@ids := @ids + (if Length(@ids) <> 0 then "," else "") + ConceptId(concept)
end
?>
<?= ConceptId(concept) ?>: <?= concept ?> <? if @included ?>(included)<? end ?>
</div>
<? end ?>
<div>
<input type="text" name="<?attr OutputId(@ConceptSetProperty) ?>" value="<?attr @ids ?>">
</div>// hello-en.js
export function setHelloText(element) {
element.textContent = "Hi there!";
}// hello-nl.js
export function setHelloText(element) {
element.textContent = "Hallo daar!";
}<div id="<?attr OutputId() ?>-en"></div>
<div id="<?attr OutputId() ?>-nl"></div>
<? startupscript ?>
const enEl = document.getElementById(<?js OutputId() ?> + "-en");
const nlEl = document.getElementById(<?js OutputId() ?> + "-nl");
import(<?js FileUrl(@HelloEnJsResource) ?>)
.then(({ setHelloText }) => {
if (enEl.parentNode) setHelloText(enEl);
});
import(<?js FileUrl(@HelloNlJsResource) ?>)
.then(({ setHelloText }) => {
if (nlEl.parentNode) setHelloText(nlEl);
});
<? end ?><? startupscript ?>
const symbol = Symbol.for(<?js OutputId() ?>);
window[symbol] = "loading";
import(<?js FileUrl(@NoteJsResource) ?>)
.then(({ Note }) => {
if (window[symbol] !== "loading") {
// unloadscript has been called; ignore initializing the note.
return;
}
const note = new Note();
window[symbol] = note;
document.documentElement.append(note.rootEl);
});
<? end ?>
<? unloadscript ?>
const symbol = Symbol.for(<?js OutputId() ?>);
const note = window[symbol];
// Ignore disposing the note. The module has not been loaded yet, and the note is not initialized.
if (note !== "loading") {
note.dispose();
}
delete window[symbol];
<? end ?><?= @d ?>
23:45:06
<?attr @d ?>
23:45:06
<?raw @d ?>
23:45:06
<?js @d ?>
50250000000
Be cautious! As you can see, when printing a variable of type duration, only the time part is displayed, not the date part. Although we specified 1 day in our variable @d, it is not printed.
There is no native "duration" type in JavaScript. However, when printing the variable as a JavaScript number, it is represented as a Number type in ticks, which is very precise. One tick represents 100 nanoseconds, and there are 10,000 ticks in a millisecond. Be careful when converting ticks to other units:
Depending on the situation, you may want to use Math.round() on the numbers.
In the example above, we register input for @DurationProperty with unit = day. This informs the WEM runtime that during a postback, a number representing the number of days will be sent. To convert our duration to days, we use FormatDuration(@DurationProperty, "days"), specifying "days" as the second argument to indicate the desired unit. If we want the input field to hold minutes instead of days, we change it to register input @DurationProperty unit = minute and use FormatDuration(@DurationProperty, "minutes").
There are numerous ways to format a duration. The following outputs are generated when we have a var @d: duration with a value of '12d34h56m1s':
FormatDuration(@d, "days")
13
FormatDuration(@d, "hours")
322
FormatDuration(@d, "minutes")
19376
FormatDuration(@d, "seconds")
1162561
Be aware that in some situations, the duration may be truncated. For instance, when using "clock", only the hours and minutes are shown. However, when formatting to "seconds", the entire duration is represented in seconds.
200.000,00However, this can lead to issues in other contexts, such as JavaScript, where numbers are written in an invariant culture format like 200000.00. We will discuss this further shortly.
If you have a number that requires exact precision, you can easily achieve this with the precision option:
There are cases where you may want to work with an invariant culture, especially in JavaScript. This can be accomplished by adding the invariant culture option. This way, the WEM runtime knows to expect number values that are posted back in the format 200000.00 (using the dot as the decimal separator), making it easier to work with in JavaScript.
However, note that encoding with <?=, <?attr, and <?raw will still output according to the current culture settings, ignoring the invariant culture setting. To clarify: The invariant culture option is only for postback, so the WEM runtime knows to expect a number value in that format.
The table below provides an overview of the results of different kinds of encoding when outputting a variable declared with var @n: number, which holds the value 123.45.
<?= @n ?>
123.45
123,45
123,45
<?attr @n ?>
123.45
123,45
123,45
<?js @n ?>
123.45
123.45
When you have a widget number property @NumberProperty, which you register with register input, and you want to perform a postback with the value 123.45, the format you need to use will depend on the culture settings:
123.45
123,45
123,45
123.45
<?
var @d: duration
/* 2 minutes */
@d := '2m'
/* 4 hours and 6 seconds */
@d := '4h6s'
/* 14 days and 5.5 hours */
@d := '14d5.5h'
?><? startupscript ?>
const durationInTicks = <?js @DurationProperty ?>;
const durationInMilliseconds = <?js @DurationProperty ?> / 10_000;
const durationInSeconds = <?js @DurationProperty ?> / 10_000_000;
const durationInMinutes = <?js @DurationProperty ?> / 600_000_000;
const durationInHours = <?js @DurationProperty ?> / 36_000_000_000;
const durationInDays = <?js @DurationProperty ?> / 864_000_000_000;
<? end ?><? register input @DurationProperty unit = day ?>
<input type="text" name="<?attr OutputId(@DurationProperty) ?>" value="<?attr FormatDuration(@DurationProperty, "days") ?>">var @n: number
@n := 1
@n := -1.0
@n := 2.7182
@n := Exp(1)<? register input @NumberProperty ?>
<input type="text" name="<?attr OutputId(@NumberProperty) ?>" value="<?attr @NumberProperty ?>"><? register input @NumberProperty precision = 2 ?>
<input type="text" name="<?attr OutputId(@NumberProperty) ?>" value="<?attr @NumberProperty ?>"><? register input @NumberProperty invariant culture ?>
<input type="hidden" name="<?attr OutputId(@NumberProperty) ?>" id="<?attr OutputId(@NumberProperty) ?>">
<? startupscript ?>
// Important to use 'js' encoding when outputting the property value in JavaScript blocks.
let value = <?js @NumberProperty ?>;
value *= value;
console.log("Result squared", value);
<? end ?>
<? submitscript ?>
const gr = 1.61803;
const el = document.getElementById(<?js OutputId(@NumberProperty) ?>);
// Since we are using culture invariance, we can write the number value as a string.
el.value = gr.toString();
<? end ?>FormatDuration(@d, "long")
13 dagen, 10 uur, 56 minuten en 1 seconde
In Dutch language
FormatDuration(@d, en_US)
13:10:56:01
FormatDuration(@d, nl_NL)
13:10:56:01
FormatDuration(@d, "clock")
10:56
FormatDuration(@d, "time")
10:56
FormatDuration(@d, "stopwatch")
10:56:01
123,45
<?raw @n ?>
123.45
123,45
123,45
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:
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:
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:
This will output roughly to:
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:
This will output roughly to:
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:
This will output to the following:
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:
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.
<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 ?><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><? scriptmodule "bobs-button" ?>
class Button {/* Bob's button */}
<? end ?><? scriptmodule "janes-button" ?>
class Button {/* Jane's button */}
<? end ?><head>
<script>
class Button {/* Bob's button */}
</script>
<script>
class Button {/* Jane's button */}
</script>
</head><? startupscript ?>
const note = new Note();
document.documentElement.append(note.rootEl);
window.note = note;
<? end ?>
<? unloadscript ?>
window.note.dispose();
<? end ?><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><? 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 ?><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><? unloadscript ?>
const noteSymbol = Symbol.for(<?js OutputId() ?>);
window[noteSymbol].dispose();
delete window[noteSymbol];
<? end ?>Encoding is an essential aspect of building widgets and web development in general for several reasons, primarily related to security and data integrity. If you are not careful, your widget can pass along incorrect values to a JavaScript library, resulting in a broken layout or even making it vulnerable to the injection of malicious scripts.
Encoding is automatically handled for you with the print statement; however, you still need to exercise caution. Common scenarios requiring encoding include displaying user input from a text field or configuring a JavaScript instance with the property values of the widget.
WEMscript offers three methods of encoding: HTML encoding, HTML attribute encoding, and JavaScript encoding. You can achieve these by calling the print statement with the first arguments html, attr, and js, respectively. There is also a fourth method: printing without encoding, often referred to as printing raw.
Since you will frequently use the print statement, WEMscript provides a few shorthand notations to simplify your work. We have already used some of these, and you should be familiar with them:
The WEMscript above can be rewritten more concisely as follows:
From now on, we will use shorthand notation whenever possible.
The simplest yet most dangerous way to print output is to do so without any encoding. This method should be handled with extreme caution. Here is an example of how not to use raw printing:
The above example is very risky. Using raw printing can lead to errors if you are not careful, not to mention the security risks! For instance, if the property @Name is set to the value: Bob<script> runMaliciousScript(); </script>, the output would be vulnerable to XSS.
We can mitigate this risk by using HTML encoding.
HTML has special characters that are reserved for specific meanings (e.g., < for opening tags, > for closing tags). If these characters are included in user input without encoding, they can disrupt the structure of the HTML document. We need to convert these characters into their corresponding HTML entities (e.g., < becomes <, > becomes >), preserving the intended content and ensuring it is displayed correctly. We can make the previous code safer by changing <?raw to <?=, as shown below:
If a hacker attempts to insert <script> blocks, they will be printed as <script> and will not be parsed as script blocks but rather as the literal text "<script>".
HTML attributes are similar to HTML and should always be encoded using either print attr "value" or <?attr "value" ?>. In the following example, we encode the title attribute:
Encoding in JavaScript is somewhat unique because you also have to consider the language settings of the portal in which the application is running. For instance, the number 3.14159 is written as 3,14159 in Dutch (note the comma). If we print a boolean value without encoding, we will receive the values yes and no in English, while in Dutch, we get ja and nee, respectively. All of these are invalid boolean values in JavaScript. Therefore, it is crucial to encode values correctly in JavaScript. Here is a bad example that demonstrates the consequences of not encoding JavaScript values.
This will translate to the following JavaScript code in the runtime if we run it with Dutch as the portal language setting.
As you can see, many issues arise in this code, and some errors may even go unnoticed! Note the comma operator (,) when trying to assign the variable b. This is not a syntax error; it is a valid operator that is rarely used in JavaScript.
Now, let’s consider what happens if we change the portal language to English.
Once again, we encounter multiple errors. However, this time the number variable b is set correctly by accident. Let’s fix this by properly encoding the JavaScript values:
This translates to:
The values are now correctly set, regardless of the portal language settings in the context of JavaScript. Note that you do not need to include quotes for text values.
You might expect that there is a similar method for CSS, such as <?css ?>, but unfortunately, WEMscript does not provide this. It is quite non-trivial. However, although it may seem hacky, there are cases where you can use <?js and <?=, but you need to be cautious. Additionally, since CSS is heavily unit-based (e.g., 20px, 4vw), you often need to manually add the units in most print statements.
Let’s look at the following example, assuming that @Width and @Height are both widget number properties:
Let’s set the @Width and @Height properties to 320 and 200, respectively. This will translate to:
This works. But what if the properties are of type unknownnumber? In that case, both values would be nullpx, resulting in an invalid CSS property value. This may need to be fixed, depending on the widget.
What if you want to set a more flexible value for a CSS property? For instance, a margin value like auto or 8px 16px 8px? We can use a text property for this case, but make sure to HTML encode it. Let’s consider the following example, assuming we have a @Margin widget text property:
If we have the text value 16px 8px for our margin, it will translate to:
Now, what if we think like a hacker and set the text value to auto;}</style><script>console.log('Hacked!')</script> as our margin?
As you can see, once again, the <?raw method is very dangerous and must be used with caution. In this case, using <?= is the better option. While it can result in CSS syntax errors if an incorrect value is set, it is far preferable to leaving the widget vulnerable to XSS attacks!
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."
<p title="<? print attr "HTML attribute encoded text" ?>">
<? print html "HTML encoded text" ?>
</p>
<? startupscript ?>
const text = <? print js "JavaScript encoded text" ?>;
<? end ?>
<?
/* Printing raw can be very dangerous. */
print "This can be dangerous"
?>print "value"
<?raw "value" ?>
print html "value"
<?= "value" ?>
print js "value"
<?js "value" ?>
print attr "value"
<?attr "value" ?>
Add it as a resource named "NoteCss."
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."
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.
An extra argument, invariant culture, has been added to the register input statements in the following lines of code:
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 ".".
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:
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.
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.
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.
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.
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.
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.
<p title="<?attr "HTML attribute encoded text" ?>">
<?= "HTML encoded text" ?>
</p>
<? startupscript ?>
const text = <?js "JavaScript encoded text" ?>;
<? end ?>
<?/* Printing raw can be very dangerous. */?>
<?raw "This can be dangerous" ?>Your name is: <?raw @Name ?><!-- Whoops! -->
Your name is: Bob<script> runMaliciousScript(); </script>Your name is: <?= @Name ?><!-- Although a hacker tried to insert malicious code, it has been transformed into HTML entities that will be parsed as literal text by the browser. -->
Your name is: Bob<script>runMaliciousScript()</script><? var @title := "The title of this paragraph" ?>
<p title="<?attr @title ?>">I am a paragraph.</p><? startupscript ?>
/* Using raw encoding in JavaScript is inadvisable. */
const a = <?raw "text value" ?>;
const b = <?raw 3.14159 ?>;
const c = <?raw true ?>;
<? end ?> // Syntax error.
const a = text value;
// Variable is incorrectly set to the value 3, and the fractional part is ignored.
const b = 3,14159;
// Incorrectly assigning the variable `c` to the value of (possibly undefined) variable `ja`.
const c = ja; // Same syntax error.
const a = text value;
// Edge case where the variable is correctly set.
const b = 3.14159;
// Incorrectly assigning the variable `c` to the value of (possibly undefined) variable `yes`.
const c = yes; <? startupscript ?>
const a = <?js "text value" ?>;
const b = <?js 3.14159 ?>;
const c = <?js true ?>;
<? end ?> // Value correctly printed as a string.
const a = "text value";
// Value correctly printed as a number.
const b = 3.14159;
// Value correctly printed as a boolean.
const c = true;<style>
#example {
width: <?js @Width ?>px;
height: <?js @Height ?>px;
}
</style><style>
#example1 {
width: 320px;
height: 200px;
}
</style><style>
#example2 {
margin: <?js @Margin ?>;
}
#example3 {
margin: <?= @Margin ?>;
}
#example4 {
margin: <?raw @Margin ?>;
}
</style><style>
#example2 {
/* Invalid property value */
margin: "16px 8px";
}
#example3 {
/* Works */
margin: 16px 8px;
}
#example4 {
/* Works, but... */
margin: 16px 8px;
}
</style><style>
#example2 {
/* Invalid property value */
margin: "auto;}</style><script>console.log('Hacked!')</" + "script>";
}
#example3 {
/* This will result in a CSS syntax error */
margin: auto;}</style><script>console.log("Hacked!")</script>
}
#example4 {
/* This is an XSS attack! */
margin: auto;}</style><script>console.log('Hacked!')</script>;
}
</style>.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;
}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);
}
}<?
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 ?>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() ?>);
const note = window[noteSymbol];
note.dispose();
delete window[noteSymbol];
<? end ?>