Details
-
Suggestion
-
Resolution: Unresolved
-
P2: Important
-
6.7
-
None
Description
Background
Loader QML Type has one fundamental limitation in its declarative API: it does not provide any way to specify initial/required properties for declaratively bound source and sourceComponent properties. This limitation affects its ergonomics and type-safety, forcing clients to resort to context properties. See also Usage #4 at https://bugreports.qt.io/browse/QTBUG-125070
Typically UI frameworks that allow dynamic component creation (which is literally all of them) provide some sort of hooks for the classes to implement after construction, e.g. NSObject -awakeFromNib, or onCreate & onStart in Android Activity lifecycle.
In QML, however, we only have Component.onCompleted, and it is not compatible with the concept of required properties — those need to be set beforehand, depending on API you need to pass them to Component.createObject in pure QML, or you may have a bit more fine-grained control with setInitialProperties on (inconsistently) various types in C++.
Problem
As the title says: Loader needs a hook to provide the initial properties for source and sourceComponent, so that it is able to load components with required properties, and so that components (even without the required properties) won't have to rely on the context for name resolution purposes.
Possible solutions
Giving the nature of Qt/QML ecosystem, a signal immediately comes to mind. However, signals can not return values, or at least not in a meaningful way which QML could use, and not in a composable way — only the last slot's value would be retained, but what if you somehow have multiple handlers across the hierarchy each trying to add some initial properties of its own?
Theoretically, things like Objective-C protocols/delegates or even a regular C++ virtual method overriding would be ideal. However, we don't usually do protocols/interfaces in Qt/QML (which IMHO is a sad omission), and I've never seen QML overriding a base C++ type's methods — even the Item::containmentMask is kind of a duck-typing with all sorts of runtime meta-method lookup horrors.
That leaves us with QVariant-wrapped function property, akin to QQC2/SpinBox::valueFromText, QQuick/TableView::rowHeightProvider org.kde.kitemmodels/KSortFilterProxyModel::filterRowCallback etc. Such function would take an instance of an incomplete object (in a state after object construction / QQmlComponent::beginCreate but before QQmlParserStatus::componentComplete / QQmlComponent::completeCreate), and return a JavaScript object containing the properties map. The reason for passing in an incomplete object as a parameter is to allow for runtime type inspection (e.g. via instanceof check), for example to return different set of properties based on the type.
Giving that QQmlIncubator class has virtual void setInitialState(QObject *) method, and that Loader's implementation already overrides it, I suggest the following property name: initialStateProvider. Or initialPropertiesProvider, to avoid confusion with the QQuickItem::state infrastructure.
Example
import QtQuick Loader { required property int index required property Message message required property string messageType source: `${messageType}Delegate.qml` initialPropertiesProvider: (object) => { return { index, message }; } }
Considerations
How would this interact with the pre-existing method `object setSource(url source, object properties)`? Would the new hook execute before or after binding those properties, or be skipped completely?
What about Instantiator, Repeater, ListView etc.?
Alternatives
An alternative approach would be to wrap a Component object within a decorator which would take care of setting initial properties itself, thus transparently implementing this feature for any dynamic Component creators such as Loader, Instantiator, Repeater, ListView etc. It almost sounds like the QQmlComponent itself would need to incorporate such feature, for example add an argument to Qt::createComponent global function. However, I'd imagine a wrapper would allow for slapping an initial properties provider to any externally received Component object without modifying it, or at least not requiring to add parameters / modify every possible API of obtaining a Component object.
For example:
import QtQuick Loader { required property int index required property Message message required property string messageType // new Component object, each with a bound custom provider source: Qt.createComponent(`${messageType}Delegate.qml`, /*parent:*/ this, /*initialPropertiesProvider:*/ object => { return { index, message }; } } ListView { id: view readonly property /*map<string, Component>*/ var componentCache: ({...}) delegate: Loader { required property int index required property Message message required property string messageType // Wraps a shared cached component without modifying it source: view.componentCache[messageType] .withInitialPropertiesProvider(object => { return { index, message }; } // or as an external API source: Qt.withInitialProperties( /*component:*/ view.componentCache[messageType], /*provider:*/ object => { return { index, message }; }) } }
Workarounds
Today it is partially possible to implement loading with the required properties, but only for source by URL. To do that you need to define a new property and on its change signal handler call setSource with any initial properties you want:
Loader {
property url sourceUrl: someUrl
onSourceUrlChanged: {
const initialProperties = {...};
setSource(sourceUrl, initialProperties);
}
}
But if you use sourceComponent API, you're out of luck, presumably for Qonsistency reasons. See this feature request about it: https://bugreports.qt.io/browse/QTBUG-125072