Building Troll's Goals 1.0

Background

For the past year, I've been using Joe's Goals to track what I do each day:

Joe's Goals

The basic idea is this: define a bunch of "goals", each with a name and point value, and add a check mark each time you work toward that goal. Alternatively, if you give a goal a negative point value, you give yourself X marks when you perform the corresponding activity. Each day's points are summed at the bottom of the table, giving you a score for that day.

I like this tracking method because:

  • It helps me to hold myself accountable for my actions, by reporting and viewing them.
  • The recording process is simple and fast, taking just a few minutes a day.
  • It's easy to see how productive I was for a given day, by comparing my check marks to my X marks, or by looking at my total score for the day.
  • I can think up rules and set goals based on these records, for example "No playing Games when my score for the day is already negative".

However, after using Joe's Goals for a year, I've come up with a number of additions I'd like to make to the app, including:

  • Goal types - Organizing my goals into groups makes the recording process somewhat quicker.
  • Neutral goals - Not every activity I track deserves points - drinking water, for example.
  • Check details - I like to record details along with certain check and X marks, keeping a log of what I've been doing.
  • Mobile support - Sometimes, the only way I can record my actions is through my smartphone.

I have a number of other features on my wishlist, but I haven't implemented these, so you can find them below in the "TODOs" section.

Objective

In Derek Sivers's How to hire a programmer to make your ideas happen, Derek advises thinking of "the bare minimum that would make you happy". Identifying this was simple: I wanted to build myself a replacement for Joe's Goals. I decided to name this app Troll's Goals, because the words rhyme, and a troll will eventually make a nice mascot for the app.

After far too much thought, I settled on my own terms for the data types that make up this system:

  • Records are recorded entries, analogous to Joe's Goals' checkmarks or X marks.
  • Categories are record types, analogous to Joe's Goals' goals..
  • Domains are category types.

In these terms, to meet my "bare minimum", the app must let me:

  • Define categories, grouped by domain.
  • Create records in a given category, for a given day.
  • Display the records I've created, both for today and for past days.

Technologies

Platform - Node.js

Node.js is a platform built on Google Chrome's V8 JavaScript engine. I've used Node.js to build nearly all of the web applications I've built since 2011, and Troll's Goals is no exception.

Web App Framework - Express

Express is a popular Node.js web application framework that provides useful functionality for handling HTTP requests. Each web app I've built - including Troll's Goals - is an Express app, and routing to these apps is handled by a "bootstrap" Express app.

Database - MongoDB

MongoDB is the leading NoSQL database, which I've used for most of my web apps. For ayoshitake.com, I use MongoLab database servers.

Database Interface - Mongoose

Mongoose is a Node.js package that serves as a higher-level interface to MongoDB, allowing me to do more with less code. Mongoose models can be treated as classes, something I find so useful that I've occasionally created Mongoose models for objects that I never save to MongoDB.

MVC Framework - AngularJS

AngularJS is the one technology in this list that I'd never used before making Troll's Goals. I recognized that I'd need to use a frontend MVW framework to provide some structure to Troll's Goals, so I figured I'd take this chance to try out Angular. I'd heard a lot about Angular:

  • It's made by Google, and it's increasingly popular among web developers.
  • It's a framework, meaning that it calls your code, as compared to a library like Backbone.js, which your code calls.
  • Its learning curve is often described as "hockey-stick-shaped", meaning that "it starts off gently and you think everything is going great until you crash face-first into a gigantic brick wall which seems to rise infinitely high above you," as Niali Ryan put it.
  • Its functionality is broken up into many modules and directives, both built-in ones and those contributed by other developers.
  • Its documentation and tutorial were confusing and unhelpful. I later learned that the tutorial was rebuilt, and it seems like a fine place to start.
  • It's supposedly good for large-scale CRUD (Create Read Update Delete) apps.

I started my AngularJS learning by watching the egghead.io AngularJS videos, typing up the code as John Lindquist did. The first 30 videos or so taught me the basic concepts of Angular, after which I went to the tutorial, which showed me how to use the $resource module to implement CRUD.

Looking back on my experience developing with AngularJS, some great "Aha!" moments stand out, as do some frustrating times when things weren't working, and I couldn't figure out why. That's the thing about ambitious frameworks like Angular - they use behind-the-scenes "magic" to make things ridiculously easy, but when the magic isn't working, a novice like me has no idea what's going wrong.

