Looking for a simpler way to work with the SproutCore store and records
This post is about SproutCore programming and assumes familiarity with the framework.
The traditional pattern of working with store's records is to set up a binding via a controller and handle a data change event. At high level, it goes something like this:
1. Request the creation of RecordArray via App.store.find(<some query>) method
2. Set the record array as the "content" in an ArrayController
3. Bind a view to the content property of that ArrayController
First two steps are done together, usually when a state is setup. For example, say you want to show some contacts. You'd write some something like this:
App.contactsState = SC.Responder.create( {
didBecomeFirstResponder : function() {
var records = App.store.find(App.contactsQuery);
App.contactsController.set('content', records);
}
});
The third step of binding is done in a view eg.
view = App.ContactsView.design({
contentBinding : 'App.contactsController.arrangedObjects'
});
This pattern works great when you need a static relationship between a piece of data and a view. In this example we have a static binding between a set of contacts and the view that shows them.
However there are cases that don't call for a static binding relationship. Cases where an app simply wants to make a query, get the data, and act on it. These cases usually happen then an app needs to process a event eg. user action (button click), or server response. Handling these cases is quite convoluted with the pattern above.
For example, say you need to implement a button that sends a message to a subset of your contacts eg. "favorite contacts".
You cannot write:
onClickSendMessageToFavoriteContacts: function() {
var favoriteContacts = App.store.find(App.favoriteContactsQuery);
favoriteContacts.forEach(function(contact) {App.sendSomeMessage(contact);});
}
because find() cannot return the results when they haven't been retrieved yet (from the server, from local DB, etc.). By the time find() returns the RecordArray favoriteContacts can be empty and would be in the BUSY_LOADING state.
Instead the button handler would have to do the following:
1. create a new query -- because we want to select a subset of the data given a specific criteria (eg. select favorite contacts)
2. create a new controller -- because we need a way to be notified when the results of the query are available
3. setup an observer/binding that will wait until the results are available
4. perform the action (sending a message to favorite contacts)
5. tear down the observer relationship -- because we no longer interested in paying the cost of having "favorite contacts" be kept up to date
6. remove the record array -- because the array is no longer needed and would simply take up memory
Ideally we'd want a more direct way to act on the results of queries. Imagine being able to write:
App.Contact.objects().filter({varotite: YES}, function(contacts) {sendMessage(contacts);});
A filter() method would perform all the necessary steps to make sure that when the callback is called, the data is available. It would also tear-down all the bindings needed to make it happen. Similarly, one can imagine an API like:
App.Contact.objects().all().del()
or
App.Contact.objects().all(function(records) {dome something with all Contacts})
Those who are familiar with Django will recognize this API as similar to QuerySet (http://docs.djangoproject.com/en/dev/ref/models/querysets/), but modified to deal with asynchronous nature of JavaScript programming.
Getting an object would become:
App.Contact.objects().runGet({guid: '123'}, function(contact) { do something with contact 123 });
A multi-step operation such as getting or creating an object can also be hidden inside a method (see Django's version here: http://www.djangoproject.com/documentation/models/get_or_create/):
App.Contact.objects().runGetOrCreate({firstName: 'John', lastName: 'Lennon'}, {'birthday': '1940-10-9'}, function(john) { ... });
I've put an implementation sketch here: and would love to hear any feedback. It is structured as a mixin to the Model and tracks the record states in order to call the callback at the right time.
Do you think SC's data model API should be improved? Can it be improved? Am I approaching the implementation from the right perspective? Does this idea have any fundamental issues with performance?