QML: How to trigger onChanged in custom Component? - javascript

Assume we have the following custom QML Components.
MyComponent.qml
//Contents for MyComponent.qml
import QtQuick 2.0
QtObject{
property real myProperty
...
}
Test.qml
import QtQuick 2.0
Item {
property var myComponent: MyComponent
onMyComponentChanged: console.log("MyComponent changed!");
}
When changing any of the properties in myComponent, I want onMyComponentChanged() to be triggered. What is the best way to accomplish this?

In QML, most of the properties have onChanged events. For example;
MyComponent {
property string name: "Super"
}
i.e, on + Property + Changed signal will be emitted (first letter of the property will be Upper case) - resulting in onNameChanged
Item {
id: mainItem
property MyComponent myCustomComponent: MyComponent {
onNameChanged: mainItem.handleMyComponentChange()
}
function handleMyComponentChange() {
-----
}
myCustomComponent.name="Duper" // triggers handleMyComponentChange()
}

There is kind of a limitation in QML in this regard.
Since your property is itself an object rather than a primitive, its changed signal will emit only when the property is changed to be assigned to another object, it WILL NOT reflect internal changes to this object. Also, you cannot manually emit the signal either, it will only automatically emit when the property is changed.
myComponent.myProperty = 1.1 // this will not trigger onMyComponentChanged
myComponent = anotherObject // this will trigger onMyComponentChanged
Since your component has only a single property and it already has a change notification, you can use that:
property var myComponent : myComp // you can omit that if you don't need it as a property
MyComponent {
id: myComp
myProperty: 13.37
onMyPropertyChanged: console.log("changed")
}
or...
property var myComponent : MyComponent {
myProperty: 13.37
}
Connections {
target: myComponent
onMyPropertyChanged: console.log("changed")
}
If your component has multiple properties, you should implement a signal changed() in it and emit it on every property changed and use that signal to reflect internal changes instead of the one automatically generated by the QML property, which will not reflect them:
QtObject {
property real myProperty
property bool otherProperty
onMyPropertyChanged: changed()
onOtherPropertyChanged: changed()
signal changed()
}
...
property var myComponent : MyComponent {
myProperty: 13.37
}
Connections {
target: myComponent
onChanged: console.log("changed")
}

You can connect the signal myPropertyChanged to the signal myComponentChanged, the same of c++.
to do that :
import QtQuick 2.2
Item {
id:idRoot
width: 800
height: 480
property var myComponent: Item
{
id:item
property int myProperty: 13
// timer to change myProperty
Timer{
running: true
interval: 2000
repeat: true
onTriggered: item.myProperty += 1
}
}
onMyComponentChanged: console.log("MyComponent changed!");
Component.onCompleted:{
// connect signals
// info: you can connect signal to signal or signal to function
item.myPropertyChanged.connect(idRoot.myComponentChanged)
// disconnect signals
//item.myPropertyChanged.disconnect(idRoot.myComponentChanged)
}
}
Edit for ddriver:
try this, you can see that I emit the signal without changing the property:
import QtQuick 2.2
Item
{
id:idRoot
width: 800
height: 480
// when you create a property its signal myPropertyChanged()
// and its slot onMyPropertyChanged are created
// you can emit this signal when you want
property int myProperty: 13
// timer to emit myPropertyChanged()
Timer{
running: true
interval: 2000
repeat: true
onTriggered: myPropertyChanged()
}
onMyPropertyChanged: console.log("MyProperty changed!",myProperty);
}

Related

In QML, could a component property be the component itself?

