QML for a JavaScript programmer

Although QML is technically a JavaScript extension, there are few issues a JavaScript programmer needs to be aware before designing the application. The typical advice from the Qt folks is “QML is not meant for JavaScript but for C++ UI” or “JavaScript is slow and you should not use it” which is not very helpful. Although all those statements are honest and at least mostly true, rest assured that applications can be programmed without C++ as long as your user interface data structures are flat objects or single dimension arrays (lists) of objects. But there are few things to know first:

The fundamental problem for the JavaScript programmer is that QML JavaScript is not really designed for JavaScript expressions but for Qt C++ objects. As the C++ and JavaScript data models are fundamentally different, the QML is designed to support only a limited set of the strongly typed QtObject derived objects and a superset of simple types over JavaScript types. Luckily, inside the JavaScript functions and namespace, you can use the full expressiveness of JavaScript but the interface between QML and JavaScript becomes C-like with named global variables declared in QML and passing the QML object references as arguments.

On the other hand, QML implements a beautiful namespace model for JavaScript. Each JavaScript file is loaded to a defined namespace in the beginning of the QML file: import ‘myapp.js’ as Code and any variables defined in the top level of the file gets loaded to the object “Code”. Also implicitly any initialization code on the top level of the file will be executed on load.

Design pattern

The design pattern I have used is to split the application to

  1. UI description in QML (myapp.qml)
  2. Application functionality in JavaScript (myapp.js)
  3. The Common data for both (in a well-defined place in the myapp.qml)
  4. Wrapper functions in QML file to create the “public api” between JavaScript and QML passing the necessary QML object references (in the myapp.qml).

The main issue even with this composition is that QML can not handle the JavaScript dynamic data structures. Even though one can declare a property variant a: [1, 2, 3] in QML, it is a readonly property. Any attempt to modify inside JavaScript silently (and miserably) fail. Do not waste your time attempting to hack yourself around it – you won’t.

However, if you stick only to one-dimensional list and grid views in the UI, you’ll be fine. The way to go is to use the built-in ListModel data structure as interface to JavaScript. It behaves quite like a JavaScript Array but with the limitation that all members have to be simple compound objects with exactly the same set of members, i.e list members can not be lists. The pattern is to define a empty ListModel in the QML as property ListModel myListModel: ListModel {} and wrap it with a JavaScript Array with all updates to the ListModel only through the wrapper and never use the ListModel member functions .get() .set() .clear() .append() etc. directly.

Code for a single threaded app

Here is my very simple boilerplate for a single threaded app:

myapp.js

  1. /*
  2.  * Array wrapper
  3.  */
  4.  
  5. // .attach() method to JavaScript Arrays: attaches a QML ListModel to an array
  6. Array.prototype.attach = function(listmodel) {
  7.     this.__model = listmodel;
  8.     this.flush();
  9. }
  10.  
  11. // .flush() method to JavaScript Arrays: updates the attached ListModel to the contents of the Array
  12. // automatically creates a role "value" for the ListModel if the array is flat
  13. Array.prototype.flush = function() {
  14.     var i;
  15.     while (this.__model.count > this.length) this.__model.remove(this.__model.count-1);
  16.     for (i = 0; i < this.__model.count; i++) this.__model.set(i, typeof this[i] === 'object' ? this[i] : {value: this[i]});
  17.     for (;i < this.length; i++) this.__model.append(typeof this[i] === 'object' ? this[i] : {value: this[i]});
  18. //  this.__model.sync();        // The model.sync() is only for updates in WorkerScript - see next example
  19. }
  20.  
  21. /*
  22.  * Application
  23.  */
  24. var myArray = [];   // This is the array to work with, you can initialize it here
  25.  
  26. var init = function(listModel) { // QML calls when ready
  27.  // Do the final initializations here
  28.  
  29.     myArray.attach(listModel);    // And finally attach the listModel to the array
  30. }
  31.  
  32. var arrayClickedAt = function(index) {   // mouse click callback for the array item [i]
  33.  // Do whatever magic needed to your array here..
  34.  
  35.     myArray.flush();    // and finally update the ListModel
  36. }

MyApp.qml

  1. import Qt 4.7
  2. import "myapp.js" as Code
  3.  
  4. Rectangle {
  5.     id: top
  6.     width: 360
  7.     height: 360
  8.     property ListModel list: ListModel {}
  9.     Component.onCompleted: Code.init(top.list);
  10.  
  11.     Component {
  12.         id: delegate
  13.         Text { // or whatever item
  14.             MouseArea { // Item click handler
  15.                 anchors.fill: parent
  16.                 onClicked: Code.listClickedAt(index); // implement the click handler in the JavaScript
  17.             }
  18.             // Implement delegate with full access to the array element properties. Use the role "value" if the array is flat
  19.             text: value
  20.         }
  21.     }
  22.  
  23.     ListView {
  24.         anchors.fill:  parent
  25.         model: top.list;
  26.         delegate: delegate;
  27.     }
  28. }

