How to use parent's mixin as a child model - vue.js

Im trying to use a parent's mixin as a child model, but I couldn't make it work as the mixin isn't resolved on the 'data' definition.
Here is a fiddle:
<div id="vue-instance">
<div>
USER: {{user}}
<br/>
EMAIL: {{email}}
</div>
<input-list :field="field" v-for="field in fields"/>
</div>
js:
Vue.component('input-list', {
name: 'InputList',
props: ['field'],
template: '<div>{{field.id}}: <input type="text" v-model="field.model"/></div>'
})
var userData = {
data() {
return {
user: 'foo',
email: 'foo#barbar'
}
}
}
var vm = new Vue({
el: '#vue-instance',
mixins: [userData],
data() {
return {
fields: [
{
id: "UserName",
model: this.user
},
{
id: "Email",
model: this.email
}
]
}
}
});
https://jsfiddle.net/rafalages/rp7bu2qt/9/
The expected result would be update parent mixin value in the child input.

This is not an issue with the mixin, but rather the overall design, if you we to use data instead of mixin, you'd see the same behaviour.
This will not work they way you intend it to.
Two things about Vue worth re-iterating:
You pass props down and emit events up
Mutating a prop will not update the parent
more reading here:
https://v2.vuejs.org/v2/guide/components-props.html#One-Way-Data-Flow
There is a note at the bottom
Note that objects and arrays in JavaScript are passed by reference, so if the prop is an array or object, mutating the object or array itself inside the child component will affect parent state.
But my recommendation is to think of this as a side-effect rather than a feature. You should not rely on the child from updating the value. The only reliable way (outside of other state management) is to pass a function.
Another issue is that referencing this in data during creation will not work
data: {
fields: [
{
id: "UserName",
model: this.user
},
{
id: "Email",
model: this.email
}
]
}
This will not use the data from this.user and will be set to null. You can use the mounted life-cycle function to set these like this:
mounted() {
this.fields = [
{
id: "UserName",
model: this.user
},
{
id: "Email",
model: this.email
},
]
}
which will set it to use the initial values. But referencing an object like this inside another object will also serve to create new bindings, essentially cutting the reactivity. This means that you'll use the values from those objects, but changes to this.fields[1].model will not update this.email
here is a semi-working fiddle that attempts to use your current design
https://jsfiddle.net/rp7bu2qt/113/
notice that...
the binding is using v-model, which is shorthand for binding a prop and an emit/update to a value
you see an error
You are binding v-model directly to a v-for iteration alias. This will not be able to modify the v-for source array because writing to the alias is like modifying a function local variable. Consider using an array of objects and use v-model on an object property instead.
changes to the input fields do not change the original data, just the content of fields[n].model

Related

How to properly watch an object in Vue?

I am trying to use a Vue watcher on a computed object and it is not working at all. It works properly if it's just a string, but not if it's an object. I followed the Vue documentation but I am still getting nothing logged to the console when the object changes. The properties of the object and computed property are changing, as I have confirmed in Vue Tools. What am I doing wrong? Thanks
<v-btn small dark #click="test1.asdf = 'blah'">Test</v-btn>
data() {
return {
test1: {},
}
},
computed: {
test() {
return this.test1
}
},
watch: {
test: {
handler: function(val, oldVal) {
console.log(oldVal, val);
},
deep: true
}
}
Try this code, its works fine
<template>
<div id="app">
{{ test1 }}
<hr />
<button #click="test1.asdf = 'blah'">Click</button>
</div>
</template>
<script >
export default {
data() {
return {
test1: { asdf: "HEY" },
};
},
computed: {
test() {
return this.test1;
},
},
watch: {
test: {
handler: function (val, oldVal) {
console.log(oldVal, val);
},
deep: true,
},
},
};
</script>
I'd guess in your case you should add .native at end of your click event like this #click.native="test1.asdf = 'blah'"
Just tried by replacing the whole object. It works pretty well, and there is no need to initialize your object with the asdf property in your data:
<v-btn small dark #click="test1 = { ...test1, asdf: 'blah'}">Test</v-btn>
The spread syntax ...test1 helps keeping other properties in the target object if there is any, so you can safely add the asdf property without worrying about losing anything.
JSFiddle: https://jsfiddle.net/vh72a3bs/22/
#BadCoder Objects and Arrays are pass by reference in JavaScript, not pass by value. This means that when you add a key to the object as you are you doing in your question, you're just adding to the same Object. It's contents have changed but your variable test1 is still referencing the original object and unaware that its contents have updated. The watcher doesn't pick this change up. You can add deep: true to your watcher as another answerer has suggested, but this only watches for a couple of levels deep, so not ideal if you have a large object with lots of nested data. The most reliable way to trigger a watcher when dealing with arrays or objects is to create a new instance of that object. You can achieve this with object destructing.
Something like,
<v-btn small dark #click="test1 = { ...test1, asdf: 'blah'}">Test</v-btn>
works because you're creating a new object (using the previous objects attributes, plus anything new), triggering the watcher.

Vue.js dynamic binding v-model and value

I use django-rest-framework + vue.js
My goal is to make a Form to edit user-profile.
Here is what i have:
<input type="email" v-model="userEdit.email">
<input type="text" v-model="userEdit.location">
<input type="text" v-model="userEdit.bio">
my inputs are bounded to data object "editUser"
data() {
return {
'editUser': {
email: '',
location: '',
bio: '',
image: '',
},
}
},
so now i can send this object to the server and change user-profile information.
sendChanges() {
const fd = new FormData();
fd.append('image', this.editUser.image, this.editUser.image.name)
fd.append('email', this.editUser.email)
fd.append('location', this.editUser.location)
fd.append('bio', this.editUser.bio)
this.axios.put(userDetailURL + this.routeUser, fd)
.then(response => {
console.log(response)
})
.catch(error => {
console.log(error.response)
})
},
this form works and updates info, but there is a thing i dont like:
The input fields are always empty, and user needs to fill all of them before can press save button.
even if the user wants to change only "location" he must fill other inputs which are empty.
adding dynamic :value="userDetail.email" to the input -- no work.
is there any other way to add current value to input field and still have v-model?
current data is here:
computed: {
userDetail() {
return this.$store.getters.userDetail;
},
},
The problem is that you are binding the values in data to the form and those values are initially empty.
The cleanest and easiest solution I can think of right now is updating the initial data in mounted lifecycle hook:
mounted () {
// Use Object.clone to prevent modifying the userDetail object directly
this.editUser = Object.clone(this.$store.getters.userDetail)
}
There are other solutions, though. You could use a computed setter whose getter defaults to whatever is in the store but can be overridden when set.

Is this the best way to handle props that could be empty?

I have an scenario where I pass an array as a prop to a component. I then filter out the object I need based on an ID I passed as another prop. I do this with a computed property.
for example
people[
{id: '1', name: 'Frank', age: '33'},
{id: '2', name: 'Bethany', age: '22'},
{id: '3', name: 'Roscoe', age: '44'},
]
<my-component :person-id="id" :peopleArray="people[]"></my-component>
I pre-fill the component data with empty strings in case the data I'm calling is empty
data: function(){
personName: '',
personAge: ''
},
and then use the computed property to to populate that data
computed: {
getActivePersonById: function(){
return this.people.find(x => x.id === '1')
},
getActivePersonName: function(){
return this.personName = this.getActivePersonById.name
},
getActivePersonId: function(){
return this.personName = this.getActivePersonById.age
},
}
I then use the values in the HTML like so
(I'm resorting to :value because v-model was not updating from the computed property)
<label for="person_name">Name</label>
<input type="text" name="person_name :value="personName">
<label for="person_age">Age</label>
<input type="text" name="person_age :value="personAge">
Finally I need to submit any updated data by the user, I can try grab the values from data or get the values with a js query. Please let me know if there is a better way of doing this. I'd love to use the props data directly but I need to filter it by ID first.
Computed properties are by default getters only i.e they just return a value. To use a computed property as a v-model you should make use of computed setters.
computed: {
getActivePersonById: function(){
return this.people.find(x => x.id === '1')
},
getActivePersonName: {
get(){
return this.getActivePersonById.name
},
set(newVal){
this.personName = newVal
}
},
getActivePersonAge: {
get(){
return this.getActivePersonById.age
},
set(newVal){
this.personAge = newVal
}
}
}
Then use these computed properties as v-model for your inputs
<label for="person_name">Name</label>
<input type="text" name="person_name v-model="getActivePersonName">
<label for="person_age">Age</label>
<input type="text" name="person_age v-model="getActivePersonAge">
or else
make use of the created () lifecycle hook to initialize your data properties
created(){
this.personName = this.getActivePersonName
this.personAge = this.getActivePersonAge
}
note: in your computed properties just return values do not assign like you are doing
Just to add to the accepted answer. In my scenario both the computed setters and created solutions did not work.
However in a test with simpler data they did work. This was because I was initializing the component before the ID I was passing was available. I simply changed the component to only show when the ID was passed to it via v-if.
Now everything works fine. It was just a case of understanding how and when the component was initialized.

Using $refs in a computed property

How do I access $refs inside computed? It's always undefined the first time the computed property is run.
Going to answer my own question here, I couldn't find a satisfactory answer anywhere else. Sometimes you just need access to a dom element to make some calculations. Hopefully this is helpful to others.
I had to trick Vue to update the computed property once the component was mounted.
Vue.component('my-component', {
data(){
return {
isMounted: false
}
},
computed:{
property(){
if(!this.isMounted)
return;
// this.$refs is available
}
},
mounted(){
this.isMounted = true;
}
})
I think it is important to quote the Vue js guide:
$refs are only populated after the component has been rendered, and they are not reactive. It is only meant as an escape hatch for direct child manipulation - you should avoid accessing $refs from within templates or computed properties.
It is therefore not something you're supposed to do, although you can always hack your way around it.
If you need the $refs after an v-if you could use the updated() hook.
<div v-if="myProp"></div>
updated() {
if (!this.myProp) return;
/// this.$refs is available
},
I just came with this same problem and realized that this is the type of situation that computed properties will not work.
According to the current documentation (https://v2.vuejs.org/v2/guide/computed.html):
"[...]Instead of a computed property, we can define the same function as a method. For the end result, the two approaches are indeed exactly the same. However, the difference is that computed properties are cached based on their reactive dependencies. A computed property will only re-evaluate when some of its reactive dependencies have changed"
So, what (probably) happen in these situations is that finishing the mounted lifecycle of the component and setting the refs doesn't count as a reactive change on the dependencies of the computed property.
For example, in my case I have a button that need to be disabled when there is no selected row in my ref table.
So, this code will not work:
<button :disabled="!anySelected">Test</button>
computed: {
anySelected () {
if (!this.$refs.table) return false
return this.$refs.table.selected.length > 0
}
}
What you can do is replace the computed property to a method, and that should work properly:
<button :disabled="!anySelected()">Test</button>
methods: {
anySelected () {
if (!this.$refs.table) return false
return this.$refs.table.selected.length > 0
}
}
For others users like me that need just pass some data to prop, I used data instead of computed
Vue.component('my-component', {
data(){
return {
myProp: null
}
},
mounted(){
this.myProp= 'hello'
//$refs is available
// this.myProp is reactive, bind will work to property
}
})
Use property binding if you want. :disabled prop is reactive in this case
<button :disabled="$refs.email ? $refs.email.$v.$invalid : true">Login</button>
But to check two fields i found no other way as dummy method:
<button :disabled="$refs.password ? checkIsValid($refs.email.$v.$invalid, $refs.password.$v.$invalid) : true">
{{data.submitButton.value}}
</button>
methods: {
checkIsValid(email, password) {
return email || password;
}
}
I was in a similar situation and I fixed it with:
data: () => {
return {
foo: null,
}, // data
And then you watch the variable:
watch: {
foo: function() {
if(this.$refs)
this.myVideo = this.$refs.webcam.$el;
return null;
},
} // watch
Notice the if that evaluates the existence of this.$refs and when it changes you get your data.
What I did is to store the references into a data property. Then, I populate this data attribute in mounted event.
data() {
return {
childComps: [] // reference to child comps
}
},
methods: {
// method to populate the data array
getChildComponent() {
var listComps = [];
if (this.$refs && this.$refs.childComps) {
this.$refs.childComps.forEach(comp => {
listComps.push(comp);
});
}
return this.childComps = listComps;
}
},
mounted() {
// Populates only when it is mounted
this.getChildComponent();
},
computed: {
propBasedOnComps() {
var total = 0;
// reference not to $refs but to data childComps array
this.childComps.forEach(comp => {
total += comp.compPropOrMethod;
});
return total;
}
}
Another approach is to avoid $refs completely and just subscribe to events from the child component.
It requires an explicit setter in the child component, but it is reactive and not dependent on mount timing.
Parent component:
<script>
{
data() {
return {
childFoo: null,
}
}
}
</script>
<template>
<div>
<Child #foo="childFoo = $event" />
<!-- reacts to the child foo property -->
{{ childFoo }}
</div>
</template>
Child component:
{
data() {
const data = {
foo: null,
}
this.$emit('foo', data)
return data
},
emits: ['foo'],
methods: {
setFoo(foo) {
this.foo = foo
this.$emit('foo', foo)
}
}
}
<!-- template that calls setFoo e.g. on click -->

vuejs2 passing data between parent-child is wiping childs value

In VueJS 2 I am trying to create a component that gets and passes data back to the parent which then passes it to another component to display.
The component that gets the data has a user input field it uses to search. When I have it pass data back to the parent using $emit the value in the input keeps being wiped.
I am receiving the below mutation error but I haven't directly tried to change the userSearch field in the component so I am not sure why.
"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: "userSearch" (found in PersonField)"
Relevant html
<person-field v-on:event_child="eventChild"></person-field>
<person-search :prop="personListArray" ></person-search>
Parent app
var app = new Vue({
el: '#app',
data: {
personListArray : [],
tempArray: []
},
methods: {
eventChild: function (arr) {
this.personListArray = arr
}
}
})
Component 1, displays a user input. Uses the input to search and bring back data. Starts search when the length of the input is more then 2. As soon as you hit the 3rd character something is causing the input to clear which I don't want.
Vue.component('person-field', {
props: ['userSearch'],
template: '<input class="form-control" v-model="userSearch" >',
watch: {
userSearch: function () {
var arr = []
if (typeof this.userSearch !== 'undefined') { //added this because once i passed 3 characters in the field the userSearch variable becomes undefined
if (this.userSearch.length > 2) {
$.each(this.getUsers(this.userSearch), function (index, value) {
var obj = {
Title: value.Title,
ID: value.ID
}
arr.push(obj)
});
this.$emit('event_child', arr) //emits the array back to parent "eventChild" method
} else {
console.log('no length')
}
} else {
console.log('cant find field')
}
},
},
methods: {
getUsers: function (filter) {
//gets and returns an array using the filter as a search
return arr
},
}
});
Component 2 - based on the personListArray which is passed as a prop, displays the results as a list (this works)
Vue.component('person-search', {
props: ['prop'],
template: '<ul id="personList">' +
'<personli :ID="person.ID" v-for="person in persons">' +
'<a class="" href="#" v-on:click="fieldManagerTest(person.Title, person.ID)">{{person.Title}}</a>' +
'</personli></ul>',
computed: {
persons: function () {
return this.prop
}
},
methods: {
fieldManagerTest: function (title, ID) { //Remove item from users cart triggered via click of remove item button
//var user = ID + ';#' + title
//this.internalValue = true
//this.$emit('fieldManagerTest');
//this.$parent.$options.methods.selectManager(user)
},
},
});
Component 3, part of component 2
Vue.component('personli', {
props: ['ID'],
template: '<transition name="fade"><li class="moving-item" id="ID"><slot></slot></li></transition>'
})
;
The reason you get the warning,
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:
"userSearch" (found in PersonField)
Is because of this line
<input class="form-control" v-model="userSearch" >
v-model will attempt to change the value of the expression you've told it to, which in this case is userSearch, which is a property.
Instead, you might copy userSearch into a local variable.
Vue.component('person-field', {
props: ['userSearch'],
data(){
return {
searchValue: this.userSearch
}
},
template: '<input class="form-control" v-model="searchValue" >',
...
})
And modify your watch to use searchValue.
Here is an example.