Day three: com.joyent.Resource hooks and the search method.
Once we’ve got our basic RESTful task resource, we’re about to convert our tasks list into a sortable collection because, well, priorities in life change, and we might need to reorder our pending stuff.
We’ll review the different options provided to filter our searches, sort them, limit the number of results. Also, we’re going to review some of the available hooks which allow us to perform additional operations during the life cycle of our Resources.
Resource sort options.
Being an observant reader, you’ve probably noticed that the default sort order for our tasks list is by id, alphabetically – remember, capital letters codes are minor than lower case letters – ascending.
So, our the next code fragment from our Tasks list action:
GET(/\/tasks\/?$/, function() {
var tasks = Task.search({});
});
really means: retrieve all the tasks, sort them using the default order , (which is the aforementioned id ascending).
We could reverse this order by just adding .reverse(); – definitely, a good name for this function – to the object returned by our .search() method:
GET(/\/tasks\/?$/, function() {
var tasks = Task.search({}).reverse();
});
Now, we need some more tasks to demonstrate the available search options so, let’s add approximately 10 tasks before continuing.
We could, for example, be interested in sorting our resources by the created field – think of a blog, for example. We can sort them either ascending:
var tasks = Task.search({}, {sort: 'created'});
or descending:
var tasks = Task.search({}, {sort: 'created'}).reverse();
Also, apart from deciding the sort order for our resources, we can limit the number of results we want to retrieve by using the limit option:
var tasks = Task.search({}, {sort: 'created', limit: 2});
Using limit alone will make the search method return, at a maximum, the specified number of results. We can also combine it with the offset option, in order to start retrieving resources from a given one.
var tasks = Task.search({}, {sort: 'created', limit: 2, offset: 2});
The previous fragment will return resources 3 and 4. Note that if you specify an offset over the current number of total resources, you’ll get an empty result set, as expected.
Resource search conditions.
I’m not going to dive too much into all the search possibilities here, since they are pretty well documented in the system.datastore docs. Anyways, we need to use them for our small application, so, let’s review searches filtered by a given property value.
Do you remember we suggested a little exercise at the end of the previous article? Either if you completed it or if you simply updated the code from the public repository, we should have another property added to our tasks: completed. Right now, we’re gonna start using it and filter the tasks being displayed, in order to do not show the completed ones.
Let’s try this into the action which retrieves our tasks list:
var tasks = Task.search({completed: {'!=': 1}});
If you hasn’t been playing with your tasks and flagged them as completed, then as not completed, you’ll probably will get a nice empty list now. That is because we haven’t assigned any default value to the task.completed property so, on the DB, that property exists only for those tasks you’ve checked/unchecked.
We can try the opposite to demonstrate our search options are fine. Just change it by:
var tasks = Task.search({completed: {'=': 1}});
and you’ll see all the completed tasks. And this is the reason to do a quick stop into the search conditions: do not use a property you don’t know it’s present for all the items into your resource collection, unless you know you explicitly need to do it that way.
Either way, we are about to fix it on the next section, by setting a default value of 0 to the task.completed property when creating the task.
Extending Resource constructor with hooks.
Until now, we’ve mostly obviated the fact the Resource constructor can take additional arguments. Those additional arguments are intended to be functions containing methods for our resources, some of them with special meaning. These are documented into the Resource section of the Smart Platform Docs.
Let’s start using it with a small example: add default value to the completed property when we create a task. We just need to modify a little our Task constructor:
var Task = new Resource('task', {
'@save': function() {
if (!this.completed) {
this.completed = 0;
}
}
});
Doing it this way, we’ll ensure that every time we save a task, the completed value will be filled with some default, avoiding our previous problem of tasks not completed disappearing when we try to filter them by that property value.
Now, we can provide an option to either display only the pending tasks – it should be the default – or display also the completed ones. We just need to add a link to our list page, and update our search options for the tasks list, depending on some query string value:
<p>
<a href="[% host %]/tasks?completed=1">Show completed tasks too</a> |
<a href="[% host %]/tasks">Show only pending tasks</a>
</p>
var tasks = ( this.request.query.completed ) ? Task.search({}) : Task.search({completed: {'!=': 1}});
More on Resource hooks: Sorting tasks by position.
A task list is a matter of priorities, and priorities might vary from time to time. It sounds like a good idea to setup a position attribute for our tasks, which will help us to sort them using our own criteria.
In order to add our position property to the Tasks, we’re going to modify the Task resource constructor, upgrading the @save filter which will take care of automatically set the position property to our tasks when saved:
var Task = new Resource('task', {
'@save': function() {
if (!this.completed) {
this.completed = 0;
}
var tasks = Task.search({});
if (!this.position) {
this.position = ( tasks.length ) ? ( tasks.length + 1 ) : 1;
}
}
});
As you can see, we’re adding typeInfo to the Resource constructor. On this case, this is our @save hook, which will be called all the times an object is saved. All the times means both, when it’s created and when it’s updated. That’s the reason we’re setting the position for our Task only when it isn’t already available – if (!this.position). We’re retrieving all the Tasks into the datastore and setting our current Task position to the end of that list.
This approach has an obvious drawback: what if somebody deletes a task?. Yeah, that will means we could add more tasks and, at some point, we will have tasks with duplicated positions. Let’s fix that with another hook, triggered when a given task is deleted:
var Task = new Resource('task', {
'@save': function() {
if (!this.completed) {
this.completed = 0;
}
var tasks = Task.search({});
if (!this.position) {
this.position = ( tasks.length ) ? ( tasks.length + 1 ) : 1;
}
},
'@remove': function() {
var nextTasks = Task.search({ position: [ { '>': this.position } ] }, {sort: 'position'});
nextTasks.forEach(function( aTask ){
var theTask = Task.get( aTask.id );
theTask.position--;
theTask.save();
});
}
});
On this case, what we’re doing into our @remove hook is to decrease the position for all the tasks after the one we’re deleting; this way, we’re completely sure that our position property will always be a list of consecutive integers.
Finally, in order to make use of the position property, let’s sort our tasks using the newly created attribute (within the tasks list action):
var tasks = ( this.request.query.completed ) ? Task.search({}, {sort: 'position'}) : Task.search({completed: {'!=': 1}}, {sort: 'position'});
And, just before to edit our tasks or create new ones, we should update our application in order to fill the property we’ve just added with some values for our existing resources; (otherwise, all tasks will get a position equal to the number of tasks on the DB, not very useful!). Straight on the tasks list function, we can add the next code fragment at the very top of our tasks list action:
// Temporary while we set values for position:
var allTheTasks = Task.search({}, {sort: 'created'});
for (var i=0; i < allTheTasks.length; i++) {
var theTask = Task.get( allTheTasks[i].id );
if (!theTask.position) {
theTask.position = i + 1;
theTask.save();
}
}
Of course, we can remove this just after the first time we reload our tasks list page. Anyway, it will not break anything if we leave it around for a bit so, let’s move forward to something necessary in order to make something really useful of our recently added position property.
Drag & Drop sortable tasks with jQuery
At the end – and at the beginning – this is about JavaScript, therefore we’re going to add some jQueryUI bits to our application in order to be able to move our tasks lists directly in our browser and automatically save their positions.
First thing to do is update our web/_header.html file and load the library:
<script type="application/javascript">
google.load("jquery", "1.3.2");
google.load("jqueryui", "1.7.2");
</script>
Then, we need to create a new JavaScript file under web/public/js/ – say sort.js – with the next content, (remember to add it to the web/_header.html too):
$(document).ready(function() {
$("#tasks").sortable({
cursor: 'move',
update: function () {
$.ajax({
type: 'put',
url: '/tasks',
dataType: 'json',
data: $(this).sortable('serialize', {expression: /(task)[=_](.+)/}),
success: function(data, textStatus) {
if (data['ok'] == true) {
// Done!.
} else {
alert( "Oooops!, something failed" );
}
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
alert("Ooooops!, request failed with status: " + XMLHttpRequest.status + ' ' + XMLHttpRequest.responseText);
}
});
}
});
});
Basically, it does pretty much the same than the other JavaScript files we created for DELETE and PUT methods for a single task, with the difference it collects now all the tasks on their current order in the browser.
Next, we’re going to write a new method for our Task resource which will handle PUT requests to the collection path:
PUT(/\/tasks\/?$/, function() {
try {
var tasksSorted = this.request.content.replace(/^task\[\]=/, '').split('&task[]=');
for ( var i=0; i < tasksSorted.length; i++ ) {
var theTask = Task.get( tasksSorted[i] );
theTask.position = i + 1;
theTask.save();
}
return JSON.stringify( { ok: true } );
} catch (e) {
return JSON.stringify( { ok: false } );
}
});
As you can see, only complex thing is that – due to what I don’t know is a bug or a feature – I’m using some JavaScript String methods to get the tasks as an Array directly from this.request.content, instead of being using the parsed parameters into the this.request.body object. Other than that, just loop over the tasks and update their positions based on their order on that Array.
And this has been day three!. I’m not gonna say goodbye before suggest something to do: a general purpose AJAX activity observer, since we’re performing many AJAX requests and it would be good to keep our application users notified about “something is happening”.
On the next article we’ll review how to use some of the libraries the Smart Platform puts at our disposal and, for the same price, we’ll even learn how to use our own libraries too, and how Smart search for libraries, and how to format dates…, but that will be next day.