hi everyone,
I have some confusion. I have two component ( child and parent component) and I pass the properties of an object as props
<child :child-data="abc" ></child>
Vue.component('childComponent', {
props: ['childData'],
data: function(){
return {
count:this.childData,// recommend use Vue.util.extend
}
},
});
Vue will recursively convert "data" properties of childComponent into getter/setters to make it “reactive”.
So why doesn't it automatic bind data to the template? I read some recommend use Vue.util.extend. Why Vue.util.extend?
UPDATE
my example:
https://jsfiddle.net/hoanghung1995/xncs5qpd/56/
when i set default value of parentData ,childDataA will display it. But when i use v-model to override parentData then childDataA not “reactive”. I must use "watch" to override "data" ,similar to childDataB
Vue.util.extend example: https://jsfiddle.net/sm4kx7p9/3/
Why do Vue.util.extend work fine but not use "watch"?,
To explain what is actually happening in the background, Linus Borg has as excellent answer for your question. To summarize his answer, the reason why your approach doesn't work is being data is a computed property while props are being passed in as primitive types. In other words, data makes a copy of your props (instead of passing by reference).
Another way to bypass this is to declare your childData as computed properties instead of data, i.e.:
computed: {
childDataA() {
return this.childPropsA;
},
childDataB() {
return this.childPropsB;
}
}
The reason why using computed works is because the computed properties now watches changes to their dependencies.
A proof-of-concept example based on your original fiddle:
Vue.component('child', {
props: ['childPropsA', 'childPropsB'],
template: "#sub",
computed: {
childDataA() {
return this.childPropsA;
},
childDataB() {
return this.childPropsB;
}
}
});
new Vue({
el: '#app',
data: {
parentData: '123'
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
parentData:{{parentData}}<br>
<input type="text" v-model="parentData">
<child :child-props-a="parentData" :child-props-b="parentData"></child>
</div>
<template id="sub">
<div>
<p> 1- {{ childDataA }}</p>
<p> 2- {{ childDataB }}</p>
</div>
</template>
The approach above is functionally identical to the data + watch approach, but I find it rather cumbersome and adds unnecessary verbosity to your code:
data: function() {
return {
childDataA: this.childPropsA,
childDataB: this.childPropsB
};
},
watch: {
childPropsA() {
this.childDataA = this.childPropsA;
},
childPropsB() {
this.childDataB = this.childPropsB;
}
}
Vue.component('child', {
props: ['childPropsA', 'childPropsB'],
template: "#sub",
data: function() {
return {
childDataA: this.childPropsA,
childDataB: this.childPropsB
};
},
watch: {
childPropsA() {
this.childDataA = this.childPropsA;
},
childPropsB() {
this.childDataB = this.childPropsB;
}
}
});
new Vue({
el: '#app',
data: {
parentData: '123'
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
parentData:{{parentData}}<br>
<input type="text" v-model="parentData">
<child :child-props-a="parentData" :child-props-b="parentData"></child>
</div>
<template id="sub">
<div>
<p> 1- {{ childDataA }}</p>
<p> 2- {{ childDataB }}</p>
</div>
</template>
Watch does only watch properties which are indeed reactive. Passing objects doesn`t makes their properties reactive. Just pass the properties that you want as props.
With Vue.util.extend you force the property to be reactive in this instance.
Related
I'm new to Vue and was hoping for some clarification on best practices.
I'm building an app that uses an array to keep a list of child components and I want to be able to update and remove components by emiting to the parent. To accomplish this I currently have the child check the parent array to find it's index with an "equals" method so that it can pass that index to the parent. This works fine for something simple but if my child components get more complex, there will be more and more data points I'll have to check to make sure I'm changing the correct one. Another way to do this that I can think of is to give the child component an ID prop when it's made and just pass that but then I'd have to handle making sure all the ids are different.
Am I on the right track or is there a better more widely accepted way to do this? I've also tried using indexOf(this._props) to get the index but that doesn't seem to work. Maybe I'm just doing something wrong?
Here's a simplified version of what I'm doing:
// fake localStorage for snippet sandbox
const localStorage = {}
Vue.component('child', {
template: '#template',
data() {
return {
newName: this.name
}
},
props: {
name: String
},
mounted() {
this.newName = this.name
},
methods: {
update() {
this.$emit(
"update-child",
this.$parent.children.findIndex(this.equals),
this.newName
)
},
equals(a) {
return a.name == this.name
}
}
})
var app = new Vue({
el: '#app',
data: {
children: []
},
methods: {
addNewChild() {
this.children.push({
name: 'New Child',
})
},
updateChild(index, newName) {
this.children[index].name = newName
}
},
mounted() {
if (localStorage.children) {
this.children = JSON.parse(localStorage.children)
}
},
watch: {
children(newChildren) {
localStorage.children = JSON.stringify(newChildren)
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js"></script>
<div id="app">
<button v-on:click="addNewChild">+ New Child</button>
<hr />
<child v-for="child in children"
:key="children.indexOf(child)"
:name="child.name"
#update-child="updateChild">
</child>
</div>
<script type="text/x-template" id="template">
<div>
<p><b>Name: {{name}}</b></p>
<input placeholder="Name" type="text" v-model="newName" />
<button #click="update">Update</button>
<hr />
</div>
</script>
The great thing about v-for is that it creates its own scope. With that in mind, you can safely reference child in the event handler. For example
<child
v-for="(child, index) in children"
:key="index"
:name="child.name"
#update-child="updateChild(child, $event)"
/>
updateChild(child, newName) {
child.name = newName
}
All you need to emit from your child component is the new name which will be presented as the event payload $event
update() {
this.$emit("update-child", this.newName)
}
A quick note about :key... it would definitely be better to key on some unique property of the child object (like an id as you suggested).
Keying on array indices is fine if your array only changes in size but if you ever decide to splice or sort it, Vue won't be able to react to that change correctly since the indices never change.
I am quite new with VueJS and I have been having trouble lately with some computed properties which do not update as I would like. I've done quite some research on Stack Overflow, Vue documentation and other ressources but i haven't found any solution yet.
The "app" is basic. I've got a parent component (Laundry) which has 3 child components (LaundryMachine). The idea is to have for each machine a button which displays its availability and updates the latter when clicked on.
In order to store the availability of all machines, I have a data in the parent component (availabilities) which is an array of booleans. Each element corresponds to a machine's availability.
When I click on the button, I know the array availibities updates correctly thanks to the console.log. However, for each machine, the computed property "available" does not update is I would want it to and I have no clue why.
Here is the code
Parent component:
<div id="machines">
<laundry-machine
name="AA"
v-bind:machineNum="0"
v-bind:availableArray="this.availabilities"
v-on:change-avlb="editAvailabilities"
></laundry-machine>
<laundry-machine
name="BB"
v-bind:machineNum="1"
v-bind:availableArray="this.availabilities"
v-on:change-avlb="editAvailabilities"
></laundry-machine>
<laundry-machine
name="CC"
v-bind:machineNum="2"
v-bind:availableArray="this.availabilities"
v-on:change-avlb="editAvailabilities"
></laundry-machine>
</div>
</div>
</template>
<script>
import LaundryMachine from './LaundryMachine.vue';
export default {
name: 'Laundry',
components: {
'laundry-machine': LaundryMachine
},
data: function() {
return {
availabilities: [true, true, true]
};
},
methods: {
editAvailabilities(index) {
this.availabilities[index] = !this.availabilities[index];
console.log(this.availabilities);
}
}
};
</script>
Child component:
<template>
<div class="about">
<h2>{{ name }}</h2>
<img src="../assets/washing_machine.png" /><br />
<v-btn color="primary" v-on:click="changeAvailability">
{{ this.availability }}</v-btn>
</div>
</template>
<script>
export default {
name: 'LaundryMachine',
props: {
name: String,
machineNum: Number,
availableArray: Array
},
methods: {
changeAvailability: function(event) {
this.$emit('change-avlb', this.machineNum);
console.log(this.availableArray);
console.log('available' + this.available);
}
},
computed: {
available: function() {
return this.availableArray[this.machineNum];
},
availability: function() {
if (this.available) {
return 'disponible';
} else {
return 'indisponible';
}
}
}
};
</script>
Anyway, thanks in advance !
Your problem comes not from the computed properties in the children, rather from the editAvailabilities method in the parent.
The problem is this line in particular:
this.availabilities[index] = !this.availabilities[index];
As you can read here, Vue has problems tracking changes when you modify an array by index.
Instead, you should do:
this.$set(this.availabilities, index, !this.availabilities[index]);
To switch the value at that index and let Vue track that change.
On my app, I have multiple "upload" buttons and I want to display a spinner/loader for that specific button when a user clicks on it. After the upload is complete, I want to remove that spinner/loader.
I have the buttons nested within a component so on the file for the button, I'm receiving a prop from the parent and then storing that locally so the loader doesn't show up for all upload buttons. But when the value changes in the parent, the child is not getting the correct value of the prop.
App.vue:
<template>
<upload-button
:uploadComplete="uploadCompleteBoolean"
#startUpload="upload">
</upload-button>
</template>
<script>
data(){
return {
uploadCompleteBoolean: true
}
},
methods: {
upload(){
this.uploadCompleteBoolean = false
// do stuff to upload, then when finished,
this.uploadCompleteBoolean = true
}
</script>
Button.vue:
<template>
<button
#click="onClick">
<button>
</template>
<script>
props: {
uploadComplete: {
type: Boolean
}
data(){
return {
uploadingComplete: this.uploadComplete
}
},
methods: {
onClick(){
this.uploadingComplete = false
this.$emit('startUpload')
}
</script>
Fixed event name and prop name then it should work.
As Vue Guide: Custom EventName says, Vue recommend always use kebab-case for event names.
so you should use this.$emit('start-upload'), then in the template, uses <upload-button #start-upload="upload"> </upload-button>
As Vue Guide: Props says,
HTML attribute names are case-insensitive, so browsers will interpret
any uppercase characters as lowercase. That means when you’re using
in-DOM templates, camelCased prop names need to use their kebab-cased
(hyphen-delimited) equivalents
so change :uploadComplete="uploadCompleteBoolean" to :upload-complete="uploadCompleteBoolean"
Edit: Just noticed you mentioned data property=uploadingComplete.
It is easy fix, add one watch for props=uploadComplete.
Below is one simple demo:
Vue.config.productionTip = false
Vue.component('upload-button', {
template: `<div> <button #click="onClick">Upload for Data: {{uploadingComplete}} Props: {{uploadComplete}}</button>
</div>`,
props: {
uploadComplete: {
type: Boolean
}
},
data() {
return {
uploadingComplete: this.uploadComplete
}
},
watch: { // watch prop=uploadComplete, if change, sync to data property=uploadingComplete
uploadComplete: function (newVal) {
this.uploadingComplete = newVal
}
},
methods: {
onClick() {
this.uploadingComplete = false
this.$emit('start-upload')
}
}
})
new Vue({
el: '#app',
data() {
return {
uploadCompleteBoolean: true
}
},
methods: {
upload() {
this.uploadCompleteBoolean = false
// do stuff to upload, then when finished,
this.uploadCompleteBoolean = true
},
changeStatus() {
this.uploadCompleteBoolean = !this.uploadCompleteBoolean
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<button #click="changeStatus()">Toggle Status {{uploadCompleteBoolean}}</button>
<p>Status: {{uploadCompleteBoolean}}</p>
<upload-button :upload-complete="uploadCompleteBoolean" #start-upload="upload">
</upload-button>
</div>
The UploadButton component shouldn't have uploadingComplete as local state (data); this just complicates the component since you're trying to mix the uploadComplete prop and uploadingComplete data.
The visibility of the spinner should be driven by the parent component through the prop, the button itself should not be responsible for controlling the visibility of the spinner through local state in response to clicks of the button.
Just do something like this:
Vue.component('upload-button', {
template: '#upload-button',
props: ['uploading'],
});
new Vue({
el: '#app',
data: {
uploading1: false,
uploading2: false,
},
methods: {
upload1() {
this.uploading1 = true;
setTimeout(() => this.uploading1 = false, Math.random() * 1000);
},
upload2() {
this.uploading2 = true;
setTimeout(() => this.uploading2 = false, Math.random() * 1000);
},
},
});
<script src="https://rawgit.com/vuejs/vue/dev/dist/vue.js"></script>
<div id="app">
<upload-button :uploading="uploading1" #click="upload1">Upload 1</upload-button>
<upload-button :uploading="uploading2" #click="upload2">Upload 2</upload-button>
</div>
<template id="upload-button">
<button #click="$emit('click')">
<template v-if="uploading">Uploading...</template>
<slot v-else></slot>
</button>
</template>
Your question seems little bit ambiguë, You can use watch in that props object inside the child component like this:
watch:{
uploadComplete:{
handler(val){
//val gives you the updated value
}, deep:true
},
}
by adding deep to true it will watch for nested properties in that object, if one of properties changed you ll receive the new prop from val variable
for more information : https://v2.vuejs.org/v2/api/#vm-watch
if not what you wanted, i made a real quick example,
check it out hope this helps : https://jsfiddle.net/K_Younes/64d8mbs1/
I have used data option in two ways. In first snippet data object contains a key value, however, in second data is a function. Is there any benefits of individuals.Not able to find relevant explanations on Vue.js Docs
Here are two code snippets:
new Vue({
el: "#app",
data: {
message: 'hello mr. magoo'
}
});
new Vue({
el: "#app",
data() {
return {
message: 'hello mr. magoo'
}
}
});
Both are giving me the same output.
It seems as though the comments on your question missed a key point when considering your specific code example.
In a root Vue instance i.e. constructed via new Vue({ ... }), you can simply use data: { ... } without any problems. The issue is when you have reusable components that are defined via Vue.component(...). In these instances, you need to either use data() {return { ... };} or data: function() {return { ... };}.
The reason for this is to ensure that for each individual instance of the reusable child component, there is a unique object containing all of the data being operated on. If, in a child component, you instead use data: { ... }, that same data object will be shared between the child components which can cause some nasty bugs.
Please review the corresponding section of the Vue.js documentation for more information regarding this problem.
[Vue warn]: The "data" option should be a function that returns a per-instance value in component definitions.
so initiating a new vue instance dose not matter between data:{} as a object or data(){return{}} or data:function(){return{}}.
It matters when it comes to component lets try an example:
<body>
<div id="app">
<counter></counter>
<counter></counter>
</div>
<script>
Vue.component('counter', {
template: '<button v-on:click="counter += 1">{{ counter }}</button>',
data: {
counter:0
}
});
</script>
Output:
[Vue warn]: The "data" option should be a function that returns a per-instance value in component definitions.
Now lets watch in vue object:
<body>
<div id="app">
<button v-on:click="counter += 1">{{ counter }}</button>
<button v-on:click="counter += 1">{{ counter }}</button>
</div>
<script>
new Vue({
el: '#app',
/*data() {
return {
counter:0
}
},*/
//or (output same)
/*data: function () {
return {
counter: 0
}
}*/
//or (output same)
data:{
counter:0
}
});
</script>
</body>
//Now let's try data as a function in the component to reuse same component over and over again.
<body>
<div id="app">
<counter></counter>
<counter></counter>
<counter></counter>
</div>
<script>
Vue.component('counter', {
template: '<button v-on:click="counter += 1">{{ counter }}</button>',
data: function () {
return {
counter: 0
}
}
})
new Vue({
el: '#app'
});
</script>
</body>
in my vuejs program i am trying to make a global instance of an alert/notification system. This would be at the rootmost instance of the app. and then my plan was to push to an array of objects and pass that through to the component.
This only half works.
in my app.vue i have
<template>
<div id="app">
<alert-queue :alerts="$alerts"></alert-queue>
<router-view></router-view>
</div>
</template>
in my main.js i have
exports.install = function (Vue, options) {
Vue.prototype.$alerts = []
}
and my alert_queue.vue is
<template>
<div id="alert-queue">
<div v-for="alert in alerts" >
<transition name="fade">
<div>
<div class="alert-card-close">
<span #click="dismissAlert(alert)"> × </span>
</div>
<div class="alert-card-message">
{{alert.message}}
</div>
</div>
</transition>
</div>
</div>
</template>
<script>
export default {
name: 'alert',
props: {
alerts: {
default: []
}
},
data () {
return {
}
},
methods: {
dismissAlert (alert) {
for (let i = 0; i < this.alerts.length; i++) {
if (this.alerts[i].message === alert.message) {
this.alerts.splice([i], 1)
}
}
}
}
}
</script>
I can add to this list now by using this.$alerts.push({}) and i can see they are added by console.logging the results.
The problem is that the component doesn't recognise them unless i manually go in and force it to refresh by changing something in code and having webpack reload the results. as far as i can see, there is no way to do this programatically.... Is there a way to make prototype components be watched like the rest of the application?
I have tried making the root most file have a $alerts object but when i use $root.$alerts.push({}) it doesn't work because $root is readonly.
Is there another way i can go about this ?
You could make $alerts a Vue instance and use it as an event bus:
exports.install = function (Vue, options) {
Vue.prototype.$alerts = new Vue({
data: {alerts: []},
events: { ... },
methods: { ... }
})
}
Then in your components you might call a method this.$alerts.addAlert() which in turn pushes to the array and broadcasts an event alert-added. In other places you could use this.$alerts.on('alert-added', (alert) => { ... }
Other than that, I think this is a good use case for Vuex, which is pretty much designed for this: https://github.com/vuejs/vuex
Properties defined on Vue.prototype are not reactive like a Vue instance's data properties.
I agree that, in most cases, Jeff's method or using Vuex is the way to go.
However, you could simply set this.$alerts as a Vue instance's data property and then updating that property (which would be reactive) would, by association, update the global $alerts array:
Vue.prototype.$alerts = ['Alert #1'];
Vue.component('child', {
template: `<div><div v-for="i in items">{{ i }}</div></div>`,
props: ['items'],
})
new Vue({
el: '#app',
data() {
return {
globalAlerts: this.$alerts,
}
},
methods: {
addToArray() {
this.globalAlerts.push('Alert #' + (this.globalAlerts.length + 1));
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.1/vue.min.js"></script>
<div id="app">
<child :items="$alerts"></child>
<button #click="addToArray">Add alert</button>
</div>