QML supports also the concept of a worker thread with nice integration to the ListModel, although the thread pool size seems to be only one in the current implementation. Worker code can not access the properties on the main thread and thus data needs to be passed through messages. ListModel is a special case and has a special .sync() method that can be conveniently integrated to the array wrapper.

Code for an app with UI and logic in separate threads

Here is the boilerplate for an app with UI and app logic in separate threads. Example takes long to populate 40 Fibonacci numbers to an array but shows the UI immediately.

Compare this code to the previous.

My2ThreadApp.qml

  1. import Qt 4.7
  2.  
  3. Rectangle {
  4.     id: top
  5.     width: 400
  6.     height: 800
  7.     property ListModel list: ListModel {}
  8.     Component.onCompleted: werk.sendMessage({msg: "init", arg: top.list});
  9.  
  10.     WorkerScript {
  11.         id: werk
  12.         source: "my2threadap.js"
  13.     }
  14.  
  15.     Component {
  16.         id: delegate
  17.         Text { // or whatever item
  18.             MouseArea { // Item click handler
  19.                 anchors.fill: parent
  20.                 onClicked: werk.sendMessage({msg: "click", arg: index}); // implement the click handler in the JavaScript
  21.             }
  22.             // Implement delegate with full access to the array element properties. Use the role "value" if the array is flat
  23.             text: value
  24.         }
  25.     }
  26.  
  27.     ListView {
  28.         anchors.fill:  parent
  29.         model: top.list;
  30.         delegate: delegate;
  31.     }
  32. }

h3. my2threadapp.js

  1. // .attach() method to JavaScript Arrays: attaches a QML ListModel to an array
  2. Array.prototype.attach = function(listmodel) {
  3.     this.__model = listmodel;
  4.     this.flush();
  5. }
  6.  
  7. // .flush() method to JavaScript Arrays: updates the attached ListModel to the contents of the Array
  8. // automatically creates a role "value" for the ListModel if the array is flat
  9. Array.prototype.flush = function() {
  10.     var i;
  11.     while (this.__model.count > this.length) this.__model.remove(this.__model.count-1);
  12.     for (i = 0; i < this.__model.count; i++) this.__model.set(i, typeof this[i] === 'object' ? this[i] : {value: this[i]});
  13.     for (;i < this.length; i++) this.__model.append(typeof this[i] === 'object' ? this[i] : {value: this[i]});
  14.     this.__model.sync();        // The model.sync() is for updates in WorkerScript
  15. }
  16.  
  17.  
  18. function fibonacci(n) { // heavy computing
  19.     if (n < 3) {
  20.         return 1;
  21.     } else {
  22.         return fibonacci(n-1)+fibonacci(n-2);
  23.     }
  24. }
  25.  
  26. var myArray = [];   // This is the array to work with
  27.  
  28. var init = function(list) { // QML is ready
  29.     var i, v;
  30.  
  31.     myArray.attach(list);    // Attach the top.list ListModel to the array
  32.  
  33.     for (i = 0; i < 40 ; i++) { // fill the array, this loop takes a long time
  34.         myArray[i] = fibonacci(i+1);
  35.         myArray.flush();    // update the ListModel
  36.     }
  37. }
  38.  
  39. var listClickedAt = function(index) {   // mouse click callback for the array item [i]
  40.     var half, i;
  41.  
  42.     if (index === 0) return;
  43.     half = myArray.splice(index, myArray.length-index);
  44.     myArray.unshift(half.shift());
  45.     for (i = 0; i < half.length; i++) myArray.push(half[i]);
  46.     myArray.flush();    // update the ListModel
  47. }
  48.  
  49. var messages = {
  50.     click: listClickedAt,
  51.     init: init
  52.     };
  53.  
  54. WorkerScript< webdata.count; i++) {
  55.      Code.processElement(webdata.get(i));
  56.     }
  57.    }
  58.   }
  59.  }
  60. // ....
  61. }

JavaScript is loaded to the same scope as the QML and thus can access any QML element by id. However, I find it most convenient to collect all properties that are shared between QML and JavaScript under one QtObject and pass it to the JavaScript side init() as argument to keep the object naming in control.

app.qml

  1. import "app.js" as Code
  2. // ....
  3.  QtObject {
  4.   id: common
  5.   property int n; // typeof a === 'number'
  6.   property string s; // typeof s === 'string'
  7.   property ListElement list: ListElement {}
  8.   Component.onCompleted: Code.init(common);
  9.  }

In fact, as JavaScript misses the capability to store JSON to the device local storage, I eventually ended up extending this concept with implementing a Storage component that persistently stores all its properties across application executions and does the Array augmentation. And it is 100% JavaScript. Duh.

Categories: