ParseJS and VueJS

Hi all!

For those unfamiliar with VueJS, it can be extremely useful to get/set properties on data, such as setting ‘Name’.

e.g:

data() {
  monster: new Parse.Object();
}
<input v-model="monster.Name" placeholder="monsterName"/>
<button @click="monster.save()"/>

However, the get/set is all done via dot notation, meaning that Parse Objects have to be converted toJSON first, and then back, which can make the code longer than needed.

The alternative is to set properties directly via subclassing. (I have also tried with Proxy but VueJS already uses Proxy internally so it doesn’t work as expected)

class Monster extends Parse.Object {
    constructor() {
      super('Monster');
      this.loadData();
    }
    loadData() {
      const internal = ['id','className','createdAt','updatedAt', 'ACL']
      const data = this.attributes;
      for (const key in data) {
        if (internal.includes(key)) {
          continue;
        }
        this[key] = data[key]; // is this dangerous
      }
    }
    async save() {
      const internal = ['id','className','createdAt','updatedAt', 'ACL']
      for (const key in this) {
        if (internal.includes(key)) {
          continue;
        }
        if (this.get(key) !== this[key]) {
          this.set(key, this[key]);
        }
      }
      await super.save();
    }
    _finishFetch(serverData) {
      super._finishFetch(serverData);
      this.loadData();
    }
  }

It might be a bit of a trivial question: but are there any risks with setting properties directly on a Parse.Object this way?

Thanks in advance :blush:

I have some thoughts, but take them with a grain of salt as I’ve never used VueJS…

One issue I can see occurring with:

this[key] = data[key]; // is this dangerous

Is this ParseObject is a class/reference type.You are probably familiar with the problems that can occur with reference types when it comes to threading, but this can also cause issues if your application is using this object in a view/onscreen or in multiple places. Basically modifications to your Monster object from anywhere will be reflected in your review even though you don’t expect them to. This may be what you want or not.

To be fair, I think most/all of the Parse SDKs suffer from depending on reference types except for the Parse-Swift SDK. These seems problematic IMO, particularly in situations when LiveQuery or GraphQL which allow objects to be modified/updated via web sockets.

If VueJS is single threaded, or 1 object is only used in one place at a time, or you protect protect against threading issues (locks, etc), I think your setup is fine, and you can disregard what I said above.

Thank you so much for the detailed reply, I really appreciate it.

VueJS allows you to bind data to UI and automatically updates UI for you reflective of the data, so I think if that’s an outcome of the subclass, it should be fine. Perhaps I should make a copy of data[key]?

The main concern I was thinking is whether this could potentially override any core properties, with unexpected impacts on other core functions.

@dblythy - in your #save fn:

this.set(key, this[key])

  • will this not cause all the keys to be registered as dirty and therefore cause the entire model to be patched rather than just the dirty fields? It seems to from my initial test and makes sense that it would as you are re-set()ting every field in the object

  • why would you set props on this instance itself and not on a nested ‘this.data’ object? What are the advantages of this?

Even though I don’t know VueJS, this sounds similar to SwiftUI. Binding the view directly to the model, in your case Monster seems like it can potentially run into the issues I mentioned since model is a reference type. In SwiftUI and MVVM, there is typically a intermediary between the Model and View. It’s suggested to have the Model as value type, the View as a value type, and the ViewModel (intermediary) as reference type. It seems like you may be using the Monster model as a ViewModel when comparing to MVVM, but if you are know how to handle the problems I mentioned then you should be fine.

The main concern I was thinking is whether this could potentially override any core properties, with unexpected impacts on other core functions.

I see, I don’t think I provided anything related to these concerns

Thanks @kulanu for looking over this.

You’re correct - my code sends data even when it’s not required. I added a line on the save function that if data isn’t changed, don’t set.

And secondly, the reason I set it directly on this was because I wanted to access monster.name. I might be incorrect, but I think VueJS’ reactivity doesn’t work as expected on “deeper” properties.

Your comments are very much appreciated. I will keep an eye out for any problems related.

I’m not overly familiar with SwiftUI yet, but I would expect that you are correct in saying it functions similarly to VueJS.

Thanks again @cbaker6 :+1:

Actually Vue’s reactivity WILL recognize deeply nested properties added to root object when that root object (eg monster.data) is set as reactive on a vue instance, FYI. In any case, it’s a good solution, I borrowed your _finishFetch and added this to my wrapper class. Shalom.

1 Like

Also FYI, this equality check will work fine for primitives but not for objects or arrays stored on those kyes as they will never be equal

1 Like

I should note on the master version of the JS SDK, you can register subclasses without needing to definitively state “Class Name”. This can allow you to easily make one core helper for all your classes, such as:

class ParseVueObject extends Parse.Object {
  constructor(className) {
    // constructor className is only available on master
    super(className);
    this.loadData();
  }
  loadData() {
    const internal = ['id', 'className', 'createdAt', 'updatedAt', 'ACL'];
    const data = this.attributes;
    for (const key in data) {
      if (internal.includes(key)) {
        continue;
      }
      this[key] = data[key];
    }
  }
  async save() {
    const internal = ['id', 'className', 'createdAt', 'updatedAt', 'ACL'];
    for (const key in this) {
      if (internal.includes(key)) {
        continue;
      }
      if (JSON.stringify(this[key]) !== JSON.stringify(this.get(key))) {
        this.set(key, this[key]);
      }
    }
    await super.save();
  }
  _finishFetch(serverData) {
    super._finishFetch(serverData);
    this.loadData();
  }
}
const classNames = ['ClassOne', 'ClassTwo', 'ClassThree'];
for (const className of classNames) {
  Parse.Object.registerSubclass(className, ParseVueObject);
}

Thanks to this PR.

1 Like

@kulanu for nested keys:

const internal = ["objectId", "id", "className", "createdAt", "updatedAt", "_localId", "_objCount"];
class ClassName extends Parse.Object {
  constructor() {
    super("ClassName");
    this.loadData(this);
  }
  loadData(object) {
    if (!object.toJSON) {
      return;
    }
    const data = object.toJSON();
    for (const key in data) {
      if (internal.includes(key)) {
        continue;
      }
      const value = data[key];
      object[key] = value;
      this.loadData(value);
    }
  }
  async save() {
    const saveNested = object => {
      for (const key in object) {
        if (internal.includes(key)) {
          continue;
        }
        let value = this[key];
        if (Array.isArray(value)) {
          const newArray = [];
          for (let i = 0; i < value.length; i++) {
            let nestedValue = value[i];
            if (nestedValue && nestedValue.__type) {
              nestedValue = Parse._decode(null, nestedValue);
            }
            newArray.push(nestedValue);
          }
          value = newArray;
        }
        if (value && value.__type) {
          const obj = Parse._decode(null, this[key]);
          saveNested(obj);
        } else if (JSON.stringify(object.get(key)) !== JSON.stringify(value)) {
          object.set(key, value);
        }
      }
    };
    saveNested(this);
    await super.save();
  }
  _finishFetch(serverData) {
    super._finishFetch(serverData);
    this.loadData(this);
  }
}

I haven’t tested this yet so use at your own risk. You’ll need to use .include in your queries for this to work.

This might help as to how I use the subclass with VueJS to achieve data binding without needing to override any VueJS config.

It does help. I have extracted a few elements from your ideas.

Just one thought - when it comes to extending the user object, you might want to make sessionToken an internal key as well - otherwise it gets set and subsequently becomes a dirty property on the user object which gets saved if you save your user.