While attempting to have Vue components react to changes in my js-data model instances and their relationships, I found a great post from Caleb Roseland with an accompanying code example. They detail how to make model properties reactive in Vue, without overriding js-data’s own reactivity. What they don’t discuss is how to make Vue react to changes in the set of models associated with another through a relationship.
For example, if you have a Post that hasMany Comments, and a component on your page that iterates over post.comments , then the comments list still won’t change in Vue when it does in js-data.
Summary / tl;dr
By adding a boolean option to specify that some js-data relations should be made reactive, and defining a VueReactiveRecord class that enables Vue reactivity on those relations, we can use related models in a Vue template and have them update when the data store changes. Check out this gist to see an example of the final result.
Making relationships reactive
My first attempt was to hew as closely to Caleb’s example as possible, automatically discovering every relationship and making them all reactive. Unfortunately the resulted in neither side of the relationship working correctly. Even though a post and some associated comment records were in the store, post.comments was empty as was each comment.post . At first I wondered if there would be an infinite loop problem, of A reacting to B reacting to A reacting to B reacting … . Instead it appeared as if neither side was able to react even once, and the models were never connected to each other. Either way, I didn’t debug that issue and instead set up a method by which relationships would only become reactive if explicitly told to do so.
Opting in
When defining the js-data mappers, I included a new optional property named vueReactive to the relationships that I explicitly wanted to make reactive within Vue components. For example, it’s much more likely that a new comment will be added to an existing post than that the post a comment was for will become a different one, so we can specify that the hasMany from posts to comments should be reactive, but leave the belongsTo from comments to posts alone:
1 2 3 4 5 6 7 8 9 10 11 12 |
defineMapper('post', ... ... relations: { hasMany: { comment: { foreignKey: 'post_id', localField: 'comments', vueReactive: true, }, }, }, ); |
With that in place, I updated Caleb’s ViewReactiveRecord class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class ViewReactiveRecord extends Record { ... // Add Vue reactivity to relationships as well, when their definitions say to. const relationsByType = this._mapper().relations; // e.g. relationsByType = { hasMany: {...}, belongsTo: {...} } for (const relType in relationsByType) { const relations = relationsByType[relType]; // e.g. relations is all hasMany relationships, or all belongsTo ones. for (const relName in relations) { const relation = relations[relName]; // Now relation is the actual definition of a single relationship on the mapper if (!relation.vueReactive) { continue; } const key = relation.localField; Vue.util.defineReactive(this, key, this[key]); } } ... } |
Using the reactive relationships
Now we can have a Vue template that does something such as this:
1 2 3 4 5 6 7 |
<div> <h1>{{ user.name }}</h1> <h2>Post titles</h2> <p v-for="post in user.posts" :key="post.id"> {{ post.title }} </p> </div> |
and know that if the set of associated posts in the js-data store changes, the list of titles displayed will reactively update accordingly.
Doug
1st: I love it, thank you.
I’m having a small problem with a ‘belongsTo’ relationship if the object which it belongs to is not yet created when the object is created. i.e.
employee: belongsTo position
position: hasMany employee
If an employee is created before the position, the employee.position is forever ‘undefined’ and will not be reactive (or even found by jsData)
Any Ideas??? Thanks again
munderwood
That’s one that I haven’t encountered myself so far, but my first reaction (so to speak) is that if the relationship is not even found in js-data, then it’s really something to do with that, and not with Vue or its reactivity.
The only thing I can think of off the top of my head without seeing more code would be if the position is being created only in the front end, and not persisted somewhere that would assign it an id. That is, if you say
const bossPosition = store.createRecord({ title: 'CEO' })
thenbossPosition.id
will be undefined (until after a call tobossPosition.save()
resolves), so assigningbossEmployee.position_id = bossPosition.id
won’t actually do anything…Doug
It appears my issue is related to what you referenced above as as a possible infinite loop.
I do need to have vueReactive set as true on both sides of some relations. Have you looked any more into this? Any ideas where I should start with debugging?