Uploaded image for project: 'Qt'
  1. Qt
  2. QTBUG-125071

Loader needs a hook to provide initial properties for source and sourceComponent

    XMLWordPrintable

Details

    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

      See also

      1. https://bugreports.qt.io/browse/QTBUG-93086

      Attachments

        No reviews matched the request. Check your Options in the drop-down menu of this sections header.

        Activity

          People

            qtqmlteam Qt Qml Team User
            ratijas ivan tkachenko
            Votes:
            1 Vote for this issue
            Watchers:
            3 Start watching this issue

            Dates

              Created:
              Updated:

              Gerrit Reviews

                There are no open Gerrit changes