I'm trying to assing a Component itself to one of its property, and then passing that property to a function inside an external file (import "Scripts.js" as Scripts)
The first thing that came to my mind was (in my opinion) the most obvious method:
//MyComponent.qml
Item {
id: comp
// property var target: comp
// doesn't work either
property var target: this
...
onActiveFocusChanged: {
// Scripts.foo(this)
// and Scripts.foo(tf)
// don't work either
if(this.activeFocus) {
Scripts.foo(target)
}
}
}
But that doesn't work (the window crashes after giving activeFocus to the Component).
So, I assigned undefined to the target as default:
...
property var target: undefined
...
and then assigned the Component itself when declared:
MyComponent {
id: myComponent
...
target: this
// target: myComponent
// also works
...
}
When the activeFocus is triggered, everything works fine. Can someone tell me why / what I'm doing wrong?
It's impossible to assing to a component property the component itself?
And why it's not impossible to do this after declared?
As folibis already commented you should use ids instead of this keyword. Have a look at the following SO post.
import QtQuick
Window {
id: root
width: 800
height: 600
visible: true
title: qsTr("Hello Component")
function foo(parameter) { console.log(parameter) }
component MyComponent : Item {
id: component
property var target: component
Component.onCompleted: {
root.foo(component.target)
root.foo(component)
}
}
MyComponent {}
MyComponent { target: root }
}
This works without an issue.
import QtQuick
Window {
id: root
width: 800
height: 600
visible: true
title: qsTr("Hello Component")
function foo(parameter) { console.log(parameter) }
component MyComponent : Item {
id: component
property var target: component
onActiveFocusChanged: {
if(component.activeFocus)
root.foo(component.target)
}
}
MyComponent { id: test }
Component.onCompleted: test.forceActiveFocus()
}
The this keyword has special meaning in Javascript and can change it's meaning depending on context. Since you declared it as target: this you made it property bind so that every change to this will trigger a new value in target. To work around that problem, you can ensure that you capture this exactly once with:
// MyComponent.qml
import QtQuick
import QtQuick.Controls
Item {
property Item target
Component.onCompleted: target = this
}
Alternatively, if you want to keep your code clean of imperative code, you can make use of parent in the following way:
// MyComponent.qml
import QtQuick
import QtQuick.Controls
Item {
readonly property alias target: internal.target
Item {
id: internal
property Item target: parent
}
}
In the above, internal is clearly a subitem of Item and therefore, there can only be one meaning for parent which is your original item itself.

methods/computed cant access data in vue component

I have a simple vue component where i defined a boolean constant in data with start value false. I now want to create a method to change it to true and bind certain stuff conditionally to that in the template when it changes. But somehow i get the error "property value does not exist on type...". when i move computed out of data i get "Property 'computed' has no initializer"
export default class Something extends Vue {
data() {
return {
value: false,
computed: {
valueTransform() {
this.value = true
alert(this.value)
},
},
}
}
}
This syntax is not valid in class components, you should have something like :
export default class Something extends Vue {
//data
value = false,
//computed
get valueTransform(){
return this.value
}
}

How to Watch Props Change with Vue Composition API / Vue 3?

