Coding with XMLUI
When you create apps, sometimes you need to declare UI logic. The XMLUI components and the framework relieve you of many aspects other web UI frameworks require you to add code to.
However, there are a few occasions where you need to add code. XMLUI strives to reduce coding to using expressions instead of utilizing advanced concepts like function declarations, conditional statements, loops, etc.
This article will teach you the fundamental concepts and constructs of working with XMLUI code.
Fundamental Concepts
There are a few fundamental concepts the understanding of which help you add UI logic to your apps.
XMLUI has a scripting language (XmlUiScript), a subset of JavaScript that follows its syntax and semantics.
Do not feel intimidated if you do not have experience with JavaScript. You rarely have to write complex code with XMLUI. In most cases, your intention can be described with short expressions.
You can learn more details about the subset of JavaScript supported by XMLUI in these articles:
Inline Code Snippets
You can place code snippets inline into component properties, variable values, and event handlers.
This technique works well when the code snippets are short and close to the components using them.
<App var.result="???" >
<Button
label="Calculate the sum of 1 to 100"
onClick="
let sum = 0;
for (let i = 1; i <= 100; i++) sum += i;
result = sum;
" />
<Text>Result: {result}</Text>
</App>
This technique works well when the code snippets are short and close to the components using them. In the following markup, the value of the onClick
attribute is a code snippet just like the {result}
expression within Text
. Now, accept them as they are; later, you will learn about their syntax.
Code-Behind Files
You can put code (variables, functions) into code-behind files. Markup files (.xmlui
extension) may have a
code-behind file using the markup file name but the .xmlui.xs
extension.
XMLUI automatically binds component markup with code-behind files. This technique is helpful when you have long code snippets that would otherwise disturb your understanding of the markup.
<Button
id="calcButton"
label="Show delay"
onClick="showDelay()"/>
function showDelay() {
const delay = 500 * 2;
toast('Delay calculated: ' + delay);
}
The code you declare in a code-behind file is visible in the entire component but not outside.
Later, you will learn about how you can use scripts in the code-behind files.
Component IDs
With the id
attribute of a component tag, you can assign an identifier to a component that you can use to refer to that particular component in code (expressions). Component IDs are visible only in the scope of their markup (and associated code-behind) file.
You can access the exposed methods and properties of the particular component through its component ID. The following section shows a sample.
Expressions
Expressions are the primary tools for binding components to work one with the state of another.
<App>
<TextBox id="myText" initialValue="Hello!" />
<Text>You typed: {myText.value}</Text>
</App>
Type some text into the textbox to discover how this small example works.
The {myText.value}
part in the Text component is an expression. The wrapping braces sign the engine that the content within will be evaluated as the app runs.
The TextBox component's identifier is myText
. You can access the TextBox's exposed values (and methods) through this ID. myText.value
represents the current text content within the box. So, this expression binds the TextBox's value to the Text's displayed label.
When the XMLUI engine observes that a dependency of an expression changes, it re-evaluates the expression and re-renders the affected part of the UI (and only that part).
In the small app above, when you type something in the text box, every key you press triggers such a change.
When you set property values, you can mix literal strings with expressions.
<App>
<Text>I am a string literal.</Text>
<Text>{"I am" + " an " + "expression."}</Text>
<Text>I see {2+3} apples and {3*4} oranges.</Text>
</App>
Here, the first text uses a string literal, the second is a sheer expression, and the third combines string literals and expressions.
Variable Declarations
Variables are indispensable to the UI logic as they store some state. You can use expressions to set a variable's initial value:
<App
var.myValue="{6 * 7}">
Life, universe, and everything: {myValue}
</App>
Here, var.myValue
is a variable declaration with a variable name (myValue
) and an initial value ({6 * 7}
). Before displaying the text, the engine initializes the myValue
variable. Thus, when that is displayed, {myValue}
is replaced with its current value, 42.
You can move variable declarations into code-behind files.
The following code has the same behavior as the previous one.
<App>
Life, universe, and everything: {myValue}
</App>
var myValue = 42;
XMLUI attribute values are strings by default. While components transform string attribute values to the expected types, there is no such an expected type of the variable so that they will be initialized to a string. Use an expression to set the initial variable value to utilize the proper type.
The following sample demonstrates this concept:
<App
var.stringValue="true"
var.boolValue="{true}"
var.num1Value="123"
var.num2Value="{123}">
<Text>'stringValue' is a {typeof stringValue}, it's value is {stringValue}</Text>
<Text>'boolValue' is a {typeof boolValue}, it's value is {boolValue}</Text>
<Text>'num1Value' is a {typeof num1Value}, it's value is {num1Value}</Text>
<Text>'num2Value' is a {typeof num2Value}, it's value is {num2Value}</Text>
</App>
Check the variable types in the output:
XMLUI variables are reactive: whenever their dependencies change, their value is reevaluated.
<App var.count="{0}" var.countTimes3="{3 * count}" >
<Button
label="Click to increment!"
onClick="count++" />
<Text>Click count = {count}</Text>
<Text>Click count * 3 = {countTimes3}</Text>
</App>
Clicking the button increments only the count
variable. Because countTimes3
is a reactive variable that depends on count
, incrementing count
triggers updating countTimes3
too.
Event Handlers
When a user interaction or system event occurs, the XMLUI engine executes component event handlers. You can override the default event handler with your custom event handler.
In an event handler, the entire attribute value is an XMLUI Script code snippet, so you do not need to wrap the code into curly braces:
<App>
<Button
label="Greet me!"
onClick="toast('Howdy!')" />
</App>
If you click the button, the engine executes the toast('Howdy!')
code and displays a toast message.
Event handlers often contain not only expressions but also statements.
You can use local variables within expressions and function declarations. The let
keyword allows using a
mutable variable; const
declares an immutable one (following the JavaScript semantics). In contrast to var
,
local variables declared with let
and const
are not reactive.
<App var.result="???" >
<Button
label="Calculate the sum of 1 to 100"
onClick="
let sum = 0;
for (let i = 1; i <= 100; i++) sum += i;
result = sum;
" />
<Text>Result: {result}</Text>
</App>
You can move event handler declarations into code-behind files.
The following code has the same behavior as the previous one.
<App>
<Button
label="Calculate the sum of 1 to 100"
onClick="calculate()"/>
<Text>Result: {result}</Text>
</App>
var result = "???";
function calculate() {
let sum = 0;
for (let i = 1; i <= 100; i++) sum += i;
result = sum;
}
When your component has an ID, the engine automatically binds component event handlers with functions in the code-behind file.
For example, in the following markup, the button has the id calcButton
. The engine automatically binds the button's click
event with the calcButton_onClick
function in the code-behind file.
<App>
<Button
id="calcButton"
label="Calculate the sum of 1 to 100"/>
<Text>Result: {result}</Text>
</App>
var result = "???";
function calcButton_onClick() {
let sum = 0;
for (let i = 1; i <= 100; i++) sum += i;
result = sum;
}
Some event handlers may have one or more arguments.
In the following example, the didChange
event of TextBox
has a single argument: the new value after the change.
<App var.message="(not changed yet)">
<TextBox
initialValue="Hello!"
onDidChange="(newValue) => message = newValue" />
<Text>The new TexBox value: {message}</Text>
</App>
The (newValue) => message = newValue
expression is an arrow function (with the same syntax as JavaScript). In the expression, newValue
is the function argument, message = newValue
is the function's body.
When you change the text in the TextBox, the message
variable is updated with the new value.
XMLUI understands your intention when defining an event handler. In addition to the arrow function syntax, it accepts several other alternatives, as here the Event Handlers section describes.
Exposed Methods
Reusable components can expose methods to other components.
These methods can use XMLUI Script statements like event handler functions. You can learn more about them in the Reusable Components article.
Code-Behind Variables and Functions
Code-behind files may contain only reactive variables (var
keyword) and function declarations on their top-level
scope. Any other statement or expression declaration is forbidden.
Thus, the following code-behind file is valid:
var count = 0;
function increment() {
count++;
}
However, this declaration is not, as it contains a local variable and an expression statement:
var selectedIndex = -1;
const MY_LABEL = "My label";
selectedIndex++;
function selectItem(index) {
selectedIndex = index;
}
Function declarations can be nested in other functions.
In this case, markup files can see only the outermost declarations; inner functions are unavailable.
While the following code-behind file exposes the calculateValue
function, it hides square
:
function calculateValue(n) {
function square(x) {
return x * x;
}
let sum = 0;
for (let i = 1; i <= n; i++) sum += square(i);
return sum;
}
Currently, XMLUI does not implement the concept of imports, so you cannot create code files and import them into code-behind files (for example, to share utility functions). This feature is being added to the framework in the future.
How XMLUI Code Runs
The engine does not create JavaScript code to execute it in the browser; it runs interpreted code created from the compiled source script.
These are the main reasons XMLUI prefers this technique:
- It is easier to avoid potential security breaches and work with the browser's CSP (Content Security Policy)
- XMLUI makes intelligent decisions about when to run the code synchronously and when to opt for asynchronous execution.
- The rendering engine can make the UI responsive without any special care in the code.
No Frozen UI
You do not need to use async
..await
constructs.
These keywords are not even part of the XMLUI scripting syntax. The framework will ensure that even if you start an infinite loop in the code, the UI remains responsive:
<App var.count="{0}" var.loopRuns="{false}">
<HStack>
<Button enabled="{!loopRuns}"
label="Start infinite loop"
themeColor="attention"
onClick="loopRuns = true; while (loopRuns) count++;"
/>
<Button
enabled="{loopRuns}"
label="Exit infinite loop"
onClick="loopRuns = false;"/>
</HStack>
Counter value: {count}
</App>
Though the first button starts an infinite loop (if it were native JavaScript, it would block the UI), you can still click the other button, which allows the code to break out of the loop, by setting the loopRuns
variables to false
.
This is the event handler with the infinite while loop:
loopRuns = true; while (loopRuns) count++;
Now, try the code and break out of the loop by clicking the second button:
Async and Sync Code Execution
The engine runs event handler code asynchronously. However, synchronous code immediately gets the expression value to render the component's UI when evaluating expressions.
This behavior has a significant consequence: You cannot use property values of variable initializations that run asynchronously.
For example, in the following markup, using the delay()
function would raise an error with this message: "Promises (async function calls) are not allowed in binding expressions":
<Text value="My value {delay(100), 123}" />
Identifier Resolution
When you refer to a particular variable, XMLUI starts resolving it from the innermost scope of its usage (exactly like JavaScript). If an identifier cannot be resolved in its local scope (inline expression, inline event handler, or code-behind file), the engine tries to resolve it in other scopes in this order:
- Context values (such as
$props
for reusable components,$item
for lists, etc. - Declarations in the current markup file (for example, the markup file may declare component IDs or variables.
- XMLUI utility functions. The framework provides several global utility functions.
- The global JavaScript scope (
window
).