Basic Use

This is an overview of Jester syntax, and not an exhaustive API, which has yet to be written. For this overview, we’ll use a simple example: blog comments. We will assume the system has Posts and Comments, and the server follows Rails-like REST conventions.

First, define your models. The following creates a global Post class, and makes many assumptions from the name “Post”.

Resource.model("Post")

For comments, you have a Comment model, shared through XML, and it is scoped underneath the Post class. So, a Comment with ID #2, for a Post with ID #1, would live at /posts/1/comments/2.xml. You can specify this when you define the model:

Resource.model("Comment", {prefix: "/posts/:post_id"})

From here, you can use the Post and Comment classes to indulge in as much CRUD as your server allows.

// Find post #1 -- /posts/1.xml
post = Post.find(1)
// First approved comment on Post #1 -- /posts/1/comments.xml?approved=true
comment = Comment.find("first", {post_id: 1, approved: true})

// All comments on Post #1 -- /posts/1/comments.xml
comments = Comment.find("all", {post_id: 1})

// Make a new comment, then save it
comment = Comment.build({post_id: 1, body: "New comment body"})
comment.save()

// Make a new comment and immediately save it
comment = Comment.create({post_id: 1, body: "New comment body"})

// Destroy post #1
Post.find(1).destroy()

If the server provides the courtesy of returning the object’s error messages to clients, Jester will interpret them and set them as an array of strings, accessible in the “errors” property.

comment = Comment.create()
comment.errors // ["Post cannot be blank", "Body cannot be blank"]

Associations

If an object includes associated data in their XML representation, Jester will load them in as an array or a property, depending on whether it is one or many related objects. If Jester comes across a type of object you didn’t explicitly model, it will model it for you automatically, making limited assumptions.

// All comments for Post #1
Post.find(1).comments

// Kill the foolish user who owns Post #1 -- assumes DELETE request to /users/1.xml
Post.find(1).user.destroy()

Asynchronicity

In practice, you’d want to do all this asynchronously and not freeze up the browser. To use Jester asynchronously, provide a callback function as the final argument to find(), create(), save(), and destroy(). All of these methods will return XmlHttpRequest objects when called with an asynchronous callback, and the callbacks will receive the values that the method would return if called synchronously.

// This might be the onsubmit callback of a form.
Comment.create({post_id: 1, body: $('comment_body').value}, updateCommentList)

// All posts from 2006
Post.find("all", {year: 2006}, {onSuccess: displayPosts})
// All posts -- you can put the callback as the second argument, butonly if it's a function (not a hash)
Post.find("all", displayPosts)

Advanced Use

You can specify a custom URL for each RESTful action on your server, by providing a hash of URLs when you first define your models. You need only define the URLs which differ from the defaults. As an example:

Resource.model("User", {
  format: "json",
  urls: {
    list: "/admins.json",
    show: "/admins/:id.json",
    create: "/:department/users/create.json" 
  }
}

Jester can work with remote APIs that support JSON callbacks. This works by adding a “script” element to the DOM that loads in remote JavaScript, with a callback method name appended to the query string. The loaded JavaScript will call this method with the JSON representation of the remote data. Some people have started calling this method “JSONP”.

The poster child for this approach is <a href=”http://www.twitter.com”>Twitter, so that will be the example here:

Resource.model("Twitter", {
  format: "json",
  prefix: "http://twitter.com",
  urls: {
    list: "/statuses/user_timeline/:username.json",
    show: "/statuses/show/:id.json" 
  }
})

There are some caveats here. Because loading remote data does not use XmlHttpRequest, no such object is returned from calls to find(). In addition, all calls to find() must be asynchronous, meaning you must provide a callback method. Lastly, since only GET requests are possible, the only operation you can perform on remote models is “find()”.

// Loads http://twitter.com/statuses/user_timeline/jesterjs.json?callback=jesterCallback into the DOM
Twitter.find("all", {username: "jesterjs"}, listTwitters)

// Loads http://twitter.com/statuses/show/12345678.json?jesterCallback into the DOM
Twitter.find(12345678, showTwitter)