• Meteor
  • Tutorial

Make a Reactive Join in the UI

In this video, I'd like to show you how you can do a join across collections and the user interface of your Meteor application. Let me start by showing you a simple data model that we'll be using for the video. This is similar to the new Evented Mind data model. At the top, you can see I have a tracks collection. And I have three tracks in that collection. Each have a title.

And then down at the bottom, I have a Contents collection. And the Contents collection has documents that have a title, a description, and then just some image file. And the idea here is that I want contents to be able to be shared across multiple tracks.

And so one way to do that is to have a Join table or a join collection. So if you're coming from SQL, this should be familiar to you. But if you've only used Mongo, it might be a little bit foreign. So the Join table will join by having a property for the track ID. So in this first row here, you can see that the track corresponds to Track 0 because of the ID here, and then a content ID, which corresponds to one of the content records.

And so in this case, this corresponds to this Content 0 record here, because the ID is the same. And then I've added a couple other properties, like an index so that we can sort this table. And then, of course, it gets its own ID, because it's just a regular Mongo document.

In my Applications HTML file, I'm going to simulate being on a particular track page by using the With Helper and providing a Track Data context. And within that data context, I'll render the Track Details template.

Within that template, we'll iterate over something called Track Contents, which will be a helper that returns a cursor. And for each one, we'll render a list item with the ID of the item and the content ID just for now. And if there's no items in the list, we'll just say there's no content yet. Let's take a look at it in the browser.

So over in the browser, you can see that for this particular track, I have one, two, three, four, five contents associated with the track. So this isn't a very useful user interface. What I'd like to really show here is the title of the contents and maybe the description as well.

So I need to get that data from the Contents table. In my code file on the left, you can see the three collections declared at the top, and then the helper that's declared on the body called Track. And that's how we get our data context.

And you can think of this as being on a particular route for a track, but we're just simulating this for now to make things simple. And then in the Track Details area, we have this Track Contents helper, which right now is just returning a cursor with this Find call for all track contents that are associated with this particular track and then sorting by the index.

Now, what we want to do is to grab the actual contents for each one of these items so that we can display the title and description. One way to do that is to add a Transform function as an option to the find call. To do that, in my Options area, I can provide a Transform option. And that Transform Function will take a document, which will be the specific Track Contents document. And what it's expecting us to do is to mutate the document in some way and then just to return it.

So one thing we could do inside of this Transform function is to grab the particular content associated with the document. So I'll do a contents.find1, and we'll grab it based on the content ID. And then we can assign the content to a content property and document. Or if we really wanted to simplify things, we could just mix it indirectly using the _extend method and extending the document with some of our own properties-- in this case, with content.

Now, we don't want to actually override the IDs and all of that. So what I'm going to do is actually use the omit method of underscore, and we're going to omit the ID and just return everything else. So back in the user interface, nothing has quite changed yet, because we're not making use of these new fields. Let's do that now.

So I'll get rid of this line here. And instead of just printing out not very useful ID and content ID, I'll print out the title, and then a dash and the description. And when I hit Save, the user interface is looking a little bit more useful now. But there's a problem with this implementation. If I change one of the underlying content documents, its respective row in this user interface won't be updated. And that's because the Transform function is run inside of a non-reactive block.

To demonstrate this, I'm going to update the first content in the table here. So what I've done is made a Find 1 call to grab the first content document, and then I'm going to update it and set its title to Updated. And when I press Enter, the content record was updated, but you can see the user interface is not updated.

So what we want is to reflect that change in the user interface. Now, the problem, as I had mentioned before, is that the Transform function is run inside of a non-reactive callback. And I think that's to prevent infinite loops. And so if this content changes, this function will not be invalidated.

So I think an easy solution for this is to create another helper function that does the join. So I'll create a Helper function called join with content. And that Helper function will just essentially do the same thing that the Transform function was doing.

But instead of having a doc, what we'll want to do is say that the track content is equal to this. This will help us keep track of what this points to. And what we'll do is we'll find contents where the doc is actually track content. And we'll get rid of this line here. And we'll still use the Extend function of underscore, and we'll extend the track content with the content, but we'll omit the ID.

And next up, what we would need to do is inside of the Each call, we're going to use with block helper, and then we'll call our join with content helper. OK. Now we're back in business. In the user interface, you can see content, titles, and descriptions are showing for each row. But this Helper function is run in a reactive context. So if we change one of these Content documents, the user interface will be updated. Let's just confirm to make sure.

So I'll update the first row and the collection, and you can see that the title gets updated. Now, you might be worried about performance in that we're making a database query in Mini Mongo for each record. And so if you're coming from Rails, they call this the n plus 1 problem.

But in this case, you don't need to worry. Because making a Find 1 call is essentially just calling a property key on an object literal. And so it does a lookup in almost constant time. That's one of the benefits of writing queries against Mini Mongo in the browser. And now we get the added benefit of having reactivity with our join.