Base Styles and Icons - Bootstrap

Bootstrap is a popular framework that provides base CSS styles and icons. As an added bonus, the UI Bootstrap project provides common UI components, implemented as AngularJS directives with Bootstrap styling.

Problems Solved

Shared CRUD code with cascading

Implementing CRUD (Create Read Update Delete) often involves a lot of repeated code; each model (Record, Area, and Domain) has at least four routes that perform operations on that model:

  • GET /api/:model_name (Read all) - Returns all saved documents of the given model type.
  • GET /api/:model_name/:id (Read) - Returns a single document of the given model type, specified by its ID.
  • POST /api/:model_name (Create) - Creates and returns a document of the given model type, given its non-ID attributes.
  • PUT /api/:model_name/:id (Update) - Modifies a document of the given model type, specified by its ID.
  • DELETE /api/:model_name/:id (Delete) - Deletes a document of the given model type, specified by its ID.

To create these routes with a minimum of code, I iterated over each model, defining these five routes for each model, in routes.js.

routes.js also defines an /api/record/query route, allowing complex queries to be made for records. Additionally, the DELETE route calls an optional Model.deleteHook function to "cascade" model deletions - deleting a domain causes its areas to be deleted, and deleting an area causes its records to be deleted.

The end result: models share route-handling code that is common between all of them, while code that is unique to a model can be defined directly on the model.

CRUD Tooltips

To add details to records, I decided to attach a tooltip to each record, exposing the record's details, and allowing the user to edit them. The UI Bootstrap Tooltip directive didn't meet my needs for the Troll's Goals CRUD tooltips:

  • Tooltips must have isolate scope, as each tooltip displays information read from a different model, and each tooltip's buttons must do different things.
  • I had to dynamically close tooltips in response to clicking any of the buttons on the tooltip.
  • I needed to use custom attributes to select and change the template to be used: one attribute for the model type (Record, Category or Domain), and another for the CRUD type (Create or Edit).

My solution was to find this modified version of Tooltip and further modify it to suit my needs. The result is crud_tooltip.js.

I put a lot of work into making the CRUD tooltips work for records, but once I got it working, it was really easy to generalize the tooltips to work for categories and domains as well. This was one of my "Aha!" moments - all five types of CRUD tooltips use exactly the same code, with only their HTML attributes and templates differing.

Directives

I wrote a number of Angular directives to implement some usability improvements; each one is a custom HTML attribute that, when applied to an element, causes Angular to run some custom JavaScript code on the element. These directives can be found in directives.js.

trigger-click-on-load

Used to immediately open record tooltips when they're created in an area with "Enter Record Details?" set to true.

focus-on-load

Used to focus the "Details" field of a record's tooltip whenever it's opened.

focus-next-when-clicked

Used to make Bootstrap input-group-addons focus their corresponding input field when clicked.

ng-enter, ng-ctrl-delete, and ng-escape

Used to set handy keyboard shortcuts for each of the tooltip buttons.

click-on-arrow

Used to click the arrows at the top of the page (changing the day/week being displayed) when the user hits the left or right arrows. I'm including the code for this directive below.

directivesModule.directive('clickOnArrow', function($document, $timeout) {
  var directions_to_key_codes = {
    left: 37
  , up: 38
  , right: 39
  , down: 40
  }; // JavaScript key codes for each arrow
  return function(scope, element, attrs) {
    // translate from element attribute, e.g. click-on-arrow="left", to key code
    var key_code = directions_to_key_codes[attrs.clickOnArrow]
      // this handler will be called for all keydown and keypress events
      , handler = function(e) {
      if (e.which !== key_code) {
        return; // key pressed wasn't the one we want
      }
      if (element.is(':hidden')) {
        return; // element is hidden, meaning it shouldn't be clicked
      }
      var focused_tag_name = e.target.tagName.toUpperCase();
      if (focused_tag_name === 'INPUT' || focused_tag_name === 'TEXTAREA') {
        return; // user probably meant to navigate within the focused text box
      }
      $timeout(function() {
        element.click();
      });

      e.preventDefault();
    };
    // listen for all keydown and keypress events at the top level ($document)
    $document.bind('keydown keypress', handler);
    // unbind handler, since body persists beyond element
    scope.$on('$destroy', function() {
      $document.unbind('keydown keypress', handler);
    });
  };
});

