I decided to make a very simple webapp to get my feet wet with the Meteor framework. The result is countwith.me! The site is basically just a real-time, anonymous counter. Users can submit the next number in the sequence. If their number is incorrect, the count resets to 1. Even though it’s a mind-numbingly simple app, I learned quite a bit from it.
Using Grunt
My first instinct when building out a front-end for a project is to use Grunt and Bower. In order to prevent Meteor from trying to process and bundle all of my Grunt and Bower resources and dependencies, I decided to keep them in a hidden directory called .grunt
, which Meteor ignores. Gruntfile.js
is set up to watch scss
for SASS changes, and then dump the compiled css file in the root directory. I have a grunt copy target set up to manually copy select resources (lodash.min.js
, moment.min.js
) out of .grunt/bower_components
into js
. Meteor finds these css and js resources and bundles them.
From what I’ve seen, using Grunt and Bower in this way isn’t the “Meteor way” of doing things. Instead, I should be using meteor packaged versions of the tools I need, like meteor-scss, meteor-lodash and meteor-moment. I’m not sure how I feel about this. I keep see the benefits of using things that only exist within the Meteor ecosystem, but at this point it seems like we’re just adding an extra, unnecessary layer of package management.
Subscription onReady
One of the first things I did was set up a template to iterate over a Counts
collection and display the number in a span:
<template name="numbers">
{{#each counts}}
<span class="number {{#if wrong}}wrong{{/if}}">{{number}}</span>
{{/each}}
</template>
As the collection began to grow, I was surprised to see that the data in the DOM would be initially incorrect when the app first started or was refreshed. A quick search led me to a StackOverflow answer that suggested not displaying data until the subscription onReady
callback is called. A quick check of the docs made it pretty clear how the onReady
callback works. By setting a session variable when the subscription was ready, I was able to hide the collection’s DOM elements until they were ready to be seen:
<section class="numbers {{#if notReady}}not-ready{{/if}}">
<span class="number" contenteditable>???</span>
{{> numbers}}
</section>
Publish/Subscribe and MiniMongo - Ohhhh, Now I Get It
As my Counts
collection grew in size, I was beginning to have some severe performance problems. I spent several hours slamming my head against the Chrome Dev Tools trying to track down what I thought was some kind of memory leak. If only I had ready the Understanding Meteor Publications & Subscriptions article, I would be 3 hours richer.
Initially, I was using auto-publishing and setting up my sorting and limiting Counts
query on the client:
if (Meteor.isClient()) {
...
counts: function() {
return Counts.find({}, {sort: {timestamp: -1}, limit: 30});
},
After removing autopublish
, I wanted to move that logic to the server, so I did just that:
if (Meteor.isClient()) {
...
counts: function() {
return Counts.find();
},
...
if (Meteor.isServer()) {
Meteor.publish('counts', function () {
return Counts.find({}, {sort: {timestamp: -1}, limit: 30});
});
However, after my client refreshed itself, I noticed that my data was not being updated when I submitted new numbers. I was under the impression that this server-side Counts
query was not returning a reactive collection to the client. In a desperate attempt to fix the problem I removed the sorting and limiting from the server and moved them to the client. This worked, and I went about my business, oblivious to my impending doom.
if (Meteor.isClient()) {
...
counts: function() {
return Counts.find({}, {sort: {timestamp: -1}, limit: 30});
},
...
if (Meteor.isServer()) {
Meteor.publish('counts', function () {
return Counts.find();
});
Hours later, after reading through the previously linked Understanding Meteor article, I realized the errors of my ways. I also realized that the data being passed to the client was reactive, it simply didn’t look like it was updating because the default sort order was placing the new counts at the bottom of the list, out of sight. The ultimate fix was to sort and limit on the server, and sort on the client.
if (Meteor.isClient()) {
...
counts: function() {
return Counts.find({}, {sort: {timestamp: -1}});
},
...
if (Meteor.isServer()) {
Meteor.publish('counts', function () {
return Counts.find({}, {sort: {timestamp: -1}, limit: 30});
});
Conclusion
Check out countwith.me. Also check out its github repo. Ultimately, I’m very happy with how the project turned out. I never thought that such an incredibly simple webapp would teach me so much about a framework. Every little thing I learned about the framework gave me glimpses into much deeper topics that I’m anxious to explore. It’s interesting how such a seemsly simple framework can be so deeply nuanced.