[Repeater][SQLite] Make lists from Database data

Hello,

I’m trying to insert the values from a Database (SQLite) into a MenuItem (so with a repeater). The database consists of two tables with text data. As such: xx|XXXX. The end goal being to retrieve all lines from the DB and make a MenuItem of each line. I think the Repeater is the optimal solution.

As you can see in the code bellow, I have a function “get()” that actually gets the data from the DB. And then I call that function from within the Repeater, so it can be used for the Model.

That’s where it doesn’t workout for me, no data gets to the Repeater part (or the Model), even when using a global function. I don’t think (and i tried), putting the function inside the Repeater would be the correct option either.

ComboBox {
id: comboBox
label: “List”

            function getDatabase() {
                return LocalStorage.openDatabaseSync("MyDatabase", "1", "My DataBase", 100000);
             }
            function get() {
              var db = getDatabase();
               db.transaction(function(tx) {
                   var rs = tx.executeSql('SELECT * FROM Table1');
                   var r = "";
                   for (var i = 0; i < rs.rows.length; i++) {
                       r += rs.rows.item(i).table1_value + "\n"
                   }
                   return r;
                }
               );
            }

        menu : ContextMenu {
            id: contextMenu

                    Repeater {
                        id: repeaterLang

                        Component.onCompleted: comboBox.get()
                        
                        model: comboBox.get()

                        MenuItem {
                            id: menuItem
                            text: "%1".arg(modelData)

                        }
                    }
                }
        }

Do you have any suggestion or code examples ?

Thank you !

Your get() function doesn’t return anything.

The return r is lost because it’s from the anonymous calllback function.

1 Like

I’d define a ListModel and a populate function, and call populate(r) from the callback to fill it.

Do you have an example maybe ? I don’t know the populate function yet.

Well you will have to write one :wink:

My own code is always very convoluted because I have a kind of stream-of-consciousness method of coding.

Also I don’t have an app where I use LocalStorage.

But.

What you have is a data source which returns a list of things. That is common among LocalStorage, a DBus call, or an XMLHttpRequest.
Also, the method of getting the list of things involves a callback function, and you want to popuplate a QML datatype.

So what I do usually is something like this (pseudo-QML):

ListModel { id: myModel } // this is an empty model
function populateModel(data) { // where data is an array

    for (i=0; i<data.length; i++) {
       // ... mangle the input data to something that can be added to a listmodel
       var mangledElement = // whatever you need to do with data[i];
       myModel.append(mangledElement);
    }
}

