One of the holiest grails of web development is without a doubt reusable components.

For years now we’ve been waiting for adaptable, plug-and-play modules you can just drop into your app. And whether it’s Angular or React, each successive framework promises its own solution to that conundrum.

Now Meteor may not be quite there yet, but there are already some simple steps you can take to work towards this goal. So in this article, I’ll show you how to take a simple pattern and make it more “component-y”.

Paginated Lists

Let’s pick a concrete example straight out of Telescope: a paginated list of posts.

This component needs to accomplish two main tasks: display a list of items, and provide a “load more” button that lets you load in more data.

Now in order to understand what our component should be like, let’s first take a look at the traditional way to do things – assuming anything could be called “traditional” in the Meteor world.

Router-Level Subscriptions

First, we need to subscribe to a publication in order to obtain the necessary data. In Discover Meteor, we do this at the router level, which lets us easily pass a URL segment as a subscription parameter to achieve easy reactive pagination.

We then take the resulting cursor, and pass it as data context to the template, who simply acts as a view to display our data. So far, so good!

Subscribing at the router level.

This approach has its limits, though. For example, what happens when you want to have two post lists on the same page? Do you take in two URL parameters and declare two subscriptions? What happens when we need three lists? Or four?

By tying subscriptions to the router, we risk painting ourselves into a corner where you can only have a fixed number of subscriptions for any given route. And after all, isn’t the whole point of modern, single-page web apps to break away from the screen-by-screen model?

A Concrete Example

To give you a concrete example, take the Product Hunt homepage.

The Product Hunt homepage.

It’s a list of days, each of which can contain an indefinite number of products. In other words, it’s a paginated list of paginated lists. So while you can find out the number of days to display from the route and subscribe to that days publication, you get stuck once you need to go one level deeper and subscribe to the products for each day.

Template-Level Subscriptions

Which brings us to template-level subscriptions. If we’re handling subscriptions at the template level, we can now show as many lists as we need at the same time.

In our afore-mentioned Product Hunt example, you’d have one big loop iterating over a list of days, and then inside it each day would have its own products subscription. And in fact, that’s exactly how I implemented this pattern for Telescope’s “daily” view.

Subscribing at the template level.

But this power comes at the cost of flexibility. For example, what happens when we want to display the same data, but in a table instead of a list?

In the previous, router-based scenario, we could’ve just pointed our router controller or route to a different template, while keeping the same subscriptions and data context. But since we’ve now hard-linked our template to our subscription, we lose that option.

Telescope’s daily view. Each day has its own paginated subscription.

Introducing Template Controllers

Which brings us to the final, best-of-both-worlds approach: template controllers. This is not a new concept by any means, and it’s already popular in the React world. But let’s see how it applies to Meteor.

The idea is very simple: we’ll keep the same approach as template-level subscriptions, except that we’ll split the subscription and the actual template markup into two different templates.

Practically speaking, the controller template will receive a set of arguments (in our case, a limit), use them to subscribe to the publication, wait for the result, and then generate a data context; which it will then pass on to the view template.

The template controller pattern.

So we now have a three-tiered structure:

  • The app passes a set of arguments to the template controller.
  • The template controller takes these arguments, subscribes to a publication, and returns a data context.
  • The template view takes this data, and displays it as markup.

The key point is that each part can operate completely independently from the others. The controller doesn’t care where it’s receiving its arguments from. It could be a route, a session variable, or even another template.

And the view doesn’t care where it’s receiving its data from. It could be receiving it from a controller, a route, or even a plain JSON object: its only job is to take the data and display it, no questions asked.

Telescope’s profile page. The template controller controls multiple lists, and outputs them as tables.

Template Controllers vs Controller Templates

If we wanted to be perfectly accurate, we’d be talking about “template controller templates”, since our controllers control other templates, while being templates themselves.

But that’s a bit of a mouthful, so we think it’s fine to use either “template controller” or ”controller template” instead.

Template Controllers In Practice

Here’s a simplified overview of how Telescope uses template controllers.

First, when the user hits a route for one of Telescope’s many lists of posts, the router kicks in and generates the correct arguments based on the route:

