Skip to content

Instantly share code, notes, and snippets.

@samuelantonioli
Last active March 6, 2021 02:06
Show Gist options
  • Save samuelantonioli/50b01142ce15d8a069fc23ea8b975873 to your computer and use it in GitHub Desktop.
Save samuelantonioli/50b01142ce15d8a069fc23ea8b975873 to your computer and use it in GitHub Desktop.
Vue.js and Parse.Object
/**
*
* The problem:
* Parse SDK JS uses a Backbone.Model-inspired API with .get/.set
* Vue.js doesn't know how to interact with it because it works with POJOs.
*
* It's hard to change both codebases so they work together.
* (failed project that tried to do that: https://github.com/parse-community/ParseReact)
* So the solution has to proxy between an POJO (using Object.defineProperty) and Parse.Object.
*
* The solution:
* I've created a layer called ParseObjectPOJO which bridges attributes from a Parse.Object to
* a POJO which can be used in a normal way i.e. "var m = ModelVM(); m.title = 'something';" (or "v-model='m.title';").
* Parse.Object provides toJSON() methods for all its types. It can therefore serialize all of its
* data to POJOs which can be used normally. The bridging happens when we use the original
* Parse.Object (under "m.$p"), when we save the model ("m.$save().then(...)") and when we fetch
* it ("m.$fetch().then(...)"). The POJO and the Parse.Object get synchronized so they contain the same values.
*
*
* I love Vue.js and parse-server because they speed up the development especially for rapid prototyping.
* But I always had the problem that I had to use the REST API directly and transform all the data instead
* of using the developer-friendly Parse SDK with Parse.Query and others.
* Now I can use both tools without getting problems with Vue's two-way-binding.
*
* It's not a perfect solution (many toJSON- and defineProperty-calls and you have to know which fields
* get populated so you don't miss out on IE support - otherwise it could be done using ES6-Proxy),
* but it's definitely better than doing data transformations and using the REST API.
*
**/
// Your Parse.Object Model (example from docs.parseplatform.org)
var Monster = Parse.Object.extend("Monster", {
// Instance methods
hasSuperHumanStrength: function () {
return this.get("strength") > 18;
},
// Instance properties go in an initialize method
initialize: function (attrs, options) {
this.sound = 'Rawr';
},
}, {
// Class methods
spawn: function(strength) {
var monster = new Monster();
monster.set('strength', strength);
return monster;
},
});
// example vue instance (to see the data synchronisation)
var vue_instance = new Vue({
data: {
inst: null,
loading: true,
},
});
vue_instance.$watch('inst', function(val) {
console.log('watch triggered');
console.log(val);
});
////////////////////////////////////////////////////////////////////////////////////
var MonsterVM = ParseViewModel(Monster, [
'title', 'age', 'file',
]);
// which attributes are there? (necessary for defineProperty)
// create an instance (without id: new one, with id: existing one)
var m = MonsterVM('v5aR6P2zpO');
// sync the model instance with the vue instance
// so we can use e.g. v-model="inst.title"
m.$sync(vue_instance, 'inst');
// load the model data from the server
m.$fetch().then(function() {
vue_instance.loading = false;
});
// -> vue_instance.inst gets automatically updated (through m.$sync)
// clone the object to work safely on it (deep clone)
var working_copy = m.$clone();
working_copy.title = 'a new title';
// works with files (uploads on working_copy.$save call)
var input_file = document.querySelector('#file').files[0];
working_copy.file = new Parse.File(input_file.name, input_file);
// and with ACLs
var acl = new Parse.ACL(Parse.User.current());
acl.setRoleWriteAccess('editor', true);
acl.setRoleReadAccess('editor', true);
working_copy.$acl(acl);
// get the current acl:
// var current_acl = working_copy.$acl();
// we can also work with the original Parse.Object (under .$p)
working_copy.$p.set('age', 55);
working_copy.$p.set('title', 'another title');
// but when we use this, we have to call .$update() after those calls to tell the view model
// that we've changed some attributes on the Parse.Object:
working_copy.$update();
working_copy.age == 55;
// same works for the other direction
// it gets automatically reflected on the model while using .$p (internally it's a function)
working_copy.age = 67;
working_copy.$p.get('age') == 67;
// done with it. we can update the original object:
m.$update(working_copy);
m.$save();
// or we can continue to use the working copy
working_copy.$save().then(function() {
alert('everything\'s saved!');
});
// using Parse.Query
var query = new Parse.Query(Monster);
query.find().then(function(results) {
results = results.map(MonsterVM);
console.log(results);
});
/**
*
* Vue.js and Parse.Object
*
* Transform Parse.Object into POJO so we can use it in Vue.js
* Make it possible to use the Parse.Object and sync changes to the model data.
* Easily $fetch and $save model data.
*
* needs:
* - Lodash
* - Parse SDK JS
* - Vue.js
*
**/
function ParseObjectPOJO(obj, cloned) {
// keep track of changed attributes and attribute names to watch
// ob: use observable to proxy data
// sync: variables to set when updating data
var changed = {}, attrs = {}, data = {}, ob = {}, sync = [];
if (cloned) {
if (cloned.data) data = cloned.data;
if (cloned.attrs) attrs = cloned.attrs;
}
var update_data = function(obj) {
var _data = obj.toJSON();
delete _data['updatedAt'];
delete _data['createdAt'];
// special attributes (clone them):
_data.objectId = obj.id ? ''+obj.id : undefined;
_data.className = obj.className ? ''+obj.className : undefined;
//_data.updatedAt = obj.updatedAt;
//_data.createdAt = obj.createdAt;
Object.keys(_data).forEach(function(key) {
data[key] = _data[key];
});
//data = _data;
sync_handler();
},
sync_handler = function() {
// sync changes to variables (they use defineProperty internally)
sync.forEach(function(s) {
Vue.set(s[0], s[1], ob);
});
};
// convert json back to Parse.Object (only changed attributes)
var parse_obj;
var toParseObject = function() {
var use_parse_obj = false;
if (
parse_obj &&
parse_obj.objectId == data.objectId &&
parse_obj.className == data.className
) {
use_parse_obj = true;
}
// https://github.com/parse-community/Parse-SDK-JS/issues/161
var new_data = {
objectId: data.objectId,
className: data.className,
};
Object.keys(changed).forEach(function(changed_key) {
if (use_parse_obj) {
// reflect changes on parse_obj
parse_obj.set(changed_key, data[changed_key]);
} else {
new_data[changed_key] = data[changed_key];
}
});
changed = {};
if (use_parse_obj) {
// sync from Parse.Object to data
var dirty_keys = parse_obj.dirtyKeys();
dirty_keys.forEach(function(dirty_key) {
data[dirty_key] = parse_obj.get(dirty_key);
});
sync_handler();
return parse_obj;
} else {
parse_obj = new Parse.Object(new_data);
return parse_obj;
}
},
destroy = function() {
// reset data structures
ob = {};
data = {};
changed = {};
attrs = {};
sync = [];
};
// can't use normal assignment because it goes to the JSON structure
Object.defineProperty(ob, '$p', {
get: toParseObject,
enumerable: false,
configurable: false,
});
// watch for changes and save changes in 'changed'
var watch = function(keys) {
keys.forEach(function(key) {
if (!data.hasOwnProperty(key)) {
data[key] = null;
}
Object.defineProperty(ob, key, {
get: function() {
return data[key];
},
set: function(val) {
if (!changed[key]) {
changed[key] = true;
}
return data[key] = val;
},
enumerable: true,
configurable: true,
});
});
// comment out if you're not using Vue.js
// this line is crucial for Vue.js!
// without this line it doesn't synchronize with Vue.js
// and everything stops working
new Vue({
data: function() {
return {data: ob.$data};
},
});
},
add_attrs = function() {
// call this with all the attribute names you want to watch
var attrs_names = Array.from(arguments);
attrs_names.forEach(function(name) {
attrs[name] = true;
});
return watch(attrs_names);
},
update_watch = function() {
watch(Object.keys(data).concat(Object.keys(attrs)));
};
// operations
var self = this;
var save = function() {
var p = ob.$p, promise = new Parse.Promise();
p.save.apply(p, Array.from(arguments)).then(function(data) {
update(data);
promise.resolve(ob);
}, function(err) {
promise.reject(err);
});
return promise;
},
fetch = function() {
var p = ob.$p, promise = new Parse.Promise();
p.fetch.apply(p, Array.from(arguments)).then(function(data) {
update(data);
promise.resolve(ob);
}, function(err) {
promise.reject(err);
});
return promise;
},
_sync = function(target, key) {
sync.push([target, key]);
//variable = data;
// remove line if not using Vue.js
Vue.set(target, key, ob);
},
update = function(new_obj) {
// if vm.$p.set('somekey', val) was used, tell it here
// using vm.$update() so we can check it.
if ((!new_obj) && parse_obj) {
new_obj = parse_obj;
} else if (!new_obj) {
return;
}
if (new_obj.hasOwnProperty('$p')) {
// unfortunately we can't use instanceof bc of ob.defineProperty
// (would need a bigger refactoring)
new_obj = new_obj.$p;
}
update_data(new_obj);
update_watch();
},
set_acl = function(acl) {
if (!acl) {
return (new Parse.ACL(data['ACL'])).toJSON();
} else if (acl instanceof Parse.ACL) {
changed['ACL'] = true;
data.ACL = acl.toJSON();
}
},
clone = function() {
// use the following line if you don't use Vue.js:
// return new ParseObjectPOJO(ob.$p);
return new ParseObjectPOJO(ob.$p, {
attrs: _.cloneDeep(attrs),
data: _.cloneDeep(data),
});
// TODO: try/except for "Error: Tried to encode an unsaved file."
},
get_data = function() {
return data;
};
// internal helper functions
var add_functions = function(fns) {
Object.keys(fns).forEach(function(fn) {
var call = fns[fn];
Object.defineProperty(ob, fn, {
get: function() {
return call;
},
enumerable: false,
configurable: false,
});
});
}
// init
update_data(obj);
update_watch();
add_functions({
'$destroy': destroy,
'$attrs': add_attrs,
//
'$sync': _sync,
'$update': update,
'$acl': set_acl,
'$clone': clone,
'$data': get_data(),
//
'$save': save,
'$fetch': fetch,
});
if (cloned) {
new Vue({
data: function() {
return {data: ob.$data};
},
});
}
return ob;
}
function ParseViewModel(model, attrs) {
return function(_id) {
var m;
if (_id instanceof model) {
m = _id;
} else if (typeof(_id) === 'string') {
m = model.createWithoutData(_id);
} else {
m = new model();
}
var vm = new ParseObjectPOJO(m);
vm.$attrs.apply(vm, attrs);
return vm;
};
}
@dblythy
Copy link

dblythy commented Mar 6, 2021

I have submitted 73 PRs to Parse as a regular contributor - so I might be able to give some insight.

I would guess:

  • The only way to convert .get(key) to .key is with a Proxy. And as discussed, because VueJS wraps all data in a Proxy, this doesn't work. I don't think there's a workaround for this.
  • Migrating the core JS to Proxy / work with Vue natively might have unwanted performance or support issues on other platforms (e.g, Proxy isn't supported on IE)
  • VueJS doesn't recommend setting class objects to data(), only flat, simple data. Parse Objects are complex and have a lot of properties and methods.

I have simplified your solution a little bit by overriding _finishFetch. Have a look if you're interested.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment