Support for nested models and collections
Opened this issue ยท 11 comments
Can the enhanced interface apply recursively?
For example, if I'm using backbone-associations (or Backbone-relational) with deeply nested models and collections, it would be great if a single root Vue instance could render data a few levels deep.
For example you might have a collection of people, where each Person model could have nested models or collections:
- Person collection
- Person model
- Employer model
- Children (Person collection)
- Spouse (Person model)
- Person model
Would vue-backbone currently only enhance the top-most model?
Yeah, currently it would only enhance the top-most model, but this sounds like a good feature and at first glance looking at the code, it shouldn't take too much to add it in ๐
Can you prepare a JSFiddle with backbone-associations and/or Backbone-relational (the latter is more popular so it would be nice to support that too)? Setup the fiddle(s) with Vue, Backbone, the relational library and vue-backbone.
Feel free to submit a PR yourself too, but if not I should be able to sort this out within a few days of getting the fiddles.
Thanks @wuservices!
Hi @mikeapr4, thanks so much for taking a look at this and the great library. We're really hoping to get a lot of stuff moved from Backbone to Vue, and this would be huge and helping our phased / gradual migration.
I've set up an example with Backbone and backbone-associations in this JSFiddle: https://jsfiddle.net/8y0wc79x/3/. We used to use Backbone-relational in our app, and for the purposes of how vue-backbone would integrate or how you access data (vs how you define the relationships), there should be no difference, so I think a PR that addresses this JSFiddle would work equally well with Backbone-relational.
In the JSFiddle, I borrowed tutorial code from the backbone-associations tutorial and modified it a little to show an example with a few deeply nested models and collections. This also includes one cycle / circular reference back to the original node in case that causes any issues that need to be addressed. I think you'd basically loop through all the attributes and detect any instances of Backbone.Model or Backbone.Collection and then recurse through, making sure you don't proxy something twice if you get to something you've already handled.
In the example, I rendered a version using the plain Backbone Models to show what I'd expect the output to look like once everything is working.
I've got some development in progress on a branch (PR #3).
In the end, Backbone-relational is more complicated, so we'll stick with Backbone-associations for the moment.
See the example HTML included. Note @wuservices, that you had an error in your JSFiddle, you included a circular dependency (which I have taken into account), but you never included the relations
record to match, so you had created just a normal Model attribute. I have updated the example to include it.
If you want to test it out, the dist folder is checked in on the branch, but as of right now it is incomplete.
Great I'll take a look! Is it possible to do this in a nested framework agnostic way?
Object.keys(model.attributes).forEach(function (attr) {
if (model.attributes[attr] instanceof Backbone.Collection) {
// Proxy nested collection
} else if (model.attributes[attr] instanceof Backbone.Model) {
// Proxy nested model
} else {
proxyModelAttribute(proxy, model, attr, conflictPrefix);
}
});
I'm still trying to figure out if it would make sense to think about it that way, but wanted to float the idea while I was still pondering.
Yeah, I would prefer that, but in the example here: http://backbonerelational.org/#examples
The attribute hash for paul
contains the user
attributes nested as an array and object, the Backbone objects aren't in the attributes structure. I'm not very familiar with Backbone Relational, but it is much more complex than Backbone Associations.
As for your solution above for Backbone Associations, it would add performance cost that is avoidable using the model meta data. Also it would allow for behaviour to be correctly setup in the case that a relational attribute was set to null
initially.
Yes Backbone Relational is kind of a beast, which is also why it's so slow. I was wondering how slow a few instanceof
checks would be, if only done when the proxies are first set up (once per model or collection).
Good point about your solution working for relational attributes that were initially null
. I kind of have things working for any nested model / collection (even ones not defined in relations
), by using the same proxy for all getters and setters. By always checking to see if anything has a proxy from the getter
and lazily figuring things out, it seems to work decently.
function proxyModelAttribute(proxy, model, attr, conflictPrefix) {
let getter = function() {
const val = model.get(attr);
// If val is a nested model or collection, return the proxy.
// Nested models or collections may not exist during construction,
// so evaluating this logic during get allows this to work with nested
// models and collections that are added after construction, as long as
// the attributes were initially defined.
return val && val._vuebackbone_proxy || val;
};
let setter = function(val) {
// If val is a proxied model or collection, set the original
model.set(attr, val && val._vuebackbone_original || val);
};
// If there's a conflict with a function from the model, add the attribute with the prefix
let safeAttr = proxy[attr] ? conflictPrefix + attr : attr;
Object.defineProperty(proxy, safeAttr, {
enumerable: true,
get: getter,
set: setter
});
}
Perhaps this still isn't desirable though, and ultimately for me, as long as it works with backbone-associations only, I'd be perfectly happy :).
As for the other issues, I coupled the above with more recursive calls to logic similar to the stuff in bindModelToVue
. Right now, it's still a bit hairy though. Compared to your initial PR, my proxies are staying updated and getting added to new nested models or collections, but the Vue instance still isn't updating.
I've experimented with something like adding vm.$data[dataKey] = rawSrc(bb, []);
or the collection equivalent on any nested change, but wasn't sure if that was the right place to go, or if it was too heavy handed. I guess the reactivity isn't quite hooked in right. Another though I had is somehow passing a reference to the Vue root down to all the nested proxies so that they could notify it of changes if needed without bubbling events all the way up a deeply nested tree, but should the root even need to know about changes, or does that just mean I didn't hook up the reactivity correctly?
...but the Vue instance still isn't updating.
I have an idea of how to get this working, but I won't be able to work on this for a few days, I initially expected it to take less time.
...the collection equivalent on any nested change, but wasn't sure if that was the right place to go, or if it was too heavy handed.
I see what you are thinking, but I think it is a bit heavy handed, we gotta watch out for false positives when triggering reactivity. There are definitely performance implications for that.
For the moment I want to keep all this work toggleable with config anyway, as it is important to consider that Backbone Associations use is still an edge case and I wouldn't want to impact performance for anyone not using it.
I should be able to find some time later in the week to do some more with this ๐
I see what you are thinking, but I think it is a bit heavy handed, we gotta watch out for false positives when triggering reactivity. There are definitely performance implications for that.
Yeah I figured and agree we want to be optimal for performance.
I should be able to find some time later in the week to do some more with this ๐
I recognize that this is an edge case so I'm very grateful that you're taking the time to work on this! ๐
Have you had a chance to give this another shot? If there's anything I can help with, especially on the associations side that in more familiar with, please let me know. I'm hoping I can use this on a big project I'm working on right now.
Hey @wuservices, yeah, I tried the approach I was thinking of, but I came across a fundamental problem. One of the core parts of vue-backbone is to be able to use the internal model attribute hash as a reactive object, not a copy, but the same object. I had been a bit hasty in my initial attempt and broke that concept.
To get this to work I'd have to go back to the drawing board with the library, and even then there's a big question over performance. For the moment I can't justify any of that, so I'm shelving this issue for now, sorry.
Thanks for giving it a shot and sharing the challenges. This definitely added a few challenges (especially with changing nested models and collections) that I didn't originally consider.
For now, I'll see if I could make vue-backbone work as it. Maybe by keeping nesting to a minimum within a single component on the Vue side, I could just keep wiring up new proxies at each level. Then if necessary I could always fall back to throwing some Backbone-aware code into the Vue components.