Posts.controllers.list = RouteController.extend({

  template: "posts_list_controller",

  data: function () {

    var terms = {
      view: this.view, // the view to use to sort posts
      limit: this.params.limit || Settings.get("postsPerPage", 10)
    };

    return {
      template: "posts_list", // the template to use to display the actual list
      terms: terms
    };

  }

});

Here, we’re defining a Posts.controllers.list router controller from which all post list routes will inherit.

Note the template: "posts_list_controller" line: that’s what tells the router to call in our controller, and pass it the result returned by our data function.

But wait, remember how I said our controller didn’t care where it received its arguments from? In other parts of Telescope, these same arguments are provided not by a route, but by another template!

<template name="user_upvoted_posts">
  <div class="user-profile-votes grid grid-module">
    <h3>{{_ "upvoted_posts"}}</h3>
    {{> posts_list_controller arguments}}
  </div>
</template>

So what does this posts_list_controller template look like anyway? Glad you asked. The “template” part of the template is extremely simple:

<template name="posts_list_controller">
  {{> Template.dynamic template=template data=data}}
</template>

All it does is include the template template with the data data context (I know, my variable naming skills need work…). The interesting part is the associated code that controls our controller.

I’ll skip the part that actually governs subscriptions for now, since we covered that pretty extensively in our template-level subscriptions blog post. For now, let’s focus on the template helpers:

Template.posts_list_controller.onCreated(function () {

  // see template-level subscription article

});

Template.posts_list_controller.helpers({
  data: function () {

    // a bunch of code goes here, but let's ignore it for now

    return {

      postsCursor: postsCursor,

      postsReady: postsReady,

      hasPosts: !!postsCursor.count(),

      hasMorePosts: postsCursor.count() >= postsLimit,

      loadMoreHandler: function () {

        // increase pagination limit

      },

      controllerInstance: instance 

    };

  }
});

We’re passing on five things as part of the data object we return:

  • postsCursor contains the cursor we want to iterate over (in other words, the result of Posts.find()).
  • postsReady indicates whether the subscription is ready or not.
  • hasPosts keeps track of whether there are any posts to display or not.
  • hasMorePosts indicates whether we have loaded all posts, and is used to know if we should keep showing the “load more” button or not.
  • loadMoreHandler contains the function to be executed when the user clicks “load more”.
  • controllerInstance contains a reference pointing back to the current controller template instance, in case we need to access it from the view template.

Meteor makes it tricky to access a parent template instance from one of its children. That’s why we pass on a reference to instance, and also why we define loadMoreHandler here and not inside the view template.

Note that you don’t have to pass these specific five properties, they’re just the ones that make sense for Telescope. The idea is just to generate the properties you need inside the controller, and keep the view as skinny (or “dumb”) as possible.

Finally, let’s look at our third tier: the view template.

<template name="posts_list">
  <div class="posts-list">
    <div class="posts">
      {{#each postsCursor}}
        {{> post_item}}
      {{/each}}
    </div>
    {{> postsLoadMore}}
  </div>
</template>

<template name="postsLoadMore">
  <div class="posts-load-more">
    {{#if postsReady}}
      {{#if hasPosts}}
        {{#if hasMorePosts}}
          <a class="more-button" href="#"><span>{{_ "load_more"}}</span></a>
        {{/if}}
      {{else}}
        <div class="no-posts">{{_ "sorry_we_couldnt_find_any_posts"}}</div>
      {{/if}}
    {{else}}
      <div class="loading-module">{{> spinner}}</div>
    {{/if}}
  </div>
</template>

You can see how the template reuses the various objects passed to it by the template controller. And in the associated template code, all we have to do is wire up our “load more” button click event:

Template.postsLoadMore.events({
  'click .more-button': function (event) {
    event.preventDefault();
    this.loadMoreHandler();
  }
});

Conclusion

When Meteor first launched, it basically gave developers a set of tools and left them to their own devices to figure out the best way to use them.

A few years have passed since then and while some patterns have slowly emerged, we’re still far from a single “this is how it’s done” best practice. And some would argue that we’ll never find one-size-fits-all solutions to many of these problems.

Still, it can’t hurt to try and iterate towards greater reusability and flexibility. Are template controllers the right pattern for every single Meteor app out there? Probably not. But give them a try, and let us know what you think!