Thomas Van der Veen
Thomas Van der Veen

Reputation: 3226

Vue v-model and proxy first set is not called

Currently I am working on a Vue project where I want to have a model class that stores all its initial data in two properties: original and attributes. However I am having difficulties with binding properties to an input via v-model.

Lets say I have the following class:

class Model
{
    constructor () {
        return new Proxy(this, {
            get: (target, property, receiver) => {
                console.log('get');
                return target[property];
            },

            set: (target, property, value, receiver) => {
                console.log('set');
                target[property] = value + 123;
                return true;
            }
        });
    }
}

This class will log get when getting a property and set when setting a property. Also while setting it will add 123 at the end of the value. The following will be executed:

let model = new Model();

model.name = 'test'; // Logs: 'set'

console.log(model.name); // Logs: 'get' and 'test123'

This works as expected.

The problems start when I bind these properties to an input via v-model. The following happens:

Actions are executed on:

<input v-model="model.name" />
...
data () {
    return {
        model: new Model
    };
}

On the first change the property name will be added to the model object. No log at all. No added 123 at the end of the value. The model value has changed to { model: 't' }.

Second change it will log set and it will add 123. The model value has changed to { model: 'te123' }.

Logging the arguments of of the set call during the second change: Model {__ob__: Observer}, "name", "te", Proxy {__ob__: Observer}

Anyone had this problem before or knows what is going on here?

After answer of @sphinx

Thank you for your answer! It works!

To make things more complicated I want to have the properties that are in a certain fillable array to be set in a property called attributes. However the same behaviour returns.

class Model
{
    constructor () {
        this.attributes = {};
        this.fillable = ['name'];

        return new Proxy(this, {
            get: (target, property, receiver) => {
                console.log('get', property);

                if (target.fillable.indexOf(property) > -1) { // Check if in fillable
                    if (!(property in  target.attributes)) {
                        Vue.set(target.attributes, property, null);
                    }

                    return target.attributes[property];
                }

                return target[property];
            },
            set: (target, property, value, receiver) => {
                console.log('set', property, value);

                if (target.fillable.indexOf(property) > -1) {
                    Vue.set(target.attributes, property, value + 123);
                } else {
                    target[property] = value
                }

                return true;
            }
        });
    }
}
Vue.config.productionTip = false

new Vue({
  el: '#app',
  data() {
    return {
      test: new Model
    }
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>

<div id="app">
  <span>{{test.name}}</span>
    <input v-model="test.name" type="text">
</div>

During the first change the name property is set directly on the model object. But the set method is not called.

During the second change the name property is changing and the set method is called.

Any suggestions on why this happens?

Upvotes: 1

Views: 1390

Answers (1)

Sphinx
Sphinx

Reputation: 10729

In your exammple, the getter will create one property, but it is not reactivity (check Vue Guide: Reactivity In Depth).

So Vue can't track the changes.

The solution is easy as above guide introduces, uses Vue.set or vm.$set to make sure your object is reactivity.

Like below demo:

class Model
{
    constructor () {
        return new Proxy(this, {
            get: (target, property, receiver) => {
                console.log('get', property);
                if(!(property in target)) Vue.set(target, property, null)
                return target[property]
            },
            set: (target, property, value, receiver) => {
                console.log('set', property, value);
                Vue.set(target, property, value + 123)
                return true;
            }
        });
    }
}
Vue.config.productionTip = false
new Vue({
  el: '#app',
  data() {
    return {
      test: new Model
    }
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>

<div id="app">
  <span>{{test.name}}</span>
    <input v-model="test.name" type="text">
</div>

Upvotes: 2

Related Questions