// data source, pretty much similar for XMLHTTPRequest, DBus calls, or LocalStorage:
WhateverInterface {
   function getData(foo, bar, function(re) {
        console.debug("Got data:", JSON.stringify(re, null, 4);
        populateModel(re);
   });
}

You can see this pattern here:

Another app of mine does it the same way:

Note, I am very much self-taught-through-trial-and-error QML coder, these might not be the best of examples, might even be outright misuse of the languages, but I think the basic concept is sound.

1 Like

A note about ListModel.append():

To use append() from javascript, you need to pass an object to that function.

So where you would normally construct such a model like this:

import QtQuick 2.0

ListModel { id: fruitModel
    ListElement { name: "Apple"; cost: 2.45 }
    ListElement { name: "Banana"; cost: 3.25 }
 }

… from JS you would:

function addFruit() {
    var a = {};
    a.name = "Apple";
    a.cost = 2.45;
    fruitModel.append(a);

    // alternatively
    var b = {
        "name": "Banana",
        "cost": 3.25
    };
    fruitModel.append(b);
}

as you can see, the JS object keys become model “roles” in the ListModel.

This is a primitive use of Localstorage, cribbed from another app that I updated to keep it going. DB functions in:

The id: sourcesListDelegate is used in a SilicaListView in: https://github.com/poetaster/allthenews/blob/main/qml/pages/SourcesPage.qml

It’s a primitive example.

Heh. I just realized that this is a ‘bad’ example for your purposes. I’m actually on reading ‘settings’ from via Localstorage. I’ll find a more suitable example.

Sorry, better example:

The delegate: https://github.com/poetaster/harbour-dwd/blob/main/qml/delegates/LocationItem.qml

Used in this list view: https://github.com/poetaster/harbour-dwd/blob/main/qml/pages/StartPage.qml

With js in: https://github.com/poetaster/harbour-dwd/blob/main/qml/js/storage.js

Not exactly poster child code :slight_smile:

Hi @poetaster , I tried using your code witch seemed more accessible for me. But I still fail to create “content”.
//here the database is filled with data. But no MenuItem is created.

I understand that in your code you crafted a specific delegate called “LocationItem” which creates this table “city”|“delete option” in the Stored Locations. But shouldn’t I be able just by specifying “MenuItem” to create the MenuItems for each DB entry ?

IE:

    function getDatabase2() {
    return LocalStorage.openDatabaseSync("LanguageOptions",    "0.1", "All Language Options", 100000);
 }
function getLanguages() {
  var db = getDatabase2();
  var returned_languages
   db.transaction(function tempo (tx) {
       var selectLang = tx.executeSql('SELECT * FROM Languages');
       var listOfLangs
       for (var i = 0; i < selectLang.rows.length; i++) {
           listOfLangs += selectLang.rows.item(i).language_code + "\n"
       }
       returned_languages = listOfLangs;
    }
   );
    //console.log(returned_languages) - returns: "XX" "YY" "WW" as expected
    return returned_languages
}

SilicaFlickable {
    anchors.fill: parent

    Column {
        width: parent.width

        SilicaListView {
            id:listView
            
            model:   ListModel {
                id: listModel
                function update() {
                    getLanguages()
                }
                Component.onCompleted:update()
            }
            delegate: MenuItem {
                id:locations
            }
        }

I also tried building your app to play arround and understand the working parts but when building it i get the Installed (but unpackaged) file(s) found: /usr/bin/harbour-dwd.debug error (i cannot find that file in the repo).

The dwd app contains a ‘dump’ of location names with gps coordinates (in json format read from file in /usr/share/harbour-dwd/qml/pages/LocationSearchPage.qml). These are read and ‘filtered’ when you do a search term (or via gps_coords). The database storage is done when you click on one of the filtered selections.

In /LocationSearchPage.qml look for:

    onClicked: {
       Store.addLocation(model);

which uses the javascript
In /usr/share/harbour-dwd/qml/js/storage.js

function addLocation(locationData)

In this case, directly after storing, the pageStack is popped in you return to the ‘main’ page which now has a new entry to read / show…

I could have simply created a db with those locations, but I wasn’t certain how stable the list (obtained from the DWD) is, so I stuck to the json I can fetch from them.

That sounds like something is a bit wonky with the sdk. The debug file would be dynamically created but (so not packaged), but I’ve not seen it befor.

You need to append to the listModel, not return the list :

       for (var i = 0; i < selectLang.rows.length; i++) {
           listModel.append(selectLang.rows.item(i).language_code)
       }
1 Like

MenuItem is a bit special in that it is supposed to be created within a menu such as PullDownMenu or ContextMenu. It won’t work well as a delegate in a ListView (as you see).

So you can either Create a menu and attach it somewhere:

ContextMenu { id: ctxmenu
    Repeater {
        model: listModel
        delegate: MenuItem { text: modelData }
    }
}

ListItem {
    menu: ctxmenu
}

or create a different delegate:

        SilicaListView {
            id:listView
            
            model: listModel
            delegate: ListItem {
                id:locations
                Label { text: modelData }
            }
        }
1 Like

Ah, dear. I completely missed the menu. Menus, with the versions of qt at our disposal are kinda suck. I try to avoid them. I tend to use them where I have a small number of static elements.

Thank you both as this led me to the right path I think.
But the data doesn’t updates itself after being changed (.append). I think I mixed both of your solutions, but even based on the doc about the ListModel there is no mention of a need to “refresh” the ListModel to get the new data displayed. I followed your tips and moved out of the Menu/ContextMenu display to the list one. Here the data is indeed appended but the displayed text remains “text2”.

I also don’t get the need for the initial ListModel declaration as I can just call the function from the ListView itself, but nothing will be displayed then - but the values are there.

Page {

ListModel{
    id: listModel
    ListElement {
        name : "text2"
    }
}

function getDatabase2() {
    return LocalStorage.openDatabaseSync("LanguageOptions5",    "0.1", "All Language Options", 100000);
 }

function getLanguages() {
  var db = getDatabase2();
   db.transaction(function tempo (tx) {
       var selectLang = tx.executeSql('SELECT * FROM Languages');
       var listOfLangs
       for (var i = 0; i < selectLang.rows.length; i++) {
            listModel.append({"name": selectLang.rows.item(i).language_code})
       }
   }
   );
}

SilicaFlickable {
    anchors.fill: parent

    Column {
        width: parent.width

        SilicaListView {
            id:listView
            model: listModel //i can also just { getLanguages() }
            delegate: ListItem {
                Label { text: modelData }
            }
        }

        Button {
            text: "Click me"
            onClicked: {
                getLanguages()
            }
                //listModel.append({"name": "Doe"}) - works
        }

        Button {
            text: "Check me"
            onClicked: {
                console.log("zero", listModel.get(0).name)
                console.log("one", listModel.get(1).name)
                console.log("two", listModel.get(2).name)
                console.log("nine", listModel.get(9).name)
            }

        }
    }
}

}

In your ListItem, try text: name, now that you have a name role in your model.

1 Like

I tried by setting

Label { text: modelData }

to

Label { text: name }

but the result is the same. Is that what you meant ?

1 Like

So I’ve been trying to find a way, but I’m unable to update the data (aka the name value) when the button is pressed. But no even the second ListElement aka “text2” is displayed in the app.

Page {

ListModel{
    id: listModel
    ListElement {
        name : "text"
    }
   ListElement {
       name : "text2"
   }
}

function getDatabase2() {
    return LocalStorage.openDatabaseSync("LanguageOptions5",    "0.1", "All Language Options", 100000);
 }

function getLanguages() {
  var db = getDatabase2();
   db.transaction(function tempo (tx) {
       var selectLang = tx.executeSql('SELECT * FROM Languages');
       var listOfLangs
       for (var i = 0; i < selectLang.rows.length; i++) {
            listModel.append({"name": selectLang.rows.item(i).language_code})
       }
   }
   );
}

SilicaFlickable {
    anchors.fill: parent

    Column {
        width: parent.width

        SilicaListView {
            id:listView
            model: listModel
            delegate: ListItem {
                Label { text: name }
            }
        }
            Button {
            text: "Click me"
            onClicked: {
                getLanguages()
            }
                //listModel.append({"name": "Doe"}) - works
        }