Calculating today

To calculate the current date, I call a new Date object's getTime method, converting it the number of milliseconds since Unix Epoch (1970 January 1, 12:00 AM). I add or subtract time to match the user's local time, and convert this number to the number of days since Unix Epoch. This takes place on page load, in main.js:

var now = new Date();
  now = now.getTime() - now.getTimezoneOffset() * 60000;
  $rootScope.today = Math.floor(now / 86400000) + 1;

When I need to convert dates from this number - - to a human-readable format - Thursday 7.24 - I use two AngularJS filters I wrote, both in filters.js. The first filter converts the day value back to a Date object, while the second calls the built-in Angular date filter to format the date the way I like:

filtersModule.filter('dayToDate', function() {
  return function(day) {
    var timestamp = day * 86400000 // multiply by ms/day
      , date = new Date(timestamp);
    return date;
  };
});

filtersModule.filter('dateFormat', function($filter) {
  var angularDateFilter = $filter('date');
  return function(date) {
    return angularDateFilter(date, 'EEEE M.d');
  };
});

The Result

The result of this work is Troll's Goals version 1.0, live at trollsgoals.ayoshitake.com.

Joe's Goals

For information about how to use Troll's Goals (and how I use it), see Getting Started with Troll's Goals.

TODOs

New Features

  • Add support for goals, e.g. "Business > Pleasure", "Business >= 20/week", "Service >= 1/day", etc.
  • Add support for projects, e.g. "Build Troll's Goals 2.0", "Read The Brothers Karamazov", "Watch Fringe", etc.
  • (COMPLETE) Add support for hiding areas - my Wedding area will be obsolete once the wedding is over, but I'd prefer hiding the area to deleting it.
  • Add units to records, e.g. minutes, milligrams, milestones, words, etc., and allow users to enter numbers of units.
    • One issue I faced when trying to design this was how to display these values in the records table. I want to replace the check icons with the respective numbers of units, but unless I can assume every area in a domain shares units, I can't simply add these numbers up to a domain total.
  • Add support for record templates, with predefined areas, numbers of units, and details. For example: "16 oz. green tea", Caffeine, 50 mg.
    • This feature is most useful when combined with units (as described above), and a quick way to create new records based on templates.
  • Add icon options for domains and areas, e.g. ☺, ☻, ☹, ★, ✓, ✗, etc.
  • Incorporate the troll into Troll's Goals, displaying a different troll based on whether the user is meeting his or her goals - fat, lazy trolls for lazy users, and fit, strong trolls for productive users.
  • Add visualizations to summarize data over a given time periods: pie charts, bar/line graphs, Aster Plot, Radar Chart. Goal: visualize in what areas user is investing time.
  • Add support for sub-areas. Emulate Mint: a record can go into the "Exercise" area, or into a sub-area: "Cardio", "Resistance Training", etc.

Best Practices

  • Write automated tests, both unit tests for individual features and end-to-end tests for the main use cases.
  • Use a build process to minify all JavaScript and CSS files, and gzip HTML.
  • Test and/or fix with other browsers - I've only tested Troll's Goals with Chrome and Chrome for Android.

Usability

  • (COMPLETE) Modify CRUD code to make frontend immediately reflect changes, simulating zero latency to server and from server to database. (done for Create, not for Delete)
  • (COMPLETE) Recalculate the today value when necessary, so that users don't have to refresh to view today's page.
  • (COMPLETE) Create default domains and areas for new users.
  • (COMPLETE) Preserve records for days that have already been displayed, eliminating the record-display delay when switching days back to a previously-viewed day.
  • (POSTPONED) Add right-click handlers to edit domains, areas, and records.
  • Add confirmation dialogs for delete actions - at least for deleting domains and areas.
  • Add a tutorial to quickly introduce users to the interface/app, rather than pointing them to the "Getting Started" article.
  • Add frontend validation for creating and updating domains and areas.
  • Modify popup code to not squish record-editing tooltip for the rightmost day.
  • Only show days/dates for the first domain table on the week page.
  • Indicate hidden areas "Excel-style" - extra lines indicate that a row is missing; clicking these lines reveals the area.

Bugs

  • (FIXED) On first load, new users can't delete domains/areas automatically created for them.

Tags: 

Add new comment

Markdown

  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.

Filtered HTML

  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
By submitting this form, you accept the Mollom privacy policy.