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.

@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.

@dblythy Following up on this thread from the spring.

If one wanted to tap into the _finishFetch method globally (across all classes) and without having to make a new Parse.Object.extend for every class, what is the right way to do this? Calling super is not allowed in non-class definition methods so I can’t a way to do it.

Eg: the following cannot work:
Parse.Object.prototype._finishFetch = function (serverData) { ... }

Object.defineProperty(Parse.Object.prototype, '_finishFetch', { ... })

I’m stuck on this one … I simply don’t want to have to create a new class for all 100+ classes I have to work with just to get access to this hook.

Of course the long term and better solution would be if Parse.Object (and Parse.Query) offered hooks that you could tap into, as it down with #initialize - eg beforeRead, afterRead, beforeSave, afterSave

Thoughts?

Hi @kulanu,

I wasn’t quite happy with my previous solution to this and felt like there could be a cleaner way to solve this issue.

I have created a new approach and a PR which should in theory be able to automatically traverse multiple levels of pointers, allow dot notation for every class, etc.

Let me know if you have any feedback.

Hi! I’m new here, so please forgive me if my answer is off in time or context or both.

I had the same issue, and ended up doing what I believe you addressed initially this way:

  const props = defineProps<{
      task: any
    }>()

  const done = computed({
    get: () => props.task.get('done'),
    set: val => { props.task.set('done',val) }
  })

and in html:
<input type="checkbox" v-model="done" />

This seems to work fine, but obviously, there will be a lot of code when you have to do this for many fields in many places

Please do let me know if you think this is an ok way of solving this, or if I would be better off look into the suggestion above instead.

This would work fine, but you would have to write it for every single column.

I tend to use:

import { Parse } from 'parse';
Parse.initialize(...);
Parse.serverURL = ...
const _internalFields = Object.freeze([
  'objectId',
  'id',
  'className',
  'attributes',
  'createdAt',
  'updatedAt',
  'then',
]);
const proxyHandler = {
  get(target, key, receiver) {
    const value = target[key];
    const reflector = Reflect.get(target, key, receiver);
    if (
      typeof value === 'function' ||
      key.toString().charAt(0) === '_' ||
      _internalFields.includes(key.toString())
    ) {
      return reflector
    }
    return receiver.get(key) ?? reflector;
  },

  set(target, key, value, receiver) {
    const current = target[key];
    if (
      typeof current !== 'function' &&
      !_internalFields.includes(key.toString()) &&
      key.toString().charAt(0) !== '_'
    ) {
      receiver.set(key, value);
    }
    return Reflect.set(target, key, value, receiver);
  },
};
class ParseVueObject extends Parse.Object {
  constructor(...args) {
    super(...args);
    return new Proxy(this, proxyHandler);
  }
}
const subclasses = ["TestObject"]
for (const sub of subclasses) {
  Parse.Object.registerSubclass(sub, ParseVueObject);
}
app.config.globalProperties.$Parse = Parse

This way you can get and set TestObject properties via dot notation without needing computed.

Not sure what the equivalent typescript would be.

Thank you! Looks great. Will try it out.

I use Vuejs too (mostly with Nuxtjs) so this PR looks very interesting (not sure how I missed it :slight_smile: ). I’ve taken to not using the SDK because of all that’s mentioned here, but this could seriously change that, if it gets merged. I suspect this will help most modern frameworks!

1 Like

@woutercouvaras Would you want to test out the PR and give some feedback? The reason it hasn’t been merged yet becase we want to know about potential side effects before merging.