Getting around list<> properties being readonly - qt

I have two custom components written in QML
//XOption.qml
Container {
id: xOption
property string title;
property bool active: false;
function makeActive() {active=true}
onActiveChanged {
//alter appearance of option to reflect whether active/not
}
onTouch {
if (touchEvent reflects a tap) {
//notify underlying c++ engine that I was tapped
//engine will notify parent control that I was tapped by setting the flag var
}
}
//label to display title and some other visual components
}
//XParent.qml
Container {
id: XParent;
property list<XOption> options;
property int selectedOption: 0;
property string flag: cppengine.flag;
onCreationCompleted {
for (var k = 0; k < children.length; ++k) {
if (k==selectedOption)
options[k].makeActive()
}
cppengine.declareParentage(options);
}
onFlagChanged {
if (flag indicates that one of my child options was tapped) {
//determine from flag which option was tapped
tappedOption.makeActive()
//make other options inactive
}
}
}
But now I want to use XParent in another QML document and assign any number of different XOptions to it like so:
Container {
XParent {
options: [
XOption {
title: "title1";
},
XOption {
title: "title2";
}
]
}
}
However, when doing so, I get the error:
Invalid property assignment: "options" is a read-only property
Is there any way I could get around this? I've tried making options a string array type variant, that would contain the title of every child option to create, and then adding a ComponentDefinition for XOption to XParent and creating one for every title that was specified, but if I do that I am unable to call XOption's makeActive(), which is absolutely necessary.

Related

How can I reflect to attribute but not trigger a rerender in lit-element?

I am converting a custom element dropdown over to lit-element. The way the existing element shows the dropdown options is by setting an expanded boolean attribute on the element, and the options are shown/hidden via css:
my-element:not([expanded]) .options-container {
display: none;
}
my-element[expanded] .options-container {
display: block;
}
The component doesn't need to do any rerenders because the logic is all in the css.
How can I achieve this behavior with lit-element, and not rerender the component? Rerendering can be costly if there are a lot of dropdown options.
I have tried implementing a shouldUpdate that returns false if only expanded has changed - but this causes lit-element not to reflect expanded to the attribute when set via a property, which is necessary in order to show/hide via css.
This is what I have, which doesn't work:
class MyDropdown extends LitElement {
static get properties() {
return {
expanded: { type: Boolean, reflect: true },
...
};
}
shouldUpdate(changedProperties) {
if (changedProperties.has('expanded') && changedProperties.size === 1) {
return false;
}
return true;
}
// disable shadow-dom
createRenderRoot() {
return this;
}
}
Note that I am not using shadow dom yet, not sure if that would change the solution. I'm on lit-element 2.2.1.
The idea is to not use LitElement's static properties or #Property decorator. Write your own property implementation like this:
class MyDropdown extends LitElement {
_expanded = false;
get expanded() {
return this._expanded;
}
set expanded(val) {
this._expanded = val;
// Manually setting the property and reflecting attribute.
if (val) {
this.setAttribute('expanded', '');
} else {
this.removeAttribute('expanded');
}
}
// disable shadow-dom
createRenderRoot() {
return this;
}
}
Similarly, you can listen for attributeChangedCallback lifecycle event and adjust _expanded property whenever user changes the attribute and not property.

How to listen for property collection size changes

I want to automatically show/hide view depending on a property's collection size, here's the code:
QtObject {
property var controlWindow: Window {
property var collection: []
signal sigAddElement(var element)
onSigAddElement: {
collection.push(element)
}
signal sigEraseAllElements()
onSigEraseAllElements: {
collection.length = 0
}
onCollectionChanged: {
console.log("collection.len = " + collection.length)
}
Rectangle {
id: autoHidableView
visible: collection.length != 0
}
}
}
but visible property of autoHidableView evaluates only once on startup and never evaluates again
The onCollectionChanged handler is never get called which is understandable since collection object itself stays the same
So is it possible to listen for collection's size change event?
The problem is that the javascript array that you create with property var collection: [] does not have any signals (the onCollectionChanged is indeed when you would assign a new collection to it). You better use ListModel:
QtObject {
property var controlWindow: Window {
ListModel {
id: collection
}
signal sigAddElement(var element)
onSigAddElement: {
collection.append(element)
}
signal sigEraseAllElements()
onSigEraseAllElements: {
collection.clear()
}
Rectangle {
id: autoHidableView
visible: collection.count > 0
}
}
}
Note that you need to change the push to append
Listening to the array length is not enough.
See var QML Basic Type:
It is important to note that changes in regular properties of JavaScript objects assigned to a var property will not trigger updates of bindings that access them.
It is the same behavior when assigning an array to the var. The binding will only be reevaluated if the property is reassigned with an entire new object/array.
You have 2 ways to make your binding listen to the collection length change:
Reassign the entire array:
onSigAddElement: {
collection = collection.concat(element)
}
// ...
onSigEraseAllElements: {
collection = []
}
Trigger the change signal manually:
onSigAddElement: {
collection.push(element)
collectionChanged()
}
// ...
onSigEraseAllElements: {
collection.length = 0
collectionChanged()
}

Invalid grouped property access

