I have a component whose purpose is to display a list of items and let the user select one or more of the items.
This component is populated from a backend API and fed by a parent component with props.
However, since the data passed from the prop doesn't have the format I want, I need to transform it and provide a viewmodel with a computed property.
I'm able to render the list and handle selections by using v-on:click, but when I set selected=true the list is not updated to reflect the change in state of the child.
I assume this is because children property changes are not tracked by Vue.js and I probably need to use a watcher or something, but this doesn't seem right. It seems too cumbersome for a trivial operation so I must assume I'm missing something.
Here's the full repro: https://codesandbox.io/s/1q17yo446q
By clicking on Plan 1 or Plan 2 you will see it being selected in the console, but it won't reflect in the rendered list.
Any suggestions?
In your example, vm is a computed property.
If you want it to be reactive, you you have to declare it upfront, empty.
Read more here: reactivity in depth.
Here's your example working.
Alternatively, if your member is coming from parent component, through propsData (i.e.: :member="member"), you want to move the mapper from beforeMount in a watch on member. For example:
propsData: {
member: {
type: Object,
default: null
}
},
data: () => ({ vm: {}}),
watch: {
member: {
handler(m) {
if (!m) { this.vm = {}; } else {
this.vm = {
memberName: m.name,
subscriptions: m.subscriptions.map(s => ({ ...s }))
};
}
},
immediate: true
}
}
Related
I just noticed an unexpected behaviour and now I don't know if it is normal or not.
I have a component named follows and a child component named follow-list-modal
I'm passing a followList (pagination ) from follows to its child component follow-list-modal
In the follow-list-modal I store the paginated array in the variable members
Follows.vue
<template>
<div>
<follow-list-modal
:follow-list="dataset">
</follow-list-modal>
</div>
</template>
<script>
export default {
props: {
dataset: {
type: Object,
default: {},
},
},
}
</script>
FollowListModal.vue
<template>
<div>
<button #click="fetchMore"> More </button>
</div>
</template>
<script>
export default {
props: {
followList: {
type: Object,
default: {},
},
data() {
return {
members: this.followList.data,
dataset: this.followList,
};
},
methods: {
fetchMore() {
let nextPage = parseInt(this.dataset.current_page) + 1;
axios
.get(this.dataset.path + '?page=' + nextPage)
.then(({ data }) => this.refresh(data))
.catch((error) => console.log(error));
}
},
refresh(paginatedCollection) {
this.dataset = paginatedCollection;
this.members = this.members.concat(...paginatedCollection.data);
},
}
When I click the button More in the follow-list-modal to get more data, I then want to append the new data to the members array.
The unexpected behaviour ( for me at least ). is that if I use push in the refresh method
this.members.push(..paginatedCollection.data);
It appends data not only to members but also to followList which is data that comes from the parent component follows
But if I use concat instead of push, it appends data only to members variable and not to followList
this.members = this.members.concat(..paginatedCollection.data);
Is this behaviour normal ?
I don't get why the followList changes when the members variable changes, I thought that reactivity is one way.
In other words, the members changes when the followList changes, but not the other way around
P.S I don't emit any events from follow-list-modal to follows or change the data of the follows component in any way from the follow-list-modal
In JavaScript, the properties of an Object that are also Objects themselves, are passed by reference, and not by value. Or you might say that they are shallow copied.
Thus, in your example, this.members and this.followList.data are pointing to the same variable in memory.
So, if you mutate this.members, it will mutate this.followList.data as well.
You could avoid this by doing a deep copy of the objects. The easiest method, and arguably the fastest, would be to use JSON.parse(JSON.stringify(obj)), but look at this answer for more examples.
data() {
return {
members: [],
dataset: [],
};
},
created() {
this.members = JSON.parse(JSON.stringify(this.followList.data));
this.dataset = JSON.parse(JSON.stringify(this.followList));
}
You instantiate your data with a direct link to the (initially undefined) property of your prop. This property is a complex entity like an Object (Arrays are Objects), and is thus called via reference. Since members references the same thing in memory as followList.data, when you're calling members, it will follow the reference to the same entity as followList.data. This doesn't have to do with Vue2 reactivity, but here's a link nontheless.
push mutates the array it is called on; it will follow the reference through members and change followList.data, updating its value when called through followList as well. Because the data key is not present on instantiation of the component, Vue can't watch it (just like you need to use Vue.set when adding a new key to a data object).
concat returns a new array of merged elements, and then replaces
the reference in members with the new array. Therefore from this point on you'll
no longer mutate followList.data, even with a push, as the reference has changed to a new entity.
When trying to set your initial members and dataset, I suggest using an initialization method that creates a clone of your followList and writes that to dataset, and running this on the created() or mounted() hook of your component lifecycle. Then create a computed property for members, no need to store followList.data thrice and potentially have dataset and members diverge.
Slall question:
I have a 2 parent components nesting the same child component inside of them.
I use props so the parents can tell the child what title to show.
this child component is a photo gallery. It makes a query to the database, download photos and show them up. classic.
I need the parents to tell the child where to get the photos from:
Get the photos from All users, for the home page
or
get only the photos from a specific user for a user's page.
I'm wondering if I can pass this information through a prop.
Is that possible? Can we use the info from a prop as a varialble inside of the setup() function?
Is there a better way to do this?
Passing objects from one component to a child component is the purpose of props.
You can pass many items through props. VueJS has the following types built-in:
String
Number
Boolean
Array
Object
Function
Promise
In the V3 VueJS guide it gives the following example of a prop being passed into a component and then being logged to the console inside the setup() method.
export default {
props: {
title: String
},
setup(props) {
console.log(props.title)
}
}
However, when using props, you should always mark whether the is required, what its type is and what the default is if it is not required.
For example:
export default {
props: {
title: String, // This is the type
required: false, // If the prop is required
default: () => 'defaultText' // Default needed if required false
},
setup(props) {
console.log(props.title)
}
}
It's important to note that when using default, the value must be returned from a function. You cannot pass a default value directly.
I created the child using:
const ComponentClass = Vue.extend(someComponent);
const instance = new ComponentClass({
propsData: { prop: this.value }
})
instance.$mount();
this.$refs.container.appendChild(instance.$el);
When this.value is updated in the parent, its value doesn't change in the child. I've tried to watch it but it didn't work.
Update:
There's an easier way to achieve this:
create a <div>
append it to your $refs.container
create a new Vue instance and .$mount() it in the div
set the div instance's data to whatever you want to bind dynamically, getting values from the parent
provide the props to the mounted component from the div's data, through render function
methods: {
addComponent() {
const div = document.createElement("div");
this.$refs.container.appendChild(div);
new Vue({
components: { Test },
data: this.$data,
render: h => h("test", {
props: {
message: this.msg
}
})
}).$mount(div);
}
}
Important note: this in this.$data refers the parent (the component which has the addComponent method), while this inside render refers new Vue()'s instance. So, the chain of reactivity is: parent.$data > new Vue().$data > new Vue().render => Test.props. I had numerous attempts at bypassing the new Vue() step and passing a Test component directly, but haven't found a way yet. I'm pretty sure it's possible, though, although the solution above achieves it in practice, because the <div> in which new Vue() renders gets replaced by its template, which is the Test component. So, in practice, Test is a direct ancestor of $refs.container. But in reality, it passes through an extra instance of Vue, used for binding.
Obviously, if you don't want to add a new child component to the container each time the method is called, you can ditch the div placeholder and simply .$mount(this.$refs.container), but by doing so you will replace the existing child each subsequent time you call the method.
See it working here: https://codesandbox.io/s/nifty-dhawan-9ed2l?file=/src/components/HelloWorld.vue
However, unlike the method below, you can't override data props of the child with values from parent dynamically. But, if you think about it, that's the way data should work, so just use props for whatever you want bound.
Initial answer:
Here's a function I've used over multiple projects, mostly for creating programmatic components for mapbox popups and markers, but also useful for creating components without actually adding them to DOM, for various purposes.
import Vue from "vue";
// import store from "./store";
export function addProgrammaticComponent(parent, component, dataFn, componentOptions) {
const ComponentClass = Vue.extend(component);
const initData = dataFn() || {};
const data = {};
const propsData = {};
const propKeys = Object.keys(ComponentClass.options.props || {});
Object.keys(initData).forEach(key => {
if (propKeys.includes(key)) {
propsData[key] = initData[key];
} else {
data[key] = initData[key];
}
});
const instance = new ComponentClass({
// store,
data,
propsData,
...componentOptions
});
instance.$mount(document.createElement("div"));
const dataSetter = data => {
Object.keys(data).forEach(key => {
instance[key] = data[key];
});
};
const unwatch = parent.$watch(dataFn || {}, dataSetter);
return {
instance,
update: () => dataSetter(dataFn ? dataFn() : {}),
dispose: () => {
unwatch();
instance.$destroy();
}
};
}
componentOptions is to provide any custom (one-off) functionality to the new instance (i.e.: mounted(), watchers, computed, store, you name it...).
I've set up a demo here: https://codesandbox.io/s/gifted-mestorf-297xx?file=/src/components/HelloWorld.vue
Notice I'm not doing the appendChild in the function purposefully, as in some cases I want to use the instance without adding it to DOM. The regular usage is:
const component = addProgrammaticComponent(this, SomeComponent, dataFn);
this.$el.appendChild(component.instance.$el);
Depending on what your dynamic component does, you might want to call .dispose() on it in parent's beforeDestroy(). If you don't, beforeDestroy() on child never gets called.
Probably the coolest part about it all is you don't actually need to append the child to the parent's DOM (it can be placed anywhere in DOM and the child will still respond to any changes of the parent, like it would if it was an actual descendant). Their "link" is programmatic, through dataFn.
Obviously, this opens the door to a bunch of potential problems, especially around destroying the parent without destroying the child. So you need be very careful and thorough about this type of cleanup. You either register each dynamic component into a property of the parent and .dispose() all of them in the parent's beforeDestroy() or give them a particular selector and sweep the entire DOM clean before destroying the parent.
Another interesting note is that in Vue 3 all of the above will no longer be necessary, as most of the core Vue functionality (reactivity, computed, hooks, listeners) is now exposed and reusable as is, so you won't have to $mount a component in order to have access to its "magic".
I have standard Vue.js component and I'd like to convert attributes in data property to watcher or in other words I want to construct a watch object based on data property attributes automatically
my idea looks something like this
watch: {
...(() => {
const watchers = {}
Object.keys(this.$data).forEach(key => {
watchers[key] = () => {
new ProductNutrientUpdate(this).run()
}
})
return watchers
})(),
},
the problem with this approach is that this.$data is not constructed yet
maybe there is some way how I can add watchers in created hook for example??
Vue already watches properties of the data object (note if any of these values are themselves objects, I think you need to update the whole object, i.e. change its value to a shallow copy with the desired nested key-values).
Refer to: https://v2.vuejs.org/v2/guide/reactivity.html
You can then use the update lifecycle hook to watch for all changes to data: https://v2.vuejs.org/v2/api/#updated
I was able to resolve a challenge using the following approach
created() {
Object.keys(this.$data).forEach(key => {
this.$watch(key, function() {
// ... some logic to trigger on attribute change
})
})
}
This is more a "which method should I use" question, rather than a how to.
I have the following in my Vuex.Store() instance:
store.js:
export default new Vuex.Store({
state: {
acceptedTermsAndConditions: false
},
})
From various components I'm emitting an event which sets this.$store.state.acceptedTermsAndConditions to true or false, dependent on different User inputs.
However, in my component I would set the checked value of a "Accepts T&Cs" checkbox to this value, something like this:
components/Component.Vue:
data () {
return {
form: {
checkboxTermsAndConditions: this.$store.state.acceptedTermsAndConditions
}
}
}
I'm just not sure what method handles this? Does a solution require a getter? If not, what is the best way to watch for state changes and set data values accordingly?
If you want to set the checkbox state based on the stored value, you should use the computed object and the mapGetters helper function in your component:
https://vuex.vuejs.org/guide/getters.html#the-mapgetters-helper
computed: {
...mapGetters(['acceptedTermsAndConditions'])
}
Like this, the value will be accessible in your component. If you want to do the contrary (refresh the store based on the checkbox value), you should create a mutation in your store and you should use this in your component:
methods: {
...mapMutations(['setTACcheckbox'])
}
This way, inside your component you can refresh the store value with this.setTACcheckbox(value).