Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

How to add Items dynamically to a Row? (in javascript)



  • Hi,
    I'm just learning QML and I'd like to factorize the creation of a Row using a js function.
    So basically in a js function create an Item using Qt.createComponent, affect all the properties and then add it to my Row.
    Is that possible?
    I'd expect sth like this

        Row {
            id: toolBar
            anchors.bottom: parent.bottom
            anchors.bottomMargin: 20;
            anchors.horizontalCenter: parent.horizontalCenter
            spacing: 15
    
            height: 50
    
            function addItem(idName, imagePath){
                console.log("adding item: " + idName);
                var button = Qt.createComponent("ToolBarButton.qml");
                button.id = idName;
                button.imagePath = imagePath;
                button.height = toolBar.height;
                button.width = 50;
                button.state = "Inactive";
                button.barIndex = toolBar.children.length;
                toolBar.add(button);
            }
    
            Component.onCompleted: {
                addItem("home", "img/home.png");
            }
        }
    
    

    But I'm getting the error qrc:/main.qml:131: TypeError: Property 'add' of object QQuickRow_QML_2(0x56297448e670) is not a function



  • hi

    @mbruel said in How to add Items dynamically to a Row? (in javascript):

    toolBar.add(button);

    @mbruel said in How to add Items dynamically to a Row? (in javascript):

    TypeError: Property 'add' of object QQuickRow_QML_2

    QML Row type has no add mathod

    see Dynamic QML Object Creation from JavaScript

    example

        Component.onCompleted: {
            createButton();
        }
    
        Row{
            id: row
            width: parent.width
            height: 50
        }
    
        function createButton() {
            var btn = btnComponent.createObject(row,{ text : "myButton"});
        }
    
        Component{
            id: btnComponent
            Button{}
        }
    


  • hum I see, so I've updated my code to be:

            function addItem(idName, imagePath){
                console.log("adding item: " + idName);
                var c = Qt.createComponent("ToolBarButton.qml");
                var button = c.createObject(toolBar, {
                                                        id: idName,
                                                        imagePath: imagePath,
                                                        height: toolBar.height,
                                                        width: 50,
                                                        state: "Inactive",
                                                        barIndex: toolBar.children.length
                                                    });
            }
    
            Component.onCompleted: {
                addItem("profile", "img/profile.png");
            }
    
            Connections{
                target: profile
                onSelected: changeMainMenu(idx);
            }
    

    I'm getting now getting the issue: qrc:/main.qml:186: ReferenceError: profile is not defined

    So if I understand well I can't set the id of an object created dynamically? Is that right?
    This wouldn't be such a big deal but then how can I connect a signal of that object created dynamically?



  • @mbruel

    function createButton() {
           var btn = btnComponent.createObject(row,{ text : "myButton"});
           btn.onClicked.connect(btnClicked)
       }
    
       function btnClicked(){console.log("button clicked")}
    


  • great, I got what I wanted with that piece of code:

        Row {
            id: toolBar
            anchors.bottom: parent.bottom
            anchors.bottomMargin: 20;
            anchors.horizontalCenter: parent.horizontalCenter
            spacing: 15
    
            height: 50
    
    
            function addItem(idName){
                console.log("adding item: " + idName);
                var c = Qt.createComponent("ToolBarButton.qml");
                var barIdx = toolBar.children.length;
                var button = c.createObject(toolBar, {
                                                        id: idName,
                                                        imagePath: "img/"+idName+".png",
                                                        height: toolBar.height,
                                                        width: 50,
                                                        state: barIdx === 0 ? "Selected": "Inactive",
                                                        barIndex: toolBar.children.length
                                                    });
                button.onSelected.connect(changeMainMenu);
            }
    
    
            Component.onCompleted: {
                addItem("home");
                addItem("mail");
                addItem("compas");
                addItem("stack");
                addItem("profile");
            }
        }
    

    my ToolBarButton Item is emitting the "selected" signal.

    Last question that is a bit out of the scope of this thread. How can I create a List of string that I would iterate in my onCompleted method?
    Like a property or simple variable of my root Window.



  • @mbruel said in How to add Items dynamically to a Row? (in javascript):

    Last question that is a bit out of the scope of this thread. How can I create a List of string that I would iterate in my onCompleted method?

    Like a property or simple variable of my root Window.

    https://doc.qt.io/qt-5/qtqml-syntax-objectattributes.html

    property var someList: [ "three", "four"]
    


  • great, @LeLev you rock!

    here is the updated code:

        Row {
            property var menuList : ["home", "mail", "compas", "stack", "profile"]
            
            id: toolBar
            anchors.bottom: parent.bottom
            anchors.bottomMargin: 20;
            anchors.horizontalCenter: parent.horizontalCenter
            spacing: 15
    
            height: 50
    
    
            function addItem(idName){
                console.log("adding item: " + idName);
                var c = Qt.createComponent("ToolBarButton.qml");
                var barIdx = toolBar.children.length;
                var button = c.createObject(toolBar, {
                                                        id: idName,
                                                        imagePath: "img/"+idName+".png",
                                                        height: toolBar.height,
                                                        width: 50,
                                                        state: barIdx === 0 ? "Selected": "Inactive",
                                                        barIndex: toolBar.children.length
                                                    });
                button.onSelected.connect(changeMainMenu);
            }
    
            Component.onCompleted: {
                for (var i=0; i < menuList.length; ++i)
                    addItem(menuList[i]);
            }
        }
    


  • Here is another way to add unique items using a repeater and a loader (see last post):
    https://forum.qt.io/topic/117076/random-customs-controls-in-a-row/3



  • @fcarney
    sounds less intuitive but I'll have a look, thanks ;)


  • Qt Champions 2018

    You should refrain from using Qt.createComponent and manual dynamic instantiation in QML.

    Stay declarative.

    Your task can be achieved like so :

    Row {
       id: toolBar
       property var menuList : ["home", "mail", "compas", "stack", "profile"]
       property int currentIndex: 0
        
        anchors {
            bottom: parent.bottom
            bottomMargin: 20;
            horizontalCenter: parent.horizontalCenter
        }
        spacing: 15
        height: 50
    
        Repeater {
            model: toolBar.menuList
            ToolBarButton {
                id: button
                imagePath: "img/" + model.modelData + ".png"
                height: toolBar.height
                width: 50
                barIndex: model.index
                state:barIndex === toolBar.currentIndex ?  "Selected" : "Inactive"
                onSelected: toolBar.currentIndex = barIndex
            }
        }
    }
    

    I also changed the selected button logic here to be self contained and because I didn't know what were the definition of your selected signal or changeMainMenu function.



  • @GrecKo
    Nice, thanks a lot!
    Indeed it looks much better that way. I've checked the Repeater documentation and it's defo what I should use.
    Here is the updated code which is nearly exactly what you gave me:

        Row {
            id: toolBar
            anchors {
                bottom: parent.bottom
                bottomMargin: toolBarMargin;
                horizontalCenter: parent.horizontalCenter
            }
    
            height:  toolBarHeight
            spacing: toolBarSpacing
    
            Repeater {
                model: menuList
                ToolBarButton {
                    id: button
                    imagePath: "img/" + model.modelData + ".png"
                    height: toolBar.height
                    width: toolBarButtonWidth
                    barIndex: model.index
                    state: model.index === 0 ?  "Selected" : "Inactive"
                    onSelected: changeMainMenu(barIndex);
                }
            }
    
            Component.onCompleted: {
                toolBar.children[1].setBadge("3");
                toolBar.children[3].setBadge("12");
            }
        }
    

    The result is something like this:
    alt text

    My ToolBarButton have their opacity changing whether their state is Inactive, Hovered or Selected. When they emit the selected signal, the changeMainMenu function would change the state of the other buttons and the content of the main area (not sure yet how I'll achieve that, probably using a StackedView)

        function changeMainMenu(selectedMenu){
            console.log("New idx: "+selectedMenu)
            for (var i = 0; i < toolBar.children.length; ++i)
            {
                if (i !== selectedMenu)
                    toolBar.children[i].state = "Inactive";
            }
        }
    

  • Qt Champions 2018

    @mbruel said in How to add Items dynamically to a Row? (in javascript):

    My ToolBarButton have their opacity changing whether their state is Inactive, Hovered or Selected. When they emit the selected signal, the changeMainMenu function would change the state of the other buttons and the content of the main area (not sure yet how I'll achieve that, probably using a StackedView)

    That's still not very declarative :)
    Act on your data, not your visual items.
    Items should follow the data.

    I believe my proposed solution is better, maybe relocate the currentIndex property if you want.

    Your solution:

               // in your main or elsewhere:
               function changeMainMenu(selectedMenu) {
                    for (var i = 0; i < toolBar.children.length; ++i) {
                          if (i !== selectedMenu)    
                              toolBar.children[i].state = "Inactive";
                    }
                }
               // in your button
                state: model.index === 0 ?  "Selected" : "Inactive"
                onSelected: changeMainMenu(barIndex);
    

    You manipulate the properties of your items imperatively, you define in the state binding of the button (not really an actual binding since it will be overwritten by the changeMainMenu function anyway) that the first button is selected

    My solution:

                // in the toolbar or elsewhere:
                property int selectedIndex: 0 // 0 is the initial selectedIndex
                // in your buttons
                state: barIndex === toolBar.selectedIndex ?  "Selected" : "Inactive"    
                onClicked: toolBar.selectedIndex = barIndex
                // ^ note that I changed it to a clicked signal, the button just tells how the user interacted with it
                //   it doesn't set itself as selected, the state binding does that.
                //   both your parent code and the button code becomes simpler
    

    @mbruel said in How to add Items dynamically to a Row? (in javascript):

        Component.onCompleted: {
            toolBar.children[1].setBadge("3");
            toolBar.children[3].setBadge("12");
        }
    

    Put that information in your model instead:

    Repeater {
        model: ListModel { // an array of JS objects would work here too
            ListElement { image: "home"; badge: 0 }
            ListElement { image: "mail"; badge: 3 }
            ListElement { image: "compas"; badge: 0 }
            ListElement { image: "stack"; badge: 12 }
            ListElement { image: "profile"; badge: 0 } 
        }
        ToolBarButton {
            ...
            imagePath: "img/" + model.image+ ".png"
            badge: model.badge
        }
    }
    

    Sorry if I'm being a purist, I just want to share my vision of how one should write QML ;)

    I should also mention that I'm not a fan of the state property, but that's more opinion based.
    In your case I would add a property bool selected property. Bind like so in the repeater: selected: model.index === selectedIndex
    and in the button itself do: opacity: selected ? 1 : (hovered ? 0.7 : 0.4)



  • @GrecKo said in How to add Items dynamically to a Row? (in javascript):

            state: barIndex === toolBar.selectedIndex ?  "Selected" : "Inactive"    
            onClicked: toolBar.selectedIndex = barIndex
    

    Hum I don't catch something.... The "state binding" doesn't seem to work correctly.
    When I click on a button, so the selectedIndex "main variable" is set to the index of my button but then the state of the other buttons is not updated due to the change of the value of selectedIndex.
    What in your solution would trigger that? (What my changeMainMenu is actually doing)

    Put that information in your model instead

    Cool will do, it sounds better but for now it is just a mock, I would get this info from C++. Hum, still for the initialization I could use that ListModel. I didn't read the QML model view framework yet...

    Sorry if I'm being a purist, I just want to share my vision of how one should write QML ;)

    don't be sorry, that's exactly what I'm looking for, get the best practices asap. Thanks for helping ;)

    I should also mention that I'm not a fan of the state property, but that's more opinion based.

    well I'm not a big fan either of states but I saw that in an example and it seems the easiest way to use Transitions.

    // an array of JS objects would work here too

    how you do that?

    Otherwise for the mainArea I used a Loader that is reloaded in my changeMainMenu function. How would this work with your solution?

    Here is my full main.qml:

    import QtQuick 2.12
    import QtQuick.Window 2.12
    
    Window {
        id: root
    
        visible: true
        width: 400
        height: 600
        title: qsTr("my QML app!")
    
        property int toolBarIndex       : 0
        property int toolBarHeight      : 50;
        property int toolBarSpacing     : 15;
        property int toolBarMargin      : 20;
        property int toolBarButtonWidth : 50;
    
        property int headerButtonSize   : 30;
        property int mainMargin         : 20;
    
    //    property var menuList : ["home", "mail", "compas", "stack", "profile"]
        ListModel {
            id: toolBarModel
    
            ListElement { name : "home"    ; badge: "0"  ; color: "lightgreen" }
            ListElement { name : "mail"    ; badge: "3"  ; color: "lightblue" }
            ListElement { name : "compas"  ; badge: "0"  ; color: "lightgray" }
            ListElement { name : "stack"   ; badge: "12" ; color: "lightyellow" }
            ListElement { name : "profile" ; badge: "0"  ; color: "ivory" }
        }
    
        Rectangle {
            id: background
            anchors {
                fill: parent;
                margins: 0;
            }
            color: "green"
            Image {
                source: "img/bg.png";
                fillMode: Image.Stretch;
                anchors.fill: parent;
                opacity: 1
            }
        }
    
    
        Loader {
            id: mainArea
            anchors {
                top: parent.top
                left: parent.left
                topMargin: mainMargin + headerButtonSize / 2;
                bottomMargin: mainMargin
                leftMargin: mainMargin
                rightMargin: mainMargin
            }
            width: parent.width - 2*mainMargin
            height: parent.height - 2*mainMargin - toolBarHeight - toolBarMargin
        }
    
        ToolBarButton
        {
            id: search
            anchors {
                top: parent.top
                left: parent.left
                margins: mainMargin;
            }
            width : headerButtonSize
            height: headerButtonSize
    
            imagePath : "img/search.png"
            state     : "Inactive"
            selectable: false
        }
    
        ToolBarButton
        {
            id: chat
            anchors {
                top: parent.top
                right: parent.right
                margins: mainMargin;
            }
            width : headerButtonSize
            height: headerButtonSize
    
            imagePath : "img/chat.png"
            state     : "Inactive"
            selectable: false
        }
    
        Text {
            text: title
            anchors {
                top: parent.top
                horizontalCenter: parent.horizontalCenter
                topMargin: mainMargin;
            }
            color: "#ff0000"
            font.pointSize: 24
            font.bold: true
        }
    
        function changeMainMenu(selectedMenu){
            console.log("New idx: "+selectedMenu)
            for (var i = 0; i < toolBar.children.length; ++i)
            {
                if (i !== selectedMenu)
                    toolBar.children[i].state = "Inactive";
            }
    
            mainArea.setSource("SimpleMainArea.qml");
            mainArea.item.lbl.text = toolBarModel.get(selectedMenu).name;//menuList[selectedMenu];
            mainArea.item.color    = toolBarModel.get(selectedMenu).color;
        }
    
    
        Row {
            id: toolBar
            anchors {
                bottom: parent.bottom
                bottomMargin: toolBarMargin;
                horizontalCenter: parent.horizontalCenter
            }
    
            height : toolBarHeight
            spacing: toolBarSpacing
    
            Repeater {
                model: toolBarModel; //menuList
                ToolBarButton {
                    id: button
                    height: toolBar.height
                    width: toolBarButtonWidth
    
    //                imagePath: "img/" + model.modelData + ".png"
                    imagePath: "img/" + model.name + ".png"
                    barIndex: model.index
    //                state: root.toolBarIndex === barIndex ? "Selected" : "Inactive"
    //                onSelected: root.toolBarIndex = barIndex
                    state: barIndex === root.toolBarIndex ?  "Selected" : "Inactive"
                    onSelected: changeMainMenu(barIndex)
                }
            }
    
            Component.onCompleted: {
                for (var i = 0; i < toolBar.children.length; ++i)
                    toolBar.children[i].setBadge(toolBarModel.get(i).badge);
            }
        }
    
        Component.onCompleted: {
            changeMainMenu(0);
        }
    
    }
    
    

    The SimpleMainArea.qml

    import QtQuick 2.0
    import QtQuick.Controls 2.14
    
    Rectangle {
        property alias lbl: lbl
    
        radius: 10
        opacity: 0.8
    
        Text  {
            id: lbl
            anchors {
                horizontalCenter: parent.horizontalCenter
                verticalCenter: parent.verticalCenter;
            }
            text: "not set...";
        }
    }
    

    and my ToolBarButton.qml so you can tell me if you see bad practices ;)

    import QtQuick 2.12
    import QtQuick.Controls 2.14
    
    Item {
        id: button
    
        property string imagePath;
        property int    barIndex   : 0;
        property bool   selectable : true;
    
        property color color: "transparent"
    
        property double opacityInactive: 0.2
        property double opacityHover   : 0.4
        property double opacitySelected: 1
    
        property int borderWidth : 0
        property int borderRadius: 0
    
    
        property string previousState: "Inactive";
    
        property color badgeColor : "#ec3e3a";  // redish color (exactly the one used in OS X 10.10)
    
    
    
        // Allow the programmer to define the text to display.
        // Note that this control can display both text and numbers.
    //    property alias badgeText: badgeLbl.text
    
    
        signal selected(int idx);
    
    
    
        //RectangItemle to draw the button
        Rectangle {
            id: rect
    
            anchors.fill: parent
            radius: borderRadius
            color: button.enabled ? button.color : "grey"
    
            border.width: borderWidth
            border.color: "black"
    
            opacity: opacityInactive
    
            Image {
                id: img
                anchors.fill: parent;
                source: imagePath;
                fillMode: Image.PreserveAspectFit;
            }
        }
    
        Rectangle {
            id: badge
    
            visible: false
            smooth: true
    
            // Create an animation when the opacity changes
            Behavior on opacity {NumberAnimation{}}
    
            // Setup the anchors so that the badge appears on the bottom right
            // area of its parent
            anchors.right: rect.right
            anchors.top: rect.top
    
            // This margin allows us to be "a little outside" of the object in which
            // we add the badge
    //        anchors.margins: -parent.width / 5 + device.ratio(1)
    
            color: badgeColor
    
            // Make the rectangle a circle
            radius: width / 2
    
            // Setup height of the rectangle (the default is 18 pixels)
            height: 18
    
            // Make the rectangle and ellipse if the length of the text is bigger than 2 characters
            width: badgeLbl.text.length > 2 ? badgeLbl.paintedWidth + height / 2 : height
    
            // Create a label that will display the number of connected users.
            Label {
                id: badgeLbl
                color: "#fdfdfdfd"
                font.pixelSize: 9
                anchors.fill: parent
                verticalAlignment: Text.AlignVCenter
                horizontalAlignment: Text.AlignHCenter
    
                // We need to have the same margins as the badge so that the text
                // appears perfectly centered inside its parent.
                anchors.margins: parent.anchors.margins
            }
        }
    
    
        //Mouse area to react on click events
        MouseArea {
            hoverEnabled: true
            anchors.fill: button
            onEntered: {
                button.previousState = button.state;
                if (button.state != 'Selected')
                    button.state='Hover';
    
            }
    
            onExited : { button.state = previousState; }
    
            onClicked: {
                if (selectable)
                {
                    button.previousState = 'Selected';
                    button.state         = 'Selected';
                }
                button.selected(button.barIndex);
            }
        }
    
    
        function setBadge(msg)
        {
            if (msg !== "" && msg !== "0")
            {
                badge.visible = true;
                badgeLbl.text = msg;
    
                badge.width = badgeLbl.text.length > 2 ? badgeLbl.paintedWidth + badgeLbl.height / 2 : badgeLbl.height;
            }
            else
                badge.visible = false;
    
        }
    
        function clearBadge() {badge.visible = false;}
    
    
        //change the color of the button in differen button states
        states: [
            State {
                name: "Inactive"
                PropertyChanges {
                    target : rect
                    opacity: opacityInactive
                }
            },
            State {
                name: "Hover"
                PropertyChanges {
                    target : rect
                    opacity: opacityHover
                }
            },
            State {
                name: "Selected"
                PropertyChanges {
                    target : rect
                    opacity: opacitySelected
                }
            }
        ]
    
        state: previousState;
    
    
        //define transmission for the states
        transitions: [
            Transition {
                from: ""; to: "Hover"
                OpacityAnimator { duration: 200 }
            },
            Transition {
                from: "*"; to: "Selected"
                OpacityAnimator { duration: 10 }
            }
        ]
    }
    

    Edit: here is a small video of the desired behaviour

    Edit2: my commented

    //                state: root.toolBarIndex === barIndex ? "Selected" : "Inactive"
    //                onSelected: root.toolBarIndex = barIndex
    

    is not stable, it is working the first time but then it seems the change of root.toolBarIndex doesn't impact all the buttons.. Any idea why?


Log in to reply