In this qml code:
Component {
id: userdelegate
PictureBox {
...
icon: model.icon
icon.heigth: 50
}
}
PictureBox comes from the PictureBox.qml system file in this way:
...
Image {
id: icon
...
width: parent.width; height: 150
}
Running qml, I have the error in the title.
I need to use PictureBox.qml, but I can't change it.
How can I override default height value for PictureBox.qml icon?
You can try to bypass QML's scoping rules by traversing Item children until you can find the Image and manipulate it directly. It's possible it could break in the future, but item.toString() gives you something useful:
item.toString() -> "QQuickImage(0x114054350)"
So, you can try something like this (not tested):
function findItemOfType(type, item) {
if (item.toString().indexOf(type) != -1) {
return child;
}
for (var i=0;i < children.length;++i) {
var child = children[i];
var result = findItemOfType(type, child.children);
if (result != null) {
return result;
}
}
return null;
}
Using it like this:
findItemOfType("QQuickImage", pictureBoxId);

Make a unknown number of URL requests asynchronously in Swift

I need to traverse a tree with an unknown number of nodes by making asynchronous URL requests. My current approach looks like this:
class Parent {
var foo: String
var id: Int
var children: [Child]?
func loadChildren() {
for child in self.children {
self.getAllChildren(child)
}
}
func getAllChildren(current: Child) {
current.load() {(success) in
if (success) {
if let children = current.children {
for child in children {
self.getAllChildren(child)
}
}
}
}
}
}
class Child {
var bar: String
var id: Int
var children: [Child]?
func load(success: (Bool) -> ()) {
// Load from API and initalize values
}
}
The problem with my current approach above is that I don't known when the loading has finished. I don't care if some children fail to load, but i need to make UI updates when all children (and their children) of an parent have been loaded.
I looked into various approaches like promises and dispatch groups but i'm struggling to get it work. I'm using Swift 2 and ideally the parent would have a function like this:
func loadChildren(success: (Bool) -> ()) {
// do stuff
}

Conditionally enable/disable fields in AEM 6.1 (granite.ui) TouchUI dialogs

Does anyone have any experience with conditionally disabling fields based on value of a previous field in an AEM6.1 TouchUI dialog?
To give some context I have a checkbox in my TouchUI dialog used to enable/disable (hide/show) a Call To Action button within a component. I'd like to disable the CTA buttonText and href fields in the dialog itself where the author has disabled the CTA via the checkbox. Adversely I'd like to enable these fields where the CTA checkbox is checked enabling CTA.
I have investigated /libs/cq/gui/components/authoring/dialog/dropdownshowhide/clientlibs/dropdownshowhide.js but it's not really fit for purpose given that this is specifically designed for hiding or showing fields based on value of dropdown list and my attempts to modify it to allow similar funationality on a checkbox have been less than fruitful. I want to enable/disabled fields rather than hide of show them.
After a bit of messing around I got this working by adding class="cq-dialog-checkbox-enabledisable" to my sling:resourceType="granite/ui/components/foundation/form/checkbox" and class="cq-dialog-checkbox-enabledisable-target" to the sling:resourceType="granite/ui/components/foundation/form/textarea" that I wanted to disable in my cq:dialog.xml.
I then created my own clientLib that has dependencies on granite.jquery and categories cq.authoring.dialog.
UPDATE: turns out the disabled property can't be set programatically on pathbrowser field types at the top level, so you neeed to disable the child fields contained inside it (js-coral-pathbrowser-input and js-coral-pathbrowser-button) code snippet below updated to reflect this.
/**
* Extension to the standard checkbox component. It enables/disables other components based on the
* selection made in the checkbox.
*
* How to use:
*
* - add the class cq-dialog-checkbox-enabledisable to the checkbox element
* - add the class cq-dialog-checkbox-enabledisable-target to each target component that can be enabled/disabled
*/
(function(document, $) {
"use strict";
// when dialog gets injected
$(document).on("foundation-contentloaded", function(e) {
// if there is already an inital value make sure the according target element becomes visible
enableDisable($(".cq-dialog-checkbox-enabledisable", e.target));
});
$(document).on("change", ".cq-dialog-checkbox-enabledisable", function(e) {
enableDisable($(this));
});
function enableDisable(el){
el.each(function(i, element) {
if ($(element).attr("type") === "checkbox"){
if ($(element).prop('checked')){
$('.cq-dialog-checkbox-enabledisable-target').enable();
} else {
$('.cq-dialog-checkbox-enabledisable-target').disable();
}
}
})
}
//recurse all pathbrowser children and grandchildren etc
function iteratePathBrowserDescendants (node, enable) {
for (var i = 0; i < node.childNodes.length; i++) {
var child = node.childNodes[i];
if ((child.className.indexOf('js-coral-pathbrowser-input') > -1 ) || (child.className.indexOf('js-coral-pathbrowser-button') > -1 )) {
enablePathBrowser(child, enable);
} else {
iteratePathBrowserDescendants(child, enable);
}
}
}
function enablePathBrowser(node, enable) {
node.disabled = enable;
}
//iterate class cq-dialog-checkbox-enabledisable-target's and enable
$.prototype.enable = function () {
$.each(this, function (index, el) {
//special treatment for pathBrowser as it is made up of multiple fields and cannot be disabled at the top level
if (el.hasAttribute('data-init')) {
if (el.getAttribute('data-init') == 'pathbrowser'){
iteratePathBrowserDescendants(el, false);
};
} else {
el.disabled = false;
}
});
}
//iterate class cq-dialog-checkbox-enabledisable-target's and disable
$.prototype.disable = function () {
$.each(this, function (index, el) {
//special treatment for pathBrowser as it is made up of multiple fields and cannot be disabled at the top level
if (el.hasAttribute('data-init')) {
if (el.getAttribute('data-init') == 'pathbrowser'){
iteratePathBrowserDescendants(el, true);
};
} else {
el.disabled = true;
}
});
}
})(document,Granite.$);

Resources