scopchanov
scopchanov

Reputation: 8399

How to bind to a property of a repeater-generated item outside of the repeater?

I would like to be able to bind to a property of an item generated by Repeater to do something with it, e.g. to show its coordinates. For that purpose I am using itemAt() like this:

ListModel {
    id: modelNodes

    ListElement { name: "Banana"; x: 100; y: 200 }
    ListElement { name: "Orange"; x: 150; y: 100 }
}

Repeater {
    id: foo
    model: modelNodes

    Rectangle {
        x: model.x; y: model.y
        width: textBox.implicitWidth + 20
        height: textBox.implicitHeight + 20
        color: "red"

        Drag.active: true

        Text {
            id: textBox
            anchors.centerIn: parent
            color: "white"
            text: model.name + ": " + foo.itemAt(index).x
        }

        MouseArea {
            anchors.fill: parent
            drag.target: parent
        }
    }
}

Text {
    id: moo

    Binding {
        target: moo
        property: "text"
        value: foo.itemAt(0).x + " -> " + foo.itemAt(1).x
    }
}

Inside the delegate this works fine, but when I attempt to use it outside of the Repeater (i.e. to bind moo's text to it), I get the following error:

TypeError: Cannot read property 'x' of null

How to fix this?

Upvotes: 1

Views: 1342

Answers (2)

GrecKo
GrecKo

Reputation: 7150

You don't.
(or more precisely, you shouldn't)

Delegates shouldn't store state or data, just display it or be able to interact with it. In your case what you are after is the data stored in the model.

Your solution should be to modify your model in your delegates and get the data from your model if you want.

I've created a small example of what I mean:

import QtQuick 2.15
import QtQuick.Window 2.12
import QtQuick.Controls 2.12

Window {
    visible: true
    width: 800
    height: 640

    ListModel {
        id: modelNodes

        ListElement { name: "Banana"; x: 50; y: 50 }
        ListElement { name: "Orange"; x: 50; y: 100 }
    }

    Row {
        anchors.centerIn: parent
        spacing: 1
        Repeater {
            model: 2 // display 2 copy of the delegates for demonstration purposes

            Rectangle {
                color: "transparent"
                width: 300
                height: 300
                border.width: 1

                Repeater {
                    id: foo
                    model: modelNodes

                    Rectangle {
                        x: model.x; y: model.y
                        width: textBox.implicitWidth + 20
                        height: textBox.implicitHeight + 20
                        color: "red"

                        DragHandler {
                            dragThreshold: 0
                        }

                        onXChanged: model.x = x // modify model data when dragging
                        onYChanged: model.y = y

                        Text {
                            id: textBox
                            anchors.centerIn: parent
                            color: "white"
                            text: model.name + ": " + foo.itemAt(index).x
                        }
                    }
                }
            }
        }
    }

    Instantiator {
        model: modelNodes
        delegate: Binding { // the hacky solution to the initial problem.
            target: myText
            property: model.name.toLowerCase() + "Point"
            value: Qt.point(model.x, model.y)
        }
    }

    Text {
        id: myText
        property point bananaPoint
        property point orangePoint
        anchors.right: parent.right
        text: JSON.stringify(bananaPoint)
    }


    ListView {
        anchors.fill: parent
        model: modelNodes
        delegate: Text {
            text: `${model.name} - (${model.x} - ${model.y})`
        }
    }

}

I've used a hacky solution to your initial problem with an Instantiator of Bindings, I don't really understand the usecase so that might not be the ideal solution. Here it creates a binding for every element of your model but that's weird. If you only want data from your first row, you may want to do when: index === 0 in the Binding. I've created a third party library to get a cleaner code : https://github.com/okcerg/qmlmodelhelper

This will result in the following code for your outside Text (and allowing you to get rid of the weird Instantiator + Binding part):

Text {
    readonly property var firstRowData: modelNodes.ModelHelper.map(0)
    text: firstRowData.x + ", " + firstRowData.y
}

Note that my point about not storing data in delegates (or accessing them from outside) still stands for whatever solution you chose.

Upvotes: 0

JarMan
JarMan

Reputation: 8277

The reason the Binding object doesn't work outside of the Repeater is because the Repeater has not constructed its items yet when the binding is being evaluated. To fix this, you can move the binding into the Component.onCompleted handler. Then just use the Qt.binding() function to do binding from javascript (docs).

Text { 
    Component.onCompleted: { 
        text = Qt.binding(function() { return foo.itemAt(0).x + ", " + foo.itemAt(1).x }) 
    }
}

Upvotes: 3

Related Questions