While Vue Composition API RFC Reference site has many advanced use scenarios with the watch module, there is no examples on how to watch component props?
Neither is it mentioned in Vue Composition API RFC's main page or vuejs/composition-api in Github.
I've created a Codesandbox to elaborate this issue.
<template>
<div id="app">
<img width="25%" src="./assets/logo.png">
<br>
<p>Prop watch demo with select input using v-model:</p>
<PropWatchDemo :selected="testValue"/>
</div>
</template>
<script>
import { createComponent, onMounted, ref } from "#vue/composition-api";
import PropWatchDemo from "./components/PropWatchDemo.vue";
export default createComponent({
name: "App",
components: {
PropWatchDemo
},
setup: (props, context) => {
const testValue = ref("initial");
onMounted(() => {
setTimeout(() => {
console.log("Changing input prop value after 3s delay");
testValue.value = "changed";
// This value change does not trigger watchers?
}, 3000);
});
return {
testValue
};
}
});
</script>
<template>
<select v-model="selected">
<option value="null">null value</option>
<option value>Empty value</option>
</select>
</template>
<script>
import { createComponent, watch } from "#vue/composition-api";
export default createComponent({
name: "MyInput",
props: {
selected: {
type: [String, Number],
required: true
}
},
setup(props) {
console.log("Setup props:", props);
watch((first, second) => {
console.log("Watch function called with args:", first, second);
// First arg function registerCleanup, second is undefined
});
// watch(props, (first, second) => {
// console.log("Watch props function called with args:", first, second);
// // Logs error:
// // Failed watching path: "[object Object]" Watcher only accepts simple
// // dot-delimited paths. For full control, use a function instead.
// })
watch(props.selected, (first, second) => {
console.log(
"Watch props.selected function called with args:",
first,
second
);
// Both props are undefined so its just a bare callback func to be run
});
return {};
}
});
</script>
EDIT: Although my question and code example was initially with JavaScript, I'm actually using TypeScript. Tony Tom's first answer although working, lead to a type error. Which was solved by Michal Levý's answer. So I've tagged this question with typescript afterwards.
EDIT2: Here is my polished yet barebones version of the reactive wirings for this custom select component, on top of <b-form-select> from bootstrap-vue (otherwise agnostic-implementation but this underlying component does emit #input and #change events both, based on whether change was made programmatically or by user interaction).
<template>
<b-form-select
v-model="selected"
:options="{}"
#input="handleSelection('input', $event)"
#change="handleSelection('change', $event)"
/>
</template>
<script lang="ts">
import {
createComponent, SetupContext, Ref, ref, watch, computed,
} from '#vue/composition-api';
interface Props {
value?: string | number | boolean;
}
export default createComponent({
name: 'CustomSelect',
props: {
value: {
type: [String, Number, Boolean],
required: false, // Accepts null and undefined as well
},
},
setup(props: Props, context: SetupContext) {
// Create a Ref from prop, as two-way binding is allowed only with sync -modifier,
// with passing prop in parent and explicitly emitting update event on child:
// Ref: https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier
// Ref: https://medium.com/#jithilmt/vue-js-2-two-way-data-binding-in-parent-and-child-components-1cd271c501ba
const selected: Ref<Props['value']> = ref(props.value);
const handleSelection = function emitUpdate(type: 'input' | 'change', value: Props['value']) {
// For sync -modifier where 'value' is the prop name
context.emit('update:value', value);
// For #input and/or #change event propagation
// #input emitted by the select component when value changed <programmatically>
// #change AND #input both emitted on <user interaction>
context.emit(type, value);
};
// Watch prop value change and assign to value 'selected' Ref
watch(() => props.value, (newValue: Props['value']) => {
selected.value = newValue;
});
return {
selected,
handleSelection,
};
},
});
</script>
If you take a look at watch typing here it's clear the first argument of watch can be array, function or Ref<T>
props passed to setup function is reactive object (made probably by readonly(reactive()), it's properties are getters. So what you doing is passing the value of the getter as the 1st argument of watch - string "initial" in this case. Because Vue 2 $watch API is used under the hood (and same function exists in Vue 3), you are effectively trying to watch non-existent property with name "initial" on your component instance.
Your callback is called only once and never again. Reason it is called at least once is because new watch API is behaving like current $watch with immediate option (UPDATE 03/03/2021 - this was later changed and in release version of Vue 3, watch is lazy same way as it was in Vue 2)
So by accident you doing the same thing Tony Tom suggested but with wrong value. In both cases it's not valid code if you are using TypeScript
You can do this instead:
watch(() => props.selected, (first, second) => {
console.log(
"Watch props.selected function called with args:",
first,
second
);
});
Here the 1st function is executed immediately by Vue to collect dependencies (to know what should trigger the callback) and 2nd function is the callback itself.
Other way would be to convert props object using toRefs so it's properties would be of type Ref<T> and you can pass them as a 1st argument of watch
Anyway, most of the time watching props is just not needed - simply use props.xxx directly in your template (or setup) and let the Vue do the rest
I just wanted to add some more details to the answer above. As Michal mentioned, the props coming is an object and is reactive as a whole. But, each key in the props object is not reactive on its own.
We need to adjust the watch signature for a value in the reactive object compared to a ref value
// watching value of a reactive object (watching a getter)
watch(() => props.selected, (selection, prevSelection) => {
/* ... */
})
// directly watching a ref
const selected = ref(props.selected)
watch(selected, (selection, prevSelection) => {
/* ... */
})
Just some more info even though it's not the mentioned case in the question:
If we want to watch on multiple properties, one can pass an array instead of a single reference
// Watching Multiple Sources
watch([ref1, ref2, ...], ([refVal1, refVal2, ...],[prevRef1, prevRef2, ...]) => {
/* ... */
})
This does not address the question of how to "watch" properties. But if you want to know how to make props responsive with Vue's Composition API, then read on. In most cases you shouldn't have to write a bunch of code to "watch" things (unless you're creating side effects after changes).
The secret is this: Component props IS reactive. As soon as you access a particular prop, it is NOT reactive. This process of dividing out or accessing a part of an object is referred to as "destructuring". In the new Composition API you need to get used to thinking about this all the time--it's a key part of the decision to use reactive() vs ref().
So what I'm suggesting (code below) is that you take the property you need and make it a ref if you want to preserve reactivity:
export default defineComponent({
name: 'MyAwesomestComponent',
props: {
title: {
type: String,
required: true,
},
todos: {
type: Array as PropType<Todo[]>,
default: () => [],
},
...
},
setup(props){ // this is important--pass the root props object in!!!
...
// Now I need a reactive reference to my "todos" array...
var todoRef = toRefs(props).todos
...
// I can pass todoRef anywhere, with reactivity intact--changes from parents will flow automatically.
// To access the "raw" value again:
todoRef.value
// Soon we'll have "unref" or "toRaw" or some official way to unwrap a ref object
// But for now you can just access the magical ".value" attribute
}
}
I sure hope the Vue wizards can figure out how to make this easier... but as far as I know this is the type of code we'll have to write with the Composition API.
Here is a link to the official documentation, where they caution you directly against destructuring props.
In my case I solved it using key
<MessageEdit :key="message" :message="message" />
Maybe on your case would look something like this
<PropWatchDemo :key="testValue" :selected="testValue"/>
But I don't have any idea of its pros and cons versus watch
Change your watch method like below.
watch("selected", (first, second) => {
console.log(
"Watch props.selected function called with args:",
first,second
);
// Both props are undefined so its just a bare callback func to be run
});
None of the options above worked for me but I think I found a simple way that seems to works very well to keep vue2 coding style in composition api
Simply create a ref alias to the prop like:
myPropAlias = ref(props.myProp)
and you do everything from the alias
works like a charm for me and minimal

How can I, in Vue, define a local data property that uses a prop as its initial value, in Typescript syntax?

When passing a prop to a child component in Vue, the documentation says:
In addition, every time the parent component is updated, all props in the child component will be refreshed with the latest value. This means you should not attempt to mutate a prop inside a child component. If you do, Vue will warn you in the console.
The prop is used to pass in an initial value; the child component wants to use it as a local data property afterwards. In this case, it’s best to define a local data property that uses the prop as its initial value:
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}
We are using typescript. The syntax for "defining a local data property" is as follows (to my understanding):
<script lang="ts">
import Vue from 'vue'
import { Component } from 'vue-property-decorator'
#Component
export default class App extends Vue {
// Data property
myDataProperty: string;
</script>
And the syntax for a prop is:
#Component
export default class App extends Vue {
// Makes a "exampleProperty" a component prop with the default value of 'Example'
#Prop({default: 'Example'})
exampleProperty: string
}
So, we tried to follow the documentation, and ended up with:
parentComponent.vue
<template>
<childComponent testProperty='test' />
</template>
childComponent.vue
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
#Component
export default class childComponent extends Vue {
#Prop(
{
default: 'notTest',
validator: (component) => {
return [
'notTest',
'test',
].indexOf(component) > -1;
},
},
)
testProperty!: string;
testProperty = this.testProperty;
</script>
That, predictably, errored with `Duplicate identifier testProperty.
So, we tried
...
testProperty!: this.testProperty;
...
which resulted in
Duplicate identifier 'testProperty'.
Property 'testProperty' has no initializer and is not definitely assigned in the constructor.
Subsequent property declarations must have the same type. Property 'testProperty' must be of type 'this', but here has type 'any'.
So, I decided to try the "vue-class-component" decorator.
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
#Component({
data: function(){
return {
testProperty: this.testProperty,
}
}
})
export default class childComponent extends Vue {
#Prop(
{
default: 'notTest',
validator: (component) => {
return [
'notTest',
'test',
].indexOf(component) > -1;
},
},
)
testProperty!: string;
testProperty = this.testProperty;
</script>
This resulted in the error Property 'testProperty' does not exist on type 'Vue'.
I would like to, in a handler, do this.testProperty = 'newProperty' at some point, but cannot, because that would be directly modifying a prop.
How can I define a local data property that uses a prop as its initial value in Typescript?
EDIT:
If I do none of the above, and simply define the prop, with no attempt to define a local data property that uses the prop as its initial value, and then do
this.testProperty = 'test'
in a handler, this error is displayed in the chrome console:
vue.runtime.esm.js[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "testProperty"
I will summarise my comments into a single coherent answer: the problem you are seeing is that you have already defined this.testProperty by declaring it as a prop: doing testProperty = this.testProperty is a circular reference at best. Using the #Prop decorator alone will do the mapping of the attribute in the template to the variable.
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
#Component
export default class childComponent extends Vue {
#Prop(
{
default: 'notTest',
validator: (component) => {
return [
'notTest',
'test',
].indexOf(component) > -1;
},
},
)
testProperty!: string;
// Map prop to local data property
testPropertyLocal = this.testProperty;
</script>
Also, remember this caveat: VueJS properties must be kebab-case in templates and camelCase in JS. So, you need to update your child component reference to:
<template>
<childComponent test-property='test' />
</template>

typescript + react - mixed variable in state

I have this code in my react application, as per interface, a variable items should be an array.
But, in my initial state, there it is initialized as null (for I need as null as initial state).
Within the interface declaration I could have written ?Array, but it would mean, that the key items may not be in the state at all - and provided that this key is present within the object, it is going to be an array.
What are options? Is my architecture bad? Or how to declare required variable as mixed array|null?
declare interface StateInterface {
items: Array
}
class MyComponent extends Component {
state: StateInterface = {
items: null
};
}
You could initialize it to an empty array:
class MyComponent extends Component<{}, StateInterface> {
constructor() {
this.state = { items: [] };
}
}

Categories