Creating a simple Entity Reference system for Ember.js

I had some data stored in a structure similar to this:

image

Nothing too fancy. A table of Gift(s) and a table of Person(s). Each gift had a foreign key relationship to a Person (the person who “gave” the gift).

Since some people may give several gifts, I didn’t want to load the same data multiple times (each gift shouldn’t have the person’s name for example spelled out). I wanted to build a RESTful web service to return the gifts and persons as independent queries, with the resulting JavaScript objects being very similar to the original data storage in the database.

Using Ember.JS I built a small demonstration of one way that this could be done. I am aware of the in-progress data library for ember.js on github, but it was way more than I wanted (or needed). Furthermore, I found it more far complex than I wanted (and I’m not particularly fond of the syntax it requires). So, I created a simple system that I’ll likely expand upon over time and post here.

Here’s the very basic demo using handlebars templates (a core feature of Ember.js).

<!DOCTYPE html>

<html>
<head>
    <title>Demo2-EmberJS</title>
</head>
<body>

    <script type="text/x-handlebars" data-template-name="gifts">
      <h1>{{ giftReason }}</h1>
      <table>
          <thead>
          <tr>
              <td>Description</td>
              <td>Thrill</td>
              <td>From</td>
          </tr>
          </thead>
          {{#each gifts}}
          <tr>
              <td>
                  {{ name }} 
              </td>
              <td>
                  {{ excitement }}
              </td>
              <td>
                  {{ person.fullName }}
              </td>
          </tr>
          {{/each}}
      </table>
    </script>

    <script src="demo2/jquery-1.7.1.js" type="text/javascript"></script>
    <script src="demo2/ember-0.9.3.js" type="text/javascript"></script>
    <script src="demo2/app.js" type="text/javascript"></script>
</body>
</html>

And here is the content of app.js file referenced above (the other files can be downloaded from the web):

var Demo2App;
Demo2App = Ember.Application.create();
window.Demo2App = Demo2App;

// standard class that has an 'id' property
var Entity = Ember.Object.extend({
    id:null
});

(function () {
    Ember.entityRef = function (property, type) {
        // auto build connection if property is in the form
        // of 'typenameID' if type is not specified
        if (arguments.length === 1 && property) {
            var l = property.length;
            if (l > 2 && property.substr(l - 2).toLowerCase() === 'id') {
                type = property.substr(0, l - 2);
            } else {
                throw new Error("Type not specified, and cannot automatically determine store name. Referenced property name must end with Id.");
            }
        }
        var fn = new Function("return Demo2App.Stores.find('" + type + "', this.get('" + property + "')); ");
        return Ember.computed(fn).property(property);
    };
})();

/*
 Class definitions
 */

Demo2App.Person = Entity.extend({
    firstName:'',
    lastName:'',

    fullName:function () {
        return this.get('lastName') + ", " + this.get('firstName');
    }.property('firstName', 'lastName')
});


Demo2App.Gift = Ember.Object.extend({
    personId:null,
    person:Ember.entityRef('personId')
});


// build in a data store API
(function () {
    // within a closure, we'll store the current stores

    var stores = {};

    var Store = Ember.Object.extend({
        name:null,
        _data:null,
        add:function (obj) {
            if (!obj) {
                return;
            }
            if (!obj.id) {
                return;
            }
            this.get('_data')[obj.id] = obj;
        },
        remove:function (obj) {
            if (!obj) {
                return;
            }
            if (!obj.id) {
                return;
            }
            var data = this.get('_data');
            delete data[obj.id];
        },

        find:function (id) {
            if (!id) {
                return;
            }
            return this.get('_data')[id];
        }
    });

    Demo2App.Stores = Demo2App.Stores || {};

    Demo2App.Stores.create = function (name, options) {
        name = name.toLowerCase();
        var s = Store.create({
            name:name
        });
        s.set('_data', {});
        stores[name] = s;
        return s;
    };

    Demo2App.Stores.get = function (name) {
        name = name.toLowerCase();
        return stores[name];
    };

    Demo2App.Stores.remove = function (name) {
        name = name.toLowerCase();
        delete stores[name];
    };

    Demo2App.Stores.clear = function (name) {
        name = name.toLowerCase();
        stores[name].set('_data', {});
    };

    Demo2App.Stores.find = function (name, id) {
        var ds = Demo2App.Stores.get(name);
        var entity = ds.find(id);
        if (entity) {
            return entity;
        }
        return null;
    }

})();


var personsDS = Demo2App.Stores.create('person');
personsDS.add(Demo2App.Person.create({
    id:"123",
    firstName:"Aaron",
    lastName:"Bourne"
})
);

personsDS.add(Demo2App.Person.create({
    id:"234",
    firstName:"Bonnie",
    lastName:"Highways"
})
);

personsDS.add(Demo2App.Person.create({
    id:"345",
    firstName:"Daddy",
    lastName:"Peacebucks"
})
);

personsDS.add(Demo2App.Person.create({
    id:"456",
    firstName:"Cotton",
    lastName:"Kandi"
})
);


Demo2App.mainController = Ember.Object.create({

    gifts:Ember.ArrayController.create({
        content:[],

        newGift:function (details) {
            var gift = Demo2App.Gift.create(details);
            this.addObject(gift);
            return gift;
        }
    })
});

var giftsView = Ember.View.create({
    templateName:'gifts',
    giftsBinding:'Demo2App.mainController.gifts',
    giftReason:'Birthday 2011'
});

var moreGifts = [
    { name:'Book', excitement:'3', personId:'123' },
    { name:'Shirt', excitement:'1', personId:'234'},
    { name:'Game System', excitement:'5', personId:'123'},
    { name:'Movie', excitement:'4', personId:'345'},
    { name:'Gift Card', excitement:'3', personId:'123'},
    { name:'MP3 Player', excitement:'3', personId:'456'},
    { name:'Tie', excitement:'1', personId:'456'},
    { name:'Candy', excitement:'3', personId:'234'},
    { name:'Coffee', excitement:'3', personId:'123'},
    { name:'Blanket', excitement:'2', personId:'456'},
    { name:'Camera', excitement:'4', personId:'234'},
    { name:'Phone', excitement:'5', personId:'234'},
    { name:'Socks', excitement:'1', personId:'123'},
    { name:'Game', excitement:'5', personId:'456'}
];

var moreGiftsIndex = moreGifts.length;

$(function () {

    function addMoreGifts() {
        moreGiftsIndex--;
        if (moreGiftsIndex >= 0) {
            Demo2App.mainController.gifts.newGift(moreGifts[moreGiftsIndex]);
        }
        setTimeout(addMoreGifts, 2000);
    }

    giftsView.append();
    addMoreGifts();
});

Here’s a few details about the JavaScript code. #1, it’s got very little in the way of error checking. I’ll add that later. And if you use it, you should add it. :)

Recall the simple data model with each gift having a foreign key reference to a person. If you look at each Gift, it contains keys that mirror the DB: name, excitement, and personId. All 3 are handled as strings. The Person class has an Id, lastName, and firstName.

When creating the Ember Gift class, I made a connection to the Person class using a new function I added called entityRef.

Demo2App.Gift = Ember.Object.extend({
    personId:null,
    person:Ember.entityRef('personId') // [3] 
});

As you can see on line [3] above, a person property is declared not with a value, but as a function. Ember.js includes support for computed properties which, when properly used, return a computed value and can be automatically updated when a dependency is established between the function and the values the computed property requires. Following is the code for the entityRef function.

(function () {
    Ember.entityRef = function (property, type) {
        // auto build connection if property is in the form
        // of 'typenameID' if type is not specified
        if (arguments.length === 1 && property) {   // [1]
            var l = property.length;
            if (l > 2 && property.substr(l - 2).toLowerCase() === 'id') {
                type = property.substr(0, l - 2);
            } else {
                throw new Error("Type not specified, and cannot automatically determine store name. Referenced property name must end with Id.");
            }
        }
        var fn = new Function("return Demo2App.Stores.find('" + type + "', this.get('" + property + "')); ");
        return Ember.computed(fn).property(property);
    };
})();

Thankfully, the entityRef function doesn’t need to do much, and much of what it does is make it easy to make a connection automatically between the local data store and the source property by using a simple naming convention.

Starting at [1], the code makes a simple attempt at doing an “auto-map” when a type isn’t specified specifically. In this case, when “personId” is passed to the function, it chops the “Id” off and uses “person” as the name of the data store automatically. It doesn’t validate the name or anything sophisticated (as I said, it’s light on error checking), but it works when everything is specified correctly. In this case, there’s a “person” data store created in the JavaScript code created a little bit later.

var personsDS = Demo2App.Stores.create('person');

Once the data store type name is located, it creates a new function which returns the object by Id stored in the named data store. That function is wrapped in an Ember computed function, and then the dependency on the “personId” is established. Both of these features are core to the Ember JavaScript library (check out the Ember JavaScript library web site for more information). While they may look a bit strange if you’re not familiar with Ember, trust me, they’re perfectly normal. :)

The remaining code is standard Ember JavaScript code with the addition of a simple data store object that manages objects by Id.

Have fun!

(And if you haven’t transitioned from the earlier SproutCore 2.0 betas to Ember, this code should work with a tiny bit of namespace fixing).