Module 4: Programming basics in QML and JavaScript
In the previous two modules you learned a lot about QML components that make up an Ubuntu Touch app. We focused on the visual aspects there. In this module we’ll focus more on the programming aspects by adding more complex JavaScript code.
You can find this module’s code of the app in the GitLab repository of the course. |
1. Introduction
In the previous modules you learned how to let your app do some simple actions like adding items to your shopping list and removing them from the same list. However, we glossed over a lot of the programming concepts that make your app tick.
This changes in this module, were we introduce the basic programming concepts in QML and JavaScript that you’ve been using, and more. You’ll learn about some basic data types in JavaScript and QML. You’ll also learn how to test for specific values, how to repeat statements, and how to create your own functions for more complex sequences of actions. We’ll use all this information to extend the functionality of our shopping list app.
2. Data types, variables and properties
In the previous modules you already encountered a couple of data types. A data type is a 'sort' of data, such as a number or a string. Let’s have a look at them in more detail in this section.
The QML type system has three sorts of types: basic types, QML object types, and JavaScript types. We’ll explain the first two in the next subsections. JavaScript types will be explained later in this module as part of the use of JavaScript expressions in QML.
2.1. Basic types
The QML language has built-in support for various basic types, including integers, double-precision floating-point numbers, strings, and boolean values (true or false).
QML objects may have properties of these types, and you can pass values of these types as arguments to methods of objects.
The QtQuick module extends the QML language with more basic types. |
Let’s take a look at the basic types provided by the QML language:
2.1.1. bool
The bool
type can have two values: true
or false
. This is often used to enable or disable specific functionality. For instance, the MainView
component has a property automaticOrientation
that you can set to true
or false
. Many other QML components support boolean properties.
2.1.2. int
The int
type refers to a whole number, so without a decimal point, for example 0, 7, or -15. You already used this type for the numberOfSlots
property of the ActionBar
in the previous course module.
2.1.3. real and double
The real
type refers to a number with a decimal point, such as 1.5 or -75.3. We’ve already used it in grid units: we passed the real value 0.5 to units.gu(0.5)
to compute the size of half a grid unit. If you look at the documentation of Ubuntu.Components Units, you see that the method gu
expects a real value as its argument and returns a real value too.
double is also a number with a decimal point, stored in double precision according to the IEEE 754 floating-point format.
|
2.1.4. string
The string
type refers to a free form text string. You need to put a string between double or single quotes, as in "Shopping List" or 'Shopping List'. For instance, the property applicationName
of the MainView
component is a string
. We defined it as:
applicationName: 'shoppinglist.koenvervloesem'
Each string has a length attribute that holds the number of characters in the string. For instance, if you want to know the number of characters in the text in the TextField of your app, you can get it with textFieldInput.text.length .
|
2.1.5. enumeration
The enumeration
type refers to a value that can be one of a fixed set of values, each with their own name. We already saw this in the wrapMode property of the Label
component in the about dialog you created in the previous course module. This property can have one of four values: Text.NoWrap
, Text.WordWrap
, Text.WrapAnywhere
, and Text.Wrap
.
The Text component has quite a lot of properties with an enumeration type.
|
2.1.6. list
A list
type refers to a list of QML objects. You assign values to the list between square brackets and separated by a comma. We already saw it in the actions
property of the ActionBar
, to which we assigned a list of two actions:
actions: [ Action { iconName: "settings" text: i18n.tr("Settings") }, Action { iconName: "info" text: i18n.tr("About") onTriggered: PopupUtils.open(aboutDialog) } ]
If a list only contains one object, you can omit the square brackets. For instance, you could write the leadingActions
property of the ListItem
from our shopping list app as follows:
leadingActions: ListItemActions { actions: Action { iconName: "delete" onTriggered: shoppinglistModel.remove(index) } }
A list can only store QML objects and not basic type values. If you want to store basic types in a list, use the var type instead.
|
2.1.7. url
The url
type refers to a uniform resource locator (URL). In most cases, this will be a file name, for instance for an image.
The Action
component has an iconSource
property of the url
type that you can use instead of iconName
to set your own custom image. Just put the file in your assets directory and then set the property as follows:
iconSource: "../assets/custom-image.png"
Use the .. to navigate to the top directory, because the directory assets sits on the same level as the directory qml. |
2.1.8. var
The var
type is a generic type that can refer to any data type. It’s equivalent to a regular JavaScript variable. You can even assign functions to a var
property, or an array of basic types (an array is the JavaScript equivalent of a list).
2.2. Object types
A QML object type is a type from which you can instantiate a QML object. You’ve been doing this since the first module of this course: time after time you’ve specified a type followed by a set of curly braces that encompass the object’s attributes.
For instance, this is how you defined the button to add an item to your shopping list:
Button { id: buttonAdd anchors { top: header.bottom right: parent.right topMargin: units.gu(2) rightMargin: units.gu(2) } text: i18n.tr('Add') onClicked: shoppinglistModel.append({"name": textFieldInput.text}) }
Button
is a QML object type, and it has a couple of attributes: id
, anchors
, text
, and onClicked
.
Let’s take a look at the various types of attributes a QML object type can have.
2.2.1. The id attribute
Every QML object instance has an id
attribute that you can assign a value to, so other objects can refer to this object.
The value of the id attribute has to begin with a lower-case letter or an underscore. It can’t contain any characters other than letters, numbers and underscores.
|
You’ve used this is the code above with the add button. In the onClicked
signal handler you referred to the text of the input text field as textFieldInput.text
. This works because you added id: textFieldInput
to the declaration of the TextField
object, and text
is a property of this TextField
object.
2.2.2. Property attributes
A property is an attribute of an object that can be assigned a static value or bound to a dynamic expression. You’ve already done this countless times in this course.
You assign a value to a property on initialization of the object by typing the property name, a colon, and the property value. For instance, in the MainView
object:
objectName: 'mainView'
You can also bind a property to a dynamic expression, such as the following one in the add button:
text: i18n.tr('Add')
This dynamically computes a string with the i18n.tr method.
You can also use any JavaScript expression, such as the following one in the Button
object with id buttonRemoveAll
:
width: parent.width / 2 - units.gu(0.5)
Other objects can read and often also modify an object’s property values. That’s why you could access the text field’s text
property in the add button, or the width
property of the parent of the remove all button.
The QML engine enforces the relationship between a property and the variables in a JavaScript expression. When any of the variables change in value, the QML engine automatically re-evaluates the binding expression and assigns the new result to the property. |
In some cases properties contain a logical group of subproperties. The anchors
property is a typical example:
anchors { top: header.bottom right: parent.right topMargin: units.gu(2) rightMargin: units.gu(2) }
The previous code is called the group notation of the group property anchors
. You can also use the dot notation, where you set the group properties individually:
anchors.top: header.bottom anchors.right: parent.right anchors.topMargin: units.gu(2) anchors.rightMargin: units.gu(2)
The behavior of both notations is the same. However, when you only need to set one of the subproperties, the dot notation is a bit easier to type, such as in anchors.fill: parent
for the Page
element.
2.2.3. Method attributes
A method of an object type is a function that you can call to perform some processing. You can add your own methods to a QML type to define reusable blocks of JavaScript code.
For instance, you could add a method addItem
to the ListModel
object:
ListModel { id: shoppinglistModel function addItem(name) { shoppinglistModel.append({"name": name}) } }
This method has one parameter: name
.
Now in the button with id buttonAdd
, change the signal handler if you click on the button to:
onClicked: shoppinglistModel.addItem(textFieldInput.text)
So when you click on the button, it calls the addItem
method of the shoppinglistMode
object, with textFieldInput.text
as the value for its name
parameter.
The specific value given to a parameter when you call a function (or a method) is called an argument. |
Of course, this is just a simple method. But later in this module, when you learn some JavaScript basics, we’ll create some more complex functions and methods.
2.2.4. Signal attributes
A signal is a notification from an object that some event has occurred. In OKCancelDialog.qml you defined your own signal attribute:
signal doAction()
Because this signal has no parameters, the parentheses are optional. |
To emit a signal, you need to invoke it as a method, in this case by calling doAction()
in JavaScript code. This invokes the corresponding signal handler then.
2.2.5. Signal handler attributes
A signal handler is a special type of method attribute: the method is invoked by the QML engine whenever the associated signal is emitted.
If you add a signal to a QML object definition, this will automatically add an associated signal handler to the object definition. Its implementation is empty by default, and you can provide your own implementation to react to the emitted signal.
For instance, because you defined the signal attribute doAction
in OKCancelDialog.qml, you’ve also access to the signal handler attribute onDoAction
in the OKCancelDialog
object type. You’ve instantiated this object in the components with id removeAllDialog
and removeSelectedDialog
, and you added your own implementation to the signal handler, for instance by:
onDoAction: console.log("Remove all items")
For existing QML object type, look at the QML API documentation of the object type to see which signals it emits.
3. JavaScript expressions in QML
QML provides a JavaScript Host Environment that is similar to the JavaScript environments of your web browser or of a server-side environment such as Node.js. Of course, the JavaScript environment in QML is tailored to writing QML applications. So you don’t have access to a window
object or a DOM API found in a browser environment.
If you want to know the details of what the QML JavaScript Host Environment implements, you need to read the 7th edition of the ECMAScript Language Specification. The QML documentation has a list of supported JavaScript objects, functions and properties.
A QML document can contain JavaScript code in three places: in the body of a property binding, in the body of a signal handler, and as a method of a QML object. You’ve already seen examples of these three places, but let’s revisit this for completeness.
You can also import code in a QML document from a standalone JavaScript file. You can then use the functions and variables defined in the JavaScript in any property bindings, signal handlers and methods in the QML document. |
3.1. In property bindings
You’ve seen the use of JavaScript in properties like this one for buttons:
width: parent.width / 2 - units.gu(0.5)
This gets the width
property of the button’s parent, divides it by two and subtracts units.gu(0.5)
from it, the latter being a function call.
You can use any JavaScript expression in the body of a property, and you’ll see some more complex examples later.
3.2. In signal handlers
Another place where you’ve seen the use of JavaScript is in signal handlers, for instance when you click on a button. Until now the body of those signal handlers was quite simple. For instance, the add button has the following signal handler, calling the addItem
method of the shopping list model:
onClicked: shoppinglistModel.addItem(textFieldInput.text)
You’ve probably noticed that adding an item to our shopping list isn’t as user-friendly as it could be. After entering a text in the text field and clicking on the add button, the item is added to the shopping list, but the text stays in the text field. So if you now want to add another item, you first have to clear the text field by clicking on the icon with the cross at the right of the text field or by typing backspaces.
Let’s fix this issue in the onClicked
signal handler:
onClicked: { shoppinglistModel.addItem(textFieldInput.text); textFieldInput.text = ""; }
So if you click now on the button, the text is added to an item in the shopping list, and after that the text field is made empty.
3.3. As methods
Earlier in this module you’ve seen how you create a function inside a QML object type, which makes it a method of that object. The JavaScript code in a method can be as complex as you want, and it has access to the object’s properties.
4. JavaScript basics
This is not a JavaScript course, so if you want to learn JavaScript, you should read a book or an online tutorial such as the W3Schools JavaScript reference. However, in the remainder of this module we’ll give some JavaScript basics to get you up to speed for Ubuntu Touch app development.
Many learning resources focus on JavaScript in the web browser. For instance, in the W3Schools example, only the JavaScript Reference section is relevant for QML, not the other ones such as Window Reference, HTML DOM Reference and Web APIs. |
4.1. JavaScript types
As you’ve already seen, any standard JavaScript type can be created using the var
type. You can declare properties of these types, and you can declare and use them in your JavaScript code. Let’s take a look at some common JavaScript types.
4.1.1. String
A String
stores a series of characters. For instance:
var itemName = "apples";
You can get the number of characters in the string by using the length
property:
var itemName = "apples"; var itemLength = itemName.length;
A string has a lot of useful methods, for instance concat
to concatenate multiple strings into one string:
var item1 = "apples"; var item2 = "bananas"; var items = item1.concat(" and ", item2);
After this code, the variable items
contains the string "apples and bananas".
4.1.2. Number
In contrast to many other languages, including QML, JavaScript has only one type of number: Number
. You can write numbers with or without decimals, and even in scientific notation:
var x = 3.14; var y = 20; var z = 6.0221409e+23;
You can convert a number to a string representation of this number with the method toString()
:
var x = 3.14; var text = x.toString();
After this code, the variable text
contains the string "3.14".
You can also check whether a number is an integer:
var x = 3.14; var isint = Number.isInteger(x);
After this, the variable isint
has the boolean value false
. If you would have called the isInteger
method on a value such as 20, it would have returned the value true
.
4.1.3. Boolean
A Boolean
can have one of two values: true
or false
. This is mostly used inside expressions, such as with the ternary operator (see below). But you can also define a boolean variable to store information about something that can have two values, for instance:
var enabled = true;
You can convert a boolean to a string representation with the method toString()
:
var enabled = true; var text = enabled.toString();
After this code, the variable text
contains the string "true".
4.1.4. Array
We’ve already hinted at the use of a JavaScript array. An array is used to store multiple values in a single variable. You can create it like this:
var shoppingList = ["apples", "bananas", "water"];
An array has a property length
, which returns the number of elements in the array:
var shoppingList = ["apples", "bananas", "water"]; var n = shoppingList.length;
After this, the variable n
contains the value 3.
4.2. JavaScript operators
JavaScript has a lot of operators, used to assign and compare values, to perform arithmetic operations, and much more. We’ll give a brief overview of the most useful operators.
If you want to know all JavaScript operators, read the JavaScript Operators Reference at W3Schools. |
4.2.1. Arithmetic operators
Arithmetic operators are used to make calculations with numbers. These are the ones you’ve learned in mathematics lessons in primary school. You add numbers with +
and subtract them with -
. For multiplication you use *
and for division /
.
Another interesting operator is %
, the modulus operator. This computes the remainder of the division of the first number by the second:
var remainder = 7 % 3;
The value of remainder
is 1, because 7 divided by 3 is 2 with remainder 1.
Especially useful are the increment (++) and decrement (--
) operators. They’re generally used to add or subtract one to a counter variable:
var x = 5; x++;
After this, x
has the value 6.
You’ll see an example of this later in this module when we’re covering loops.
4.2.2. Assignment operators
You’ve used the basic assignment operator every time you assigned a value to a variable in JavaScript:
var x = 5; var y = x;
There are some shorthand assignment operators that do an arithmetical calculation and assignment in one time. For instance, instead of x = x + y
, you can use x += y
.
4.2.3. String operators
You’ve seen how to concatenate strings with the concat
method. However, the +
operator does the same when used on strings:
var item1 = "apples"; var item2 = "bananas"; var items = item1 + " and " + item2;
After this, the variable items
contains the string "apples and bananas".
The +=
assignment operator also works for strings:
var item1 = "apple"; item1 += " pie";
Now item1
contains the string "apple pie".
4.2.4. Comparison operators
A lot of times, you need to compare two variables, or a variable to a type. We can use this to fix another bug in the signal handler of the add button. Maybe you didn’t notice, but have you tried clicking the add button without entering a text in the text field? The app happily adds an empty item to the shopping then.
Let’s fix this issue in the onClicked
signal handler:
onClicked: { if(textFieldInput.text != "") { shoppinglistModel.addItem(textFieldInput.text); textFieldInput.text = ""; } }
So if you click on the add button now, the signal handler first checks whether the text field has a text typed into it. If it has (textFieldInput.text
doesn’t equal the empty string ""), the text is added to an item in the shopping list, and after that the text of the text field is made empty. If the text field didn’t have a text when clicked on the button, nothing happens.
The if
statement only executes the code block between the curly braces if the condition between parentheses is true. In this case the condition uses the !=
operator, in textFieldInput.text != ""
. This operator returns true when both sides are not equal.
The ==
operator, on the other hand, returns true when both sides are equal. Other comparison operators are >
(greater than), <
(less than), >=
(greater than or equal to), and <=
(less than or equal to).
4.2.5. Logical operators
Logical operators are useful to change or combine logical expressions.
For instance, the !
(not) operator returns true when its operand is false and false otherwise. So the expression !(x == y)
is the same as x != y
.
You should use parentheses ( and ) to make the precedence of operators clear.
|
The &&
(and) operator returns true only if both operands are true. In all other cases it returns false. For instance, with (x >= 1 && x <=10)
is true if x is a number between 1 and 10. In all other cases the expression evaluates to false.
In the same vein, the ||
(or) operator returns true if at least one of both operands is true. It only returns false is both operands are false.
4.2.6. Ternary operator
One JavaScript expression that is regularly used is the ternary operator, also called the conditional operator. This operator takes three operands: a condition, followed by a question mark (?
), then an expression to execute if the condition is true, followed by a colon (:
), and finally the expression to execure if the condition is false.
Let’s give an example. What if you want to give items in your shopping list another background color depending on their position in the list being even or odd? You can do this by replacing the Text
element in the ListItem
by the following rectangle code:
Rectangle { anchors.fill: parent color: index % 2 ? theme.palette.normal.selection : theme.palette.normal.background Text { text: name } }
This creates a rectangle, lets it fill the full dimensions of its parent (the list item), and puts a text inside it.
What’s special about this rectangle is the expression bound to its color
property: index % 2 ? theme.palette.normal.selection : theme.palette.normal.background
. This evaluates index % 2
, which computes the remainder of the division of index
by 2. If this is 1, the index is odd; if this is 0, the index is even. The value 1 gets evaluated as true, so the first expression after the question mark is returned. In the other case, the second expression is returned. This gives items on an odd position the background color theme.palette.normal.selection
and items on an even position the background color theme.palette.normal.background
.
By the way, if you find the ternary operator syntax difficult to follow, the above code for the color
property is equivalent to the following JavaScript code with an if
statement:
color: { if(index % 2) return theme.palette.normal.selection; else return theme.palette.normal.background; }
This executed the first code block if the condition after if
is true, and the second code block in the other case.
index is a special property that is available inside a list item delegate. It corresponds to the index of the element in the associated list model.
|
If you run this code and add a few items, you’ll see the different background colors:
5. Removing items from the shopping list
Now that you have some more knowledge about JavaScript, let’s implement the signal handlers for the buttons at the bottom to remove all or only the selected items.
The onDoAction
signal handler for the dialog to remove all items is actually quite simple:
onDoAction: shoppinglistModel.clear()
This uses the clear() method from the ListModel
component.
We’ve also added a button to remove selected items. But before we can implement its signal handler, we first should have a way to select items. A common way to do this is to let the user press or long-press on an item and then show a checkbox before all items. The user can then select one or more of these checkboxes and then click on the button to remove all selected items.
We’ll implement this way step by step.
5.1. Creating a property for selection mode
First you’ll have to recognize that our app will have two modes: in selection mode or not. So we should store the current mode in a property of the MainView
element:
property bool selectionMode: false
This is the first time you added your own custom property to a QML object. This is done with the property
keyword, then the type of the property, then the name, followed by a colon (:
) and the property’s initial value. After this, the property can be used the same way as built-in properties.
Then we create a checkbox before each text in a list item. It should only be shown in selection mode, so we use the property root.selectionMode
in a few places (root
is the ID of the MainView
element). We also have to change the Text
element:
CheckBox { id: itemCheckbox visible: root.selectionMode anchors { left: parent.left leftMargin: units.gu(2) verticalCenter: parent.verticalCenter } } Text { id: itemText text: name anchors { left: root.selectionMode ? itemCheckbox.right : parent.left leftMargin: root.selectionMode ? units.gu(1) : units.gu(2) verticalCenter: parent.verticalCenter } }
As you see, the CheckBox
component is only visible if root.selectionMode
is true. Because we set it to false by default, the checkbox won’t be visible when the app starts. For the text, we let the left
and leftMargin
properties depend on the selection mode too. In selection mode, the left of the text is anchored to the right of the checkbox and its left margin is 1 grid unit. When not in selection mode, the left of the text is anchored to the left of the parent, with a left margin of 2 grid units.
Run the app. You don’t see any difference compared to our previous version. Now change the value of the selectionMode
property of the MainView
to true. If you run the app again, there’s a checkbox shown at the left of each item you add:
Now change back the default value of the selectionMode
property to false, because we’re going to let you set the selection mode in the app.
5.2. Adding the selection status to the list model
First we need to adapt the list model: each list element not only needs to hold the item text, but also its selection status. So change the ListModel
object to:
ListModel { id: shoppinglistModel function addItem(name, selected) { shoppinglistModel.append({"name": name, "selected": selected}) } }
In the onClicked
signal handler of the Button
object with ID buttonAdd
, you need to change the addItem
method call to:
shoppinglistModel.addItem(textFieldInput.text, false);
So a newly added item isn’t selected, because you call the method with false
as the value for the selected
argument.
5.3. Using a MouseArea to handle mouse events
Now to set the selection mode yourself, we need a new component: MouseArea. This is an invisible item that provides mouse handling. It has a signal pressAndHold
that is emitted when you do a long-press (800 ms). We’ll react to this signal in the corresponding onPressAndHold
signal handler. So add the following MouseArea
object after the Text
object, but still inside the Rectangle
:
MouseArea { anchors.fill: parent onPressAndHold: root.selectionMode = true; onClicked: { if(root.selectionMode) { shoppinglistModel.get(index).selected = !shoppinglistModel.get(index).selected; shoppinglistView.refresh(); } } }
With anchors.fill
we let the mouse area fill the whole area of the rectangle that contains it. Thanks to the onPressAndHold
signal handler, the app shows a checkbox before each item in the list if you long-press on an item. You can then select items in the list by clicking on a list item. It doesn’t matter whether you click on the checkbox, the text, or next to the text: the onClicked
signal handler of the mouse area catches all those events.
You could also use the onClicked signal handler of the CheckBox , but then it would only react on the checkbox area. Our solution with the MouseArea is more user-friendly.
|
In this signal handler the selected
property of the list element in the list model corresponding to the current list item in the list view is toggled: its new value is the negation (the logical operator !
) of its old value. We can do this because this is a Boolean
value: true or false.
Then we need to refresh the list view, so it shows the status of the checkbox. We do this with the following method added to the ListView
object:
function refresh() { // Refresh the list to update the selected status var tmp = model; model = null; model = tmp; }
This is a neat trick: we store the current model associated with the list view in a temporary value, set the model to null
and then reset the model to the initial value. The app responds by redrawing the list view according to the new model.
We also need to set the checkbox status. We can do this with the following property binding in the CheckBox
element:
checked: shoppinglistModel.get(index).selected
5.4. Remove selected items
Now the only thing we need to implement is the onDoAction
signal handler of the component with ID removeSelectedDialog
. This should remove all items with a checked checkbox, and then turn off selection mode. Let’s add a method removeSelectedItems
to the ListModel
object:
function removeSelectedItems() { for(var i=shoppinglistModel.count-1; i>=0; i--) { if(shoppinglistModel.get(i).selected) shoppinglistModel.remove(i); } }
This function introduces an important statement in JavaScript: a for
loop that iterates over all elements in the shopping list model.
A for
loop iterates over a code block. Between the parentheses after the for
keywoard, there are three components:
-
Initialization of the loop: This initializes the variable
i
to the number of elements in the shopping list model decreased by one. -
Condition: The code block in the loop is executed as long as this condition is true, in this case as long as the variable
i
is greater than or equal to 0. -
Decrement: The variable
i
is decremented after every execution of the block in the loop.
So, for instance, if your shopping list model has five elements, the code block is executed five times, and every time i
gets a new value: 4, 3, 2, 1, and 0. After this, the loop ends.
Now inside the for
loop’s code block, the if
block checks the selection status of each element. We use the loop’s variable i
as an index to the get method of the ListModel
object. And if that element’s selected
property is true, we remove the element.
The index of an element in a ListModel is counted from 0: the first element in the list model has index 0, and the last element has the number of elements - 1 as its index. That’s why we initialize the variable i to shoppinglistModel.count-1 in the loop.
|
The signal handler of the dialog to remove all selected items then looks like this:
onDoAction: { shoppinglistModel.removeSelectedItems(); root.selectionMode = false; }
So if you know long-press on the item list, select a few items and click on the "Remove selected items" button and confirm, all selected items are removed from the list, and the app leaves selection mode, so no checkboxes are shown.
Now you just have to add a signal in OKCancelDialog.qml so you can also have a signal handler onCancelAction
that just leaves selection mode. We’ll keep this as an exercise for the reader.